chore: import upstream snapshot with attribution
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import random
|
||||
import time
|
||||
import numpy as np
|
||||
import pytest
|
||||
import asyncio
|
||||
from pymilvus.client.types import LoadState, DataType
|
||||
from pymilvus import AnnSearchRequest, RRFRanker
|
||||
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
prefix = "async"
|
||||
partition_prefix = "async_partition"
|
||||
async_default_nb = 5000
|
||||
default_nb = ct.default_nb
|
||||
default_dim = 2
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "id >= 0"
|
||||
exp_res = "exp_res"
|
||||
default_primary_key_field_name = "id"
|
||||
default_vector_field_name = "vector"
|
||||
default_float_field_name = ct.default_float_field_name
|
||||
default_string_field_name = ct.default_string_field_name
|
||||
|
||||
|
||||
class TestAsyncMilvusClientCollectionInvalid(TestMilvusClientV2Base):
|
||||
""" Test case of collection interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are invalid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("collection_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_create_collection_invalid_collection_name(self, collection_name):
|
||||
"""
|
||||
target: test fast create collection with invalid collection name
|
||||
method: create collection with invalid collection
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {collection_name}. the first character of a "
|
||||
f"collection name must be an underscore or letter: invalid parameter"}
|
||||
await async_client.create_collection(collection_name, default_dim,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test fast create collection with over max collection name length
|
||||
method: create collection with over max collection name length
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"the length of a collection name must be less than 255 characters"}
|
||||
await async_client.create_collection(collection_name, default_dim,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("collection_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_release_collection_invalid_collection_name(self, collection_name):
|
||||
"""
|
||||
target: test release collection with invalid collection name
|
||||
method: release collection with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. release collection
|
||||
error = {ct.err_code: 1100,
|
||||
ct.err_msg: f"Invalid collection name: {collection_name}. "
|
||||
f"the first character of a collection name must be an underscore or letter"}
|
||||
await async_client.release_collection(collection_name, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_collection_not_existed(self):
|
||||
"""
|
||||
target: test release collection with nonexistent name
|
||||
method: release collection with nonexistent name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. release collection
|
||||
collection_name = cf.gen_unique_str("nonexisted")
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"collection not found[database=default]"
|
||||
f"[collection={collection_name}]"}
|
||||
await async_client.release_collection(collection_name, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_release_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test fast create collection with over max collection name length
|
||||
method: create collection with over max collection name length
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. release collection
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"the length of a collection name must be less than 255 characters"}
|
||||
await async_client.release_collection(collection_name, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
|
||||
class TestAsyncMilvusClientCollectionValid(TestMilvusClientV2Base):
|
||||
""" Test case of collection interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are valid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_milvus_client_release_collection_default(self):
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
collections, _ = await async_client.list_collections()
|
||||
assert collection_name in collections
|
||||
desc, _ = await async_client.describe_collection(collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim,
|
||||
"consistency_level": 0})
|
||||
# 2. create partition
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
await async_client.create_partition(collection_name, partition_name)
|
||||
partitions, _ = await async_client.list_partitions(collection_name)
|
||||
assert partition_name in partitions
|
||||
# 3. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
await async_client.insert(collection_name, rows)
|
||||
tasks = []
|
||||
# 4. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
search_task = async_client.search(collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"pk_name": default_primary_key_field_name,
|
||||
"limit": default_limit})
|
||||
tasks.append(search_task)
|
||||
# 5. query
|
||||
query_task = async_client.query(collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(query_task)
|
||||
res = await asyncio.gather(*tasks)
|
||||
|
||||
# 6. release collection
|
||||
await async_client.release_collection(collection_name)
|
||||
# 7. search
|
||||
error = {ct.err_code: 101, ct.err_msg: f"collection not loaded"}
|
||||
await async_client.search(collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 8. query
|
||||
await async_client.query(collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 9. load collection
|
||||
await async_client.load_collection(collection_name)
|
||||
# 10. search
|
||||
await async_client.search(collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
# 11. query
|
||||
await async_client.query(collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
|
||||
# 12. drop action
|
||||
has_partition, _ = await async_client.has_partition(collection_name, partition_name)
|
||||
if has_partition:
|
||||
await async_client.release_partitions(collection_name, partition_name)
|
||||
await async_client.drop_partition(collection_name, partition_name)
|
||||
partitions, _ = await async_client.list_partitions(collection_name)
|
||||
assert partition_name not in partitions
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_milvus_client_truncate_collection(self):
|
||||
"""
|
||||
target: test truncate collection with strong consistency level
|
||||
method: truncate collection with strong consistency level
|
||||
expected: the collection is truncated
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
collections, _ = await async_client.list_collections()
|
||||
assert collection_name in collections
|
||||
# 2. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
await async_client.insert(collection_name, rows)
|
||||
# 3. truncate collection
|
||||
await async_client.truncate_collection(collection_name)
|
||||
# 4. query
|
||||
result = await async_client.query(collection_name, filter=default_search_exp, output_fields=["count(*)"])
|
||||
assert result[0][0].get("count(*)", -1) == 0
|
||||
seg = await async_client.list_persistent_segments(collection_name)
|
||||
assert len(seg[0]) == 0
|
||||
# 5. drop collection
|
||||
await async_client.drop_collection(collection_name)
|
||||
@@ -0,0 +1,512 @@
|
||||
import random
|
||||
import time
|
||||
import pytest
|
||||
import asyncio
|
||||
from pymilvus.client.types import LoadState, DataType
|
||||
from pymilvus import AnnSearchRequest, RRFRanker
|
||||
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
prefix = "async"
|
||||
async_default_nb = 5000
|
||||
default_pk_name = "id"
|
||||
default_vector_name = "vector"
|
||||
|
||||
|
||||
class TestAsyncMilvusClient(TestMilvusClientV2Base):
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_client_default(self):
|
||||
# init async client
|
||||
self.init_async_milvus_client()
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{default_pk_name: i, default_vector_name: [random.random() for _ in range(ct.default_dim)]}
|
||||
for i in range(async_default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, async_default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step])
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# dql tasks
|
||||
tasks = []
|
||||
# search default
|
||||
vector = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
default_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(default_search_task)
|
||||
|
||||
# search with filter & search_params
|
||||
sp = {"metric_type": "COSINE", "params": {"ef": "96"}}
|
||||
filter_params_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
filter=f"{default_pk_name} > 10",
|
||||
search_params=sp,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_params_search_task)
|
||||
|
||||
# search output fields
|
||||
output_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
output_fields=["*"],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(output_search_task)
|
||||
|
||||
# query with filter and default output "*"
|
||||
exp_query_res = [{default_pk_name: i} for i in range(ct.default_limit)]
|
||||
filter_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
filter=f"{default_pk_name} < {ct.default_limit}",
|
||||
output_fields=[default_pk_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": exp_query_res,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_query_task)
|
||||
# query with ids and output all fields
|
||||
ids_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
ids=[i for i in range(ct.default_limit)],
|
||||
output_fields=["*"],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows[:ct.default_limit],
|
||||
"with_vec": True,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(ids_query_task)
|
||||
# get with ids
|
||||
get_task = self.async_milvus_client_wrap.get(c_name,
|
||||
ids=[0, 1],
|
||||
output_fields=[default_pk_name, default_vector_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows[:2], "with_vec": True,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(get_task)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_client_partition(self):
|
||||
# init async client
|
||||
self.init_async_milvus_client()
|
||||
|
||||
# create collection & partition
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
p_name = cf.gen_unique_str("par")
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
await self.async_milvus_client_wrap.create_partition(c_name, p_name)
|
||||
partitions, _ = await self.async_milvus_client_wrap.list_partitions(c_name)
|
||||
assert p_name in partitions
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{default_pk_name: i, default_vector_name: [random.random() for _ in range(ct.default_dim)]}
|
||||
for i in range(async_default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, async_default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step], partition_name=p_name)
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# count from default partition
|
||||
count_res, _ = await self.async_milvus_client_wrap.query(c_name, output_fields=["count(*)"], partition_names=[ct.default_partition_name])
|
||||
assert count_res[0]["count(*)"] == 0
|
||||
|
||||
# dql tasks
|
||||
tasks = []
|
||||
# search default
|
||||
vector = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
default_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
partition_names=[p_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(default_search_task)
|
||||
|
||||
# search with filter & search_params
|
||||
sp = {"metric_type": "COSINE", "params": {"ef": "96"}}
|
||||
filter_params_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
filter=f"{default_pk_name} > 10",
|
||||
search_params=sp,
|
||||
partition_names=[p_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_params_search_task)
|
||||
|
||||
# search output fields
|
||||
output_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
output_fields=["*"],
|
||||
partition_names=[p_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(output_search_task)
|
||||
|
||||
# query with filter and default output "*"
|
||||
exp_query_res = [{default_pk_name: i} for i in range(ct.default_limit)]
|
||||
filter_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
filter=f"{default_pk_name} < {ct.default_limit}",
|
||||
output_fields=[default_pk_name],
|
||||
partition_names=[p_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": exp_query_res,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_query_task)
|
||||
# query with ids and output all fields
|
||||
ids_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
ids=[i for i in range(ct.default_limit)],
|
||||
output_fields=["*"],
|
||||
partition_names=[p_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows[:ct.default_limit],
|
||||
"with_vec": True,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(ids_query_task)
|
||||
# get with ids
|
||||
get_task = self.async_milvus_client_wrap.get(c_name,
|
||||
ids=[0, 1], partition_names=[p_name],
|
||||
output_fields=[default_pk_name, default_vector_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows[:2], "with_vec": True,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(get_task)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_client_with_schema(self):
|
||||
# init async client
|
||||
pk_field_name = "id"
|
||||
self.init_async_milvus_client()
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
schema = self.async_milvus_client_wrap.create_schema(auto_id=False,
|
||||
partition_key_field=ct.default_int64_field_name)
|
||||
schema.add_field(pk_field_name, DataType.VARCHAR, max_length=100, is_primary=True)
|
||||
schema.add_field(ct.default_int64_field_name, DataType.INT64, is_partition_key=True)
|
||||
schema.add_field(ct.default_float_vec_field_name, DataType.FLOAT_VECTOR, dim=ct.default_dim)
|
||||
schema.add_field(default_vector_name, DataType.FLOAT_VECTOR, dim=ct.default_dim)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, schema=schema)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{pk_field_name: str(i),
|
||||
ct.default_int64_field_name: i,
|
||||
ct.default_float_vec_field_name: [random.random() for _ in range(ct.default_dim)],
|
||||
default_vector_name: [random.random() for _ in range(ct.default_dim)],
|
||||
} for i in range(async_default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, async_default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step])
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# flush
|
||||
await self.async_milvus_client_wrap.flush(c_name)
|
||||
stats, _ = await self.async_milvus_client_wrap.get_collection_stats(c_name)
|
||||
assert stats["row_count"] == async_default_nb
|
||||
|
||||
# create index -> load
|
||||
index_params = self.async_milvus_client_wrap.prepare_index_params()[0]
|
||||
index_params.add_index(field_name=ct.default_float_vec_field_name,
|
||||
index_type="HNSW", metric_type="COSINE", M=30,
|
||||
efConstruction=200)
|
||||
index_params.add_index(field_name=default_vector_name, index_type="IVF_SQ8",
|
||||
metric_type="L2", nlist=32)
|
||||
await self.async_milvus_client_wrap.create_index(c_name, index_params)
|
||||
await self.async_milvus_client_wrap.load_collection(c_name)
|
||||
|
||||
_index, _ = await self.async_milvus_client_wrap.describe_index(c_name, default_vector_name)
|
||||
assert _index["indexed_rows"] == async_default_nb
|
||||
assert _index["state"] == "Finished"
|
||||
_load, _ = await self.async_milvus_client_wrap.get_load_state(c_name)
|
||||
assert _load['state'] == LoadState.Loaded
|
||||
|
||||
# dql tasks
|
||||
tasks = []
|
||||
# search default
|
||||
vector = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
default_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
anns_field=ct.default_float_vec_field_name,
|
||||
search_params={"metric_type": "COSINE",
|
||||
"params": {"ef": "96"}},
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(default_search_task)
|
||||
|
||||
# hybrid_search
|
||||
search_param = {
|
||||
"data": cf.gen_vectors(ct.default_nq, ct.default_dim, vector_data_type=DataType.FLOAT_VECTOR),
|
||||
"anns_field": ct.default_float_vec_field_name,
|
||||
"param": {"metric_type": "COSINE", "params": {"ef": "96"}},
|
||||
"limit": ct.default_limit,
|
||||
"expr": f"{ct.default_int64_field_name} > 10"}
|
||||
req = AnnSearchRequest(**search_param)
|
||||
|
||||
search_param2 = {
|
||||
"data": cf.gen_vectors(ct.default_nq, ct.default_dim, vector_data_type=DataType.FLOAT_VECTOR),
|
||||
"anns_field": default_vector_name,
|
||||
"param": {"metric_type": "L2", "params": {"nprobe": "32"}},
|
||||
"limit": ct.default_limit
|
||||
}
|
||||
req2 = AnnSearchRequest(**search_param2)
|
||||
_output_fields = [ct.default_int64_field_name, ct.default_string_field_name]
|
||||
filter_params_search_task = self.async_milvus_client_wrap.hybrid_search(c_name, [req, req2], RRFRanker(),
|
||||
limit=5,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={
|
||||
"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": 5,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_params_search_task)
|
||||
|
||||
# get with ids
|
||||
get_task = self.async_milvus_client_wrap.get(c_name, ids=['0', '1'], output_fields=[ct.default_int64_field_name,
|
||||
pk_field_name])
|
||||
tasks.append(get_task)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_client_dml(self):
|
||||
# init async client
|
||||
self.init_async_milvus_client()
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{default_pk_name: i, default_vector_name: [random.random() for _ in range(ct.default_dim)]}
|
||||
for i in range(ct.default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, ct.default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step])
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# dml tasks
|
||||
# query id -> upsert id -> query id -> delete id -> query id
|
||||
_id = 10
|
||||
get_res, _ = await self.async_milvus_client_wrap.get(c_name, ids=[_id],
|
||||
output_fields=[default_pk_name, default_vector_name])
|
||||
assert len(get_res) == 1
|
||||
|
||||
# upsert
|
||||
upsert_row = [{
|
||||
default_pk_name: _id, default_vector_name: [random.random() for _ in range(ct.default_dim)]
|
||||
}]
|
||||
upsert_res, _ = await self.async_milvus_client_wrap.upsert(c_name, upsert_row)
|
||||
assert upsert_res["upsert_count"] == 1
|
||||
|
||||
# get _id after upsert
|
||||
get_res, _ = await self.async_milvus_client_wrap.get(c_name, ids=[_id],
|
||||
output_fields=[default_pk_name, default_vector_name])
|
||||
for j in range(5):
|
||||
assert abs(get_res[0][default_vector_name][j] - upsert_row[0][default_vector_name][j]) < ct.epsilon
|
||||
|
||||
# delete
|
||||
del_res, _ = await self.async_milvus_client_wrap.delete(c_name, ids=[_id])
|
||||
assert del_res["delete_count"] == 1
|
||||
|
||||
# query after delete
|
||||
get_res, _ = await self.async_milvus_client_wrap.get(c_name, ids=[_id],
|
||||
output_fields=[default_pk_name, default_vector_name])
|
||||
assert len(get_res) == 0
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_client_with_db(self):
|
||||
# init async client
|
||||
self.init_async_milvus_client()
|
||||
db_name = cf.gen_unique_str("db")
|
||||
await self.async_milvus_client_wrap.create_database(db_name)
|
||||
await self.async_milvus_client_wrap.close()
|
||||
uri = cf.param_info.param_uri or f"http://{cf.param_info.param_host}:{cf.param_info.param_port}"
|
||||
self.async_milvus_client_wrap.init_async_client(uri, token=cf.param_info.param_token, db_name=db_name)
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{default_pk_name: i, default_vector_name: [random.random() for _ in range(ct.default_dim)]}
|
||||
for i in range(async_default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, async_default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step])
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# dql tasks
|
||||
tasks = []
|
||||
# search default
|
||||
vector = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
default_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(default_search_task)
|
||||
|
||||
# query with filter and default output "*"
|
||||
exp_query_res = [{default_pk_name: i} for i in range(ct.default_limit)]
|
||||
filter_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
filter=f"{default_pk_name} < {ct.default_limit}",
|
||||
output_fields=[default_pk_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": exp_query_res,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_query_task)
|
||||
|
||||
# get with ids
|
||||
get_task = self.async_milvus_client_wrap.get(c_name,
|
||||
ids=[0, 1],
|
||||
output_fields=[default_pk_name, default_vector_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows[:2], "with_vec": True,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(get_task)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_client_close(self):
|
||||
# init async client
|
||||
uri = cf.param_info.param_uri or f"http://{cf.param_info.param_host}:{cf.param_info.param_port}"
|
||||
self.async_milvus_client_wrap.init_async_client(uri, token=cf.param_info.param_token)
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
|
||||
# close -> search raise error
|
||||
await self.async_milvus_client_wrap.close()
|
||||
vector = cf.gen_vectors(1, ct.default_dim)
|
||||
error = {ct.err_code: 1, ct.err_msg: "should create connection first"}
|
||||
await self.async_milvus_client_wrap.search(c_name, vector, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
@pytest.mark.skip("connect with zilliz cloud")
|
||||
async def test_async_client_with_token(self):
|
||||
# init async client
|
||||
uri = cf.param_info.param_uri or f"http://{cf.param_info.param_host}:{cf.param_info.param_port}"
|
||||
token = cf.param_info.param_token
|
||||
self.async_milvus_client_wrap.init_async_client(uri, token=token)
|
||||
|
||||
# create collection
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
await self.async_milvus_client_wrap.create_collection(c_name, dimension=ct.default_dim)
|
||||
collections, _ = await self.async_milvus_client_wrap.list_collections()
|
||||
assert c_name in collections
|
||||
|
||||
# insert entities
|
||||
rows = [
|
||||
{default_pk_name: i, default_vector_name: [random.random() for _ in range(ct.default_dim)]}
|
||||
for i in range(ct.default_nb)]
|
||||
start_time = time.time()
|
||||
tasks = []
|
||||
step = 1000
|
||||
for i in range(0, ct.default_nb, step):
|
||||
task = self.async_milvus_client_wrap.insert(c_name, rows[i:i + step])
|
||||
tasks.append(task)
|
||||
insert_res = await asyncio.gather(*tasks)
|
||||
end_time = time.time()
|
||||
log.info("Total time: {:.2f} seconds".format(end_time - start_time))
|
||||
for r in insert_res:
|
||||
assert r[0]['insert_count'] == step
|
||||
|
||||
# dql tasks
|
||||
tasks = []
|
||||
# search default
|
||||
vector = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
default_search_task = self.async_milvus_client_wrap.search(c_name, vector, limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(default_search_task)
|
||||
|
||||
# query with filter and default output "*"
|
||||
exp_query_res = [{default_pk_name: i} for i in range(ct.default_limit)]
|
||||
filter_query_task = self.async_milvus_client_wrap.query(c_name,
|
||||
filter=f"{default_pk_name} < {ct.default_limit}",
|
||||
output_fields=[default_pk_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": exp_query_res,
|
||||
"pk_name": default_pk_name})
|
||||
tasks.append(filter_query_task)
|
||||
await asyncio.gather(*tasks)
|
||||
@@ -0,0 +1,288 @@
|
||||
import random
|
||||
import time
|
||||
import numpy as np
|
||||
import pytest
|
||||
import asyncio
|
||||
from pymilvus.client.types import LoadState, DataType
|
||||
from pymilvus import AnnSearchRequest, RRFRanker
|
||||
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
prefix = "async"
|
||||
partition_prefix = "async_partition"
|
||||
async_default_nb = 5000
|
||||
default_nb = ct.default_nb
|
||||
default_dim = 128
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "id >= 0"
|
||||
exp_res = "exp_res"
|
||||
default_primary_key_field_name = "id"
|
||||
default_vector_field_name = "vector"
|
||||
default_float_field_name = ct.default_float_field_name
|
||||
default_string_field_name = ct.default_string_field_name
|
||||
|
||||
class TestAsyncMilvusClientIndexInvalid(TestMilvusClientV2Base):
|
||||
""" Test case of index interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are invalid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_create_index_invalid_collection_name(self, name):
|
||||
"""
|
||||
target: test create index with invalid collection name
|
||||
method: create index with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector")
|
||||
# 3. create index
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"collection not found[database=default][collection={name}]"}
|
||||
await async_client.create_index(name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("name", ["a".join("a" for i in range(256))])
|
||||
async def test_async_milvus_client_create_index_collection_name_over_max_length(self, name):
|
||||
"""
|
||||
target: test create index with over max collection name length
|
||||
method: create index with over max collection name length
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector")
|
||||
# 3. create index
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"collection not found[database=default][collection={name}]"}
|
||||
await async_client.create_index(name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_index_collection_name_not_existed(self):
|
||||
"""
|
||||
target: test create index with nonexistent collection name
|
||||
method: create index with nonexistent collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
not_existed_collection_name = cf.gen_unique_str("not_existed_collection")
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector")
|
||||
# 3. create index
|
||||
error = {ct.err_code: 100,
|
||||
ct.err_msg: f"collection not found[database=default][collection={not_existed_collection_name}]"}
|
||||
await async_client.create_index(not_existed_collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("index", ["12-s", "12 s", "(mn)", "中文", "%$#", "a".join("a" for i in range(256))])
|
||||
async def test_async_milvus_client_create_index_invalid_index_type(self, index):
|
||||
"""
|
||||
target: test create index with invalid index type name
|
||||
method: create index with invalid index type name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector", index_type=index)
|
||||
# 3. create index
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"invalid parameter[expected=valid index][actual=invalid index type: {index}"}
|
||||
# It's good to show what the valid indexes are
|
||||
await async_client.create_index(collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("metric", ["12-s", "12 s", "(mn)", "中文", "%$#", "a".join("a" for i in range(256))])
|
||||
async def test_async_milvus_client_create_index_invalid_metric_type(self, metric):
|
||||
"""
|
||||
target: test create index with invalid metric type
|
||||
method: create index with invalid metric type
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector", metric_type=metric)
|
||||
# 3. create index
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"float vector index does not support metric type: {metric}"}
|
||||
# It's good to show what the valid index params are
|
||||
await async_client.create_index(collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_drop_index_before_release(self):
|
||||
"""
|
||||
target: test drop index when collection are not released
|
||||
method: drop index when collection are not released
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
|
||||
# 2. drop index
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"vector index cannot be dropped on loaded collection"}
|
||||
await async_client.drop_index(collection_name, "vector", check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
class TestAsyncMilvusClientIndexValid(TestMilvusClientV2Base):
|
||||
""" Test case of index interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
@pytest.fixture(scope="function", params=["COSINE", "L2", "IP"])
|
||||
def metric_type(self, request):
|
||||
yield request.param
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are valid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("index, params",
|
||||
zip(ct.all_index_types[:8],
|
||||
ct.default_all_indexes_params[:8]))
|
||||
async def test_async_milvus_client_create_drop_index_default(self, index, params, metric_type):
|
||||
"""
|
||||
target: test create and drop index normal case
|
||||
method: create collection, index; insert; search and query; drop index
|
||||
expected: search/query successfully; create/drop index successfully
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
collections, _ = await async_client.list_collections()
|
||||
assert collection_name in collections
|
||||
desc, _ = await async_client.describe_collection(collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim,
|
||||
"consistency_level": 0})
|
||||
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
res, _ = await async_client.list_indexes(collection_name)
|
||||
assert res == []
|
||||
|
||||
# 2. prepare index params
|
||||
index_params = async_client.prepare_index_params()[0]
|
||||
index_params.add_index(field_name="vector", index_type=index, metric_type=metric_type, params=params)
|
||||
# 3. create index
|
||||
await async_client.create_index(collection_name, index_params)
|
||||
|
||||
# 4. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
await async_client.insert(collection_name, rows)
|
||||
await async_client.load_collection(collection_name)
|
||||
|
||||
tasks = []
|
||||
# 5. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
search_task = self.async_milvus_client_wrap. \
|
||||
search(collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(search_task)
|
||||
# 6. query
|
||||
query_task = self.async_milvus_client_wrap. \
|
||||
query(collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(query_task)
|
||||
res = await asyncio.gather(*tasks)
|
||||
|
||||
# 7. drop index
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
res, _ = await async_client.list_indexes(collection_name)
|
||||
assert res == []
|
||||
|
||||
# 8. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
@@ -0,0 +1,141 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
import asyncio
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
default_nb = ct.default_nb
|
||||
default_dim = 128
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "id >= 0"
|
||||
exp_res = "exp_res"
|
||||
default_primary_key_field_name = "id"
|
||||
default_vector_field_name = "vector"
|
||||
default_float_field_name = ct.default_float_field_name
|
||||
default_string_field_name = ct.default_string_field_name
|
||||
|
||||
|
||||
class TestAsyncMilvusClientInsert(TestMilvusClientV2Base):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test insert async
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""
|
||||
Clean up async client connection after each test method.
|
||||
This ensures proper resource cleanup and prevents connection leaks.
|
||||
"""
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_insert(self):
|
||||
"""
|
||||
target: test async insert via Milvus async client
|
||||
method: insert with async milvus client
|
||||
expected: verify insert_count / row_count
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
|
||||
# 2. prepare data
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
|
||||
# 3. insert
|
||||
res, _ = await async_client.insert(collection_name, rows)
|
||||
|
||||
assert res["insert_count"] == ct.default_nb
|
||||
|
||||
# 4. verify count
|
||||
await async_client.flush(collection_name)
|
||||
num_entities, _ = await async_client.get_collection_stats(collection_name)
|
||||
assert num_entities["row_count"] == ct.default_nb
|
||||
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_insert_large(self):
|
||||
"""
|
||||
target: test insert with async
|
||||
method: insert 5w entities
|
||||
expected: verify num entities
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
nb = 50000
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(nb)]
|
||||
|
||||
res, _ = await async_client.insert(collection_name, rows)
|
||||
assert res["insert_count"] == nb
|
||||
|
||||
await async_client.flush(collection_name)
|
||||
num_entities, _ = await async_client.get_collection_stats(collection_name)
|
||||
assert num_entities["row_count"] == nb
|
||||
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_insert_invalid_data(self):
|
||||
"""
|
||||
target: test insert async with invalid data
|
||||
method: insert async with invalid data
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
|
||||
# missing vector field
|
||||
rows = [{ct.default_primary_key_field_name: 1}]
|
||||
|
||||
error = {ct.err_code: 1, ct.err_msg: "Insert missed an field `vector` to collection without set nullable==true or set default_value"}
|
||||
|
||||
await async_client.insert(collection_name, rows, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_insert_invalid_partition(self):
|
||||
"""
|
||||
target: test insert async with invalid partition
|
||||
method: insert async with invalid partition
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
partition_name = cf.gen_unique_str("partition")
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
error = {ct.err_code: 200, ct.err_msg: f"partition not found[partition={partition_name}]"}
|
||||
|
||||
await async_client.insert(collection_name, data=rows, partition_name=partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
await async_client.drop_collection(collection_name)
|
||||
@@ -0,0 +1,782 @@
|
||||
import random
|
||||
import time
|
||||
import numpy as np
|
||||
import pytest
|
||||
import asyncio
|
||||
from pymilvus.client.types import LoadState, DataType
|
||||
from pymilvus import AnnSearchRequest, RRFRanker
|
||||
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
prefix = "async"
|
||||
partition_prefix = "async_partition"
|
||||
async_default_nb = 5000
|
||||
default_nb = ct.default_nb
|
||||
default_dim = 2
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "id >= 0"
|
||||
exp_res = "exp_res"
|
||||
default_primary_key_field_name = "id"
|
||||
default_vector_field_name = "vector"
|
||||
default_float_field_name = ct.default_float_field_name
|
||||
default_string_field_name = ct.default_string_field_name
|
||||
|
||||
|
||||
class TestAsyncMilvusClientPartitionInvalid(TestMilvusClientV2Base):
|
||||
""" Test case of partition interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are invalid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("collection_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_create_partition_invalid_collection_name(self, collection_name):
|
||||
"""
|
||||
target: test create partition with invalid collection name
|
||||
method: create partition with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {collection_name}. the first character of a "
|
||||
f"collection name must be an underscore or letter: invalid parameter"}
|
||||
await async_client.create_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_partition_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test create partition with collection name over max length 255
|
||||
method: create partition with collection name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 1100,
|
||||
ct.err_msg: f"Invalid collection name: {collection_name}. the length of a collection name "
|
||||
f"must be less than 255 characters: invalid parameter"}
|
||||
await async_client.create_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_partition_collection_name_not_existed(self):
|
||||
"""
|
||||
target: test create partition with nonexistent collection name
|
||||
method: create partition with nonexistent collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str("partition_not_exist")
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 100, ct.err_msg: f"collection not found[database=default]"
|
||||
f"[collection={collection_name}]"}
|
||||
await async_client.create_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("partition_name", ["12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_create_partition_invalid_partition_name(self, partition_name):
|
||||
"""
|
||||
target: test create partition with invalid partition name
|
||||
method: create partition with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
desc, _ = await async_client.describe_collection(collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim,
|
||||
"consistency_level": 0})
|
||||
# 2. create partition
|
||||
error = {ct.err_code: 65535, ct.err_msg: f"Invalid partition name: {partition_name}"}
|
||||
await async_client.create_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_partition_partition_name_over_max_length(self):
|
||||
"""
|
||||
target: test create partition with partition name over max length 255
|
||||
method: create partition with partition name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
|
||||
partition_name = "a".join("a" for i in range(256))
|
||||
# 2. create partition
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f"Invalid partition name: {partition_name}. The length of a partition name "
|
||||
f"must be less than 255 characters."}
|
||||
await async_client.create_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_create_partition_name_lists(self):
|
||||
"""
|
||||
target: test create partition with wrong partition name format list
|
||||
method: create partition with wrong partition name format list
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_names = [cf.gen_unique_str(partition_prefix), cf.gen_unique_str(partition_prefix)]
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. create partition
|
||||
error = {ct.err_code: 999, ct.err_msg: f"`partition_name` value {partition_names} is illegal"}
|
||||
await async_client.create_partition(collection_name, partition_names,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("collection_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_drop_partition_invalid_collection_name(self, collection_name):
|
||||
"""
|
||||
target: test drop partition with invalid collection name
|
||||
method: drop partition with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {collection_name}. the first character of a "
|
||||
f"collection name must be an underscore or letter: invalid parameter"}
|
||||
await async_client.drop_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_drop_partition_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test drop partition with collection name over max length 255
|
||||
method: drop partition with collection name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 1100,
|
||||
ct.err_msg: f"Invalid collection name: {collection_name}. the length of a collection name "
|
||||
f"must be less than 255 characters: invalid parameter"}
|
||||
await async_client.drop_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_drop_partition_collection_name_not_existed(self):
|
||||
"""
|
||||
target: test drop partition with nonexistent collection name
|
||||
method: drop partition with nonexistent collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str("partition_not_exist")
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create partition
|
||||
error = {ct.err_code: 100, ct.err_msg: f"collection not found[database=default]"
|
||||
f"[collection={collection_name}]"}
|
||||
await async_client.drop_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("partition_name", ["12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_drop_partition_invalid_partition_name(self, partition_name):
|
||||
"""
|
||||
target: test drop partition with invalid partition name
|
||||
method: drop partition with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. create partition
|
||||
error = {ct.err_code: 65535, ct.err_msg: f"Invalid partition name: {partition_name}."}
|
||||
await async_client.drop_partition(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_drop_partition_name_lists(self):
|
||||
"""
|
||||
target: test drop partition with wrong partition name format list
|
||||
method: drop partition with wrong partition name format list
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_names = [cf.gen_unique_str(partition_prefix), cf.gen_unique_str(partition_prefix)]
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. create partition
|
||||
error = {ct.err_code: 1, ct.err_msg: f"`partition_name` value {partition_names} is illegal"}
|
||||
await async_client.drop_partition(collection_name, partition_names,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_load_partitions_invalid_collection_name(self, name):
|
||||
"""
|
||||
target: test load partitions with invalid collection name
|
||||
method: load partitions with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. load partitions
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {name}. the first character of a collection name "
|
||||
f"must be an underscore or letter: invalid parameter"}
|
||||
await async_client.load_partitions(name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_load_partitions_collection_not_existed(self):
|
||||
"""
|
||||
target: test load partitions with nonexistent collection name
|
||||
method: load partitions with nonexistent collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. load partitions
|
||||
collection_name = cf.gen_unique_str("nonexisted")
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"collection not found[database=default]"
|
||||
f"[collection={collection_name}]"}
|
||||
await async_client.load_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_load_partitions_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test load partitions with collection name over max length 255
|
||||
method: load partitions with collection name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. load partitions
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {collection_name}. "
|
||||
f"the length of a collection name must be less than 255 characters: "
|
||||
f"invalid parameter"}
|
||||
await async_client.load_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("name", ["12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_load_partitions_invalid_partition_name(self, name):
|
||||
"""
|
||||
target: test load partitions with invalid partition name
|
||||
method: load partitions with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. load partition
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"partition not found"}
|
||||
await async_client.load_partitions(collection_name, name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_load_partitions_partition_not_existed(self):
|
||||
"""
|
||||
target: test load partitions with nonexistent partition name
|
||||
method: load partitions with nonexistent partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_name = cf.gen_unique_str("nonexisted")
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. load partition
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"partition not found"}
|
||||
await async_client.load_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_load_partitions_partition_name_over_max_length(self):
|
||||
"""
|
||||
target: test load partitions with partition name over max length 255
|
||||
method: load partitions with partition name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_name = "a".join("a" for i in range(256))
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. load partition
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"partition not found"}
|
||||
await async_client.load_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_load_partitions_without_index(self):
|
||||
"""
|
||||
target: test load partitions after drop index
|
||||
method: load partitions after drop index
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. drop index
|
||||
await async_client.release_collection(collection_name)
|
||||
await async_client.drop_index(collection_name, "vector")
|
||||
# 3. load partition
|
||||
error = {ct.err_code: 700, ct.err_msg: f"index not found[collection={collection_name}]"}
|
||||
await async_client.load_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
# 4. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("collection_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_release_partitions_invalid_collection_name(self, collection_name):
|
||||
"""
|
||||
target: test release partitions with invalid collection name
|
||||
method: release partitions with invalid collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. release partitions
|
||||
error = {ct.err_code: 1100, ct.err_msg: f"Invalid collection name: {collection_name}. the first character of a "
|
||||
f"collection name must be an underscore or letter: invalid parameter"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_partitions_collection_name_over_max_length(self):
|
||||
"""
|
||||
target: test release partitions with collection name over max length 255
|
||||
method: release partitions with collection name over max length 255
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = "a".join("a" for i in range(256))
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. release partitions
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"Invalid collection name: {collection_name}. the length of a collection name "
|
||||
f"must be less than 255 characters: invalid parameter"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_partitions_collection_name_not_existed(self):
|
||||
"""
|
||||
target: test release partitions with nonexistent collection name
|
||||
method: release partitions with nonexistent collection name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str("collection_not_exist")
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
# 1. release partitions
|
||||
error = {ct.err_code: 999, ct.err_msg: f"collection not found[database=default]"
|
||||
f"[collection={collection_name}]"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("partition_name", ["12 s", "(mn)", "中文", "%$#"])
|
||||
async def test_async_milvus_client_release_partitions_invalid_partition_name(self, partition_name):
|
||||
"""
|
||||
target: test release partitions with invalid partition name
|
||||
method: release partitions with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. release partitions
|
||||
error = {ct.err_code: 65535, ct.err_msg: f"partition not found"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
async def test_async_milvus_client_release_partitions_invalid_partition_name_list(self):
|
||||
"""
|
||||
target: test release partitions with invalid partition name list
|
||||
method: release partitions with invalid partition name list
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. release partition
|
||||
partition_name = ["12-s"]
|
||||
error = {ct.err_code: 65535, ct.err_msg: f"partition not found"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_partitions_name_lists_empty(self):
|
||||
"""
|
||||
target: test release partitions with partition name list empty
|
||||
method: release partitions with partition name list empty
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_names = []
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. release partition
|
||||
error = {ct.err_code: 999, ct.err_msg: f"invalid parameter[expected=any partition][actual=empty partition list"}
|
||||
await async_client.release_partitions(collection_name, partition_names,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_partitions_name_lists_not_all_exists(self):
|
||||
"""
|
||||
target: test release partitions with partition name lists not all exists
|
||||
method: release partitions with partition name lists not all exists
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
not_exist_partition = cf.gen_unique_str("partition_not_exist")
|
||||
partition_names = ["_default", not_exist_partition]
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. release partitions
|
||||
error = {ct.err_code: 999, ct.err_msg: f"partition not found[partition={not_exist_partition}]"}
|
||||
await async_client.release_partitions(collection_name, partition_names,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
async def test_async_milvus_client_release_partitions_partition_name_not_existed(self):
|
||||
"""
|
||||
target: test release partitions with nonexistent partition name
|
||||
method: release partitions with nonexistent partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
partition_name = cf.gen_unique_str("partition_not_exist")
|
||||
# 1. create collection
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
# 2. release partitions
|
||||
error = {ct.err_code: 200, ct.err_msg: f"partition not found[partition={partition_name}]"}
|
||||
await async_client.release_partitions(collection_name, partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
partition_name = ""
|
||||
error = {ct.err_code: 200, ct.err_msg: f"partition not found[partition={partition_name}]"}
|
||||
# await async_client.release_partitions(collection_name, partition_name,
|
||||
# check_task=CheckTasks.err_res, check_items=error)
|
||||
# https://github.com/milvus-io/milvus/issues/38223
|
||||
# 3. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
|
||||
class TestAsyncMilvusClientPartitionValid(TestMilvusClientV2Base):
|
||||
""" Test case of partition interface """
|
||||
|
||||
def teardown_method(self, method):
|
||||
if self.async_milvus_client_wrap.async_milvus_client is not None:
|
||||
asyncio.run(self.async_milvus_client_wrap.close())
|
||||
super().teardown_method(method)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are valid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_milvus_client_create_drop_partition_default(self):
|
||||
"""
|
||||
target: test create and drop partition normal case
|
||||
method: 1. create collection, partition 2. insert to partition 3. search and query 4. drop partition, collection
|
||||
expected: run successfully
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
collections, _ = await async_client.list_collections()
|
||||
assert collection_name in collections
|
||||
desc, _ = await async_client.describe_collection(collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim,
|
||||
"consistency_level": 0})
|
||||
# 2. create partition
|
||||
partition_name = cf.gen_unique_str(partition_prefix)
|
||||
await async_client.create_partition(collection_name, partition_name)
|
||||
partitions, _ = await async_client.list_partitions(collection_name)
|
||||
assert partition_name in partitions
|
||||
# 3. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
await async_client.insert(collection_name, rows, partition_name=partition_name)
|
||||
tasks = []
|
||||
# 4. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
search_task = async_client.search(collection_name, vectors_to_search,
|
||||
partition_names=[partition_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(search_task)
|
||||
# 5. query
|
||||
query_task = async_client.query(collection_name, filter=default_search_exp,
|
||||
partition_names=[partition_name],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(query_task)
|
||||
res = await asyncio.gather(*tasks)
|
||||
|
||||
# 6. drop action
|
||||
has_partition, _ = await async_client.has_partition(collection_name, partition_name)
|
||||
if has_partition:
|
||||
await async_client.release_partitions(collection_name, partition_name)
|
||||
await async_client.drop_partition(collection_name, partition_name)
|
||||
partitions, _ = await async_client.list_partitions(collection_name)
|
||||
assert partition_name not in partitions
|
||||
await async_client.drop_collection(collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
async def test_async_milvus_client_load_release_partitions(self):
|
||||
"""
|
||||
target: test load and release partitions normal case
|
||||
method: 1. create collection, two partitions
|
||||
2. insert different data to two partitions
|
||||
3. search and query
|
||||
4. release partitions, search and query
|
||||
5. load partitions, search and query
|
||||
4. drop partition, collection
|
||||
expected: run successfully
|
||||
"""
|
||||
self.init_async_milvus_client()
|
||||
async_client = self.async_milvus_client_wrap
|
||||
|
||||
# 1. create collection
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
await async_client.create_collection(collection_name, default_dim)
|
||||
collections, _ = await async_client.list_collections()
|
||||
assert collection_name in collections
|
||||
desc, _ = await async_client.describe_collection(collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim,
|
||||
"consistency_level": 0})
|
||||
# 2. create partition
|
||||
partition_name_1 = cf.gen_unique_str(partition_prefix)
|
||||
await async_client.create_partition(collection_name, partition_name_1)
|
||||
partition_name_2 = cf.gen_unique_str(partition_prefix)
|
||||
await async_client.create_partition(collection_name, partition_name_2)
|
||||
partitions, _ = await async_client.list_partitions(collection_name)
|
||||
assert partition_name_1 in partitions
|
||||
assert partition_name_2 in partitions
|
||||
# 3. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows_default = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
await async_client.insert(collection_name, rows_default)
|
||||
rows_1 = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb, 2 * default_nb)]
|
||||
await async_client.insert(collection_name, rows_1, partition_name=partition_name_1)
|
||||
rows_2 = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(2 * default_nb, 3 * default_nb)]
|
||||
await async_client.insert(collection_name, rows_2, partition_name=partition_name_2)
|
||||
tasks = []
|
||||
# 4. search and query
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
# search single partition
|
||||
search_task = async_client.search(collection_name, vectors_to_search,
|
||||
partition_names=[partition_name_1],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(search_task)
|
||||
# search multi partition
|
||||
search_task_multi = async_client.search(collection_name, vectors_to_search,
|
||||
partition_names=[partition_name_1, partition_name_2],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(search_task_multi)
|
||||
# query single partition
|
||||
query_task = async_client.query(collection_name, filter=default_search_exp,
|
||||
partition_names=[partition_name_1],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows_1,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(query_task)
|
||||
# query multi partition
|
||||
query_task_multi = async_client.query(collection_name, filter=default_search_exp,
|
||||
partition_names=[partition_name_1, partition_name_2],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows_1 + rows_2,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks.append(query_task_multi)
|
||||
res = await asyncio.gather(*tasks)
|
||||
# 5. release partitions, search and query
|
||||
await async_client.release_partitions(collection_name, partition_name_1)
|
||||
error = {ct.err_code: 201, ct.err_msg: "partition not loaded"}
|
||||
await async_client.search(collection_name, vectors_to_search,
|
||||
partition_names=[partition_name_1],
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
await async_client.query(collection_name, filter=default_search_exp,
|
||||
partition_names=[partition_name_1],
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
await async_client.search(collection_name, vectors_to_search,
|
||||
partition_names=[partition_name_2],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
await async_client.query(collection_name, filter=default_search_exp,
|
||||
partition_names=[partition_name_2],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows_2,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
|
||||
# 6. load partitions, search and query
|
||||
tasks_after_load = []
|
||||
await async_client.load_partitions(collection_name, [partition_name_1, partition_name_2])
|
||||
search_task = async_client.search(collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks_after_load.append(search_task)
|
||||
query_task = async_client.query(collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"exp_res": rows_default + rows_1 + rows_2,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
tasks_after_load.append(query_task)
|
||||
res = await asyncio.gather(*tasks_after_load)
|
||||
|
||||
# 7. drop action
|
||||
await async_client.drop_collection(collection_name)
|
||||
@@ -0,0 +1,95 @@
|
||||
from pymilvus import DataType
|
||||
from common import common_type as ct
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class DISKANN:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
# search_list_size
|
||||
# Type: Integer Range: [1, int_max]
|
||||
# Default value: 100
|
||||
{"description": "Minimum Boundary Test", "params": {"search_list_size": 1}, "expected": success},
|
||||
{"description": "Large Value Test", "params": {"search_list_size": 10000}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"search_list_size": -1}, "expected": success},
|
||||
{"description": "String Type Test", "params": {"search_list_size": "100"}, "expected": success},
|
||||
{"description": "Float Type Test", "params": {"search_list_size": 100.0}, "expected": success},
|
||||
{"description": "Boolean Type Test", "params": {"search_list_size": True}, "expected": success},
|
||||
{"description": "None Type Test", "params": {"search_list_size": None}, "expected": success},
|
||||
# search_cache_budget_gb_ratio
|
||||
# Type: Float Range: [0.0, 0.3)
|
||||
# Default value: 0.10
|
||||
# TODO: runt he minium bourndary test after issue #43176 fixed
|
||||
# {"description": "Minimum Boundary Test", "params": {"search_cache_budget_gb_ratio": 0.0}, "expected": success},
|
||||
{"description": "Maximum Boundary Test", "params": {"search_cache_budget_gb_ratio": 0.3}, "expected": success},
|
||||
{"description": "Default value Test", "params": {"search_cache_budget_gb_ratio": 0.1}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"search_cache_budget_gb_ratio": -0.1}, "expected": success},
|
||||
{"description": "Out of Range Test - Too Large", "params": {"search_cache_budget_gb_ratio": 0.31}, "expected": success},
|
||||
{"description": "String Type Test", "params": {"search_cache_budget_gb_ratio": "0.2"}, "expected": success},
|
||||
{"description": "Boolean Type Test", "params": {"search_cache_budget_gb_ratio": True}, "expected": success},
|
||||
{"description": "None Type Test", "params": {"search_cache_budget_gb_ratio": None}, "expected": success},
|
||||
# pq_code_budget_gb_ratio
|
||||
# Type: Float Range: (0.0, 0.25]
|
||||
# Default value: 0.125
|
||||
{"description": "Minimum Boundary Test", "params": {"pq_code_budget_gb_ratio": 0.0001}, "expected": success},
|
||||
{"description": "Maximum Boundary Test", "params": {"pq_code_budget_gb_ratio": 0.25}, "expected": success},
|
||||
{"description": "Default value Test", "params": {"pq_code_budget_gb_ratio": 0.125}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"pq_code_budget_gb_ratio": -0.1}, "expected": success},
|
||||
{"description": "Out of Range Test - Too Large", "params": {"pq_code_budget_gb_ratio": 0.26}, "expected": success},
|
||||
{"description": "String Type Test", "params": {"pq_code_budget_gb_ratio": "0.1"}, "expected": success},
|
||||
{"description": "Boolean Type Test", "params": {"pq_code_budget_gb_ratio": True}, "expected": success},
|
||||
{"description": "None Type Test", "params": {"pq_code_budget_gb_ratio": None}, "expected": success},
|
||||
# max_degree
|
||||
# Type: Integer Range: [1, 512]
|
||||
# Default value: 56
|
||||
{"description": "Minimum Boundary Test", "params": {"max_degree": 1}, "expected": success},
|
||||
{"description": "Maximum Boundary Test", "params": {"max_degree": 512}, "expected": success},
|
||||
{"description": "Default value Test", "params": {"max_degree": 56}, "expected": success},
|
||||
{"description": "Large Value Test", "params": {"max_degree": 128}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"max_degree": -1}, "expected": success},
|
||||
{"description": "String Type Test", "params": {"max_degree": "32"}, "expected": success},
|
||||
{"description": "Float Type Test", "params": {"max_degree": 32.0}, "expected": success},
|
||||
{"description": "Boolean Type Test", "params": {"max_degree": True}, "expected": success},
|
||||
{"description": "None Type Test", "params": {"max_degree": None}, "expected": success},
|
||||
# 组合参数
|
||||
{"description": "Optimal Performance Combination Test", "params": {"search_list_size": 100, "beamwidth": 10, "search_cache_budget_gb_ratio": 0.5, "pq_code_budget_gb_ratio": 0.5}, "expected": success},
|
||||
{"description": "empty dict params", "params": {}, "expected": success},
|
||||
{"description": "not_defined_param in the dict params", "params": {"search_list_size": 100, "not_defined_param": "nothing"}, "expected": success},
|
||||
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# beam_width_ratio
|
||||
# Type: Float Range: [1, max(128 / CPU number, 16)]
|
||||
# Default value: 4.0
|
||||
{"description": "Minimum Boundary Test", "params": {"beam_width_ratio": 1.0}, "expected": success},
|
||||
{"description": "Maximum Boundary Test", "params": {"beam_width_ratio": 16.0}, "expected": success},
|
||||
{"description": "Default value Test", "params": {"beam_width_ratio": 4.0}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"beam_width_ratio": -0.1}, "expected": success},
|
||||
{"description": "Out of Range Test - Too Large", "params": {"beam_width_ratio": 17.0}, "expected": success},
|
||||
{"description": "String Type Test", "params": {"beam_width_ratio": "2.0"}, "expected": success},
|
||||
{"description": "Boolean Type Test", "params": {"beam_width_ratio": True}, "expected": success},
|
||||
{"description": "None Type Test", "params": {"beam_width_ratio": None}, "expected": success},
|
||||
# search_list_size
|
||||
# Type: Integer Range: [1, int_max]
|
||||
# Default value: 100
|
||||
{"description": "Minimum Boundary Test", "params": {"search_list_size": 1}, "expected": {"err_code": 999, "err_msg": "search_list_size(1) should be larger than k(10)"}},
|
||||
{"description": "Large Value Test", "params": {"search_list_size": 1000}, "expected": success},
|
||||
{"description": "Default value Test", "params": {"search_list_size": 100}, "expected": success},
|
||||
{"description": "Out of Range Test - Negative", "params": {"search_list_size": -1}, "expected": {"err_code": 999, "err_msg": "param 'search_list_size' (-1) should be in range [1, 2147483647]"}},
|
||||
{"description": "String Type Test", "params": {"search_list_size": "100"}, "expected": success},
|
||||
{"description": "Float Type Test", "params": {"search_list_size": 100.0}, "expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'search_list_size' (100.0) should be integer"}},
|
||||
{"description": "Boolean Type Test", "params": {"search_list_size": True}, "expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'search_list_size' (true) should be integer"}},
|
||||
{"description": "None Type Test", "params": {"search_list_size": None}, "expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'search_list_size' (null) should be integer"}},
|
||||
# mix params
|
||||
{"description": "mix params", "params": {"search_list_size": 100, "beam_width_ratio": 0.5}, "expected": success},
|
||||
{"description": "mix params", "params": {}, "expected": success},
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
from pymilvus import DataType
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class FAISS:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.BINARY_VECTOR,
|
||||
]
|
||||
|
||||
supported_metrics = ["L2", "IP", "COSINE"]
|
||||
|
||||
build_params = [
|
||||
{
|
||||
"description": "Flat float index",
|
||||
"params": {"faiss_index_name": "Flat"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "IVF Flat float index",
|
||||
"params": {"faiss_index_name": "IVF64,Flat"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "HNSW Flat float index",
|
||||
"params": {"faiss_index_name": "HNSW16,Flat"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "OPQ IVF PQ float index",
|
||||
"params": {"faiss_index_name": "OPQ16,IVF64,PQ16x4"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "IVF PQ RFlat float index",
|
||||
"params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "PQ float index",
|
||||
"params": {"faiss_index_name": "PQ8x4"},
|
||||
"searchable": False,
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "Binary flat index",
|
||||
"params": {"faiss_index_name": "BFlat"},
|
||||
"vector_data_type": DataType.BINARY_VECTOR,
|
||||
"metric_type": "HAMMING",
|
||||
"expected": success,
|
||||
},
|
||||
]
|
||||
|
||||
search_params = [
|
||||
{
|
||||
"description": "IVF Flat nprobe",
|
||||
"build_params": {"faiss_index_name": "IVF64,Flat"},
|
||||
"search_params": {"nprobe": 8},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "IVF Flat stringified nprobe",
|
||||
"build_params": {"faiss_index_name": "IVF64,Flat"},
|
||||
"search_params": {"nprobe": "8"},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "IVF Flat invalid nprobe string",
|
||||
"build_params": {"faiss_index_name": "IVF64,Flat"},
|
||||
"search_params": {"nprobe": "invalid"},
|
||||
"expected": {"err_code": 999, "err_msg": "expects a number"},
|
||||
},
|
||||
{
|
||||
"description": "HNSW Flat efSearch",
|
||||
"build_params": {"faiss_index_name": "HNSW16,Flat"},
|
||||
"search_params": {"efSearch": 64},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "HNSW Flat invalid efSearch string",
|
||||
"build_params": {"faiss_index_name": "HNSW16,Flat"},
|
||||
"search_params": {"efSearch": "invalid"},
|
||||
"expected": {"err_code": 999, "err_msg": "expects a number"},
|
||||
},
|
||||
{
|
||||
"description": "IVF PQ RFlat rerank",
|
||||
"build_params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
|
||||
"search_params": {"nprobe": 8, "k_factor": 4},
|
||||
"expected": success,
|
||||
},
|
||||
{
|
||||
"description": "IVF PQ RFlat invalid k_factor string",
|
||||
"build_params": {"faiss_index_name": "IVF64,PQ8x4,RFlat"},
|
||||
"search_params": {"nprobe": 8, "k_factor": "invalid"},
|
||||
"expected": {"err_code": 999, "err_msg": "expects a number"},
|
||||
},
|
||||
]
|
||||
|
||||
metric_factories = [
|
||||
{"faiss_index_name": "IVF64,Flat"},
|
||||
{"faiss_index_name": "HNSW16,Flat"},
|
||||
]
|
||||
@@ -0,0 +1,177 @@
|
||||
from pymilvus import DataType
|
||||
from common import common_type as ct
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class HNSW:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR,
|
||||
DataType.INT8_VECTOR,
|
||||
DataType.BINARY_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
# M params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"M": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"M": 2048},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"M": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'M' (-1) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Too Large",
|
||||
"params": {"M": 2049},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'M' (2049) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"M": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"M": 16.0},
|
||||
"expected": {"err_code": 999, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"M": True},
|
||||
"expected": {"err_code": 999, "err_msg": "invalid integer value, key: 'M', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"M": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"M": [16]},
|
||||
"expected": {"err_code": 999, "err_msg": "invalid integer value, key: 'M', value: '[16]': invalid parameter"}
|
||||
},
|
||||
# efConstruction params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"efConstruction": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"efConstruction": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"efConstruction": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'efConstruction' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"efConstruction": "100"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"efConstruction": 100.0},
|
||||
"expected": {"err_code": 999, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"efConstruction": True},
|
||||
"expected": {"err_code": 999, "err_msg": "invalid integer value, key: 'efConstruction', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"efConstruction": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"efConstruction": [100]},
|
||||
"expected": {"err_code": 999, "err_msg": "invalid integer value, key: 'efConstruction', value: '[100]': invalid parameter"}
|
||||
},
|
||||
# combination params test
|
||||
{
|
||||
"description": "Optimal Performance Combination Test",
|
||||
"params": {"M": 16, "efConstruction": 200},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "not_defined_param in the dict params",
|
||||
"params": {"M": 16, "efConstruction": 200, "not_defined_param": "nothing"},
|
||||
"expected": success
|
||||
},
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# ef params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"ef": 1},
|
||||
"expected": {"err_code": 999, "err_msg": "ef(1) should be larger than k(10)"} # assume default limit=10
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"ef": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"ef": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'ef' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"ef": "32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"ef": 32.0},
|
||||
"expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'ef' (32.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"ef": True},
|
||||
"expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'ef' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"ef": None},
|
||||
"expected": {"err_code": 999, "err_msg": "Type conflict in json: param 'ef' (null) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"ef": [32]},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'ef' ([32]) should be integer"}
|
||||
},
|
||||
# combination params test
|
||||
{
|
||||
"description": "Optimal Performance Combination Test",
|
||||
"params": {"ef": 64},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,529 @@
|
||||
import pytest
|
||||
from pymilvus import DataType
|
||||
from common import common_type as ct
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class HNSW_PQ:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR,
|
||||
DataType.INT8_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
|
||||
# M params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"M": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"M": 2048},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"M": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (-1) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Too Large",
|
||||
"params": {"M": 2049},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (2049) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"M": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"M": 16.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"M": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"M": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"M": [16]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: '[16]': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Nested dict in params",
|
||||
"params": {"M": {"value": 16}},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value"}
|
||||
},
|
||||
# efConstruction params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"efConstruction": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"efConstruction": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"efConstruction": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'efConstruction' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"efConstruction": "100"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"efConstruction": 100.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"efConstruction": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"efConstruction": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"efConstruction": [100]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: '[100]': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Nested List in Params",
|
||||
"params": {"efConstruction": [[100]]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value"}
|
||||
},
|
||||
# m params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"m": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Half of Dimension Value Test",
|
||||
"params": {"m": 64},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test (Dimension)",
|
||||
"params": {"m": 128},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"m": -1},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'm' (-1) should be in range [1, 65536]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Larger Value Test",
|
||||
"params": {"m": 256},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "The dimension of the vector (dim) should be a multiple of the number of subquantizers (m)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Not Divisible by Dimension Value Test",
|
||||
"params": {"m": 7},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "The dimension of the vector (dim) should be a multiple of the number of subquantizers (m)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"m": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"m": 16.0},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "wrong data type in json, key: 'm', value: '16.0': invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"m": True},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"m": [16]},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"m": None},
|
||||
"expected": success
|
||||
},
|
||||
# nbits params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"nbits": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test (doc:24) ",
|
||||
"params": {"nbits": 10},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Default Value Test",
|
||||
"params": {"nbits": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"nbits": -1},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nbits' (-1) should be in range [1, 24]: invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"nbits": 25},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nbits' (25) should be in range [1, 24]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"nbits": "8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"nbits": 8.0},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "wrong data type in json, key: 'nbits', value: '8.0'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"nbits": True},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"nbits": [8]},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"nbits": None},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
|
||||
# refine params test
|
||||
{
|
||||
"description": "refine = True",
|
||||
"params": {"refine": True},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"refine": "true"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid String Type Test",
|
||||
"params": {"refine": "test"},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine": [True]},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine": None},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
# refine_type params test
|
||||
{
|
||||
"description": "Valid refine_type - SQ6",
|
||||
"params": {"refine_type": "SQ6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - SQ8",
|
||||
"params": {"refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - BF16",
|
||||
"params": {"refine_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP16",
|
||||
"params": {"refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP32",
|
||||
"params": {"refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - unknown value",
|
||||
"params": {"refine_type": "INT8"},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : INT8, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine_type": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine_type": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1.0, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_type": ["FP16"]},
|
||||
"expected": {"err_code": 1100, "err_msg": "['FP16'], optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_type lower precision than sq_type but refine disabled",
|
||||
"params": {"sq_type": "FP16", "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_type lower than sq_type",
|
||||
"params": {"sq_type": "FP16", "refine_type": "SQ8", "refine": True},
|
||||
"expected": success
|
||||
},
|
||||
# combination params test
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "All optional parameters None",
|
||||
"params": {"M": None, "efConstruction": None, "m": None, "nbits":None, "refine": None, "refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Typical valid combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 64, "nbits": 8,"refine": True, "refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Refine Disabled",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 32, "nbits": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum Boundary Combination",
|
||||
"params": {"M": 2, "efConstruction": 1, "m": 1, "nbits": 1, "refine": True, "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
pytest.param(
|
||||
{
|
||||
"description": "Maximum Boundary Combination",
|
||||
"params": {"M": 2048, "efConstruction": 10000, "m": 128, "nbits": 10, "refine": True, "refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
marks=pytest.mark.skip(reason="Flaky in CI: index build with max params (M=2048, efConstruction=10000) "
|
||||
"takes ~30s but exceeds 120s client timeout under CI resource contention")
|
||||
),
|
||||
{
|
||||
"description": "Unknown extra parameter in combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 32, "nbits": 8, "refine": True, "refine_type": "FP16", "unknown_param": "nothing"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (M + m only)",
|
||||
"params": {"M": 32, "m": 32},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (efConstruction + nbits only)",
|
||||
"params": {"efConstruction": 500,"nbits": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid PQ m (not divisor of dimension)",
|
||||
"params": {"M": 16,"efConstruction": 200,"m": 7, "nbits": 8, "refine": True, "refine_type": "FP32"},
|
||||
"expected": {"err_code": 999, "err_msg": "The dimension of the vector (dim) should be a multiple of the number of subquantizers (m)."}
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# ef params test
|
||||
{
|
||||
"description": "Boundary Test - ef equals k",
|
||||
"params": {"ef": 10},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"ef": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"ef": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"ef": -1},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"ef": "32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"ef": 32.0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (32.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"ef": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"ef": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (null) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"ef": [32]},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' ([32]) should be integer"}
|
||||
},
|
||||
|
||||
# refine_k params test
|
||||
{
|
||||
"description": "refine_k default boundary",
|
||||
"params": {"refine_k": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k valid float",
|
||||
"params": {"refine_k": 2.5},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k out of range",
|
||||
"params": {"refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k integer type",
|
||||
"params": {"refine_k": 20},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"refine_k": "2.5"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty string type",
|
||||
"params": {"refine_k": ""},
|
||||
"expected": {"err_code": 65535, "err_msg": "invalid float value"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k boolean type",
|
||||
"params": {"refine_k": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'refine_k' (true) should be a number"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"refine_k": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_k": [15]},
|
||||
"expected": {"err_code": 65535, "err_msg":"Type conflict in json"}
|
||||
},
|
||||
|
||||
# combination params test
|
||||
{
|
||||
"description": "HNSW ef + SQ refine_k combination",
|
||||
"params": {"ef": 64, "refine_k": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid ef with invalid refine_k",
|
||||
"params": {"ef": 64, "refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg":"Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
]
|
||||
@@ -0,0 +1,591 @@
|
||||
from pymilvus import DataType
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class HNSW_PRQ:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR,
|
||||
DataType.INT8_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
|
||||
# M params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"M": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"M": 2048},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"M": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (-1) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Too Large",
|
||||
"params": {"M": 2049},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (2049) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"M": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"M": 16.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"M": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"M": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"M": [16]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: '[16]': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Nested dict in params",
|
||||
"params": {"M": {"value": 16}},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value"}
|
||||
},
|
||||
# efConstruction params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"efConstruction": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"efConstruction": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"efConstruction": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'efConstruction' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"efConstruction": "100"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"efConstruction": 100.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"efConstruction": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"efConstruction": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"efConstruction": [100]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: '[100]': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Nested List in Params",
|
||||
"params": {"efConstruction": [[100]]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value"}
|
||||
},
|
||||
# m params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"m": 1},
|
||||
"expected": success
|
||||
},
|
||||
# timeout
|
||||
# {
|
||||
# "description": "Half of Dimension Value Test",
|
||||
# "params": {"m": 64},
|
||||
# "expected": success
|
||||
# },
|
||||
# timeout
|
||||
# {
|
||||
# "description": "Maximum Boundary Test (Dimension)",
|
||||
# "params": {"m": 128},
|
||||
# "expected": success
|
||||
# },
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"m": -1},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'm' (-1) should be in range [1, 65536]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Larger Value Test",
|
||||
"params": {"m": 256},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "The dimension of a vector (dim) should be a multiple of the number of subquantizers (m)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Not Divisible by Dimension Value Test",
|
||||
"params": {"m": 7},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "The dimension of a vector (dim) should be a multiple of the number of subquantizers (m)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"m": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"m": 16.0},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "wrong data type in json, key: 'm', value: '16.0': invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"m": True},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"m": [16]},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"m": None},
|
||||
"expected": success
|
||||
},
|
||||
# nbits params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"nbits": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Default Value Test",
|
||||
"params": {"nbits": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"nbits": 10},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"nbits": -1},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nbits' (-1) should be in range [1, 24]: invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"nbits": 25},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nbits' (25) should be in range [1, 24]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"nbits": "8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"nbits": 8.0},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "wrong data type in json, key: 'nbits', value: '8.0'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"nbits": True},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"nbits": [8]},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"nbits": None},
|
||||
"expected": success
|
||||
},
|
||||
# nrq params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"nrq": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Default Value Test",
|
||||
"params": {"nrq": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"nrq": 16},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"nrq": -1},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nrq' (-1) should be in range [1, 16]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Larger Value Test",
|
||||
"params": {"nrq": 17},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "Out of range in json: param 'nrq' (17) should be in range [1, 16]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"nrq": "4"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"nrq": 4.0},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "wrong data type in json, key: 'nrq', value: '4.0': invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"nrq": True},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value, key: 'nrq', value: 'True': invalid parameter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"nrq": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"nrq": [2]},
|
||||
"expected": {
|
||||
"err_code": 1100,
|
||||
"err_msg": "invalid integer value, key: 'nrq', value: '[2]': invalid parameter"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
# refine params test
|
||||
{
|
||||
"description": "refine = True",
|
||||
"params": {"refine": True},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"refine": "true"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid String Type Test",
|
||||
"params": {"refine": "test"},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine": [True]},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine": None},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
# refine_type params test
|
||||
{
|
||||
"description": "Valid refine_type - SQ6",
|
||||
"params": {"refine_type": "SQ6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - SQ8",
|
||||
"params": {"refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - BF16",
|
||||
"params": {"refine_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP16",
|
||||
"params": {"refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP32",
|
||||
"params": {"refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - unknown value",
|
||||
"params": {"refine_type": "INT8"},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : INT8, optional types are [sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine_type": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1, optional types are [sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine_type": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1.0, optional types are [sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_type": ["FP16"]},
|
||||
"expected": {"err_code": 1100, "err_msg": "['FP16'], optional types are [sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_type lower precision than sq_type but refine disabled",
|
||||
"params": {"sq_type": "FP16", "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_type lower than sq_type",
|
||||
"params": {"sq_type": "FP16", "refine_type": "SQ8", "refine": True},
|
||||
"expected": success
|
||||
},
|
||||
# combination params test
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "All optional parameters None",
|
||||
"params": {"M": None, "efConstruction": None, "m": None, "nbits":None, "nrq":None,"refine": None, "refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Typical valid combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 32, "nbits": 8, "nrq":1,"refine": True, "refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Refine Disabled",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 32, "nbits": 8,"nrq": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum Boundary Combination",
|
||||
"params": {"M": 2, "efConstruction": 1, "m": 1, "nbits": 1, "nrq":1, "refine": True, "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Combination",
|
||||
"params": {"M": 2048, "efConstruction": 10000, "m": 128, "nbits": 8, "nrq":1, "refine": True, "refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Unknown extra parameter in combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "m": 32, "nbits": 8, "nrq":1, "refine": True, "refine_type": "FP16", "unknown_param": "nothing"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (M + m only)",
|
||||
"params": {"M": 32, "m": 32},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (efConstruction + nbits only)",
|
||||
"params": {"efConstruction": 500,"nbits": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid m (not divisor of dimension)",
|
||||
"params": {"M": 16,"efConstruction": 200,"m": 7, "nbits": 8, "refine": True, "refine_type": "FP32"},
|
||||
"expected": {"err_code": 1100, "err_msg": "The dimension of a vector (dim) should be a multiple of the number of subquantizers (m)."}
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# ef params test
|
||||
{
|
||||
"description": "Boundary Test - ef equals k",
|
||||
"params": {"ef": 10},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"ef": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"ef": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"ef": -1},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"ef": "32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"ef": 32.0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (32.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"ef": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"ef": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (null) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"ef": [32]},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' ([32]) should be integer"}
|
||||
},
|
||||
|
||||
# refine_k params test
|
||||
{
|
||||
"description": "refine_k default boundary",
|
||||
"params": {"refine_k": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k valid float",
|
||||
"params": {"refine_k": 2.5},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k out of range",
|
||||
"params": {"refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k integer type",
|
||||
"params": {"refine_k": 20},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"refine_k": "2.5"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty string type",
|
||||
"params": {"refine_k": ""},
|
||||
"expected": {"err_code": 65535, "err_msg": "invalid float value"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k boolean type",
|
||||
"params": {"refine_k": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'refine_k' (true) should be a number"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"refine_k": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_k": [15]},
|
||||
"expected": {"err_code": 65535, "err_msg":"Type conflict in json"}
|
||||
},
|
||||
|
||||
# combination params test
|
||||
{
|
||||
"description": "HNSW ef + SQ refine_k combination",
|
||||
"params": {"ef": 64, "refine_k": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid ef with invalid refine_k",
|
||||
"params": {"ef": 64, "refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg":"Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
]
|
||||
@@ -0,0 +1,416 @@
|
||||
from pymilvus import DataType
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class HNSW_SQ:
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR,
|
||||
DataType.INT8_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
# M params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"M": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"M": 2048},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"M": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (-1) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Too Large",
|
||||
"params": {"M": 2049},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'M' (2049) should be in range [2, 2048]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"M": "16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"M": 16.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"M": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"M": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"M": [16]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'M', value: '[16]': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Nested dict in params",
|
||||
"params": {"M": {"value": 16}},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value"}
|
||||
},
|
||||
# efConstruction params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"efConstruction": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"efConstruction": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"efConstruction": -1},
|
||||
"expected": {"err_code": 1100, "err_msg": "param 'efConstruction' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"efConstruction": "100"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"efConstruction": 100.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"efConstruction": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"efConstruction": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"efConstruction": [100]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid integer value, key: 'efConstruction', value: '[100]': invalid parameter"}
|
||||
},
|
||||
|
||||
# sq_type params test
|
||||
{
|
||||
"description": "Valid sq_type - SQ6",
|
||||
"params": {"sq_type": "SQ6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid sq_type - SQ8",
|
||||
"params": {"sq_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid sq_type - BF16",
|
||||
"params": {"sq_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid sq_type - FP16",
|
||||
"params": {"sq_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Unknown String",
|
||||
"params": {"sq_type": "FP32"},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid scalar quantizer type: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"sq_type": 8},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid scalar quantizer type: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"sq_type": 8.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid scalar quantizer type: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"sq_type": True},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid scalar quantizer type: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"sq_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"sq_type": ["SQ8"]},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid scalar quantizer type: invalid parameter"}
|
||||
},
|
||||
|
||||
# refine params test
|
||||
{
|
||||
"description": "refine = True",
|
||||
"params": {"refine": True},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"refine": "true"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"refine": "test"},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine": [True]},
|
||||
"expected": {"err_code": 1100, "err_msg": "should be a boolean: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine": None},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
# refine_type params test
|
||||
{
|
||||
"description": "Valid refine_type - SQ6",
|
||||
"params": {"refine_type": "SQ6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - SQ8",
|
||||
"params": {"refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - BF16",
|
||||
"params": {"refine_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP16",
|
||||
"params": {"refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid refine_type - FP32",
|
||||
"params": {"refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - unknown value",
|
||||
"params": {"refine_type": "INT8"},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : INT8, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine_type": 1},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"refine_type": 1.0},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type : 1.0, optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_type": ["FP16"]},
|
||||
"expected": {"err_code": 1100, "err_msg": "['FP16'], optional types are [sq4u, sq6, sq8, fp16, bf16, fp32, flat]: invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
# combination params test
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "All optional parameters None",
|
||||
"params": {"M": None, "efConstruction": None, "sq_type": None, "refine": None, "refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Typical valid combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "sq_type": "SQ8", "refine": True, "refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum boundary combination",
|
||||
"params": {"M": 2, "efConstruction": 1, "sq_type": "SQ6"},
|
||||
"expected": success,
|
||||
# M=2 + efConstruction=1 produces a poorly connected graph; HNSW does not
|
||||
# guarantee returning topK results under these extreme parameters.
|
||||
"relaxed_limit": True
|
||||
},
|
||||
{
|
||||
"description": "Maximum boundary combination",
|
||||
"params": {"M": 2048, "efConstruction": 10000, "sq_type": "FP16", "refine": True, "refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Unknown extra parameter in combination",
|
||||
"params": {"M": 16, "efConstruction": 200, "sq_type": "SQ8", "refine": True, "refine_type": "FP16", "unknown_param": "nothing"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (M + sq_type only)",
|
||||
"params": {"M": 32, "sq_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Partial parameters set (efConstruction + refine only)",
|
||||
"params": {"efConstruction": 500,"refine": True},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid refine_type using vector data type",
|
||||
"params": {"sq_type": "SQ8", "refine": True, "refine_type": "INT8"},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid refine type"}
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# ef params test
|
||||
{
|
||||
"description": "Boundary Test - ef equals k",
|
||||
"params": {"ef": 10},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"ef": 1},
|
||||
"expected": {"err_code": 65535, "err_msg": "ef(1) should be larger than k(10)"} # assume default limit=10
|
||||
},
|
||||
{
|
||||
"description": "Large Value Test",
|
||||
"params": {"ef": 10000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"ef": -1},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' (-1) should be in range [1, 2147483647]"}
|
||||
},
|
||||
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"ef": "32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"ef": 32.0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (32.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"ef": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"ef": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'ef' (null) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"ef": [32]},
|
||||
"expected": {"err_code": 65535, "err_msg": "param 'ef' ([32]) should be integer"}
|
||||
},
|
||||
|
||||
# refine_k params test
|
||||
{
|
||||
"description": "refine_k default boundary",
|
||||
"params": {"refine_k": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k valid float",
|
||||
"params": {"refine_k": 2.5},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "refine_k out of range",
|
||||
"params": {"refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg": "Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k integer type",
|
||||
"params": {"refine_k": 20},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"refine_k": "2.5"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty string type",
|
||||
"params": {"refine_k": ""},
|
||||
"expected": {"err_code": 65535, "err_msg": "invalid float value"}
|
||||
},
|
||||
{
|
||||
"description": "refine_k boolean type",
|
||||
"params": {"refine_k": True},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json: param 'refine_k' (true) should be a number"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"refine_k": None},
|
||||
"expected": {"err_code": 65535, "err_msg": "Type conflict in json"}
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"refine_k": [15]},
|
||||
"expected": {"err_code": 65535, "err_msg":"Type conflict in json"}
|
||||
},
|
||||
|
||||
# combination params test
|
||||
{
|
||||
"description": "HNSW ef + SQ refine_k combination",
|
||||
"params": {"ef": 64, "refine_k": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Valid ef with invalid refine_k",
|
||||
"params": {"ef": 64, "refine_k": 0},
|
||||
"expected": {"err_code": 65535, "err_msg":"Out of range in json"}
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
]
|
||||
@@ -0,0 +1,355 @@
|
||||
from pymilvus import DataType
|
||||
from common import common_type as ct
|
||||
|
||||
success = "success"
|
||||
|
||||
|
||||
class IVF_RABITQ:
|
||||
|
||||
supported_vector_types = [
|
||||
DataType.FLOAT_VECTOR,
|
||||
DataType.FLOAT16_VECTOR,
|
||||
DataType.BFLOAT16_VECTOR
|
||||
]
|
||||
|
||||
supported_metrics = ['L2', 'IP', 'COSINE']
|
||||
|
||||
build_params = [
|
||||
# nlist params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"nlist": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Boundary Test",
|
||||
"params": {"nlist": 65536},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Negative",
|
||||
"params": {"nlist": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'nlist' (-1) should be in range [1, 65536]"}
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test - Too Large",
|
||||
"params": {"nlist": 65537},
|
||||
"expected": {"err_code": 999, "err_msg": "param 'nlist' (65537) should be in range [1, 65536]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test will ignore the wrong type",
|
||||
"params": {"nlist": "128"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"nlist": 128.0},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "wrong data type in json"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"nlist": True},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid integer value, key: 'nlist', value: 'True': invalid parameter"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test, use default value",
|
||||
"params": {"nlist": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "List Type Test",
|
||||
"params": {"nlist": [128]},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid integer value, key: 'nlist', value: '[128]': invalid parameter"}
|
||||
},
|
||||
|
||||
# refine params test
|
||||
{
|
||||
"description": "Enable Refine Test",
|
||||
"params": {"refine": 'true'},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Disable Refine Test",
|
||||
"params": {"refine": 'false'},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
# refine_type test
|
||||
{
|
||||
"description": "Refine Type Test",
|
||||
"params": {"refine_type": "PQ"},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid refine type : PQ, optional types are [sq6, sq8, fp16, bf16, fp32, flat]"}
|
||||
},
|
||||
{
|
||||
"description": "SQ6 Test",
|
||||
"params": {"refine": 'true', "refine_type": "SQ6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "SQ8 Test",
|
||||
"params": {"refine": 'TRUE', "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "FP16 Test",
|
||||
"params": {"refine": True, "refine_type": "FP16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "BF16 Test",
|
||||
"params": {"refine": 'True', "refine_type": "BF16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "FP32 Test",
|
||||
"params": {"refine": True, "refine_type": "FP32"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid Refine Type Test",
|
||||
"params": {"refine": 'true', "refine_type": "INVALID"},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid refine type : INVALID, optional types are [sq6, sq8, fp16, bf16, fp32, flat]"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine": 1},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'refine' (\"1\") should be a boolean"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test will success with default value",
|
||||
"params": {"refine": None},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Lowercase String Test",
|
||||
"params": {"refine": True, "refine_type": "sq6"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Mixed Case String Test",
|
||||
"params": {"refine": True, "refine_type": "Sq8.0"},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid refine type : Sq8.0, optional types are [sq6, sq8, fp16, bf16, fp32, flat]"}
|
||||
},
|
||||
{
|
||||
"description": "Whitespace String Test",
|
||||
"params": {"refine_type": " SQ8 "},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid refine type : SQ8 , optional types are [sq6, sq8, fp16, bf16, fp32, flat]"}
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine": True, "refine_type": 8},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "invalid refine type : 8, optional types are [sq6, sq8, fp16, bf16, fp32, flat]"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"refine": True, "refine_type": None},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
# combination params test
|
||||
{
|
||||
"description": "Optimal Performance Combination Test",
|
||||
"params": {"nlist": 128, "refine": 'true', "refine_type": "SQ8"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "not refine with refine_type",
|
||||
"params": {"nlist": 127, "refine": 'false', "refine_type": "fp16"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "not_defined_param in the dict params",
|
||||
"params": {"nlist": 127, "refine": 'true', "not_defined_param": "nothing"},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
search_params = [
|
||||
# nprobe params test
|
||||
{
|
||||
"description": "Minimum Boundary Test",
|
||||
"params": {"nprobe": 1},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Equal to nlist Test",
|
||||
"params": {"nprobe": 128}, # Assuming nlist=128
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Exceed nlist Test",
|
||||
"params": {"nprobe": 129}, # Assuming nlist=128
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Exceed nprobe Test",
|
||||
"params": {"nprobe": 65537},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "should be in range [1, 65536]"}
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"nprobe": -1},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Out of range in json: param 'nprobe' (-1) should be in range [1, 65536]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test, not check data type",
|
||||
"params": {"nprobe": "32"},
|
||||
"expected": success # to be fixed: #41767
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"nprobe": 32.0},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'nprobe' (32.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"nprobe": True},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'nprobe' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"nprobe": None},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'nprobe' (null) should be integer"}
|
||||
},
|
||||
|
||||
# rbq_bits_query test
|
||||
{
|
||||
"description": "Default Value Test",
|
||||
"params": {"rbq_bits_query": 0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Maximum Value Test",
|
||||
"params": {"rbq_bits_query": 8},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Recommended Value Test - 6bit",
|
||||
"params": {"rbq_bits_query": 6},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Out of Range Test",
|
||||
"params": {"rbq_bits_query": 9},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Out of range in json: param 'rbq_bits_query' (9) should be in range [0, 8]"}
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"rbq_bits_query": -1},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Out of range in json: param 'rbq_bits_query' (-1) should be in range [0, 8]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"rbq_bits_query": "6"},
|
||||
"expected": success # to be fixed: #41767
|
||||
},
|
||||
{
|
||||
"description": "Float Type Test",
|
||||
"params": {"rbq_bits_query": 6.0},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'rbq_bits_query' (6.0) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"rbq_bits_query": True},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'rbq_bits_query' (true) should be integer"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"rbq_bits_query": None},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'rbq_bits_query' (null) should be integer"}
|
||||
},
|
||||
|
||||
# refine_k test
|
||||
{
|
||||
"description": "Default Value Test",
|
||||
"params": {"refine_k": 1.0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Recommended Value Test - 2",
|
||||
"params": {"refine_k": 2.0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Recommended Value Test - 5",
|
||||
"params": {"refine_k": 5.0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Less Than One Test",
|
||||
"params": {"refine_k": 0.5},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Out of range in json: param 'refine_k' (0.5) should be in range [1.000000, 340282346638528859811704183484516925440.000000]"}
|
||||
},
|
||||
{
|
||||
"description": "Negative Value Test",
|
||||
"params": {"refine_k": -1.0},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Out of range in json: param 'refine_k' (-1.0) should be in range [1.000000, 340282346638528859811704183484516925440.000000]"}
|
||||
},
|
||||
{
|
||||
"description": "String Type Test",
|
||||
"params": {"refine_k": "2.0"},
|
||||
"expected": success # to be fixed: #41767
|
||||
},
|
||||
{
|
||||
"description": "Integer Type Test",
|
||||
"params": {"refine_k": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Boolean Type Test",
|
||||
"params": {"refine_k": True},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'refine_k' (true) should be a number"}
|
||||
},
|
||||
{
|
||||
"description": "None Type Test",
|
||||
"params": {"refine_k": None},
|
||||
"expected": {"err_code": 999,
|
||||
"err_msg": "Type conflict in json: param 'refine_k' (null) should be a number"}
|
||||
},
|
||||
|
||||
# combination params test
|
||||
{
|
||||
"description": "Optimal Performance Combination Test",
|
||||
"params": { "nprobe": 32, "rbq_bits_query": 6, "refine_k": 2.0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Highest Recall Combination Test",
|
||||
"params": { "nprobe": 128, "rbq_bits_query": 0, "refine_k": 5.0},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "empty dict params",
|
||||
"params": {},
|
||||
"expected": success
|
||||
},
|
||||
|
||||
]
|
||||
@@ -0,0 +1,191 @@
|
||||
from pymilvus import DataType
|
||||
from common import common_type as ct
|
||||
|
||||
success = "success"
|
||||
|
||||
class NGRAM:
|
||||
supported_field_types = [
|
||||
DataType.VARCHAR,
|
||||
DataType.JSON
|
||||
]
|
||||
|
||||
# Test parameter configurations
|
||||
build_params = [
|
||||
# min_gram parameter tests
|
||||
{
|
||||
"description": "min_gram value only specified",
|
||||
"params": {"min_gram": 2},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "min_gram - negative value",
|
||||
"params": {"min_gram": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "min_gram - zero value",
|
||||
"params": {"min_gram": 0},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - string type",
|
||||
"params": {"min_gram": "2"},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - float type",
|
||||
"params": {"min_gram": 2.5},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - None value",
|
||||
"params": {"min_gram": None},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
|
||||
# max_gram parameter tests
|
||||
{
|
||||
"description": "max_gram value only specified",
|
||||
"params": {"max_gram": 5},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "max_gram - negative value",
|
||||
"params": {"max_gram": -1},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "max_gram - zero value",
|
||||
"params": {"max_gram": 0},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "max_gram - string type",
|
||||
"params": {"max_gram": "3"},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "max_gram - float type",
|
||||
"params": {"max_gram": 2.5},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
{
|
||||
"description": "max_gram - None value",
|
||||
"params": {"max_gram": None},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
|
||||
# min_gram and max_gram combination tests
|
||||
{
|
||||
"description": "min_gram equals max_gram",
|
||||
"params": {"min_gram": 2, "max_gram": 2},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "min_gram less than max_gram",
|
||||
"params": {"min_gram": 2, "max_gram": 4},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "max_gram equals a large value",
|
||||
"params": {"min_gram": 2, "max_gram": 1000000000},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "min_gram greater than max_gram",
|
||||
"params": {"min_gram": 5, "max_gram": 3},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid min_gram or max_gram value for Ngram index"}
|
||||
},
|
||||
# min_gram invalid with both specified
|
||||
{
|
||||
"description": "Invalid min_gram - negative value (both specified)",
|
||||
"params": {"min_gram": -1, "max_gram": 3},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid min_gram or max_gram value for Ngram index"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - zero value (both specified)",
|
||||
"params": {"min_gram": 0, "max_gram": 3},
|
||||
"expected": {"err_code": 1100, "err_msg": "invalid min_gram or max_gram value for Ngram index"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - string type (both specified)",
|
||||
"params": {"min_gram": "2", "max_gram": 3},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Invalid min_gram - float type (both specified)",
|
||||
"params": {"min_gram": 2.5, "max_gram": 3},
|
||||
"expected": {"err_code": 999, "err_msg": "min_gram for Ngram index must be an integer, got: 2.5"}
|
||||
},
|
||||
{
|
||||
"description": "Invalid max_gram - string type (both specified)",
|
||||
"params": {"min_gram": 2, "max_gram": "3"},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "Both parameters missing",
|
||||
"params": {},
|
||||
"expected": {"err_code": 999, "err_msg": "Ngram index must specify both min_gram and max_gram"}
|
||||
},
|
||||
|
||||
# JSON field special parameter tests
|
||||
{
|
||||
"description": "JSON field with json_path parameter",
|
||||
"params": {
|
||||
"min_gram": 2,
|
||||
"max_gram": 3,
|
||||
"json_path": "json_field['body']",
|
||||
"json_cast_type": "varchar"
|
||||
},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "JSON field with enteir json field",
|
||||
"params": {
|
||||
"min_gram": 2,
|
||||
"max_gram": 3,
|
||||
"json_path": "json_field",
|
||||
"json_cast_type": "varchar"
|
||||
},
|
||||
"expected": success
|
||||
},
|
||||
{
|
||||
"description": "JSON field with not existing path",
|
||||
"params": {
|
||||
"min_gram": 2,
|
||||
"max_gram": 3,
|
||||
"json_path": "json_field['not_existing_path']",
|
||||
"json_cast_type": "varchar"
|
||||
},
|
||||
"expected": success
|
||||
},
|
||||
# skip for https://github.com/milvus-io/milvus/issues/43934
|
||||
# {
|
||||
# "description": "JSON field with invalid json_cast_type",
|
||||
# "params": {
|
||||
# "min_gram": 2,
|
||||
# "max_gram": 3,
|
||||
# "json_path": "json_field['body']",
|
||||
# "json_cast_type": "double"
|
||||
# },
|
||||
# "expected": {"err_code": 999, "err_msg": "json_cast_type must be varchar for NGRAM index"}
|
||||
# },
|
||||
{
|
||||
"description": "JSON field missing json_cast_type",
|
||||
"params": {
|
||||
"min_gram": 2,
|
||||
"max_gram": 3,
|
||||
"json_path": "json_field['body']"
|
||||
},
|
||||
"expected": {"err_code": 999, "err_msg": "JSON field with ngram index must specify json_cast_type"}
|
||||
},
|
||||
{
|
||||
"description": "JSON field missing json_path",
|
||||
"params": {
|
||||
"min_gram": 2,
|
||||
"max_gram": 3,
|
||||
"json_cast_type": "varchar"
|
||||
},
|
||||
"expected": success
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,221 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_diskann import DISKANN
|
||||
|
||||
index_type = "DISKANN"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"search_list_size": 100, "beamwidth": 10, "pq_code_budget_gb": 1.0, "num_threads": 8, "max_degree": 64, "indexing_list_size": 100, "build_dram_budget_gb": 2.0, "search_dram_budget_gb": 1.0}
|
||||
default_search_params = {"search_list_size": 100, "beamwidth": 10, "search_dram_budget_gb": 1.0}
|
||||
|
||||
|
||||
class TestDiskannBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("params", DISKANN.build_params)
|
||||
def test_diskann_build_params(self, params):
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb * insert_times, dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) == idx_info[key]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_diskann_on_all_vector_types(self, vector_data_type):
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, nullable=True)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim, nullable=True)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
insert_times = 2
|
||||
rows = cf.gen_row_data_by_schema(insert_times * default_nb, schema=schema)
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
**default_build_params)
|
||||
if vector_data_type not in DISKANN.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index DISKANN: invalid parameter"})
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", DISKANN.supported_metrics)
|
||||
def test_diskann_on_all_metrics(self, metric):
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb*insert_times, default_dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
**default_build_params)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestDiskannSearchParams")
|
||||
class TestDiskannSearchParams(TestMilvusClientV2Base):
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestDiskannSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
client = self._client()
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
insert_times = 2
|
||||
float_vectors = cf.gen_vectors(default_nb * insert_times, dim=self.float_vector_dim,
|
||||
vector_data_type=DataType.FLOAT_VECTOR)
|
||||
for j in range(insert_times):
|
||||
rows = []
|
||||
for i in range(default_nb):
|
||||
pk = i + j * default_nb
|
||||
row = {
|
||||
pk_field_name: pk,
|
||||
self.float_vector_field_name: list(float_vectors[pk])
|
||||
}
|
||||
self.datas.append(row)
|
||||
rows.append(row)
|
||||
self.insert(client, self.collection_name, data=rows)
|
||||
self.primary_keys.extend([i + j * default_nb for i in range(default_nb)])
|
||||
self.flush(client, self.collection_name)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
self.load_collection(client, self.collection_name)
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", DISKANN.search_params)
|
||||
def test_diskann_search_params(self, params):
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
@@ -0,0 +1,402 @@
|
||||
import pytest
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from idx_faiss import FAISS
|
||||
from pymilvus import DataType
|
||||
|
||||
index_type = "FAISS"
|
||||
success = "success"
|
||||
pk_field_name = "id"
|
||||
vector_field_name = "vector"
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_search_params = {"nprobe": 8}
|
||||
|
||||
|
||||
def _default_search_params_for_faiss_factory(faiss_index_name):
|
||||
if faiss_index_name.startswith("IVF"):
|
||||
return {"nprobe": 8}
|
||||
if faiss_index_name.startswith("HNSW"):
|
||||
return {"efSearch": 64}
|
||||
return {}
|
||||
|
||||
|
||||
class TestFaissBase(TestMilvusClientV2Base):
|
||||
def _create_collection(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR):
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
return schema
|
||||
|
||||
def _insert_rows(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR):
|
||||
vectors = cf.gen_vectors(default_nb, dim=dim, vector_data_type=vector_data_type)
|
||||
rows = [{pk_field_name: i, vector_field_name: vectors[i]} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
def _create_faiss_index(
|
||||
self, client, collection_name, metric_type="L2", params=None, check_task=None, check_items=None
|
||||
):
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(
|
||||
field_name=vector_field_name, metric_type=metric_type, index_type=index_type, params=params
|
||||
)
|
||||
return self.create_index(client, collection_name, index_params, check_task=check_task, check_items=check_items)
|
||||
|
||||
def _search_and_check(self, client, collection_name, vector_data_type=DataType.FLOAT_VECTOR, search_params=None):
|
||||
nq = ct.default_nq
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={
|
||||
"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name,
|
||||
},
|
||||
)
|
||||
|
||||
def _assert_index_params(self, client, collection_name, params, metric_type):
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
assert idx_info["index_type"] == index_type
|
||||
assert idx_info["metric_type"] == metric_type
|
||||
for key, value in params.items():
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in [str(v) for v in idx_info.values()]
|
||||
|
||||
|
||||
class TestFaissBuildParams(TestFaissBase):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", FAISS.build_params)
|
||||
def test_faiss_build_params(self, params):
|
||||
"""
|
||||
Test vanilla Faiss factory build parameters.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
vector_data_type = params.get("vector_data_type", DataType.FLOAT_VECTOR)
|
||||
metric_type = params.get("metric_type", "L2")
|
||||
build_params = params.get("params", None)
|
||||
|
||||
self._create_collection(client, collection_name, vector_data_type)
|
||||
self._insert_rows(client, collection_name, vector_data_type)
|
||||
|
||||
if params.get("expected", None) != success:
|
||||
self._create_faiss_index(
|
||||
client,
|
||||
collection_name,
|
||||
metric_type=metric_type,
|
||||
params=build_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"),
|
||||
)
|
||||
else:
|
||||
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
if vector_data_type == DataType.FLOAT_VECTOR and params.get("searchable", True):
|
||||
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
|
||||
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
|
||||
elif vector_data_type != DataType.FLOAT_VECTOR:
|
||||
search_params = {}
|
||||
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
|
||||
self._assert_index_params(client, collection_name, build_params, metric_type)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", FAISS.supported_metrics)
|
||||
@pytest.mark.parametrize("build_params", [{"faiss_index_name": "Flat"}] + FAISS.metric_factories)
|
||||
def test_faiss_on_all_float_metrics(self, metric, build_params):
|
||||
"""
|
||||
Test vanilla Faiss float index factories on all supported float metrics.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type=metric, params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
|
||||
self._search_and_check(client, collection_name, DataType.FLOAT_VECTOR, search_params=search_params)
|
||||
self._assert_index_params(client, collection_name, build_params, metric)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_faiss_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test vanilla Faiss vector type support.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, vector_data_type)
|
||||
self._insert_rows(client, collection_name, vector_data_type)
|
||||
|
||||
if vector_data_type == DataType.BINARY_VECTOR:
|
||||
metric_type = "HAMMING"
|
||||
build_params = {"faiss_index_name": "BFlat"}
|
||||
else:
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
build_params = {"faiss_index_name": "Flat"}
|
||||
|
||||
if vector_data_type not in FAISS.supported_vector_types:
|
||||
self._create_faiss_index(
|
||||
client,
|
||||
collection_name,
|
||||
metric_type=metric_type,
|
||||
params=build_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999, "err_msg": "invalid parameter"},
|
||||
)
|
||||
else:
|
||||
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
self._search_and_check(client, collection_name, vector_data_type, search_params={})
|
||||
self._assert_index_params(client, collection_name, build_params, metric_type)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize(
|
||||
"params", [p for p in FAISS.build_params if p.get("expected") == success and p.get("searchable", True)]
|
||||
)
|
||||
def test_faiss_build_release_load_search(self, params):
|
||||
"""
|
||||
Test vanilla Faiss index survives the full Milvus build -> release -> load -> search flow.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
vector_data_type = params.get("vector_data_type", DataType.FLOAT_VECTOR)
|
||||
metric_type = params.get("metric_type", "L2")
|
||||
build_params = params["params"]
|
||||
|
||||
self._create_collection(client, collection_name, vector_data_type)
|
||||
self._insert_rows(client, collection_name, vector_data_type)
|
||||
self._create_faiss_index(client, collection_name, metric_type=metric_type, params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
self.release_collection(client, collection_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_params = {}
|
||||
if vector_data_type == DataType.FLOAT_VECTOR:
|
||||
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
|
||||
self._search_and_check(client, collection_name, vector_data_type, search_params=search_params)
|
||||
self._assert_index_params(client, collection_name, build_params, metric_type)
|
||||
|
||||
|
||||
class TestFaissSearchParams(TestFaissBase):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", FAISS.search_params)
|
||||
def test_faiss_search_params(self, params):
|
||||
"""
|
||||
Test vanilla Faiss search parameter forwarding.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
build_params = params["build_params"]
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
if params.get("expected", None) != success:
|
||||
nq = ct.default_nq
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
search_params=params["search_params"],
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"),
|
||||
)
|
||||
else:
|
||||
self._search_and_check(
|
||||
client, collection_name, DataType.FLOAT_VECTOR, search_params=params["search_params"]
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_faiss_incompatible_search_params(self):
|
||||
"""
|
||||
Test vanilla Faiss rejects search parameters incompatible with the factory index.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
build_params = {"faiss_index_name": "Flat"}
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
nq = ct.default_nq
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
search_params={"efSearch": 64},
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999, "err_msg": "not supported"},
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize(
|
||||
"build_params",
|
||||
[
|
||||
{"faiss_index_name": "Flat"},
|
||||
{"faiss_index_name": "IVF64,Flat"},
|
||||
{"faiss_index_name": "HNSW16,Flat"},
|
||||
],
|
||||
)
|
||||
def test_faiss_search_with_scalar_filter(self, build_params):
|
||||
"""
|
||||
Test vanilla Faiss search honors Milvus scalar filter bitset.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params=build_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_params = _default_search_params_for_faiss_factory(build_params["faiss_index_name"])
|
||||
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
results = client.search(
|
||||
collection_name,
|
||||
search_vectors,
|
||||
filter=f"{pk_field_name} >= 100",
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert len(results[0]) == ct.default_limit
|
||||
assert all(hit["id"] >= 100 for hit in results[0])
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_faiss_flat_range_search(self):
|
||||
"""
|
||||
Test vanilla Faiss float Flat range search. The current adapter implements
|
||||
RangeSearch for float FAISS indexes.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "Flat"})
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
range_params = {"radius": 100000.0, "range_filter": 0.0}
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
search_params=range_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={
|
||||
"enable_milvus_client_api": True,
|
||||
"nq": 1,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name,
|
||||
},
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_faiss_binary_range_search_not_supported(self):
|
||||
"""
|
||||
Test vanilla Faiss binary range search is explicitly not implemented.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.BINARY_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.BINARY_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="HAMMING", params={"faiss_index_name": "BFlat"})
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.BINARY_VECTOR)
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
search_params={"radius": 1000, "range_filter": 0},
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999, "err_msg": "RangeSearch unsupported for binary faiss indexes"},
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_faiss_pq_search_selector_not_supported(self):
|
||||
"""
|
||||
Test vanilla Faiss IndexPQ rejects Milvus search because Milvus passes
|
||||
an ID selector/bitset and native FAISS IndexPQ does not support it.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "PQ8x4"})
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
search_vectors,
|
||||
filter=f"{pk_field_name} >= 100",
|
||||
search_params={},
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999, "err_msg": "selector not supported"},
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_faiss_search_iterator_not_supported(self):
|
||||
"""
|
||||
Test vanilla Faiss search iterator is rejected because the adapter does
|
||||
not expose raw-vector retrieval.
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
|
||||
self._create_collection(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._insert_rows(client, collection_name, DataType.FLOAT_VECTOR)
|
||||
self._create_faiss_index(client, collection_name, metric_type="L2", params={"faiss_index_name": "Flat"})
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
search_vectors = cf.gen_vectors(1, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search_iterator(
|
||||
client,
|
||||
collection_name,
|
||||
data=search_vectors,
|
||||
batch_size=100,
|
||||
search_params={},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 65535, "err_msg": "Failed to create iterators from index"},
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_hnsw import HNSW
|
||||
|
||||
index_type = "HNSW"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"M": 16, "efConstruction": 200}
|
||||
default_search_params = {"ef": 64}
|
||||
|
||||
|
||||
class TestHnswBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW.build_params)
|
||||
def test_hnsw_build_params(self, params):
|
||||
"""
|
||||
Test the build params of HNSW index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert data in 2 batches with unique primary keys
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb * insert_times, dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
# build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
# verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_hnsw_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test HNSW index on all the vector types and metrics
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, nullable=True)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim, nullable=True)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert data with unique primary keys
|
||||
rows = cf.gen_row_data_by_schema(default_nb, schema=schema)
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
M=16,
|
||||
efConstruction=200)
|
||||
if vector_data_type not in HNSW.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index HNSW: invalid parameter"})
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", HNSW.supported_metrics)
|
||||
def test_hnsw_on_all_metrics(self, metric):
|
||||
"""
|
||||
Test the search params of HNSW index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# insert data
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb*insert_times, dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
M=16,
|
||||
efConstruction=200)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestHnswSearchParams")
|
||||
class TestHnswSearchParams(TestMilvusClientV2Base):
|
||||
"""Test search with pagination functionality for HNSW index"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestHnswSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
"""
|
||||
Initialize collection before test class runs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
insert_times = 2
|
||||
float_vectors = cf.gen_vectors(default_nb * insert_times, dim=self.float_vector_dim,
|
||||
vector_data_type=DataType.FLOAT_VECTOR)
|
||||
for j in range(insert_times):
|
||||
rows = []
|
||||
for i in range(default_nb):
|
||||
pk = i + j * default_nb
|
||||
row = {
|
||||
pk_field_name: pk,
|
||||
self.float_vector_field_name: list(float_vectors[pk])
|
||||
}
|
||||
self.datas.append(row)
|
||||
rows.append(row)
|
||||
self.insert(client, self.collection_name, data=rows)
|
||||
self.primary_keys.extend([i + j * default_nb for i in range(default_nb)])
|
||||
self.flush(client, self.collection_name)
|
||||
# Create HNSW index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
self.load_collection(client, self.collection_name)
|
||||
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW.search_params)
|
||||
def test_hnsw_search_params(self, params):
|
||||
"""
|
||||
Test the search params of HNSW index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
@@ -0,0 +1,258 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_hnsw_pq import HNSW_PQ
|
||||
|
||||
index_type = "HNSW_PQ"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"M": 16, "efConstruction": 200, "m": 64, "nbits": 8}
|
||||
default_search_params = {"ef": 64, "refine_k": 1}
|
||||
|
||||
|
||||
class TestHnswPQBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_PQ.build_params)
|
||||
def test_hnsw_pq_build_params(self, params):
|
||||
"""
|
||||
Test the build params of HNSW_PQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
# build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
# verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_hnsw_pq_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test HNSW_PQ index on all the vector types and metrics
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
if vector_data_type not in HNSW_PQ.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index HNSW_PQ: invalid parameter"})
|
||||
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", HNSW_PQ.supported_metrics)
|
||||
def test_hnsw_pq_on_all_metrics(self, metric):
|
||||
"""
|
||||
Test the search params of HNSW_PQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestHnswPQSearchParams")
|
||||
class TestHnswPQSearchParams(TestMilvusClientV2Base):
|
||||
"""Test search with pagination functionality for HNSW_PQ index"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestHnswPQSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
"""
|
||||
Initialize collection before test class runs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
|
||||
all_data = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=collection_schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
self.insert(client, self.collection_name, data=all_data)
|
||||
self.primary_keys.extend([i for i in range(default_nb)])
|
||||
self.flush(client, self.collection_name)
|
||||
# Create HNSW_PQ index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
self.load_collection(client, self.collection_name)
|
||||
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_PQ.search_params)
|
||||
def test_hnsw_pq_search_params(self, params):
|
||||
"""
|
||||
Test the search params of HNSW_PQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
@@ -0,0 +1,263 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_hnsw_prq import HNSW_PRQ
|
||||
|
||||
index_type = "HNSW_PRQ"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"M": 16, "efConstruction": 200, "m": 64, "nbits": 8, "nrq":1}
|
||||
default_search_params = {"ef": 64, "refine_k": 1}
|
||||
|
||||
|
||||
class TestHnswPRQBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.skip(reason="ci tests index creation timeout")
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_PRQ.build_params)
|
||||
def test_hnsw_prq_build_params(self, params):
|
||||
"""
|
||||
Test the build params of HNSW_PRQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
# build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
# verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values()
|
||||
|
||||
@pytest.mark.skip(reason="ci tests index creation timeout")
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_hnsw_prq_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test HNSW_PRQ index on all the vector types and metrics
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
if vector_data_type not in HNSW_PRQ.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index HNSW_PRQ: invalid parameter"})
|
||||
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.skip(reason="ci tests index creation timeout")
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", HNSW_PRQ.supported_metrics)
|
||||
def test_hnsw_prq_on_all_metrics(self, metric):
|
||||
"""
|
||||
Test the search params of HNSW_PRQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestHnswPRQSearchParams")
|
||||
class TestHnswPRQSearchParams(TestMilvusClientV2Base):
|
||||
"""Test search with pagination functionality for HNSW_PRQ index"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestHnswPRQSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.mark.skip(reason="ci tests index creation timeout")
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
"""
|
||||
Initialize collection before test class runs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
all_data = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=collection_schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
self.insert(client, self.collection_name, data=all_data)
|
||||
self.primary_keys.extend([i for i in range(default_nb)])
|
||||
|
||||
self.flush(client, self.collection_name)
|
||||
# Create HNSW_PRQ index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
self.load_collection(client, self.collection_name)
|
||||
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.skip(reason="ci tests index creation timeout")
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_PRQ.search_params)
|
||||
def test_hnsw_prq_search_params(self, params):
|
||||
"""
|
||||
Test the search params of HNSW_PRQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
@@ -0,0 +1,267 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_hnsw_sq import HNSW_SQ
|
||||
|
||||
index_type = "HNSW_SQ"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"M": 16, "efConstruction": 200, "sq_type": "SQ8"}
|
||||
default_search_params = {"ef": 64, "refine_k": 1}
|
||||
|
||||
|
||||
class TestHnswSQBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_SQ.build_params)
|
||||
def test_hnsw_sq_build_params(self, params):
|
||||
"""
|
||||
Test the build params of HNSW_SQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
# build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
if params.get("relaxed_limit"):
|
||||
# Extreme params (e.g. M=2, efConstruction=1) produce a poorly connected
|
||||
# HNSW graph that may return fewer than topK results — only assert > 0.
|
||||
results = client.search(collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit)
|
||||
for r in results:
|
||||
assert len(r) > 0, f"expected > 0 results but got {len(r)}"
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
# verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_hnsw_sq_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test HNSW_SQ index on all the vector types and metrics
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
if vector_data_type not in HNSW_SQ.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index HNSW_SQ: invalid parameter"})
|
||||
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", HNSW_SQ.supported_metrics)
|
||||
def test_hnsw_sq_on_all_metrics(self, metric):
|
||||
"""
|
||||
Test the search params of HNSW_SQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
all_rows = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
|
||||
self.insert(client, collection_name, all_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestHnswSQSearchParams")
|
||||
class TestHnswSQSearchParams(TestMilvusClientV2Base):
|
||||
"""Test search with pagination functionality for HNSW_SQ index"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestHnswSQSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
"""
|
||||
Initialize collection before test class runs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
all_data = cf.gen_row_data_by_schema(
|
||||
nb=default_nb,
|
||||
schema=collection_schema,
|
||||
start=0,
|
||||
random_pk=False
|
||||
)
|
||||
self.insert(client, self.collection_name, data=all_data)
|
||||
self.primary_keys.extend([i for i in range(default_nb)])
|
||||
|
||||
self.flush(client, self.collection_name)
|
||||
# Create HNSW_SQ index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type=index_type,
|
||||
params=default_build_params)
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
self.load_collection(client, self.collection_name)
|
||||
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", HNSW_SQ.search_params)
|
||||
def test_hnsw_sq_search_params(self, params):
|
||||
"""
|
||||
Test the search params of HNSW_SQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
@@ -0,0 +1,306 @@
|
||||
import logging
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
import pytest
|
||||
from idx_ivf_rabitq import IVF_RABITQ
|
||||
|
||||
index_type = "IVF_RABITQ"
|
||||
success = "success"
|
||||
pk_field_name = 'id'
|
||||
vector_field_name = 'vector'
|
||||
dim = ct.default_dim
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"nlist": 128, "refine": 'true', "refine_type": "SQ8"}
|
||||
default_search_params = {"nprobe": 8, "rbq_bits_query": 6, "refine_k": 1.0}
|
||||
|
||||
|
||||
class TestIvfRabitqBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", IVF_RABITQ.build_params)
|
||||
def test_ivf_rabitq_build_params(self, params):
|
||||
"""
|
||||
Test the build params of IVF_RABITQ index
|
||||
"""
|
||||
client = self._client()
|
||||
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert data in 3 batches with unique primary keys using a loop
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb * insert_times, dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
build_params = params.get("params", None)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type=index_type,
|
||||
params=build_params)
|
||||
# build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
# verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, vector_field_name)
|
||||
# check every key and value in build_params exists in idx_info
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values() # TODO: uncommented after #41783 fixed
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_vector_types)
|
||||
def test_ivf_rabitq_on_all_vector_types(self, vector_data_type):
|
||||
"""
|
||||
Test ivf_rabitq index on all the vector types and metrics
|
||||
"""
|
||||
client = self._client()
|
||||
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
if vector_data_type == DataType.SPARSE_FLOAT_VECTOR:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, nullable=True)
|
||||
else:
|
||||
schema.add_field(vector_field_name, datatype=vector_data_type, dim=dim, nullable=True)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert data unique primary keys using a loop
|
||||
rows = cf.gen_row_data_by_schema(default_nb, schema=schema)
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
metric_type = cf.get_default_metric_for_vector_type(vector_data_type)
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric_type,
|
||||
index_type=index_type,
|
||||
nlist=128, # flatten the params
|
||||
refine=True,
|
||||
refine_type="SQ8")
|
||||
if vector_data_type not in IVF_RABITQ.supported_vector_types:
|
||||
self.create_index(client, collection_name, index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999,
|
||||
"err_msg": f"can't build with this index IVF_RABITQ: invalid parameter"})
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=vector_data_type)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("metric", IVF_RABITQ.supported_metrics)
|
||||
def test_ivf_rabitq_on_all_metrics(self, metric):
|
||||
"""
|
||||
Test the search params of IVF_RABITQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# insert data
|
||||
insert_times = 2
|
||||
random_vectors = list(cf.gen_vectors(default_nb*insert_times, default_dim, vector_data_type=DataType.FLOAT_VECTOR))
|
||||
for j in range(insert_times):
|
||||
start_pk = j * default_nb
|
||||
rows = [{
|
||||
pk_field_name: i + start_pk,
|
||||
vector_field_name: random_vectors[i + start_pk]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=vector_field_name,
|
||||
metric_type=metric,
|
||||
index_type=index_type,
|
||||
nlist=128,
|
||||
refine=True,
|
||||
refine_type="SQ8")
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=default_search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestIvfRabitqSearchParams")
|
||||
class TestIvfRabitqSearchParams(TestMilvusClientV2Base):
|
||||
"""Test search with pagination functionality"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.collection_name = "TestIvfRabitqSearchParams" + cf.gen_unique_str("_")
|
||||
self.float_vector_field_name = vector_field_name
|
||||
self.float_vector_dim = dim
|
||||
self.primary_keys = []
|
||||
self.enable_dynamic_field = False
|
||||
self.datas = []
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collection(self, request):
|
||||
"""
|
||||
Initialize collection before test class runs
|
||||
"""
|
||||
# Get client connection
|
||||
client = self._client()
|
||||
|
||||
# Create collection
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(pk_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
self.create_collection(client, self.collection_name, schema=collection_schema,
|
||||
enable_dynamic_field=self.enable_dynamic_field, force_teardown=False)
|
||||
# Define number of insert iterations
|
||||
insert_times = 2
|
||||
|
||||
# Generate vectors for each type and store in self
|
||||
float_vectors = cf.gen_vectors(default_nb * insert_times, dim=self.float_vector_dim,
|
||||
vector_data_type=DataType.FLOAT_VECTOR)
|
||||
|
||||
# Insert data multiple times with non-duplicated primary keys
|
||||
for j in range(insert_times):
|
||||
# Group rows by partition based on primary key mod 3
|
||||
rows = []
|
||||
for i in range(default_nb):
|
||||
pk = i + j * default_nb
|
||||
row = {
|
||||
pk_field_name: pk,
|
||||
self.float_vector_field_name: list(float_vectors[pk])
|
||||
}
|
||||
self.datas.append(row)
|
||||
rows.append(row)
|
||||
|
||||
# Insert into respective partitions
|
||||
self.insert(client, self.collection_name, data=rows)
|
||||
# Track all inserted data and primary keys
|
||||
self.primary_keys.extend([i + j * default_nb for i in range(default_nb)])
|
||||
|
||||
self.flush(client, self.collection_name)
|
||||
|
||||
# Create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.float_vector_field_name,
|
||||
metric_type="COSINE",
|
||||
index_type="IVF_RABITQ",
|
||||
params={"nlist": 128, "refine": 'true', "refine_type": "SQ8"})
|
||||
self.create_index(client, self.collection_name, index_params=index_params)
|
||||
self.wait_for_index_ready(client, self.collection_name, index_name=self.float_vector_field_name)
|
||||
|
||||
# Load collection
|
||||
self.load_collection(client, self.collection_name)
|
||||
|
||||
def teardown():
|
||||
self.drop_collection(self._client(), self.collection_name)
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", IVF_RABITQ.search_params)
|
||||
def test_ivf_rabitq_search_params(self, params):
|
||||
"""
|
||||
Test the search params of IVF_RABITQ index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self.collection_name
|
||||
|
||||
# search
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim=self.float_vector_dim, vector_data_type=DataType.FLOAT_VECTOR)
|
||||
search_params = params.get("params", None)
|
||||
if params.get("expected", None) != success:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=params.get("expected"))
|
||||
else:
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
if len(search_params.keys()) == 3:
|
||||
# try to search again with flattened params
|
||||
search_params = {
|
||||
"nprobe": search_params["nprobe"],
|
||||
"rbq_bits_query": search_params["rbq_bits_query"],
|
||||
"refine_k": search_params["refine_k"]
|
||||
}
|
||||
self.search(client, collection_name, search_vectors,
|
||||
search_params=search_params,
|
||||
limit=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": nq,
|
||||
"limit": ct.default_limit,
|
||||
"pk_name": pk_field_name})
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
import pytest
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from idx_ngram import NGRAM
|
||||
from pymilvus import DataType
|
||||
|
||||
index_type = "NGRAM"
|
||||
success = "success"
|
||||
pk_field_name = "id"
|
||||
vector_field_name = "vector"
|
||||
content_field_name = "content_ngram"
|
||||
json_field_name = "json_field"
|
||||
dim = 32
|
||||
default_nb = ct.default_nb
|
||||
default_build_params = {"min_gram": 2, "max_gram": 3}
|
||||
|
||||
|
||||
class TestNgramBuildParams(TestMilvusClientV2Base):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("params", NGRAM.build_params)
|
||||
def test_ngram_build_params(self, params):
|
||||
"""
|
||||
Test the build params of NGRAM index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim, nullable=True)
|
||||
schema.add_field(content_field_name, datatype=DataType.VARCHAR, max_length=100, nullable=True)
|
||||
|
||||
# Check if this test case requires JSON field
|
||||
build_params = params.get("params", None)
|
||||
has_json_params = build_params is not None and ("json_path" in build_params or "json_cast_type" in build_params)
|
||||
|
||||
target_field_name = content_field_name # Default to VARCHAR field
|
||||
|
||||
if has_json_params:
|
||||
# Add JSON field for JSON-related parameter tests
|
||||
schema.add_field(json_field_name, datatype=DataType.JSON)
|
||||
target_field_name = json_field_name
|
||||
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert test data
|
||||
nb = default_nb
|
||||
rows = cf.gen_row_data_by_schema(nb=nb, schema=schema, start=0)
|
||||
|
||||
if has_json_params:
|
||||
# Generate JSON test data with varied content
|
||||
json_keywords = ["stadium", "park", "school", "library", "hospital", "restaurant", "office", "store"]
|
||||
for i, row in enumerate(rows):
|
||||
keyword_idx = i % len(json_keywords)
|
||||
keyword = json_keywords[keyword_idx]
|
||||
row[content_field_name] = f"text content {i}" # Still provide VARCHAR data
|
||||
row[json_field_name] = {
|
||||
"body": f"This is a {keyword} building",
|
||||
"title": f"Location {i}",
|
||||
"description": f"Description for {keyword} number {i}",
|
||||
}
|
||||
else:
|
||||
# Generate VARCHAR test data with varied content
|
||||
varchar_keywords = ["stadium", "park", "school", "library", "hospital", "restaurant", "office", "store"]
|
||||
for i, row in enumerate(rows):
|
||||
keyword_idx = i % len(varchar_keywords)
|
||||
keyword = varchar_keywords[keyword_idx]
|
||||
row[content_field_name] = f"The {keyword} is large and beautiful number {i}"
|
||||
|
||||
# Insert data in batches for better performance
|
||||
batch_size = 1000
|
||||
for i in range(0, nb, batch_size):
|
||||
batch_rows = rows[i : i + batch_size]
|
||||
self.insert(client, collection_name, batch_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# Create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_name = cf.gen_str_by_length(10, letters_only=True)
|
||||
index_params.add_index(
|
||||
field_name=target_field_name, index_name=index_name, index_type=index_type, params=build_params
|
||||
)
|
||||
|
||||
# Build index
|
||||
if params.get("expected", None) != success:
|
||||
self.create_index(
|
||||
client, collection_name, index_params, check_task=CheckTasks.err_res, check_items=params.get("expected")
|
||||
)
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=index_name)
|
||||
|
||||
# Create vector index before loading collection
|
||||
vector_index_params = self.prepare_index_params(client)[0]
|
||||
vector_index_params.add_index(
|
||||
field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type="IVF_FLAT",
|
||||
params={"nlist": 128},
|
||||
)
|
||||
self.create_index(client, collection_name, vector_index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
# Load collection
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# Test query based on field type
|
||||
if has_json_params:
|
||||
filter_expr = f"{json_field_name}['body'] LIKE \"%stadium%\""
|
||||
else:
|
||||
filter_expr = f'{content_field_name} LIKE "%stadium%"'
|
||||
|
||||
# Calculate expected count: 2000 data points with 8 keywords cycling
|
||||
# Each keyword appears 2000/8 = 250 times
|
||||
expected_count = default_nb // 8 # 250 matches for "stadium"
|
||||
|
||||
self.query(
|
||||
client,
|
||||
collection_name,
|
||||
filter=filter_expr,
|
||||
output_fields=["count(*)"],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"enable_milvus_client_api": True, "count(*)": expected_count},
|
||||
)
|
||||
|
||||
# Verify the index params are persisted
|
||||
idx_info = client.describe_index(collection_name, index_name)
|
||||
if build_params is not None:
|
||||
for key, value in build_params.items():
|
||||
if value is not None and key not in ["json_path", "json_cast_type"]:
|
||||
assert key in idx_info.keys()
|
||||
assert str(value) in idx_info.values()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("scalar_field_type", ct.all_scalar_data_types)
|
||||
def test_ngram_on_all_scalar_fields(self, scalar_field_type):
|
||||
"""
|
||||
Test NGRAM index on all scalar field types and verify proper error handling
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
|
||||
# Add the scalar field with appropriate parameters
|
||||
if scalar_field_type == DataType.VARCHAR:
|
||||
schema.add_field("scalar_field", datatype=scalar_field_type, max_length=1000, nullable=True)
|
||||
elif scalar_field_type == DataType.ARRAY:
|
||||
schema.add_field(
|
||||
"scalar_field",
|
||||
datatype=scalar_field_type,
|
||||
element_type=DataType.VARCHAR,
|
||||
max_capacity=10,
|
||||
max_length=100,
|
||||
)
|
||||
else:
|
||||
schema.add_field("scalar_field", datatype=scalar_field_type)
|
||||
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Generate appropriate test data for each field type
|
||||
nb = default_nb
|
||||
rows = cf.gen_row_data_by_schema(nb=nb, schema=schema, start=0)
|
||||
|
||||
# Update scalar field with appropriate test data
|
||||
if scalar_field_type == DataType.VARCHAR:
|
||||
# Generate varied VARCHAR data for better testing
|
||||
keywords = ["stadium", "park", "school", "library", "hospital", "restaurant", "office", "store"]
|
||||
for i, row in enumerate(rows):
|
||||
keyword_idx = i % len(keywords)
|
||||
keyword = keywords[keyword_idx]
|
||||
row["scalar_field"] = f"The {keyword} is a large building number {i}"
|
||||
elif scalar_field_type == DataType.JSON:
|
||||
# Generate varied JSON data for better testing
|
||||
keywords = ["school", "park", "mall", "library", "hospital", "restaurant", "office", "store"]
|
||||
for i, row in enumerate(rows):
|
||||
keyword_idx = i % len(keywords)
|
||||
keyword = keywords[keyword_idx]
|
||||
row["scalar_field"] = {
|
||||
"body": f"This is a {keyword}",
|
||||
"title": f"Location {i}",
|
||||
"category": f"Category {keyword_idx}",
|
||||
}
|
||||
elif scalar_field_type == DataType.ARRAY:
|
||||
# Generate varied ARRAY data for better testing
|
||||
base_words = ["word", "text", "data", "item", "element"]
|
||||
keywords = ["stadium", "park", "school", "library", "hospital"]
|
||||
for i, row in enumerate(rows):
|
||||
base_idx = i % len(base_words)
|
||||
keyword_idx = i % len(keywords)
|
||||
row["scalar_field"] = [f"{base_words[base_idx]}1", f"{base_words[base_idx]}2", keywords[keyword_idx]]
|
||||
# For other scalar types, keep the auto-generated data
|
||||
|
||||
# Insert data in batches for better performance
|
||||
batch_size = 1000
|
||||
for i in range(0, nb, batch_size):
|
||||
batch_rows = rows[i : i + batch_size]
|
||||
self.insert(client, collection_name, batch_rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# Create index
|
||||
index_name = cf.gen_str_by_length(10, letters_only=True)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
if scalar_field_type == DataType.JSON:
|
||||
# JSON field requires json_path and json_cast_type
|
||||
index_params.add_index(
|
||||
field_name="scalar_field",
|
||||
index_name=index_name,
|
||||
index_type=index_type,
|
||||
params={"min_gram": 2, "max_gram": 3, "json_path": "scalar_field['body']", "json_cast_type": "varchar"},
|
||||
)
|
||||
else:
|
||||
index_params.add_index(
|
||||
field_name="scalar_field", index_name=index_name, index_type=index_type, params=default_build_params
|
||||
)
|
||||
|
||||
# Check if the field type is supported for NGRAM index
|
||||
if scalar_field_type not in NGRAM.supported_field_types:
|
||||
self.create_index(
|
||||
client,
|
||||
collection_name,
|
||||
index_params,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={"err_code": 999, "err_msg": "ngram index can only be created on VARCHAR or JSON field"},
|
||||
)
|
||||
else:
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=index_name)
|
||||
|
||||
# Create vector index before loading collection
|
||||
vector_index_params = self.prepare_index_params(client)[0]
|
||||
vector_index_params.add_index(
|
||||
field_name=vector_field_name,
|
||||
metric_type=cf.get_default_metric_for_vector_type(vector_type=DataType.FLOAT_VECTOR),
|
||||
index_type="IVF_FLAT",
|
||||
params={"nlist": 128},
|
||||
)
|
||||
self.create_index(client, collection_name, vector_index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# Test query for supported types
|
||||
if scalar_field_type == DataType.VARCHAR:
|
||||
# Calculate expected count: 2000 data points with 8 keywords cycling
|
||||
# Each keyword appears 2000/8 = 250 times
|
||||
expected_count = default_nb // 8 # 250 matches for "stadium"
|
||||
filter_expr = 'scalar_field LIKE "%stadium%"'
|
||||
self.query(
|
||||
client,
|
||||
collection_name,
|
||||
filter=filter_expr,
|
||||
output_fields=["count(*)"],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"enable_milvus_client_api": True, "count(*)": expected_count},
|
||||
)
|
||||
elif scalar_field_type == DataType.JSON:
|
||||
# Calculate expected count: 2000 data points with 8 keywords cycling
|
||||
# Each keyword appears 2000/8 = 250 times
|
||||
expected_count = default_nb // 8 # 250 matches for "school"
|
||||
filter_expr = "scalar_field['body'] LIKE \"%school%\""
|
||||
self.query(
|
||||
client,
|
||||
collection_name,
|
||||
filter=filter_expr,
|
||||
output_fields=["count(*)"],
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={"enable_milvus_client_api": True, "count(*)": expected_count},
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip(reason="skip for issue #44164")
|
||||
def test_ngram_alter_index_mmap_and_gram_values(self):
|
||||
"""
|
||||
Test the alter index with mmap and gram values
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
schema.add_field("content_ngram", datatype=DataType.VARCHAR, max_length=20)
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert data
|
||||
content_keywords = ["stadium", "park", "school", "library", "hospital", "restaurant", "office", "store"]
|
||||
rows = cf.gen_row_data_by_schema(nb=default_nb, schema=schema, start=0)
|
||||
for i, row in enumerate(rows):
|
||||
row["content_ngram"] = content_keywords[i % len(content_keywords)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# Create index
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(
|
||||
field_name="content_ngram",
|
||||
index_name="content_ngram",
|
||||
index_type=index_type,
|
||||
params={"min_gram": 2, "max_gram": 3},
|
||||
)
|
||||
index_params.add_index(
|
||||
field_name=vector_field_name, index_type="IVF_FLAT", metric_type="COSINE", params={"nlist": 128}
|
||||
)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name="content_ngram")
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.load_collection(client, collection_name)
|
||||
# Query to check if the index is created
|
||||
res = self.query(
|
||||
client, collection_name, filter="content_ngram LIKE 'stad_%'", output_fields=["id", "content_ngram"]
|
||||
)[0]
|
||||
assert len(res) == default_nb // len(content_keywords)
|
||||
|
||||
# Release collection before alter ngram index
|
||||
self.release_collection(client, collection_name)
|
||||
# Alter index mmap properties
|
||||
self.alter_index_properties(
|
||||
client, collection_name, index_name="content_ngram", properties={"mmap.enabled": True}
|
||||
)
|
||||
res = self.describe_index(client, collection_name, index_name="content_ngram")[0]
|
||||
assert res.get("mmap.enabled", None) == "True"
|
||||
# Load the collection and query again
|
||||
self.load_collection(client, collection_name)
|
||||
res = self.query(
|
||||
client, collection_name, filter="content_ngram LIKE 'stad_%'", output_fields=["id", "content_ngram"]
|
||||
)[0]
|
||||
assert len(res) == default_nb // len(content_keywords)
|
||||
|
||||
# Alter index gram value properties is not supported
|
||||
self.release_collection(client, collection_name)
|
||||
error = {ct.err_code: 1, ct.err_msg: "invalid mmap.enabled value: True, expected: true, false"}
|
||||
self.alter_index_properties(
|
||||
client,
|
||||
collection_name,
|
||||
index_name="content_ngram",
|
||||
properties={"min_gram": 3, "max_gram": 4},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_ngram_search_with_diff_length_of_filter_value(self):
|
||||
"""
|
||||
Test the search params of NGRAM index
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
schema.add_field("content_no_index", datatype=DataType.VARCHAR, max_length=10)
|
||||
schema.add_field("content_ngram", datatype=DataType.VARCHAR, max_length=10)
|
||||
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Insert test data
|
||||
insert_times = 2
|
||||
content_keywords = ["stadium", "park", "school", "library", "hospital", "restaurant", "office", "store"]
|
||||
for i in range(insert_times):
|
||||
rows = cf.gen_row_data_by_schema(nb=default_nb, schema=schema, start=i * default_nb)
|
||||
for j, row in enumerate(rows):
|
||||
row["content_no_index"] = content_keywords[j % len(content_keywords)]
|
||||
row["content_ngram"] = content_keywords[j % len(content_keywords)]
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# Create vector index before loading collection
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(
|
||||
field_name=vector_field_name, metric_type="COSINE", index_type="IVF_FLAT", params={"nlist": 128}
|
||||
)
|
||||
min_gram = 2
|
||||
max_gram = 4
|
||||
index_params.add_index(
|
||||
field_name="content_ngram", index_type=index_type, params={"min_gram": min_gram, "max_gram": max_gram}
|
||||
)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.wait_for_index_ready(client, collection_name, index_name="content_ngram")
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
# Test query 0: filter value length is less than min_gram
|
||||
filter_expr = f'content_ngram LIKE "{content_keywords[0][: min_gram - 1]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "{content_keywords[0][: min_gram - 1]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query 1: filter value length is equal to min_gram
|
||||
filter_expr = f'content_ngram LIKE "{content_keywords[0][:min_gram]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "{content_keywords[0][:min_gram]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query 2: filter value length is less than max_gram
|
||||
filter_expr = f'content_ngram LIKE "{content_keywords[0][: max_gram - 1]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "{content_keywords[0][: max_gram - 1]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query 3: filter value length is equal to max_gram
|
||||
filter_expr = f'content_ngram LIKE "{content_keywords[0][:max_gram]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "{content_keywords[0][:max_gram]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query 4: filter value length is greater than max_gram
|
||||
filter_expr = f'content_ngram LIKE "{content_keywords[0][: max_gram + 1]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "{content_keywords[0][: max_gram + 1]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query with suffix match
|
||||
filter_expr = f'content_ngram LIKE "%{content_keywords[0][4:]}"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "%{content_keywords[0][4:]}"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query with infix match
|
||||
filter_expr = f'content_ngram LIKE "%{content_keywords[0][2:4]}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = f'content_no_index LIKE "%{content_keywords[0][2:4]}%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
# Test query with Mixed Wildcard Match
|
||||
filter_expr = 'content_ngram LIKE "%st_d_um%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_ngram) >= insert_times * default_nb // len(content_keywords)
|
||||
filter_expr = 'content_no_index LIKE "%st_d_um%"'
|
||||
res_no_index = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[0]
|
||||
assert len(res_no_index) >= insert_times * default_nb // len(content_keywords)
|
||||
assert res_ngram == res_no_index
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_ngram_search_with_multilingual_utf8_strings(self):
|
||||
"""
|
||||
Test NGRAM index with multilingual and UTF-8 strings for LIKE filtering
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema, _ = self.create_schema(client)
|
||||
schema.add_field(pk_field_name, datatype=DataType.INT64, is_primary=True, auto_id=False)
|
||||
schema.add_field(vector_field_name, datatype=DataType.FLOAT_VECTOR, dim=dim)
|
||||
schema.add_field("content_no_index", datatype=DataType.JSON)
|
||||
schema.add_field("content_ngram", datatype=DataType.JSON)
|
||||
|
||||
self.create_collection(client, collection_name, schema=schema)
|
||||
|
||||
# Multilingual test data with various UTF-8 characters
|
||||
multilingual_keywords = [
|
||||
"北京大学", # Chinese
|
||||
"東京大学", # Japanese
|
||||
"Московский", # Russian
|
||||
"café", # French with accent
|
||||
"naïve", # French with diaeresis
|
||||
"München", # German with umlaut
|
||||
"🏫学校🎓", # Chinese with emojis
|
||||
"🌟star⭐", # English with emojis
|
||||
"مدرسة", # Arabic
|
||||
"Γειά", # Greek
|
||||
"प्रविष्टि", # Hindi/Devanagari
|
||||
"한국어", # Korean
|
||||
"español", # Spanish
|
||||
"português", # Portuguese
|
||||
"中英mix英文", # Mixed Chinese-English
|
||||
"café☕北京🏙️", # Mixed with emojis and multiple languages
|
||||
]
|
||||
|
||||
# Insert test data
|
||||
insert_times = 2
|
||||
for i in range(insert_times):
|
||||
rows = cf.gen_row_data_by_schema(nb=default_nb, schema=schema, start=i * default_nb)
|
||||
for j, row in enumerate(rows):
|
||||
keyword_idx = j % len(multilingual_keywords)
|
||||
keyword = multilingual_keywords[keyword_idx]
|
||||
row["content_no_index"] = {
|
||||
"body": f"This is a {keyword} building",
|
||||
"title": f"Location {i}",
|
||||
"description": f"Description for {keyword} number {i}",
|
||||
}
|
||||
row["content_ngram"] = {
|
||||
"body": f"This is a {keyword} building",
|
||||
"title": f"Location {i}",
|
||||
"description": f"Description for {keyword} number {i}",
|
||||
}
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
|
||||
# Create vector index before loading collection
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(
|
||||
field_name=vector_field_name, metric_type="COSINE", index_type="IVF_FLAT", params={"nlist": 128}
|
||||
)
|
||||
|
||||
# Create NGRAM index with appropriate parameters for multilingual content
|
||||
min_gram = 1 # Use 1 for better multilingual support
|
||||
max_gram = 3
|
||||
index_params.add_index(
|
||||
field_name="content_ngram",
|
||||
index_name="content_ngram",
|
||||
index_type=index_type,
|
||||
params={
|
||||
"min_gram": min_gram,
|
||||
"max_gram": max_gram,
|
||||
"json_path": "content_ngram['body']",
|
||||
"json_cast_type": "varchar",
|
||||
},
|
||||
)
|
||||
self.create_index(client, collection_name, index_params)
|
||||
self.wait_for_index_ready(client, collection_name, index_name=vector_field_name)
|
||||
self.wait_for_index_ready(client, collection_name, index_name="content_ngram")
|
||||
self.load_collection(client, collection_name)
|
||||
|
||||
test_keywords = [
|
||||
"北京", # Chinese
|
||||
"東京", # Japanese
|
||||
"Моск", # Russian Cyrillic
|
||||
"café", # French accent
|
||||
"🏫", # Emoji
|
||||
"⭐", # Star emoji
|
||||
"مدرسة", # Arabic
|
||||
"한국", # Korean
|
||||
"München", # German umlaut
|
||||
"mix", # Mixed language
|
||||
"café☕", # Complex multilingual with emoji prefix
|
||||
"प्रविष्टि", # Hindi/Devanagari
|
||||
"Γειά", # Greek
|
||||
"português", # Portuguese with tilde
|
||||
"学", # Single CJK character
|
||||
]
|
||||
|
||||
for keyword in test_keywords:
|
||||
filter_expr = f'content_ngram["body"] LIKE "%{keyword}%"'
|
||||
res_ngram = self.query(client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"])[
|
||||
0
|
||||
]
|
||||
filter_expr = f'content_no_index["body"] LIKE "%{keyword}%"'
|
||||
res_no_index = self.query(
|
||||
client, collection_name, filter=filter_expr, output_fields=["id", "content_ngram"]
|
||||
)[0]
|
||||
|
||||
assert len(res_ngram) > 0
|
||||
assert sorted(res_ngram, key=lambda item: item["id"]) == sorted(res_no_index, key=lambda item: item["id"])
|
||||
@@ -0,0 +1,317 @@
|
||||
import time
|
||||
import random
|
||||
import pdb
|
||||
import threading
|
||||
import logging
|
||||
import json
|
||||
from multiprocessing import Pool, Process
|
||||
import pytest
|
||||
from utils.util_pymilvus import get_milvus, restart_server, gen_entities, gen_unique_str, default_nb
|
||||
from common.constants import default_fields, default_entities
|
||||
from common.common_type import CaseLabel
|
||||
|
||||
|
||||
uid = "wal"
|
||||
TIMEOUT = 120
|
||||
insert_interval_time = 1.5
|
||||
big_nb = 100000
|
||||
field_name = "float_vector"
|
||||
big_entities = gen_entities(big_nb)
|
||||
default_index = {"index_type": "IVF_FLAT", "params": {"nlist": 128}, "metric_type": "L2"}
|
||||
|
||||
|
||||
class TestRestartBase:
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test `create_partition` function
|
||||
******************************************************************
|
||||
"""
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def skip_check(self, args):
|
||||
logging.getLogger().info(args)
|
||||
if "service_name" not in args or not args["service_name"]:
|
||||
reason = "Skip if service name not provided"
|
||||
logging.getLogger().info(reason)
|
||||
pytest.skip(reason)
|
||||
if args["service_name"].find("shards") != -1:
|
||||
reason = "Skip restart cases in shards mode"
|
||||
logging.getLogger().info(reason)
|
||||
pytest.skip(reason)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_insert_flush(self, connect, collection, args):
|
||||
"""
|
||||
target: return the same row count after server restart
|
||||
method: call function: create collection, then insert/flush, restart server and assert row count
|
||||
expected: row count keep the same
|
||||
"""
|
||||
ids = connect.bulk_insert(collection, default_entities)
|
||||
connect.flush([collection])
|
||||
ids = connect.bulk_insert(collection, default_entities)
|
||||
connect.flush([collection])
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
assert res_count == 2 * default_nb
|
||||
# restart server
|
||||
logging.getLogger().info("Start restart server")
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
assert res_count == 2 * default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_insert_during_flushing(self, connect, collection, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collection, then insert/flushing, restart server and assert row count
|
||||
expected: row count equals 0
|
||||
"""
|
||||
# disable_autoflush()
|
||||
ids = connect.bulk_insert(collection, big_entities)
|
||||
connect.flush([collection], _async=True)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
if res_count < big_nb:
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_2)
|
||||
timeout = 300
|
||||
start_time = time.time()
|
||||
while new_connect.count_entities(collection) != big_nb and (time.time() - start_time < timeout):
|
||||
time.sleep(10)
|
||||
logging.getLogger().info(new_connect.count_entities(collection))
|
||||
res_count_3 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_3)
|
||||
assert res_count_3 == big_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_delete_during_flushing(self, connect, collection, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collection, then delete/flushing, restart server and assert row count
|
||||
expected: row count equals (nb - delete_length)
|
||||
"""
|
||||
# disable_autoflush()
|
||||
ids = connect.bulk_insert(collection, big_entities)
|
||||
connect.flush([collection])
|
||||
delete_length = 1000
|
||||
delete_ids = ids[big_nb//4:big_nb//4+delete_length]
|
||||
delete_res = connect.delete_entity_by_id(collection, delete_ids)
|
||||
connect.flush([collection], _async=True)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_2)
|
||||
timeout = 100
|
||||
start_time = time.time()
|
||||
while new_connect.count_entities(collection) != big_nb - delete_length and (time.time() - start_time < timeout):
|
||||
time.sleep(10)
|
||||
logging.getLogger().info(new_connect.count_entities(collection))
|
||||
if new_connect.count_entities(collection) == big_nb - delete_length:
|
||||
time.sleep(10)
|
||||
res_count_3 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_3)
|
||||
assert res_count_3 == big_nb - delete_length
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_during_indexed(self, connect, collection, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collection, then indexed, restart server and assert row count
|
||||
expected: row count equals nb
|
||||
"""
|
||||
# disable_autoflush()
|
||||
ids = connect.bulk_insert(collection, big_entities)
|
||||
connect.flush([collection])
|
||||
connect.create_index(collection, field_name, default_index)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
stats = connect.get_collection_stats(collection)
|
||||
# logging.getLogger().info(stats)
|
||||
# pdb.set_trace()
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
assert new_connect.count_entities(collection) == big_nb
|
||||
stats = connect.get_collection_stats(collection)
|
||||
for file in stats["partitions"][0]["segments"][0]["files"]:
|
||||
if file["field"] == field_name and file["name"] != "_raw":
|
||||
assert file["data_size"] > 0
|
||||
if file["index_type"] != default_index["index_type"]:
|
||||
assert False
|
||||
else:
|
||||
assert True
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_during_indexing(self, connect, collection, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collection, then indexing, restart server and assert row count
|
||||
expected: row count equals nb, server contitue to build index after restart
|
||||
"""
|
||||
# disable_autoflush()
|
||||
loop = 5
|
||||
for i in range(loop):
|
||||
ids = connect.bulk_insert(collection, big_entities)
|
||||
connect.flush([collection])
|
||||
connect.create_index(collection, field_name, default_index, _async=True)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
stats = connect.get_collection_stats(collection)
|
||||
# logging.getLogger().info(stats)
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_2)
|
||||
assert res_count_2 == loop * big_nb
|
||||
status = new_connect._cmd("status")
|
||||
assert json.loads(status)["indexing"] == True
|
||||
# timeout = 100
|
||||
# start_time = time.time()
|
||||
# while time.time() - start_time < timeout:
|
||||
# time.sleep(5)
|
||||
# assert new_connect.count_entities(collection) == loop * big_nb
|
||||
# stats = connect.get_collection_stats(collection)
|
||||
# assert stats["row_count"] == loop * big_nb
|
||||
# for file in stats["partitions"][0]["segments"][0]["files"]:
|
||||
# # logging.getLogger().info(file)
|
||||
# if file["field"] == field_name and file["name"] != "_raw":
|
||||
# assert file["data_size"] > 0
|
||||
# if file["index_type"] != default_ivf_flat_index["index_type"]:
|
||||
# continue
|
||||
# for file in stats["partitions"][0]["segments"][0]["files"]:
|
||||
# if file["field"] == field_name and file["name"] != "_raw":
|
||||
# assert file["data_size"] > 0
|
||||
# if file["index_type"] != default_ivf_flat_index["index_type"]:
|
||||
# assert False
|
||||
# else:
|
||||
# assert True
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_delete_flush_during_compacting(self, connect, collection, args):
|
||||
"""
|
||||
target: verify server work after restart during compaction
|
||||
method: call function: create collection, then delete/flush/compacting, restart server and assert row count
|
||||
call `compact` again, compact pass
|
||||
expected: row count equals (nb - delete_length)
|
||||
"""
|
||||
# disable_autoflush()
|
||||
ids = connect.bulk_insert(collection, big_entities)
|
||||
connect.flush([collection])
|
||||
delete_length = 1000
|
||||
loop = 10
|
||||
for i in range(loop):
|
||||
delete_ids = ids[i*delete_length:(i+1)*delete_length]
|
||||
delete_res = connect.delete_entity_by_id(collection, delete_ids)
|
||||
connect.flush([collection])
|
||||
connect.compact(collection, _async=True)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
assert res_count == big_nb - delete_length*loop
|
||||
info = connect.get_collection_stats(collection)
|
||||
size_old = info["partitions"][0]["segments"][0]["data_size"]
|
||||
logging.getLogger().info(size_old)
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_2)
|
||||
assert res_count_2 == big_nb - delete_length*loop
|
||||
info = connect.get_collection_stats(collection)
|
||||
size_before = info["partitions"][0]["segments"][0]["data_size"]
|
||||
status = connect.compact(collection)
|
||||
assert status.OK()
|
||||
info = connect.get_collection_stats(collection)
|
||||
size_after = info["partitions"][0]["segments"][0]["data_size"]
|
||||
assert size_before > size_after
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_insert_during_flushing_multi_collections(self, connect, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collections, then insert/flushing, restart server and assert row count
|
||||
expected: row count equals 0
|
||||
"""
|
||||
# disable_autoflush()
|
||||
collection_num = 2
|
||||
collection_list = []
|
||||
for i in range(collection_num):
|
||||
collection_name = gen_unique_str(uid)
|
||||
collection_list.append(collection_name)
|
||||
connect.create_collection(collection_name, default_fields)
|
||||
ids = connect.bulk_insert(collection_name, big_entities)
|
||||
connect.flush(collection_list, _async=True)
|
||||
res_count = connect.count_entities(collection_list[-1])
|
||||
logging.getLogger().info(res_count)
|
||||
if res_count < big_nb:
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection_list[-1])
|
||||
logging.getLogger().info(res_count_2)
|
||||
timeout = 300
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
count_list = []
|
||||
break_flag = True
|
||||
for index, name in enumerate(collection_list):
|
||||
tmp_count = new_connect.count_entities(name)
|
||||
count_list.append(tmp_count)
|
||||
logging.getLogger().info(count_list)
|
||||
if tmp_count != big_nb:
|
||||
break_flag = False
|
||||
break
|
||||
if break_flag == True:
|
||||
break
|
||||
time.sleep(10)
|
||||
for name in collection_list:
|
||||
assert new_connect.count_entities(name) == big_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def _test_insert_during_flushing_multi_partitions(self, connect, collection, args):
|
||||
"""
|
||||
target: flushing will recover
|
||||
method: call function: create collection/partition, then insert/flushing, restart server and assert row count
|
||||
expected: row count equals 0
|
||||
"""
|
||||
# disable_autoflush()
|
||||
partitions_num = 2
|
||||
partitions = []
|
||||
for i in range(partitions_num):
|
||||
tag_tmp = gen_unique_str()
|
||||
partitions.append(tag_tmp)
|
||||
connect.create_partition(collection, tag_tmp)
|
||||
ids = connect.bulk_insert(collection, big_entities, partition_name=tag_tmp)
|
||||
connect.flush([collection], _async=True)
|
||||
res_count = connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count)
|
||||
if res_count < big_nb:
|
||||
# restart server
|
||||
assert restart_server(args["service_name"])
|
||||
# assert row count again
|
||||
new_connect = get_milvus(args["ip"], args["port"], handler=args["handler"])
|
||||
res_count_2 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_2)
|
||||
timeout = 300
|
||||
start_time = time.time()
|
||||
while new_connect.count_entities(collection) != big_nb * 2 and (time.time() - start_time < timeout):
|
||||
time.sleep(10)
|
||||
logging.getLogger().info(new_connect.count_entities(collection))
|
||||
res_count_3 = new_connect.count_entities(collection)
|
||||
logging.getLogger().info(res_count_3)
|
||||
assert res_count_3 == big_nb * 2
|
||||
@@ -0,0 +1,489 @@
|
||||
##################################################################
|
||||
# All test cases in this file have been migrated to milvus_client#
|
||||
##################################################################
|
||||
'''
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from base.client_base import TestcaseBase
|
||||
from utils.util_log import test_log as log
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
|
||||
prefix = "alias"
|
||||
exp_name = "name"
|
||||
exp_schema = "schema"
|
||||
default_schema = cf.gen_default_collection_schema()
|
||||
default_binary_schema = cf.gen_default_binary_collection_schema()
|
||||
default_nb = ct.default_nb
|
||||
default_nb_medium = ct.default_nb_medium
|
||||
default_nq = ct.default_nq
|
||||
default_dim = ct.default_dim
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "int64 >= 0"
|
||||
default_search_field = ct.default_float_vec_field_name
|
||||
default_search_params = ct.default_search_params
|
||||
|
||||
|
||||
class TestAliasParamsInvalid(TestcaseBase):
|
||||
""" Negative test cases of alias interface parameters"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("alias_name", ["12-s", "12 s", "(mn)", "中文", "%$#", "a".join("a" for i in range(256))])
|
||||
def test_alias_create_alias_with_invalid_name(self, alias_name):
|
||||
"""
|
||||
target: test alias inserting data
|
||||
method: create a collection with invalid alias name
|
||||
expected: create alias failed
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
error = {ct.err_code: 1100, ct.err_msg: "Invalid collection alias"}
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
class TestAliasOperation(TestcaseBase):
|
||||
""" Test cases of alias interface operations"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_alias_alter_operation_default(self):
|
||||
"""
|
||||
target: test collection altering alias
|
||||
method:
|
||||
1. create collection_1, bind alias to collection_1 and insert 2000 entities
|
||||
2. create collection_2 with 1500 entities
|
||||
3. search on alias
|
||||
verify num_entities=2000
|
||||
4. alter alias to collection_2 and search on alias
|
||||
verify num_entities=1500
|
||||
"""
|
||||
c_name1 = cf.gen_unique_str("collection1")
|
||||
collection_w1 = self.init_collection_wrap(name=c_name1, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name1, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
# create a collection alias and bind to collection1
|
||||
self.utility_wrap.create_alias(collection_w1.name, alias_name)
|
||||
collection_alias = self.init_collection_wrap(name=alias_name)
|
||||
|
||||
nb1 = 2000
|
||||
data1 = cf.gen_default_dataframe_data(nb=nb1)
|
||||
import pandas as pd
|
||||
string_values = pd.Series(data=[str(i) for i in range(nb1)], dtype="string")
|
||||
data1[ct.default_string_field_name] = string_values
|
||||
collection_alias.insert(data1)
|
||||
collection_alias.create_index(ct.default_float_vec_field_name, ct.default_index)
|
||||
collection_alias.load()
|
||||
|
||||
assert collection_alias.num_entities == nb1 == collection_w1.num_entities
|
||||
res1 = collection_alias.query(expr="", output_fields=["count(*)"])[0]
|
||||
assert res1[0].get("count(*)") == nb1
|
||||
|
||||
# create collection2
|
||||
c_name2 = cf.gen_unique_str("collection2")
|
||||
collection_w2 = self.init_collection_wrap(name=c_name2, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name2, exp_schema: default_schema})
|
||||
nb2 = 1500
|
||||
data2 = cf.gen_default_dataframe_data(nb=nb2)
|
||||
string_values = pd.Series(data=[str(i) for i in range(nb2)], dtype="string")
|
||||
data2[ct.default_string_field_name] = string_values
|
||||
collection_w2.insert(data2)
|
||||
collection_w2.create_index(ct.default_float_vec_field_name, ct.default_index)
|
||||
collection_w2.load()
|
||||
|
||||
# alter the collection alias to collection2
|
||||
self.utility_wrap.alter_alias(collection_w2.name, alias_name)
|
||||
assert collection_alias.num_entities == nb2 == collection_w2.num_entities
|
||||
res1 = collection_alias.query(expr="", output_fields=["count(*)"])[0]
|
||||
assert res1[0].get("count(*)") == nb2
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alias_create_operation_default(self):
|
||||
"""
|
||||
target: test collection creating alias
|
||||
method:
|
||||
1.create a collection and create 10 partitions for it
|
||||
2.collection create an alias, then init a collection with this alias but not create partitions
|
||||
expected: collection is equal to alias
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
for _ in range(10):
|
||||
partition_name = cf.gen_unique_str("partition")
|
||||
# create partition with different names and check the partition exists
|
||||
self.init_partition_wrap(collection_w, partition_name)
|
||||
assert collection_w.has_partition(partition_name)[0]
|
||||
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
collection_alias = self.init_collection_wrap(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name, exp_schema: default_schema})
|
||||
# assert collection is equal to alias according to partitions
|
||||
assert [p.name for p in collection_w.partitions] == [
|
||||
p.name for p in collection_alias.partitions]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alias_drop_operation_default(self):
|
||||
"""
|
||||
target: test collection dropping alias
|
||||
method:
|
||||
1.create a collection with 10 partitions
|
||||
2.collection create an alias
|
||||
3.collection drop the alias
|
||||
expected:
|
||||
after step 2, collection is equal to alias
|
||||
after step 3, collection with alias name is not exist
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
for _ in range(10):
|
||||
partition_name = cf.gen_unique_str("partition")
|
||||
# create partition with different names and check the partition exists
|
||||
self.init_partition_wrap(collection_w, partition_name)
|
||||
assert collection_w.has_partition(partition_name)[0]
|
||||
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
# collection_w.create_alias(alias_name)
|
||||
collection_alias = self.init_collection_wrap(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name, exp_schema: default_schema})
|
||||
# assert collection is equal to alias according to partitions
|
||||
assert [p.name for p in collection_w.partitions] == [
|
||||
p.name for p in collection_alias.partitions]
|
||||
self.utility_wrap.drop_alias(alias_name)
|
||||
error = {ct.err_code: 0,
|
||||
ct.err_msg: f"Collection '{alias_name}' not exist, or you can pass in schema to create one"}
|
||||
collection_alias, _ = self.collection_wrap.init_collection(name=alias_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_called_by_utility_has_collection(self):
|
||||
"""
|
||||
target: test utility has collection by alias
|
||||
method:
|
||||
1.create collection with alias
|
||||
2.call has_collection function with alias as param
|
||||
expected: result is True
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
# collection_w.create_alias(alias_name)
|
||||
collection_alias, _ = self.collection_wrap.init_collection(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name,
|
||||
exp_schema: default_schema})
|
||||
res, _ = self.utility_wrap.has_collection(alias_name)
|
||||
|
||||
assert res is True
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_called_by_utility_drop_collection(self):
|
||||
"""
|
||||
target: test utility drop collection by alias
|
||||
method:
|
||||
1.create collection with alias
|
||||
2.call drop_collection function with alias as param
|
||||
expected: Got error: collection cannot be dropped via alias.
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
# collection_w.create_alias(alias_name)
|
||||
collection_alias, _ = self.collection_wrap.init_collection(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name,
|
||||
exp_schema: default_schema})
|
||||
assert self.utility_wrap.has_collection(c_name)[0]
|
||||
error = {ct.err_code: 1,
|
||||
ct.err_msg: f"cannot drop the collection via alias = {alias_name}"}
|
||||
self.utility_wrap.drop_collection(alias_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
self.utility_wrap.drop_alias(alias_name)
|
||||
self.utility_wrap.drop_collection(c_name)
|
||||
assert not self.utility_wrap.has_collection(c_name)[0]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_called_by_utility_has_partition(self):
|
||||
"""
|
||||
target: test utility has partition by alias
|
||||
method:
|
||||
1.create collection with partition and alias
|
||||
2.call has_partition function with alias as param
|
||||
expected: result is True
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
partition_name = cf.gen_unique_str("partition")
|
||||
self.init_partition_wrap(collection_w, partition_name)
|
||||
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
# collection_w.create_alias(alias_name)
|
||||
collection_alias, _ = self.collection_wrap.init_collection(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name,
|
||||
exp_schema: default_schema})
|
||||
res, _ = self.utility_wrap.has_partition(alias_name, partition_name)
|
||||
|
||||
assert res is True
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_enable_mmap_by_alias(self):
|
||||
"""
|
||||
target: enable or disable mmap by alias
|
||||
method: enable or disable mmap by alias
|
||||
expected: successfully enable mmap
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(c_name, schema=default_schema)
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
collection_alias, _ = self.collection_wrap.init_collection(name=alias_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name,
|
||||
exp_schema: default_schema})
|
||||
collection_alias.set_properties({'mmap.enabled': True})
|
||||
pro = collection_w.describe()[0].get("properties")
|
||||
assert pro["mmap.enabled"] == 'True'
|
||||
collection_w.set_properties({'mmap.enabled': False})
|
||||
pro = collection_alias.describe().get("properties")
|
||||
assert pro["mmap.enabled"] == 'False'
|
||||
|
||||
##########################################################
|
||||
# class TestAliasOperationInvalid() has been migrated to milvus_client
|
||||
##########################################################
|
||||
class TestAliasOperationInvalid(TestcaseBase):
|
||||
""" Negative test cases of alias interface operations"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alias_create_duplication_alias(self):
|
||||
"""
|
||||
target: test two collections creating alias with same name
|
||||
method:
|
||||
1.create a collection_1 with alias name alias_a
|
||||
2.create a collection_2 also with alias name alias_a
|
||||
expected:
|
||||
in step 2, creating alias with a duplication name is not allowed
|
||||
"""
|
||||
self._connect()
|
||||
c_1_name = cf.gen_unique_str("collection")
|
||||
collection_1 = self.init_collection_wrap(name=c_1_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_1_name, exp_schema: default_schema})
|
||||
alias_a_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_1.name, alias_a_name)
|
||||
|
||||
c_2_name = cf.gen_unique_str("collection")
|
||||
collection_2 = self.init_collection_wrap(name=c_2_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_2_name, exp_schema: default_schema})
|
||||
error = {ct.err_code: 1602,
|
||||
ct.err_msg: f"{alias_a_name} is alias to another collection: {collection_1.name}: "
|
||||
f"alias already exist[database=default][alias={alias_a_name}]"}
|
||||
self.utility_wrap.create_alias(collection_2.name, alias_a_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alias_alter_not_exist_alias(self):
|
||||
"""
|
||||
target: test collection altering to alias which is not exist
|
||||
method:
|
||||
1.create a collection with alias
|
||||
2.collection alters to an alias name which is not exist
|
||||
expected:
|
||||
in step 2, alter alias with a not exist name is not allowed
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
|
||||
alias_not_exist_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 1600,
|
||||
ct.err_msg: f"alias not found[database=default][alias={alias_not_exist_name}]"}
|
||||
self.utility_wrap.alter_alias(collection_w.name, alias_not_exist_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_drop_not_exist_alias(self):
|
||||
"""
|
||||
target: test collection dropping alias which is not exist
|
||||
method:
|
||||
1.create a collection with alias
|
||||
2.collection drop alias which is not exist
|
||||
expected: drop alias succ
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
alias_not_exist_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.drop_alias(alias_not_exist_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_drop_same_alias_twice(self):
|
||||
"""
|
||||
target: test collection dropping same alias twice
|
||||
method:
|
||||
1.create a collection with alias
|
||||
2.collection drop alias
|
||||
3.collection drop alias again
|
||||
expected: drop alias succ
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
self.utility_wrap.drop_alias(alias_name)
|
||||
# @longjiquan: dropping alias should be idempotent.
|
||||
self.utility_wrap.drop_alias(alias_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_create_dup_name_collection(self):
|
||||
"""
|
||||
target: test creating a collection with a same name as alias, but a different schema
|
||||
method:
|
||||
1.create a collection with alias
|
||||
2.create a collection with same name as alias, but a different schema
|
||||
expected: in step 2, create collection failed
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
# collection_w.create_alias(alias_name)
|
||||
|
||||
error = {ct.err_code: 0,
|
||||
ct.err_msg: "The collection already exist, but the schema is not the same as the schema passed in"}
|
||||
self.init_collection_wrap(alias_name, schema=default_binary_schema,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_alias_drop_collection_by_alias(self):
|
||||
"""
|
||||
target: test dropping a collection by alias
|
||||
method:
|
||||
1.create a collection with alias
|
||||
2.drop a collection by alias
|
||||
expected: in step 2, drop collection by alias failed by design
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
schema = cf.gen_default_collection_schema(description="this is for alias decsription")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
collection_alias = self.init_collection_wrap(name=alias_name, schema=schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: alias_name,
|
||||
exp_schema: schema})
|
||||
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"cannot drop the collection via alias = {alias_name}"}
|
||||
collection_alias.drop(check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_alias_reuse_alias_name_from_dropped_collection(self):
|
||||
"""
|
||||
target: test dropping a collection which has a alias
|
||||
method:
|
||||
1.create a collection
|
||||
2.create an alias for the collection
|
||||
3.drop the collection
|
||||
4.create a new collection
|
||||
5.create an alias with the same alias name for the new collection
|
||||
expected: in step 5, create alias with the same name for the new collection succ
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
res = self.utility_wrap.list_aliases(c_name)[0]
|
||||
assert len(res) == 1
|
||||
|
||||
# dropping collection that has an alias shall drop the alias as well
|
||||
self.utility_wrap.drop_alias(alias_name)
|
||||
collection_w.drop()
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
res2 = self.utility_wrap.list_aliases(c_name)[0]
|
||||
assert len(res2) == 0
|
||||
# the same alias name can be reused for another collection
|
||||
self.utility_wrap.create_alias(c_name, alias_name)
|
||||
res2 = self.utility_wrap.list_aliases(c_name)[0]
|
||||
assert len(res2) == 1
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_alias_rename_collection_to_alias_name(self):
|
||||
"""
|
||||
target: test renaming a collection to a alias name
|
||||
method:
|
||||
1.create a collection
|
||||
2.create an alias for the collection
|
||||
3.rename the collection to the alias name
|
||||
expected: in step 3, rename collection to alias name failed
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str("collection")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=default_schema,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
alias_name = cf.gen_unique_str(prefix)
|
||||
self.utility_wrap.create_alias(collection_w.name, alias_name)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"cannot rename collection to an existing alias: {alias_name}"}
|
||||
self.utility_wrap.rename_collection(collection_w.name, alias_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
'''
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
import random
|
||||
|
||||
import numpy
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from pymilvus import DataType
|
||||
from base.client_base import TestcaseBase
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_pymilvus import *
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
prefix = "collection"
|
||||
exp_name = "name"
|
||||
exp_schema = "schema"
|
||||
exp_num = "num_entities"
|
||||
exp_primary = "primary"
|
||||
exp_shards_num = "shards_num"
|
||||
default_term_expr = f'{ct.default_int64_field_name} in [0, 1]'
|
||||
default_schema = cf.gen_default_collection_schema()
|
||||
default_binary_schema = cf.gen_default_binary_collection_schema()
|
||||
default_shards_num = 1
|
||||
uid_count = "collection_count"
|
||||
tag = "collection_count_tag"
|
||||
uid_stats = "get_collection_stats"
|
||||
uid_create = "create_collection"
|
||||
uid_describe = "describe_collection"
|
||||
uid_drop = "drop_collection"
|
||||
uid_has = "has_collection"
|
||||
uid_list = "list_collections"
|
||||
uid_load = "load_collection"
|
||||
partition1 = 'partition1'
|
||||
partition2 = 'partition2'
|
||||
field_name = default_float_vec_field_name
|
||||
default_single_query = {
|
||||
"data": gen_vectors(1, default_dim),
|
||||
"anns_field": default_float_vec_field_name,
|
||||
"param": {"metric_type": "L2", "params": {"nprobe": 10}},
|
||||
"limit": default_top_k,
|
||||
}
|
||||
|
||||
default_index_params = {"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}}
|
||||
default_binary_index_params = {"index_type": "BIN_IVF_FLAT", "metric_type": "JACCARD", "params": {"nlist": 64}}
|
||||
default_nq = ct.default_nq
|
||||
default_search_exp = "int64 >= 0"
|
||||
default_limit = ct.default_limit
|
||||
vectors = [[random.random() for _ in range(default_dim)] for _ in range(default_nq)]
|
||||
default_search_field = ct.default_float_vec_field_name
|
||||
default_search_params = ct.default_search_params
|
||||
max_vector_field_num = ct.max_vector_field_num
|
||||
SPARSE_FLOAT_VECTOR_data_type = DataType.SPARSE_FLOAT_VECTOR
|
||||
|
||||
|
||||
class TestCollectionParams(TestcaseBase):
|
||||
""" Test case of collection interface """
|
||||
|
||||
@pytest.fixture(scope="function", params=cf.gen_all_type_fields())
|
||||
def get_unsupported_primary_field(self, request):
|
||||
if request.param.dtype == DataType.INT64 or request.param.dtype == DataType.VARCHAR:
|
||||
pytest.skip("int64 type is valid primary key")
|
||||
yield request.param
|
||||
|
||||
@pytest.fixture(scope="function", params=ct.invalid_dims)
|
||||
def get_invalid_dim(self, request):
|
||||
yield request.param
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_collection_invalid_schema_type(self):
|
||||
"""
|
||||
target: test collection with an invalid schema type
|
||||
method: create collection with non-CollectionSchema type schema
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
field, _ = self.field_schema_wrap.init_field_schema(name="field_name", dtype=DataType.INT64, is_primary=True)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Schema type must be schema.CollectionSchema"}
|
||||
self.collection_wrap.init_collection(c_name, schema=field,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_collection_none_schema(self):
|
||||
"""
|
||||
target: test collection with none schema
|
||||
method: create collection with none schema
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"Collection '{c_name}' not exist, or you can pass in schema to create one."}
|
||||
self.collection_wrap.init_collection(c_name, schema=None, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
|
||||
class TestCollectionDataframe(TestcaseBase):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test construct_from_dataframe
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_construct_from_dataframe(self):
|
||||
"""
|
||||
target: test collection with dataframe data
|
||||
method: create collection and insert with dataframe
|
||||
expected: collection num entities equal to nb
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
# flush
|
||||
assert self.collection_wrap.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_construct_from_binary_dataframe(self):
|
||||
"""
|
||||
target: test binary collection with dataframe
|
||||
method: create binary collection with dataframe
|
||||
expected: collection num entities equal to nb
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df, _ = cf.gen_default_binary_dataframe_data(nb=ct.default_nb)
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_binary_schema})
|
||||
assert self.collection_wrap.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_none_dataframe(self):
|
||||
"""
|
||||
target: test create collection by empty dataframe
|
||||
method: invalid dataframe type create collection
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 999, ct.err_msg: "Data type must be pandas.DataFrame"}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, None, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_dataframe_only_column(self):
|
||||
"""
|
||||
target: test collection with dataframe only columns
|
||||
method: dataframe only has columns
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = pd.DataFrame(columns=[ct.default_int64_field_name, ct.default_float_vec_field_name])
|
||||
error = {ct.err_code: 0, ct.err_msg: "Cannot infer schema from empty dataframe"}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_inconsistent_dataframe(self):
|
||||
"""
|
||||
target: test collection with data inconsistent
|
||||
method: create and insert with inconsistent data
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
# one field different type df
|
||||
mix_data = [(1, 2., [0.1, 0.2]), (2, 3., 4)]
|
||||
df = pd.DataFrame(data=mix_data, columns=list("ABC"))
|
||||
error = {ct.err_code: 1,
|
||||
ct.err_msg: "The Input data type is inconsistent with defined schema, "
|
||||
"{C} field should be a FLOAT_VECTOR, but got a {<class 'list'>} instead."}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field='A', check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_non_dataframe(self):
|
||||
"""
|
||||
target: test create collection by invalid dataframe
|
||||
method: non-dataframe type create collection
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Data type must be pandas.DataFrame."}
|
||||
df = cf.gen_default_list_data(nb=10)
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_data_type_dataframe(self):
|
||||
"""
|
||||
target: test collection with invalid dataframe
|
||||
method: create with invalid dataframe
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = pd.DataFrame({"date": pd.date_range('20210101', periods=3), ct.default_int64_field_name: [1, 2, 3]})
|
||||
error = {ct.err_code: 0, ct.err_msg: "Cannot infer schema from empty dataframe."}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_from_invalid_field_name(self):
|
||||
"""
|
||||
target: test collection with invalid field name
|
||||
method: create with invalid field name dataframe
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = pd.DataFrame({'%$#': cf.gen_vectors(3, 2), ct.default_int64_field_name: [1, 2, 3]})
|
||||
error = {ct.err_code: 1, ct.err_msg: "Invalid field name"}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_none_primary_field(self):
|
||||
"""
|
||||
target: test collection with none primary field
|
||||
method: primary_field is none
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Schema must have a primary key field."}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=None,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_not_existed_primary_field(self):
|
||||
"""
|
||||
target: test collection with not existed primary field
|
||||
method: primary field not existed
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Primary field must in dataframe."}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=c_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_with_none_auto_id(self):
|
||||
"""
|
||||
target: test construct with non-int64 as primary field
|
||||
method: non-int64 as primary field
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Param auto_id must be bool type"}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
auto_id=None, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_auto_id_true_insert(self):
|
||||
"""
|
||||
target: test construct with true auto_id
|
||||
method: auto_id=True and insert values
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(nb=100)
|
||||
error = {ct.err_code: 0, ct.err_msg: "Auto_id is True, primary field should not have data."}
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
auto_id=True, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_auto_id_true_no_insert(self):
|
||||
"""
|
||||
target: test construct with true auto_id
|
||||
method: auto_id=True and not insert ids(primary fields all values are None)
|
||||
expected: verify num entities
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data()
|
||||
# df.drop(ct.default_int64_field_name, axis=1, inplace=True)
|
||||
df[ct.default_int64_field_name] = None
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
auto_id=True)
|
||||
assert self.collection_wrap.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_none_value_auto_id_true(self):
|
||||
"""
|
||||
target: test construct with none value, auto_id
|
||||
method: df primary field with none value, auto_id=true
|
||||
expected: todo
|
||||
"""
|
||||
self._connect()
|
||||
nb = 100
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
df.iloc[:, 0] = numpy.NaN
|
||||
res, _ = self.collection_wrap.construct_from_dataframe(cf.gen_unique_str(prefix), df,
|
||||
primary_field=ct.default_int64_field_name, auto_id=True)
|
||||
mutation_res = res[1]
|
||||
assert cf._check_primary_keys(mutation_res.primary_keys, 100)
|
||||
assert self.collection_wrap.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_auto_id_false(self):
|
||||
"""
|
||||
target: test construct with false auto_id
|
||||
method: auto_id=False, primary_field correct
|
||||
expected: verify auto_id
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
auto_id=False)
|
||||
assert not self.collection_wrap.schema.auto_id
|
||||
assert self.collection_wrap.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_construct_none_value_auto_id_false(self):
|
||||
"""
|
||||
target: test construct with none value, auto_id
|
||||
method: df primary field with none value, auto_id=false
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
nb = 100
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
df.iloc[:, 0] = numpy.NaN
|
||||
error = {ct.err_code: 0, ct.err_msg: "Primary key type must be DataType.INT64"}
|
||||
self.collection_wrap.construct_from_dataframe(cf.gen_unique_str(prefix), df,
|
||||
primary_field=ct.default_int64_field_name, auto_id=False,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_auto_id_false_same_values(self):
|
||||
"""
|
||||
target: test construct with false auto_id and same value
|
||||
method: auto_id=False, primary field same values
|
||||
expected: verify num entities
|
||||
"""
|
||||
self._connect()
|
||||
nb = 100
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
df.iloc[1:, 0] = 1
|
||||
res, _ = self.collection_wrap.construct_from_dataframe(cf.gen_unique_str(prefix), df,
|
||||
primary_field=ct.default_int64_field_name, auto_id=False)
|
||||
collection_w = res[0]
|
||||
collection_w.flush()
|
||||
assert collection_w.num_entities == nb
|
||||
mutation_res = res[1]
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_auto_id_false_negative_values(self):
|
||||
"""
|
||||
target: test construct with negative values
|
||||
method: auto_id=False, primary field values is negative
|
||||
expected: verify num entities
|
||||
"""
|
||||
self._connect()
|
||||
nb = 100
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
new_values = pd.Series(data=[i for i in range(0, -nb, -1)])
|
||||
df[ct.default_int64_field_name] = new_values
|
||||
self.collection_wrap.construct_from_dataframe(cf.gen_unique_str(prefix), df,
|
||||
primary_field=ct.default_int64_field_name, auto_id=False)
|
||||
assert self.collection_wrap.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_construct_from_dataframe_dup_name(self):
|
||||
"""
|
||||
target: test collection with dup name and insert dataframe
|
||||
method: create collection with dup name, none schema, dataframe
|
||||
expected: two collection object is correct
|
||||
"""
|
||||
self._connect()
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
self.collection_wrap.construct_from_dataframe(c_name, df, primary_field=ct.default_int64_field_name,
|
||||
check_task=CheckTasks.check_collection_property,
|
||||
check_items={exp_name: c_name, exp_schema: default_schema})
|
||||
# flush
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
assert collection_w.num_entities == self.collection_wrap.num_entities
|
||||
|
||||
|
||||
class TestLoadCollection(TestcaseBase):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test `collection.load()` function
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
def test_load_replica_non_shard_leader(self):
|
||||
"""
|
||||
target: test replica groups which one of QN is not shard leader
|
||||
method: 1.deploy cluster with 5 QNs
|
||||
2.create collection with 2 shards
|
||||
3.insert and flush
|
||||
4.load with 2 replica number
|
||||
5.insert growing data
|
||||
6.search and query
|
||||
expected: Verify search and query results
|
||||
"""
|
||||
# create and insert entities
|
||||
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix), shards_num=2)
|
||||
df = cf.gen_default_dataframe_data()
|
||||
collection_w.insert(df)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
||||
|
||||
# load with multi replica and insert growing data
|
||||
collection_w.load(replica_number=2)
|
||||
df_growing = cf.gen_default_dataframe_data(100, start=ct.default_nb)
|
||||
collection_w.insert(df_growing)
|
||||
|
||||
replicas = collection_w.get_replicas()[0]
|
||||
# verify there are 2 groups (2 replicas)
|
||||
assert len(replicas.groups) == 2
|
||||
log.debug(replicas)
|
||||
all_group_nodes = []
|
||||
for group in replicas.groups:
|
||||
# verify each group have 3 shards
|
||||
assert len(group.shards) == 2
|
||||
all_group_nodes.extend(group.group_nodes)
|
||||
# verify all groups has 5 querynodes
|
||||
assert len(all_group_nodes) == 5
|
||||
|
||||
# Verify 2 replicas segments loaded
|
||||
seg_info, _ = self.utility_wrap.get_query_segment_info(collection_w.name)
|
||||
for seg in seg_info:
|
||||
assert len(seg.nodeIds) == 2
|
||||
|
||||
# verify search successfully
|
||||
res, _ = collection_w.search(vectors, default_search_field, default_search_params, default_limit)
|
||||
assert len(res[0]) == ct.default_limit
|
||||
|
||||
# verify query sealed and growing data successfully
|
||||
collection_w.query(expr=f"{ct.default_int64_field_name} in [0, {ct.default_nb}]",
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={'exp_res': [{'int64': 0}, {'int64': 3000}]})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
def test_load_replica_multiple_shard_leader(self):
|
||||
"""
|
||||
target: test replica groups which one of QN is shard leader of multiple shards
|
||||
method: 1.deploy cluster with 5 QNs
|
||||
2.create collection with 3 shards
|
||||
3.insert and flush
|
||||
4.load with 2 replica number
|
||||
5.insert growng data
|
||||
6.search and query
|
||||
expected: Verify search and query results
|
||||
"""
|
||||
# craete and insert
|
||||
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix), shards_num=3)
|
||||
df = cf.gen_default_dataframe_data()
|
||||
collection_w.insert(df)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
||||
|
||||
# load with multi replicas and insert growing data
|
||||
collection_w.load(replica_number=2)
|
||||
df_growing = cf.gen_default_dataframe_data(100, start=ct.default_nb)
|
||||
collection_w.insert(df_growing)
|
||||
|
||||
# verify replica infos
|
||||
replicas, _ = collection_w.get_replicas()
|
||||
log.debug(replicas)
|
||||
assert len(replicas.groups) == 2
|
||||
all_group_nodes = []
|
||||
for group in replicas.groups:
|
||||
# verify each group have 3 shards
|
||||
assert len(group.shards) == 3
|
||||
all_group_nodes.extend(group.group_nodes)
|
||||
# verify all groups has 5 querynodes
|
||||
assert len(all_group_nodes) == 5
|
||||
|
||||
# Verify 2 replicas segments loaded
|
||||
seg_info, _ = self.utility_wrap.get_query_segment_info(collection_w.name)
|
||||
for seg in seg_info:
|
||||
assert len(seg.nodeIds) == 2
|
||||
|
||||
# Verify search successfully
|
||||
res, _ = collection_w.search(vectors, default_search_field, default_search_params, default_limit)
|
||||
assert len(res[0]) == ct.default_limit
|
||||
|
||||
# Verify query sealed and growing entities successfully
|
||||
collection_w.query(expr=f"{ct.default_int64_field_name} in [0, {ct.default_nb}]",
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={'exp_res': [{'int64': 0}, {'int64': 3000}]})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
def test_load_replica_sq_count_balance(self):
|
||||
"""
|
||||
target: test load with multi replicas, and sq request load balance cross replicas
|
||||
method: 1.Deploy milvus with multi querynodes
|
||||
2.Insert entities and load with replicas
|
||||
3.Do query req many times
|
||||
4.Verify the querynode sq_req_count metrics
|
||||
expected: Infer whether the query request is load balanced.
|
||||
"""
|
||||
from utils.util_k8s import get_metrics_querynode_sq_req_count
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data(nb=5000)
|
||||
mutation_res, _ = collection_w.insert(df)
|
||||
assert collection_w.num_entities == 5000
|
||||
total_sq_count = 20
|
||||
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
||||
|
||||
collection_w.load(replica_number=3)
|
||||
for i in range(total_sq_count):
|
||||
ids = [random.randint(0, 100) for _ in range(5)]
|
||||
collection_w.query(f"{ct.default_int64_field_name} in {ids}")
|
||||
|
||||
replicas, _ = collection_w.get_replicas()
|
||||
log.debug(replicas)
|
||||
sq_req_count = get_metrics_querynode_sq_req_count()
|
||||
for group in replicas.groups:
|
||||
group_nodes = group.group_nodes
|
||||
group_sq_req_count = 0
|
||||
for node in group_nodes:
|
||||
group_sq_req_count += sq_req_count[node]
|
||||
log.debug(f"Group nodes {group_nodes} with total sq_req_count {group_sq_req_count}")
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_get_collection_replicas_not_loaded(self):
|
||||
"""
|
||||
target: test get replicas of not loaded collection
|
||||
method: not loaded collection and get replicas
|
||||
expected: raise an exception
|
||||
"""
|
||||
# create, insert
|
||||
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data()
|
||||
insert_res, _ = collection_w.insert(df)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
res, _ = collection_w.get_replicas()
|
||||
assert len(res.groups) == 0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
import time
|
||||
import pytest
|
||||
import json
|
||||
from time import sleep
|
||||
from pymilvus import connections
|
||||
from chaos.checker import (InsertChecker,
|
||||
UpsertChecker,
|
||||
SearchChecker,
|
||||
HybridSearchChecker,
|
||||
QueryChecker,
|
||||
DeleteChecker,
|
||||
Op,
|
||||
ResultAnalyzer
|
||||
)
|
||||
from utils.util_log import test_log as log
|
||||
from chaos import chaos_commons as cc
|
||||
from common import common_func as cf
|
||||
from chaos.chaos_commons import assert_statistic
|
||||
from common.common_type import CaseLabel
|
||||
from chaos import constants
|
||||
from delayed_assert import assert_expectations
|
||||
|
||||
|
||||
def get_all_collections():
|
||||
try:
|
||||
with open("/tmp/ci_logs/all_collections.json", "r") as f:
|
||||
data = json.load(f)
|
||||
all_collections = data["all"]
|
||||
except Exception as e:
|
||||
log.warning(f"get_all_collections error: {e}")
|
||||
return [None]
|
||||
return all_collections
|
||||
|
||||
|
||||
class TestBase:
|
||||
expect_create = constants.SUCC
|
||||
expect_insert = constants.SUCC
|
||||
expect_flush = constants.SUCC
|
||||
expect_compact = constants.SUCC
|
||||
expect_search = constants.SUCC
|
||||
expect_query = constants.SUCC
|
||||
host = '127.0.0.1'
|
||||
port = 19530
|
||||
_chaos_config = None
|
||||
health_checkers = {}
|
||||
|
||||
|
||||
class TestOperations(TestBase):
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def connection(self, host, port, user, password, milvus_ns):
|
||||
if user and password:
|
||||
connections.connect('default', host=host, port=port, user=user, password=password)
|
||||
else:
|
||||
connections.connect('default', host=host, port=port)
|
||||
if connections.has_connection("default") is False:
|
||||
raise Exception("no connections")
|
||||
log.info("connect to milvus successfully")
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
def init_health_checkers(self, collection_name=None):
|
||||
c_name = collection_name
|
||||
checkers = {
|
||||
Op.insert: InsertChecker(collection_name=c_name),
|
||||
Op.upsert: UpsertChecker(collection_name=c_name),
|
||||
Op.search: SearchChecker(collection_name=c_name),
|
||||
Op.hybrid_search: HybridSearchChecker(collection_name=c_name),
|
||||
Op.query: QueryChecker(collection_name=c_name),
|
||||
Op.delete: DeleteChecker(collection_name=c_name),
|
||||
}
|
||||
self.health_checkers = checkers
|
||||
|
||||
@pytest.fixture(scope="function", params=get_all_collections())
|
||||
def collection_name(self, request):
|
||||
if request.param == [] or request.param == "":
|
||||
pytest.skip("The collection name is invalid")
|
||||
yield request.param
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
def test_operations(self, request_duration, collection_name):
|
||||
# start the monitor threads to check the milvus ops
|
||||
log.info("*********************Test Start**********************")
|
||||
log.info(connections.get_connection_addr('default'))
|
||||
c_name = collection_name if collection_name else cf.gen_unique_str("Checker_")
|
||||
self.init_health_checkers(collection_name=c_name)
|
||||
cc.start_monitor_threads(self.health_checkers)
|
||||
log.info("*********************Load Start**********************")
|
||||
request_duration = request_duration.replace("h", "*3600+").replace("m", "*60+").replace("s", "")
|
||||
if request_duration[-1] == "+":
|
||||
request_duration = request_duration[:-1]
|
||||
request_duration = eval(request_duration)
|
||||
for i in range(10):
|
||||
sleep(request_duration//10)
|
||||
for k, v in self.health_checkers.items():
|
||||
v.check_result()
|
||||
time.sleep(60)
|
||||
ra = ResultAnalyzer()
|
||||
ra.get_stage_success_rate()
|
||||
assert_statistic(self.health_checkers)
|
||||
assert_expectations()
|
||||
log.info("*********************Test Completed**********************")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,941 @@
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from base.client_base import TestcaseBase
|
||||
from common.common_type import CheckTasks, CaseLabel
|
||||
from common.common_func import param_info
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
prefix = "db"
|
||||
|
||||
|
||||
@pytest.mark.skip("removed to test_milvus_client_database.py")
|
||||
class TestDatabaseParams(TestcaseBase):
|
||||
""" Test case of database """
|
||||
|
||||
def setup_method(self, method):
|
||||
param_info.param_user = ct.default_user
|
||||
param_info.param_password = ct.default_password
|
||||
super().setup_method(method)
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""
|
||||
teardown method: drop collection and db
|
||||
"""
|
||||
log.info("[database_teardown_method] Start teardown database test cases ...")
|
||||
self._connect()
|
||||
|
||||
# clear db
|
||||
for db in self.database_wrap.list_database()[0]:
|
||||
# using db
|
||||
self.database_wrap.using_database(db)
|
||||
|
||||
# drop db collections
|
||||
colls, _ = self.utility_wrap.list_collections()
|
||||
for coll in colls:
|
||||
self.utility_wrap.drop_collection(coll)
|
||||
|
||||
# drop db
|
||||
if db != ct.default_db:
|
||||
self.database_wrap.drop_database(db)
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert dbs == [ct.default_db]
|
||||
|
||||
super().teardown_method(method)
|
||||
|
||||
def test_db_default(self):
|
||||
"""
|
||||
target: test normal db interface
|
||||
method: 1. connect with default db
|
||||
2. create a new db
|
||||
3. list db and verify db created successfully
|
||||
4. using new db and create collection without specifying a name
|
||||
4. using default db
|
||||
5. create new collection and specify db name
|
||||
6. list collection in the new db
|
||||
7. drop db, collections will also be dropped
|
||||
expected: 1. all db interface
|
||||
2. using db can change the default db
|
||||
3. drop databases also drop collections
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
# using default db and create collection
|
||||
collection_w_default = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# create db
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# list db and verify db
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert db_name in dbs
|
||||
|
||||
# using db and create collection
|
||||
self.database_wrap.using_database(db_name)
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# using default db
|
||||
self.database_wrap.using_database(ct.default_db)
|
||||
collections_default, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w_default.name in collections_default
|
||||
assert collection_w.name not in collections_default
|
||||
|
||||
# using db
|
||||
self.database_wrap.using_database(db_name)
|
||||
collections, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w.name in collections
|
||||
assert collection_w_default.name not in collections
|
||||
|
||||
# drop collection and drop db
|
||||
collection_w.drop()
|
||||
self.database_wrap.drop_database(db_name=db_name)
|
||||
dbs_afrer_drop, _ = self.database_wrap.list_database()
|
||||
assert db_name not in dbs_afrer_drop
|
||||
|
||||
@pytest.mark.parametrize("db_name", ct.invalid_resource_names)
|
||||
def test_create_db_invalid_name_value(self, db_name):
|
||||
"""
|
||||
target: test create db with invalid name
|
||||
method: create db with invalid name
|
||||
expected: error
|
||||
"""
|
||||
self._connect()
|
||||
error = {ct.err_code: 802, ct.err_msg: "invalid database name[database=%s]" % db_name}
|
||||
if db_name is None:
|
||||
error = {ct.err_code: 999, ct.err_msg: f"`db_name` value {db_name} is illegal"}
|
||||
self.database_wrap.create_database(db_name=db_name, check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
def test_create_db_without_connection(self):
|
||||
"""
|
||||
target: test create db without connection
|
||||
method: create db without connection
|
||||
expected: exception
|
||||
"""
|
||||
self.connection_wrap.disconnect(ct.default_alias)
|
||||
error = {ct.err_code: 1, ct.err_msg: "should create connect first"}
|
||||
self.database_wrap.create_database(cf.gen_unique_str(), check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
def test_create_default_db(self):
|
||||
"""
|
||||
target: test create db with default db name "default"
|
||||
method: create db with name "default"
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
error = {ct.err_code: 1, ct.err_msg: "database already exist: default"}
|
||||
self.database_wrap.create_database(ct.default_db, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.parametrize("invalid_name", ct.invalid_resource_names)
|
||||
def test_drop_db_invalid_name(self, invalid_name):
|
||||
"""
|
||||
target: test drop db with invalid name
|
||||
method: drop db with invalid name
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
# create db
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
# drop db
|
||||
error = {ct.err_code: 802, ct.err_msg: "invalid database name[database=%s]" % db_name}
|
||||
if db_name is None:
|
||||
error = {ct.err_code: 999, ct.err_msg: f"`db_name` value {db_name} is illegal"}
|
||||
self.database_wrap.drop_database(db_name=invalid_name, check_task=CheckTasks.err_res, check_items=error)
|
||||
# created db is existing
|
||||
self.database_wrap.create_database(db_name, check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 65535,
|
||||
ct.err_msg: "database already exist: %s" % db_name})
|
||||
self.database_wrap.drop_database(db_name)
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert db_name not in dbs
|
||||
|
||||
def test_list_db_not_existed_connection_using(self):
|
||||
"""
|
||||
target: test list db with a not existed connection using
|
||||
method: list db with a random using
|
||||
expected: exception
|
||||
"""
|
||||
# connect with default alias using
|
||||
self._connect()
|
||||
|
||||
# list db with not existed using
|
||||
self.database_wrap.list_database(using="random", check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1, ct.err_msg: "should create connect first."})
|
||||
|
||||
@pytest.mark.parametrize("timeout", ["", -1, 0])
|
||||
def test_list_db_with_invalid_timeout(self, timeout):
|
||||
"""
|
||||
target: test lst db with invalid timeout
|
||||
method: list db with invalid timeout
|
||||
expected: exception
|
||||
"""
|
||||
# connect with default alias using
|
||||
self._connect()
|
||||
|
||||
# list db with not existed using
|
||||
self.database_wrap.list_database(timeout=timeout, check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1,
|
||||
ct.err_msg: "StatusCode.DEADLINE_EXCEEDED"})
|
||||
|
||||
@pytest.mark.parametrize("invalid_db_name", [(), [], 1, [1, "2", 3], (1,), {1: 1}])
|
||||
def test_using_invalid_db(self, invalid_db_name):
|
||||
"""
|
||||
target: test using with invalid db name
|
||||
method: using invalid db
|
||||
expected: exception
|
||||
"""
|
||||
# connect with default alias using
|
||||
self._connect()
|
||||
|
||||
# create collection in default db
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# using db with invalid name
|
||||
self.database_wrap.using_database(db_name=invalid_db_name, check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1, ct.err_msg: "db existed"})
|
||||
|
||||
# verify using db is default db
|
||||
collections, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w.name in collections
|
||||
|
||||
@pytest.mark.parametrize("invalid_db_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
def test_using_invalid_db_2(self, invalid_db_name):
|
||||
# connect with default alias using
|
||||
self._connect()
|
||||
|
||||
# create collection in default db
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# using db with invalid name
|
||||
error = {ct.err_code: 800, ct.err_msg: "database not found[database=%s]" % invalid_db_name}
|
||||
if invalid_db_name == "中文":
|
||||
error = {ct.err_code: 1, ct.err_msg: "<metadata was invalid: [('dbname', '中文')"}
|
||||
self.database_wrap.using_database(db_name=invalid_db_name, check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
|
||||
@pytest.mark.skip("removed to test_milvus_client_database.py")
|
||||
class TestDatabaseOperation(TestcaseBase):
|
||||
|
||||
def setup_method(self, method):
|
||||
param_info.param_user = ct.default_user
|
||||
param_info.param_password = ct.default_password
|
||||
super().setup_method(method)
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""
|
||||
teardown method: drop collection and db
|
||||
"""
|
||||
log.info("[database_teardown_method] Start teardown database test cases ...")
|
||||
self._connect()
|
||||
# clear db
|
||||
for db in self.database_wrap.list_database()[0]:
|
||||
# using db
|
||||
self.database_wrap.using_database(db)
|
||||
|
||||
# drop db collections
|
||||
colls, _ = self.utility_wrap.list_collections()
|
||||
for coll in colls:
|
||||
self.utility_wrap.drop_collection(coll)
|
||||
|
||||
# drop db
|
||||
if db != ct.default_db:
|
||||
self.database_wrap.drop_database(db)
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert dbs == [ct.default_db]
|
||||
|
||||
super().teardown_method(method)
|
||||
|
||||
def test_create_db_name_existed(self):
|
||||
"""
|
||||
target: create db with a existed db name
|
||||
method: create db repeatedly
|
||||
expected: exception
|
||||
"""
|
||||
# create db
|
||||
self._connect()
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# create existed db again
|
||||
error = {ct.err_code: 1, ct.err_msg: "database already exist"}
|
||||
self.database_wrap.create_database(db_name, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
def test_create_db_exceeds_max_num(self):
|
||||
"""
|
||||
target: test db num exceeds max num
|
||||
method: create many dbs and exceeds max
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
|
||||
# because max num 64 not include default
|
||||
for i in range(ct.max_database_num + 1 - len(dbs)):
|
||||
self.database_wrap.create_database(cf.gen_unique_str(prefix))
|
||||
|
||||
# there are ct.max_database_num-1 dbs (default is not included)
|
||||
error = {ct.err_code: 801,
|
||||
ct.err_msg: f"exceeded the limit number of database[limit={ct.max_database_num}]"}
|
||||
self.database_wrap.create_database(cf.gen_unique_str(prefix), check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.skip(reason="https://github.com/milvus-io/milvus/issues/24182")
|
||||
def test_create_collection_exceeds_per_db(self):
|
||||
"""
|
||||
target: test limit collection num per db
|
||||
method: 1. create collections in the db and exceeds perDbCollections
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create collections
|
||||
collections, _ = self.utility_wrap.list_collections()
|
||||
for i in range(ct.max_collections_per_db - len(collections)):
|
||||
self.init_collection_wrap(cf.gen_unique_str(prefix))
|
||||
|
||||
error = {ct.err_code: 1,
|
||||
ct.err_msg: f"failed to create collection, maxCollectionNumPerDB={ct.max_collections_per_db}, exceeded the limit number of "
|
||||
f"collections per DB)"}
|
||||
self.collection_wrap.init_collection(cf.gen_unique_str(prefix), cf.gen_default_collection_schema(),
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.skip(reason="https://github.com/milvus-io/milvus/issues/24182")
|
||||
def test_create_db_collections_exceeds_max_num(self):
|
||||
"""
|
||||
target: test create collection in different db and each db's colelction within max,
|
||||
but total exceeds max collection num
|
||||
method: 1. create a db and create 10 collections in db
|
||||
2. create another db and create collection larger than (max - 10) but less than collectionPerDb
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
# create and using db_a
|
||||
db_a = cf.gen_unique_str("a")
|
||||
self.database_wrap.create_database(db_a)
|
||||
self.database_wrap.using_database(db_a)
|
||||
|
||||
# create 50 collections in db_a
|
||||
collection_num_a = 50
|
||||
for i in range(collection_num_a):
|
||||
self.init_collection_wrap(cf.gen_unique_str(prefix))
|
||||
|
||||
# create and using db_b
|
||||
db_b = cf.gen_unique_str("b")
|
||||
self.database_wrap.create_database(db_b)
|
||||
self.database_wrap.using_database(db_b)
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
exist_coll_num = 0
|
||||
for db in dbs:
|
||||
self.database_wrap.using_database(db)
|
||||
exist_coll_num += len(self.utility_wrap.list_collections()[0])
|
||||
|
||||
# create collection so that total collection num exceed maxCollectionNum
|
||||
self.database_wrap.using_database(db_b)
|
||||
log.debug(f'exist collection num: {exist_coll_num}')
|
||||
collections, _ = self.utility_wrap.list_collections()
|
||||
for i in range(ct.max_collection_num - exist_coll_num):
|
||||
self.init_collection_wrap(cf.gen_unique_str(prefix))
|
||||
|
||||
log.debug(f'db_b collection num: {len(self.utility_wrap.list_collections()[0])}')
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
total_coll_num = 0
|
||||
for db in dbs:
|
||||
self.database_wrap.using_database(db)
|
||||
total_coll_num += len(self.utility_wrap.list_collections()[0])
|
||||
|
||||
log.debug(f'total collection num: {total_coll_num}')
|
||||
error = {ct.err_code: 1,
|
||||
ct.err_msg: f"failed to create collection, maxCollectionNum={ct.max_collection_num}, exceeded the limit number of"}
|
||||
self.collection_wrap.init_collection(cf.gen_unique_str(prefix), cf.gen_default_collection_schema(),
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
def test_create_collection_name_same_db(self):
|
||||
"""
|
||||
target: test create collection in db and collection name same sa db name
|
||||
method: 1.create a db
|
||||
2.create collection in the db and collection name same as db name
|
||||
expected: no error
|
||||
"""
|
||||
self._connect()
|
||||
coll_db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(coll_db_name)
|
||||
|
||||
self.database_wrap.using_database(coll_db_name)
|
||||
|
||||
collection_w = self.init_collection_wrap(name=coll_db_name)
|
||||
|
||||
collection_w.insert(cf.gen_default_dataframe_data())
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
colls, _ = self.utility_wrap.list_collections()
|
||||
assert coll_db_name in colls
|
||||
|
||||
self.database_wrap.using_database(ct.default_db)
|
||||
coll_default, _ = self.utility_wrap.list_collections()
|
||||
assert coll_db_name not in coll_default
|
||||
|
||||
def test_different_db_same_collection_name(self):
|
||||
"""
|
||||
target: test create same collection name in different db
|
||||
method: 1. create 2 dbs
|
||||
2. create same collection name in the 2 dbs
|
||||
expected: verify db isolate collection
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
# create a db
|
||||
db_a = cf.gen_unique_str("a")
|
||||
self.database_wrap.create_database(db_a)
|
||||
|
||||
# create b db
|
||||
db_b = cf.gen_unique_str("b")
|
||||
self.database_wrap.create_database(db_b)
|
||||
|
||||
# create same collection name in db_a and db_b
|
||||
same_coll_name = cf.gen_unique_str(prefix)
|
||||
|
||||
# create and insert in db_a
|
||||
self.database_wrap.using_database(db_a)
|
||||
collection_w_a = self.init_collection_wrap(name=same_coll_name)
|
||||
collection_w_a.insert(cf.gen_default_dataframe_data(nb=100))
|
||||
assert collection_w_a.num_entities == 100
|
||||
collections_a, _ = self.utility_wrap.list_collections()
|
||||
assert same_coll_name in collections_a
|
||||
|
||||
# create and insert in db_b
|
||||
self.database_wrap.using_database(db_b)
|
||||
collection_w_b = self.init_collection_wrap(name=same_coll_name)
|
||||
collection_w_b.insert(cf.gen_default_dataframe_data(nb=200))
|
||||
assert collection_w_b.num_entities == 200
|
||||
collections_a, _ = self.utility_wrap.list_collections()
|
||||
assert same_coll_name in collections_a
|
||||
|
||||
def test_drop_default_db(self):
|
||||
"""
|
||||
target: test drop default db
|
||||
method: drop default db
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
# drop default db
|
||||
self.database_wrap.drop_database(db_name=ct.default_db, check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1, ct.err_msg: "can not drop default database"})
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert ct.default_db in dbs
|
||||
|
||||
def test_drop_db_has_collections(self):
|
||||
"""
|
||||
target: test drop the db that still has collections
|
||||
method: drop db that still has some collections
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
# create db and using db
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create collection in db
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str())
|
||||
|
||||
# drop db
|
||||
self.database_wrap.drop_database(db_name, check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 65535,
|
||||
ct.err_msg: "database:%s not empty, must drop all "
|
||||
"collections before drop database" % db_name})
|
||||
|
||||
# drop collection and drop db
|
||||
collection_w.drop()
|
||||
self.database_wrap.drop_database(db_name)
|
||||
|
||||
def test_drop_not_existed_db(self):
|
||||
"""
|
||||
target: test drop not existed db
|
||||
method: drop a db repeatedly
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# drop a not existed db
|
||||
self.database_wrap.drop_database(cf.gen_unique_str(prefix))
|
||||
|
||||
# drop db
|
||||
self.database_wrap.drop_database(db_name)
|
||||
self.database_wrap.drop_database(db_name)
|
||||
|
||||
def test_drop_using_db(self):
|
||||
"""
|
||||
target: drop the db in use
|
||||
method: drop the using db
|
||||
expected: operation in the db gets exception, need to using other db
|
||||
"""
|
||||
# create db
|
||||
self._connect()
|
||||
|
||||
# create collection in default db
|
||||
collection_w_default = self.init_collection_wrap(name=cf.gen_unique_str(prefix), db_name=ct.default_db)
|
||||
|
||||
# create db
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# using db
|
||||
self.database_wrap.using_database(db_name)
|
||||
# collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# drop using db
|
||||
self.database_wrap.drop_database(db_name)
|
||||
|
||||
# verify current db
|
||||
self.utility_wrap.list_collections(check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 800,
|
||||
ct.err_msg: "database not found[database=%s]" % db_name})
|
||||
self.database_wrap.list_database()
|
||||
self.database_wrap.using_database(ct.default_db)
|
||||
|
||||
using_collections, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w_default.name in using_collections
|
||||
|
||||
def test_using_db_not_existed(self):
|
||||
"""
|
||||
target: test using a not existed db
|
||||
method: using a not existed db
|
||||
expected: exception
|
||||
"""
|
||||
# create db
|
||||
self._connect()
|
||||
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix))
|
||||
|
||||
# list collection with not exist using db -> exception
|
||||
self.database_wrap.using_database(db_name=cf.gen_unique_str(), check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1, ct.err_msg: "database not found"})
|
||||
|
||||
# change using to default and list collections
|
||||
self.database_wrap.using_database(db_name=ct.default_db)
|
||||
colls, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w.name in colls
|
||||
|
||||
def test_create_same_collection_name_different_db(self):
|
||||
"""
|
||||
target: test create same collection name in different db
|
||||
method: 1. create a db and create 1 collection in db
|
||||
2. create the collection in another db
|
||||
expected: exception
|
||||
"""
|
||||
# check default db is empty
|
||||
self._connect()
|
||||
assert self.utility_wrap.list_collections()[0] == []
|
||||
|
||||
# create a collection in default db
|
||||
c_name = "collection_same"
|
||||
self.init_collection_wrap(c_name)
|
||||
assert self.utility_wrap.list_collections()[0] == [c_name]
|
||||
|
||||
# create a new database
|
||||
db_name = cf.gen_unique_str("db")
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create a collection in new db using same name
|
||||
self.init_collection_wrap(c_name)
|
||||
assert self.utility_wrap.list_collections()[0] == [c_name]
|
||||
|
||||
def test_rename_existed_collection_name_new_db(self):
|
||||
"""
|
||||
target: test create same collection name in different db
|
||||
method: 1. create a db and create 1 collection in db
|
||||
2. create the collection in another db
|
||||
expected: exception
|
||||
"""
|
||||
# check default db is empty
|
||||
self._connect()
|
||||
assert self.utility_wrap.list_collections()[0] == []
|
||||
|
||||
# create a collection in default db
|
||||
c_name1 = "collection_1"
|
||||
self.init_collection_wrap(c_name1)
|
||||
assert self.utility_wrap.list_collections()[0] == [c_name1]
|
||||
|
||||
# create a new database
|
||||
db_name = cf.gen_unique_str("db")
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create a collection in new db
|
||||
c_name2 = "collection_2"
|
||||
self.init_collection_wrap(c_name2)
|
||||
assert self.utility_wrap.list_collections()[0] == [c_name2]
|
||||
|
||||
# rename the collection and move it to default db
|
||||
error = {ct.err_code: 65535, ct.err_msg: "duplicated new collection name default:collection_1 "
|
||||
"with other collection name or alias"}
|
||||
self.utility_wrap.rename_collection(c_name2, c_name1, "default",
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
def test_rename_collection_in_new_db(self):
|
||||
"""
|
||||
target: test rename collection in new created db
|
||||
method: 1. create a db and create 1 collection in db
|
||||
2. rename the collection
|
||||
expected: exception
|
||||
"""
|
||||
self._connect()
|
||||
# check default db is empty
|
||||
assert self.utility_wrap.list_collections()[0] == []
|
||||
|
||||
# create a new database
|
||||
db_name = cf.gen_unique_str("db")
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create 1 collection in new db
|
||||
old_name = "old_collection"
|
||||
self.init_collection_wrap(old_name)
|
||||
assert self.utility_wrap.list_collections()[0] == [old_name]
|
||||
|
||||
# rename the collection
|
||||
new_name = "new_collection"
|
||||
self.utility_wrap.rename_collection(old_name, new_name)
|
||||
|
||||
# check the collection still in new db
|
||||
assert self.utility_wrap.list_collections()[0] == [new_name]
|
||||
|
||||
# check the collection not in default db
|
||||
self.database_wrap.using_database("default")
|
||||
assert self.utility_wrap.list_collections()[0] == []
|
||||
|
||||
|
||||
@pytest.mark.skip("removed to test_milvus_client_database.py")
|
||||
class TestDatabaseOtherApi(TestcaseBase):
|
||||
""" test other interface that has db_name params"""
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""
|
||||
teardown method: drop collection and db
|
||||
"""
|
||||
log.info("[database_teardown_method] Start teardown database test cases ...")
|
||||
param_info.param_user = ct.default_user
|
||||
param_info.param_password = ct.default_password
|
||||
self._connect()
|
||||
|
||||
# clear db
|
||||
for db in self.database_wrap.list_database()[0]:
|
||||
# using db
|
||||
self.database_wrap.using_database(db)
|
||||
|
||||
# drop db collections
|
||||
colls, _ = self.utility_wrap.list_collections()
|
||||
for coll in colls:
|
||||
self.utility_wrap.drop_collection(coll)
|
||||
|
||||
# drop db
|
||||
if db != ct.default_db:
|
||||
self.database_wrap.drop_database(db)
|
||||
|
||||
dbs, _ = self.database_wrap.list_database()
|
||||
assert dbs == [ct.default_db]
|
||||
|
||||
super().teardown_method(method)
|
||||
|
||||
@pytest.mark.parametrize("invalid_db_name", [(), [], 1, [1, "2", 3], (1,), {1: 1}])
|
||||
def test_connect_invalid_db_name(self, host, port, invalid_db_name):
|
||||
"""
|
||||
target: test conenct with invalid db name
|
||||
method: connect with invalid db name
|
||||
expected: connect fail
|
||||
"""
|
||||
# connect with invalid db
|
||||
self.connection_wrap.connect(host=host, port=port, db_name=invalid_db_name,
|
||||
user=ct.default_user, password=ct.default_password,
|
||||
secure=cf.param_info.param_secure,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1, ct.err_msg: "is illegal"})
|
||||
|
||||
@pytest.mark.parametrize("invalid_db_name", ["12-s", "12 s", "(mn)", "中文", "%$#"])
|
||||
def test_connect_invalid_db_name_2(self, host, port, invalid_db_name):
|
||||
# connect with invalid db
|
||||
error = {ct.err_code: 800, ct.err_msg: "database not found[database=%s]" % invalid_db_name}
|
||||
if invalid_db_name == "中文":
|
||||
error = {ct.err_code: 1, ct.err_msg: "<metadata was invalid: [('dbname', '中文')"}
|
||||
self.connection_wrap.connect(host=host, port=port, db_name=invalid_db_name,
|
||||
user=ct.default_user, password=ct.default_password,
|
||||
secure=cf.param_info.param_secure,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
def test_connect_not_existed_db(self, host, port):
|
||||
"""
|
||||
target: test connect with not existed db succ
|
||||
method: 1.connect with not existed db
|
||||
2.list collection and gets exception
|
||||
3.create db and create collection in the db
|
||||
3.using default db
|
||||
4.list collections succ
|
||||
expected: parameters db_name is not validated when connecting
|
||||
"""
|
||||
# connect with not existed db
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.connection_wrap.connect(host=host, port=port, db_name=db_name,
|
||||
user=ct.default_user, password=ct.default_password,
|
||||
secure=cf.param_info.param_secure,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 2, ct.err_msg: "database not found"})
|
||||
|
||||
def test_connect_db(self, host, port):
|
||||
"""
|
||||
target: test connect with db
|
||||
method: 1.create db and create collection in db
|
||||
2.disconnect and connect with db
|
||||
3.list collections
|
||||
expected: verify connect db is the using db
|
||||
"""
|
||||
# create db
|
||||
self._connect()
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# create collection
|
||||
self.database_wrap.using_database(db_name)
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
|
||||
# re-connect with db name
|
||||
self.connection_wrap.disconnect(ct.default_alias)
|
||||
self.connection_wrap.connect(host=host, port=port, db_name=db_name, user=ct.default_user,
|
||||
password=ct.default_password, secure=cf.param_info.param_secure)
|
||||
|
||||
# verify connect db_name is the specify db
|
||||
collections_db, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w.name in collections_db
|
||||
|
||||
# verify db's collection not in default db
|
||||
self.connection_wrap.disconnect(ct.default_alias)
|
||||
self.connection_wrap.connect(host=host, port=port, db_name=ct.default_db, user=ct.default_user,
|
||||
password=ct.default_password, secure=cf.param_info.param_secure)
|
||||
|
||||
collections_default, _ = self.utility_wrap.list_collections()
|
||||
assert collection_w.name not in collections_default
|
||||
|
||||
def test_connect_after_using_db(self):
|
||||
"""
|
||||
target: test connect after using db
|
||||
method: 1. connect
|
||||
2. create a db and using db
|
||||
3. create collection in the db
|
||||
3. connect
|
||||
4. list collections
|
||||
expected: current db is connect params db, if None db is default
|
||||
"""
|
||||
# create db
|
||||
self._connect()
|
||||
db_name = cf.gen_unique_str(prefix)
|
||||
self.database_wrap.create_database(db_name)
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create collection
|
||||
self.collection_wrap.init_collection(name=cf.gen_unique_str(prefix), schema=cf.gen_default_collection_schema())
|
||||
|
||||
# connect again
|
||||
self._connect()
|
||||
collections_default, _ = self.utility_wrap.list_collections()
|
||||
assert self.collection_wrap.name not in collections_default
|
||||
|
||||
def test_search_db(self):
|
||||
"""
|
||||
target: test search with db
|
||||
method: 1. create collection in a db
|
||||
2. search with expr on some partitions
|
||||
3. search with output_fields
|
||||
4. search output vector field and ignore growing
|
||||
5. search with pagination
|
||||
6. range search (filter with radius)
|
||||
7. search iterator
|
||||
expected: no error
|
||||
"""
|
||||
# prepare data:
|
||||
# 1. create collection with pk_field + vector_field, enable dynamic field
|
||||
# 2. insert [0, nb) into default partition and flush
|
||||
# 3. create index and load
|
||||
# 4. insert data with dynamic extra field into new partition, pk from [nb, 2*nb)
|
||||
_, partition_name = self.prepare_data_for_db_search()
|
||||
|
||||
query_vec = cf.gen_vectors(ct.default_nq, ct.default_dim)
|
||||
|
||||
# search with dynamic field expr and from partition
|
||||
self.collection_wrap.search(data=query_vec, anns_field=ct.default_float_vec_field_name,
|
||||
param=ct.default_search_params, limit=ct.default_limit,
|
||||
expr=f'{ct.default_int64_field_name} < 2800 or {ct.default_int8_field_name} > 500',
|
||||
partition_names=[ct.default_partition_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": ct.default_nq,
|
||||
"limit": ct.default_limit})
|
||||
|
||||
# search with output pk + dynamic fields
|
||||
ignore_growing_search_params = {"metric_type": "COSINE", "params": {"nprobe": 10}, "ignore_growing": True}
|
||||
search_res, _ = self.collection_wrap.search(data=query_vec, anns_field=ct.default_float_vec_field_name,
|
||||
param=ignore_growing_search_params, limit=ct.default_limit,
|
||||
output_fields=[ct.default_int64_field_name,
|
||||
ct.default_string_field_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": ct.default_nq,
|
||||
"limit": ct.default_limit})
|
||||
assert ct.default_int64_field_name in set(search_res[0][0].entity.fields)
|
||||
|
||||
# search with output vector fields and ignore growing
|
||||
ignore_growing_search_params = {"metric_type": "COSINE", "params": {"nprobe": 10}, "ignore_growing": False}
|
||||
self.collection_wrap.search(data=query_vec, anns_field=ct.default_float_vec_field_name,
|
||||
param=ignore_growing_search_params, limit=ct.default_limit,
|
||||
output_fields=[ct.default_int64_field_name,
|
||||
ct.default_float_vec_field_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": ct.default_nq,
|
||||
"limit": ct.default_limit,
|
||||
"output_fields": [ct.default_int64_field_name,
|
||||
ct.default_float_vec_field_name]})
|
||||
|
||||
# search with pagination
|
||||
self.collection_wrap.search(data=query_vec, anns_field=ct.default_float_vec_field_name,
|
||||
param=ct.default_search_params, limit=ct.default_limit, offset=ct.default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": ct.default_nq,
|
||||
"limit": ct.default_limit})
|
||||
|
||||
# range search
|
||||
range_search_params = {"metric_type": "COSINE", "params": {"radius": 0.0,
|
||||
"range_filter": 1000}}
|
||||
self.collection_wrap.search(query_vec, ct.default_float_vec_field_name,
|
||||
range_search_params, ct.default_limit,
|
||||
expr=None,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": ct.default_nq,
|
||||
"limit": ct.default_limit})
|
||||
|
||||
# search iterator
|
||||
self.collection_wrap.search_iterator(query_vec[:1], ct.default_float_vec_field_name, ct.default_search_params,
|
||||
ct.default_limit * 100, partition_names=[partition_name],
|
||||
check_task=CheckTasks.check_search_iterator,
|
||||
check_items={"limit": ct.default_limit * 100})
|
||||
|
||||
def test_query_db(self):
|
||||
"""
|
||||
target: test search with db
|
||||
method: 1. create collection in a db
|
||||
2. query from partitions
|
||||
3. query output fields: pk + dynamic
|
||||
4. query output vector field and ignore growing
|
||||
5. query with pagination
|
||||
6. query iterator
|
||||
expected: no error
|
||||
"""
|
||||
# prepare data:
|
||||
# 1. create collection with pk_field + vector_field, enable dynamic field
|
||||
# 2. insert [0, nb) into default partition and flush
|
||||
# 3. create index and load
|
||||
# 4. insert data with dynamic extra field into new partition, pk from [nb, 2*nb)
|
||||
_, partition_name = self.prepare_data_for_db_search()
|
||||
|
||||
# query from partition
|
||||
query_expr = f'{ct.default_int64_field_name} in [0, {ct.default_nb}]'
|
||||
res, _ = self.collection_wrap.query(query_expr, partition_names=[partition_name])
|
||||
assert len(res) == 1
|
||||
|
||||
# query output pk + dynamic fields
|
||||
res_dynamic, _ = self.collection_wrap.query(query_expr, output_fields=[ct.default_int64_field_name,
|
||||
ct.default_string_field_name])
|
||||
assert ct.default_int64_field_name in res_dynamic[0].keys()
|
||||
|
||||
# query output vector field
|
||||
vec_res, _ = self.collection_wrap.query(query_expr, output_fields=[ct.default_float_vec_field_name])
|
||||
assert set(vec_res[0].keys()) == {ct.default_float_vec_field_name, ct.default_int64_field_name}
|
||||
|
||||
# query with pagination
|
||||
expr = f'1000 <= {ct.default_int64_field_name} < 4000 '
|
||||
page_res, _ = self.collection_wrap.query(expr, offset=1000, limit=1000)
|
||||
assert len(page_res) == 1000
|
||||
|
||||
# delte and query
|
||||
del_expr = f'{ct.default_int64_field_name} in [0, {ct.default_nb}]'
|
||||
self.collection_wrap.delete(del_expr)
|
||||
self.collection_wrap.query(del_expr, check_task=CheckTasks.check_query_empty)
|
||||
|
||||
# upsert and query
|
||||
# TODO https://github.com/milvus-io/milvus/issues/26595
|
||||
# upsert_data = cf.gen_default_rows_data(start=0, nb=1, with_json=False)
|
||||
# upsert_df= pd.DataFrame({
|
||||
# ct.default_int64_field_name: pd.Series(data=[0]),
|
||||
# ct.default_float_vec_field_name: cf.gen_vectors(1, ct.default_dim)
|
||||
# })
|
||||
# self.collection_wrap.upsert(data=upsert_df)
|
||||
# upsert_entity, _ = self.collection_wrap.query(del_expr, output_fields=[ct.default_string_field_name])
|
||||
# assert set(vec_res[0].keys()) == {ct.default_int64_field_name}
|
||||
|
||||
# query iterator
|
||||
self.collection_wrap.query_iterator(expr=f"{ct.default_int64_field_name} <= 3000", batch_size=ct.default_limit * 10,
|
||||
partition_names=[partition_name],
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": 1000,
|
||||
"pk_name": self.database_wrap.primary_field.name,
|
||||
"batch_size": ct.default_limit * 10})
|
||||
|
||||
def prepare_data_for_db_search(self):
|
||||
"""
|
||||
prepare data in db collection
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
param_info.param_user = ct.default_user
|
||||
param_info.param_password = ct.default_password
|
||||
self._connect()
|
||||
|
||||
# create a db
|
||||
db_name = cf.gen_unique_str("a")
|
||||
self.database_wrap.create_database(db_name)
|
||||
|
||||
# using db
|
||||
self.database_wrap.using_database(db_name)
|
||||
|
||||
# create collection and a partition
|
||||
partition_name = "p1"
|
||||
self.collection_wrap.init_collection(name=cf.gen_unique_str(prefix),
|
||||
schema=cf.gen_default_collection_schema(enable_dynamic_field=True))
|
||||
self.partition_wrap.init_partition(self.collection_wrap.collection, partition_name)
|
||||
|
||||
# insert data into collection
|
||||
df = pd.DataFrame({
|
||||
ct.default_int64_field_name: pd.Series(data=[i for i in range(ct.default_nb)]),
|
||||
ct.default_float_vec_field_name: cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
})
|
||||
self.collection_wrap.insert(df)
|
||||
self.collection_wrap.flush()
|
||||
|
||||
# create index with COSINE metrics
|
||||
_index = {"index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 8, "efConstruction": 200}}
|
||||
self.collection_wrap.create_index(ct.default_float_vec_field_name, _index)
|
||||
|
||||
# load collection
|
||||
self.collection_wrap.load()
|
||||
|
||||
# insert data into partition with dynamic field
|
||||
data_par = cf.gen_default_rows_data(start=ct.default_nb)
|
||||
log.info(data_par[0].keys())
|
||||
self.collection_wrap.insert(data_par, partition_name=self.partition_wrap.name)
|
||||
|
||||
return db_name, partition_name
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from base.client_base import TestcaseBase
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel
|
||||
from utils.util_log import test_log as log
|
||||
|
||||
prefix = "e2e_"
|
||||
|
||||
|
||||
class TestE2e(TestcaseBase):
|
||||
""" Test case of end to end"""
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_milvus_default(self):
|
||||
# create
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
t0 = time.time()
|
||||
collection_w = self.init_collection_wrap(name=collection_name, active_trace=True)
|
||||
tt = time.time() - t0
|
||||
assert collection_w.name == collection_name
|
||||
|
||||
# index
|
||||
index_params = {"index_type": "IVF_SQ8", "params": {"nlist": 64}, "metric_type": "L2"}
|
||||
t0 = time.time()
|
||||
index, _ = collection_w.create_index(field_name=ct.default_float_vec_field_name,
|
||||
index_params=index_params,
|
||||
index_name=cf.gen_unique_str())
|
||||
index, _ = collection_w.create_index(field_name=ct.default_string_field_name,
|
||||
index_params={},
|
||||
index_name=cf.gen_unique_str())
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert index: {tt}")
|
||||
assert len(collection_w.indexes) == 2
|
||||
|
||||
entities = collection_w.num_entities
|
||||
log.info(f"assert create collection: {tt}, init_entities: {entities}")
|
||||
|
||||
# insert
|
||||
data = cf.gen_default_list_data()
|
||||
t0 = time.time()
|
||||
_, res = collection_w.insert(data)
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert insert: {tt}")
|
||||
assert res
|
||||
|
||||
# flush
|
||||
t0 = time.time()
|
||||
_, check_result = collection_w.flush(timeout=180)
|
||||
assert check_result
|
||||
assert collection_w.num_entities == len(data[0]) + entities
|
||||
tt = time.time() - t0
|
||||
entities = collection_w.num_entities
|
||||
log.info(f"assert flush: {tt}, entities: {entities}")
|
||||
|
||||
# load
|
||||
collection_w.load()
|
||||
|
||||
# search
|
||||
search_vectors = cf.gen_vectors(1, ct.default_dim)
|
||||
search_params = {"metric_type": "L2", "params": {"nprobe": 16}}
|
||||
t0 = time.time()
|
||||
res_1, _ = collection_w.search(data=search_vectors,
|
||||
anns_field=ct.default_float_vec_field_name,
|
||||
param=search_params, limit=1)
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert search: {tt}")
|
||||
assert len(res_1) == 1
|
||||
|
||||
# release
|
||||
collection_w.release()
|
||||
|
||||
# insert
|
||||
d = cf.gen_default_list_data()
|
||||
collection_w.insert(d)
|
||||
|
||||
# search
|
||||
t0 = time.time()
|
||||
collection_w.load()
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert load: {tt}")
|
||||
nq = 5
|
||||
topk = 5
|
||||
search_vectors = cf.gen_vectors(nq, ct.default_dim)
|
||||
t0 = time.time()
|
||||
res, _ = collection_w.search(data=search_vectors,
|
||||
anns_field=ct.default_float_vec_field_name,
|
||||
param=search_params, limit=topk)
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert search: {tt}")
|
||||
assert len(res) == nq
|
||||
assert len(res[0]) <= topk
|
||||
# query
|
||||
term_expr = f'{ct.default_int64_field_name} in [1, 2, 3, 4]'
|
||||
t0 = time.time()
|
||||
res, _ = collection_w.query(term_expr)
|
||||
tt = time.time() - t0
|
||||
log.info(f"assert query result {len(res)}: {tt}")
|
||||
assert len(res) >= 4
|
||||
@@ -0,0 +1,471 @@
|
||||
import pytest
|
||||
from base.client_base import TestcaseBase
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_pymilvus import *
|
||||
|
||||
@pytest.mark.skip(reason="field partial load behavior changing @congqixia")
|
||||
class TestFieldPartialLoad(TestcaseBase):
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_field_partial_load_default(self):
|
||||
"""
|
||||
target: test field partial load
|
||||
method:
|
||||
1. create a collection with fields
|
||||
2. index/not index fields to be loaded; index/not index fields to be skipped
|
||||
3. load a part of the fields
|
||||
expected:
|
||||
1. verify the collection loaded successfully
|
||||
2. verify the loaded fields can be searched in expr and output_fields
|
||||
3. verify the skipped fields not loaded, and cannot search with them in expr or output_fields
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 128
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_int64_field = cf.gen_int64_field(name="int64_load")
|
||||
not_load_int64_field = cf.gen_int64_field(name="int64_not_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
not_load_string_field = cf.gen_string_field(name="string_not_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_int64_field, not_load_int64_field,
|
||||
load_string_field, not_load_string_field, vector_field],
|
||||
auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
int_values = [i for i in range(nb)]
|
||||
string_values = [str(i) for i in range(nb)]
|
||||
float_vec_values = gen_vectors(nb, dim)
|
||||
collection_w.insert([int_values, int_values, string_values, string_values, float_vec_values])
|
||||
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name, load_int64_field.name],
|
||||
replica_number=1)
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 2
|
||||
search_vectors = float_vec_values[0:nq]
|
||||
res, _ = collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=100, output_fields=["*"])
|
||||
assert pk_field.name in res[0][0].fields.keys() \
|
||||
and vector_field.name in res[0][0].fields.keys() \
|
||||
and load_string_field.name in res[0][0].fields.keys() \
|
||||
and load_int64_field.name in res[0][0].fields.keys() \
|
||||
and not_load_string_field.name not in res[0][0].fields.keys() \
|
||||
and not_load_int64_field.name not in res[0][0].fields.keys()
|
||||
|
||||
# release and reload with some other fields
|
||||
collection_w.release()
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name,
|
||||
not_load_string_field.name, not_load_int64_field.name])
|
||||
res, _ = collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=100, output_fields=["*"])
|
||||
assert pk_field.name in res[0][0].fields.keys() \
|
||||
and vector_field.name in res[0][0].fields.keys() \
|
||||
and load_string_field.name not in res[0][0].fields.keys() \
|
||||
and load_int64_field.name not in res[0][0].fields.keys() \
|
||||
and not_load_string_field.name in res[0][0].fields.keys() \
|
||||
and not_load_int64_field.name in res[0][0].fields.keys()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_skip_load_dynamic_field(self):
|
||||
"""
|
||||
target: test skip load dynamic field
|
||||
method:
|
||||
1. create a collection with dynamic field
|
||||
2. load
|
||||
3. search on dynamic field in expr and/or output_fields
|
||||
expected: search successfully
|
||||
4. release and reload with skip load dynamic field=true
|
||||
5. search on dynamic field in expr and/or output_fields
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 128
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_int64_field = cf.gen_int64_field(name="int64_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_int64_field, load_string_field, vector_field],
|
||||
auto_id=True, enable_dynamic_field=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
data = []
|
||||
for i in range(nb):
|
||||
data.append({
|
||||
f"{load_int64_field.name}": i,
|
||||
f"{load_string_field.name}": str(i),
|
||||
f"{vector_field.name}": [random.uniform(-1, 1) for _ in range(dim)],
|
||||
"color": i,
|
||||
"tag": i,
|
||||
})
|
||||
collection_w.insert(data)
|
||||
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
collection_w.load()
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 2
|
||||
search_vectors = cf.gen_vectors(nq, dim)
|
||||
res, _ = collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
expr="color > 0",
|
||||
limit=100, output_fields=["*"],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": nq, "limit": 100})
|
||||
|
||||
collection_w.release()
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name],
|
||||
skip_load_dynamic_field=True)
|
||||
error = {ct.err_code: 999, ct.err_msg: f"field color cannot be returned since dynamic field not loaded"}
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=100, output_fields=["color"],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
error = {ct.err_code: 999, ct.err_msg: f"field color is dynamic but dynamic field is not loaded"}
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
expr="color > 0", limit=100, output_fields=["*"],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_skip_load_some_vector_fields(self):
|
||||
"""
|
||||
target: test skip load some vector fields
|
||||
method:
|
||||
1. create a collection with multiple vector fields
|
||||
2. not create index for skip load vector fields
|
||||
2. load some vector fields
|
||||
3. search on vector fields in expr and/or output_fields
|
||||
expected: search successfully
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 128
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
vector_field = cf.gen_float_vec_field(name="vec_float32", dim=dim)
|
||||
sparse_vector_field = cf.gen_float_vec_field(name="sparse", vector_data_type=DataType.SPARSE_FLOAT_VECTOR)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_string_field, vector_field, sparse_vector_field],
|
||||
auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
string_values = [str(i) for i in range(nb)]
|
||||
float_vec_values = cf.gen_vectors(nb, dim)
|
||||
sparse_vec_values = cf.gen_vectors(nb, dim, vector_data_type=DataType.SPARSE_FLOAT_VECTOR)
|
||||
collection_w.insert([string_values, float_vec_values, sparse_vec_values])
|
||||
|
||||
# build index on one of vector fields
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# not load sparse vector field
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name])
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 2
|
||||
search_vectors = float_vec_values[0:nq]
|
||||
res, _ = collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=100, output_fields=["*"],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": nq, "limit": 100})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_partial_load_with_partition(self):
|
||||
"""
|
||||
target: test partial load with partitions
|
||||
method:
|
||||
1. create a collection with fields
|
||||
2. create 2 partitions: p1, p2
|
||||
3. partial load p1
|
||||
4. search on p1
|
||||
5. load p2 with different fields
|
||||
expected: p2 load fail
|
||||
6. load p2 with the same partial fields
|
||||
7. search on p2
|
||||
expected: search successfully
|
||||
8. load the collection with all fields
|
||||
expected: load fail
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 128
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_int64_field = cf.gen_int64_field(name="int64_load")
|
||||
not_load_int64_field = cf.gen_int64_field(name="int64_not_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
not_load_string_field = cf.gen_string_field(name="string_not_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_int64_field, not_load_int64_field,
|
||||
load_string_field, not_load_string_field, vector_field],
|
||||
auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
p1 = self.init_partition_wrap(collection_w, name='p1')
|
||||
p2 = self.init_partition_wrap(collection_w, name='p2')
|
||||
int_values = [i for i in range(nb)]
|
||||
string_values = [str(i) for i in range(nb)]
|
||||
float_vec_values = gen_vectors(nb, dim)
|
||||
p1.insert([int_values, int_values, string_values, string_values, float_vec_values])
|
||||
p2.insert([int_values, int_values, string_values, string_values, float_vec_values])
|
||||
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# p1 load with partial fields
|
||||
p1.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name, load_int64_field.name])
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 2
|
||||
search_vectors = float_vec_values[0:nq]
|
||||
res, _ = p1.search(data=search_vectors, anns_field=vector_field.name, params=search_params,
|
||||
limit=100, output_fields=["*"])
|
||||
assert pk_field.name in res[0][0].fields.keys() \
|
||||
and vector_field.name in res[0][0].fields.keys()
|
||||
# load p2 with different fields
|
||||
error = {ct.err_code: 999, ct.err_msg: f"can't change the load field list for loaded collection"}
|
||||
p2.load(load_fields=[pk_field.name, vector_field.name, not_load_string_field.name, not_load_int64_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# load p2 with the same partial fields
|
||||
p2.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name, load_int64_field.name])
|
||||
res, _ = p2.search(data=search_vectors, anns_field=vector_field.name, params=search_params,
|
||||
limit=100, output_fields=["*"])
|
||||
assert pk_field.name in res[0][0].fields.keys() \
|
||||
and vector_field.name in res[0][0].fields.keys()
|
||||
|
||||
# load the collection with all fields
|
||||
collection_w.load(check_task=CheckTasks.err_res, check_items=error)
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=100, output_fields=["*"],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": nq, "limit": 100})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_skip_load_on_all_scalar_field_types(self):
|
||||
"""
|
||||
target: test skip load on all scalar field types
|
||||
method:
|
||||
1. create a collection with fields define skip load on all scalar field types
|
||||
expected:
|
||||
1. load and search successfully
|
||||
"""
|
||||
prefix = "partial_load"
|
||||
collection_w = self.init_collection_general(prefix, insert_data=True, is_index=True,
|
||||
is_all_data_type=True, with_json=True)[0]
|
||||
collection_w.release()
|
||||
# load with only pk field and vector field
|
||||
collection_w.load(load_fields=[ct.default_int64_field_name, ct.default_float_vec_field_name])
|
||||
search_vectors = cf.gen_vectors(1, ct.default_dim)
|
||||
search_params = {"params": {}}
|
||||
res = collection_w.search(data=search_vectors, anns_field=ct.default_float_vec_field_name,
|
||||
param=search_params, limit=10, output_fields=["*"],
|
||||
check_tasks=CheckTasks.check_search_results, check_items={"nq": 1, "limit": 10})[0]
|
||||
assert len(res[0][0].fields.keys()) == 2
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="field partial load behavior changing @congqixia")
|
||||
class TestFieldPartialLoadInvalid(TestcaseBase):
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_skip_load_on_pk_field_or_vector_field(self):
|
||||
"""
|
||||
target: test skip load on pk field
|
||||
method:
|
||||
1. create a collection with fields define skip load on pk field
|
||||
expected:
|
||||
1. raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_int64_field = cf.gen_int64_field(name="int64_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_int64_field, vector_field], auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# load without pk field
|
||||
error = {ct.err_code: 999, ct.err_msg: f"does not contain primary key field {pk_field.name}"}
|
||||
collection_w.load(load_fields=[vector_field.name, load_int64_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
error = {ct.err_code: 999, ct.err_msg: f"does not contain vector field"}
|
||||
collection_w.load(load_fields=[pk_field.name, load_int64_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_skip_load_on_partition_key_field(self):
|
||||
"""
|
||||
target: test skip load on partition key field
|
||||
method:
|
||||
1. create a collection with fields define skip load on partition key field
|
||||
expected:
|
||||
1. raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
partition_key_field = cf.gen_int64_field(name="int64_load", is_partition_key=True)
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, partition_key_field, vector_field], auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# load without pk field
|
||||
error = {ct.err_code: 999, ct.err_msg: f"does not contain partition key field {partition_key_field.name}"}
|
||||
collection_w.load(load_fields=[vector_field.name, pk_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_skip_load_on_clustering_key_field(self):
|
||||
"""
|
||||
target: test skip load on clustering key field
|
||||
method:
|
||||
1. create a collection with fields define skip load on clustering key field
|
||||
expected:
|
||||
1. raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
clustering_key_field = cf.gen_int64_field(name="int64_load", is_clustering_key=True)
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, clustering_key_field, vector_field], auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# load without pk field
|
||||
error = {ct.err_code: 999, ct.err_msg: f"does not contain clustering key field {clustering_key_field.name}"}
|
||||
collection_w.load(load_fields=[vector_field.name, pk_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_update_load_fields_list_when_reloading_collection(self):
|
||||
"""
|
||||
target: test update load fields list when reloading collection
|
||||
method:
|
||||
1. create a collection with fields
|
||||
2. load a part of the fields
|
||||
3. update load fields list when reloading collection
|
||||
expected:
|
||||
1. raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
not_load_int64_field = cf.gen_int64_field(name="not_int64_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, not_load_int64_field, load_string_field, vector_field],
|
||||
auto_id=True, enable_dynamic_field=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
int_values = [i for i in range(nb)]
|
||||
string_values = [str(i) for i in range(nb)]
|
||||
float_vec_values = cf.gen_vectors(nb, dim)
|
||||
collection_w.insert([int_values, string_values, float_vec_values])
|
||||
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name])
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 1
|
||||
search_vectors = float_vec_values[0:nq]
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=10, output_fields=[load_string_field.name],
|
||||
check_task=CheckTasks.check_search_results, check_items={"nq": nq, "limit": 10})
|
||||
# try to add more fields in load fields list when reloading
|
||||
error = {ct.err_code: 999, ct.err_msg: f"can't change the load field list for loaded collection"}
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name,
|
||||
load_string_field.name, not_load_int64_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# try to remove fields in load fields list when reloading
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_one_of_dynamic_fields_in_load_fields_list(self):
|
||||
"""
|
||||
target: test one of dynamic fields in load fields list
|
||||
method:
|
||||
1. create a collection with fields
|
||||
3. add one of dynamic fields in load fields list when loading
|
||||
expected: raise exception
|
||||
4. add non_existing field in load fields list when loading
|
||||
expected: raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
load_int64_field = cf.gen_int64_field(name="int64_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, load_int64_field, load_string_field, vector_field],
|
||||
auto_id=True, enable_dynamic_field=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
data = []
|
||||
for i in range(nb):
|
||||
data.append({
|
||||
f"{load_int64_field.name}": i,
|
||||
f"{load_string_field.name}": str(i),
|
||||
f"{vector_field.name}": [random.uniform(-1, 1) for _ in range(dim)],
|
||||
"color": i,
|
||||
"tag": i,
|
||||
})
|
||||
collection_w.insert(data)
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
# add one of dynamic fields in load fields list
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"failed to get field schema by name: fieldName(color) not found"}
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, "color"],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
# add non_existing field in load fields list
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"failed to get field schema by name: fieldName(not_existing) not found"}
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, "not_existing"],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_search_on_not_loaded_fields(self):
|
||||
"""
|
||||
target: test search on skipped fields
|
||||
method:
|
||||
1. create a collection with fields
|
||||
2. load a part of the fields
|
||||
3. search on skipped fields in expr and/or output_fields
|
||||
expected:
|
||||
1. raise exception
|
||||
"""
|
||||
self._connect()
|
||||
name = cf.gen_unique_str()
|
||||
dim = 32
|
||||
nb = ct.default_nb
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
not_load_int64_field = cf.gen_int64_field(name="not_int64_load")
|
||||
load_string_field = cf.gen_string_field(name="string_load")
|
||||
vector_field = cf.gen_float_vec_field(dim=dim)
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, not_load_int64_field, load_string_field, vector_field],
|
||||
auto_id=True, enable_dynamic_field=True)
|
||||
collection_w = self.init_collection_wrap(name=name, schema=schema)
|
||||
int_values = [i for i in range(nb)]
|
||||
string_values = [str(i) for i in range(nb)]
|
||||
float_vec_values = cf.gen_vectors(nb, dim)
|
||||
collection_w.insert([int_values, string_values, float_vec_values])
|
||||
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
collection_w.load(load_fields=[pk_field.name, vector_field.name, load_string_field.name])
|
||||
# search
|
||||
search_params = ct.default_search_params
|
||||
nq = 1
|
||||
search_vectors = float_vec_values[0:nq]
|
||||
error = {ct.err_code: 999, ct.err_msg: f"field {not_load_int64_field.name} is not loaded"}
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
limit=10, output_fields=[not_load_int64_field.name, load_string_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
error = {ct.err_code: 999, ct.err_msg: f"cannot parse expression"}
|
||||
collection_w.search(data=search_vectors, anns_field=vector_field.name, param=search_params,
|
||||
expr=f"{not_load_int64_field.name} > 0",
|
||||
limit=10, output_fields=[load_string_field.name],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,874 @@
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from pymilvus import DataType, Function, FunctionChain, FunctionChainStage, FunctionScore, FunctionType
|
||||
from pymilvus.function_chain import col, fn
|
||||
from pymilvus.function_chain.chain import FunctionChainExpr
|
||||
|
||||
prefix = "function_chain"
|
||||
|
||||
|
||||
class TestFunctionChain(TestMilvusClientV2Base):
|
||||
"""Test pymilvus FunctionChain SDK integration."""
|
||||
|
||||
dim = 2
|
||||
vector_field = "vector"
|
||||
scalar_field = "ts"
|
||||
|
||||
def _create_function_chain_collection(self, client):
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
schema = self.create_schema(client, auto_id=False, enable_dynamic_field=False)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True)
|
||||
schema.add_field(self.scalar_field, DataType.INT64)
|
||||
schema.add_field(self.vector_field, DataType.FLOAT_VECTOR, dim=self.dim)
|
||||
|
||||
return self._create_collection_with_schema_and_rows(
|
||||
client,
|
||||
collection_name,
|
||||
schema,
|
||||
[
|
||||
{"id": 1, self.scalar_field: 10, self.vector_field: [0.0, 0.0]},
|
||||
{"id": 2, self.scalar_field: 20, self.vector_field: [0.01, 0.0]},
|
||||
{"id": 3, self.scalar_field: 30, self.vector_field: [0.02, 0.0]},
|
||||
],
|
||||
)
|
||||
|
||||
def _create_collection_with_schema_and_rows(self, client, collection_name, schema, rows):
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(field_name=self.vector_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_collection(
|
||||
client,
|
||||
collection_name,
|
||||
schema=schema,
|
||||
index_params=index_params,
|
||||
consistency_level="Strong",
|
||||
)
|
||||
self.insert(client, collection_name, rows)
|
||||
self.flush(client, collection_name)
|
||||
self.load_collection(client, collection_name)
|
||||
return collection_name
|
||||
|
||||
def _score_plus_ts_chain(self, stage):
|
||||
chain = FunctionChain(stage, name="score_plus_ts").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"),
|
||||
)
|
||||
if stage == FunctionChainStage.L2_RERANK:
|
||||
chain.sort(col("$score"), desc=True, tie_break_col=col("$id"))
|
||||
return chain
|
||||
|
||||
def _assert_search_error(self, client, collection_name, function_chains, err_msg, **kwargs):
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
function_chains=function_chains,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1100, ct.err_msg: err_msg},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _generate_xgboost_model(tmp_path):
|
||||
xgb = pytest.importorskip("xgboost")
|
||||
features = np.array([[0.1], [0.8], [0.2], [0.9]], dtype=np.float32)
|
||||
labels = np.array([0.2, 0.9, 0.4, 0.7], dtype=np.float32)
|
||||
dtrain = xgb.DMatrix(features, label=labels)
|
||||
booster = xgb.train(
|
||||
{
|
||||
"objective": "reg:squarederror",
|
||||
"max_depth": 2,
|
||||
"eta": 1.0,
|
||||
"lambda": 0.0,
|
||||
"alpha": 0.0,
|
||||
"base_score": 0.5,
|
||||
"tree_method": "exact",
|
||||
"seed": 7,
|
||||
},
|
||||
dtrain,
|
||||
num_boost_round=1,
|
||||
)
|
||||
model_path = tmp_path / "xgboost_l0_rerank.ubj"
|
||||
booster.save_model(model_path)
|
||||
expected = booster.predict(dtrain, output_margin=True).astype(float).tolist()
|
||||
return model_path, features[:, 0].astype(float).tolist(), expected
|
||||
|
||||
@staticmethod
|
||||
def _generate_unsupported_xgboost_model(tmp_path, name, params):
|
||||
xgb = pytest.importorskip("xgboost")
|
||||
features = np.array([[0.1], [0.8], [0.2], [0.9]], dtype=np.float32)
|
||||
labels = np.array([0.0, 1.0, 2.0, 3.0], dtype=np.float32)
|
||||
dtrain = xgb.DMatrix(features, label=labels)
|
||||
train_params = {
|
||||
"objective": "reg:squarederror",
|
||||
"max_depth": 2,
|
||||
"eta": 1.0,
|
||||
"lambda": 0.0,
|
||||
"alpha": 0.0,
|
||||
"base_score": 0.5,
|
||||
"seed": 7,
|
||||
}
|
||||
train_params.update(params)
|
||||
if train_params.get("objective") == "rank:pairwise":
|
||||
dtrain.set_group([len(labels)])
|
||||
booster = xgb.train(train_params, dtrain, num_boost_round=1)
|
||||
model_path = tmp_path / f"{name}.ubj"
|
||||
booster.save_model(model_path)
|
||||
return model_path
|
||||
|
||||
@staticmethod
|
||||
def _new_minio_client(minio_host):
|
||||
from minio import Minio
|
||||
|
||||
return Minio(
|
||||
f"{minio_host}:9000",
|
||||
access_key="minioadmin",
|
||||
secret_key="minioadmin",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
def _upload_file_resource_bytes(self, client, minio_host, bucket, resource_name, remote_path, data):
|
||||
minio_client = self._new_minio_client(minio_host)
|
||||
if not minio_client.bucket_exists(bucket):
|
||||
minio_client.make_bucket(bucket)
|
||||
minio_client.put_object(bucket, remote_path, io.BytesIO(data), len(data))
|
||||
self.add_file_resource(client, resource_name, remote_path)
|
||||
return minio_client
|
||||
|
||||
def _create_l0_xgboost_collection(self, client, fields, rows):
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
schema = self.create_schema(client, auto_id=False, enable_dynamic_field=False)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True)
|
||||
for name, data_type, kwargs in fields:
|
||||
schema.add_field(name, data_type, **kwargs)
|
||||
schema.add_field(self.vector_field, DataType.FLOAT_VECTOR, dim=self.dim)
|
||||
return self._create_collection_with_schema_and_rows(client, collection_name, schema, rows)
|
||||
|
||||
@staticmethod
|
||||
def _l0_xgboost_chain(resource_name, feature_columns, output="raw"):
|
||||
return FunctionChain(FunctionChainStage.L0_RERANK, name="l0_xgboost").map(
|
||||
"$score",
|
||||
FunctionChainExpr(
|
||||
"xgboost",
|
||||
args=tuple(col(name) for name in feature_columns),
|
||||
params={"model_resource": resource_name, "output": output},
|
||||
),
|
||||
)
|
||||
|
||||
def _assert_l0_xgboost_search_error(self, client, collection_name, chain, err_msg, limit=3):
|
||||
self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=limit,
|
||||
function_chains=chain,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1100, ct.err_msg: err_msg},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hit_field(hit, field):
|
||||
if field in hit:
|
||||
return hit[field]
|
||||
return hit.get("entity", {}).get(field)
|
||||
|
||||
@staticmethod
|
||||
def _expected_l0_score(hit):
|
||||
vector = hit.get("entity", {}).get("vector", hit.get("vector"))
|
||||
l2_distance = sum(value * value for value in vector)
|
||||
return TestFunctionChain._hit_field(hit, "ts") - l2_distance
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_with_l0_function_chain_xgboost_matches_local_predict(self, file_resource_env, tmp_path, minio_host):
|
||||
"""
|
||||
target: test L0 function chain can rerank search results with a real XGBoost UBJ model
|
||||
method: generate a tiny XGBoost model locally, upload it as a Milvus file resource, run L0 xgboost rerank
|
||||
expected: Milvus search scores and order match local XGBoost raw predictions
|
||||
"""
|
||||
from minio import Minio
|
||||
|
||||
client = self._client()
|
||||
resource_name = cf.gen_unique_str("xgboost_model")
|
||||
remote_path = f"xgboost/{resource_name}.ubj"
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
model_path, feature_values, expected_scores = self._generate_xgboost_model(tmp_path)
|
||||
|
||||
minio_client = Minio(
|
||||
f"{minio_host}:9000",
|
||||
access_key="minioadmin",
|
||||
secret_key="minioadmin",
|
||||
secure=False,
|
||||
)
|
||||
bucket = file_resource_env["bucket"]
|
||||
if not minio_client.bucket_exists(bucket):
|
||||
minio_client.make_bucket(bucket)
|
||||
model_bytes = model_path.read_bytes()
|
||||
minio_client.put_object(bucket, remote_path, io.BytesIO(model_bytes), len(model_bytes))
|
||||
|
||||
try:
|
||||
self.add_file_resource(client, resource_name, remote_path)
|
||||
|
||||
schema = self.create_schema(client, auto_id=False, enable_dynamic_field=False)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True)
|
||||
schema.add_field("xgb_f0", DataType.FLOAT)
|
||||
schema.add_field(self.vector_field, DataType.FLOAT_VECTOR, dim=self.dim)
|
||||
rows = [
|
||||
{
|
||||
"id": idx + 1,
|
||||
"xgb_f0": value,
|
||||
self.vector_field: [idx * 0.01, 0.0],
|
||||
}
|
||||
for idx, value in enumerate(feature_values)
|
||||
]
|
||||
self._create_collection_with_schema_and_rows(client, collection_name, schema, rows)
|
||||
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="l0_xgboost").map(
|
||||
"$score",
|
||||
FunctionChainExpr(
|
||||
"xgboost",
|
||||
args=(col("xgb_f0"),),
|
||||
params={"model_resource": resource_name, "output": "raw"},
|
||||
),
|
||||
)
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=len(rows),
|
||||
output_fields=["xgb_f0"],
|
||||
function_chains=chain,
|
||||
)
|
||||
|
||||
expected_by_id = {idx + 1: score for idx, score in enumerate(expected_scores)}
|
||||
expected_ids = [
|
||||
idx + 1 for idx, _ in sorted(enumerate(expected_scores), key=lambda item: item[1], reverse=True)
|
||||
]
|
||||
assert [hit["id"] for hit in res[0]] == expected_ids
|
||||
for hit in res[0]:
|
||||
assert abs(hit["distance"]) == pytest.approx(expected_by_id[hit["id"]], rel=1e-5, abs=1e-5)
|
||||
finally:
|
||||
try:
|
||||
client.remove_file_resource(name=resource_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
minio_client.remove_object(bucket, remote_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_xgboost_missing_resource(self):
|
||||
"""
|
||||
target: test L0 xgboost rejects a model_resource that is not registered
|
||||
method: run xgboost rerank with a missing FileResource name
|
||||
expected: search fails with file resource not found
|
||||
"""
|
||||
client = self._client()
|
||||
rows = [
|
||||
{"id": 1, "xgb_f0": 0.1, self.vector_field: [0.0, 0.0]},
|
||||
{"id": 2, "xgb_f0": 0.8, self.vector_field: [0.01, 0.0]},
|
||||
]
|
||||
collection_name = self._create_l0_xgboost_collection(
|
||||
client,
|
||||
[("xgb_f0", DataType.FLOAT, {})],
|
||||
rows,
|
||||
)
|
||||
chain = self._l0_xgboost_chain("missing_xgboost_model", ["xgb_f0"])
|
||||
|
||||
self._assert_l0_xgboost_search_error(client, collection_name, chain, "file resource")
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_xgboost_invalid_output_param(self):
|
||||
"""
|
||||
target: test L0 xgboost rejects an invalid output parameter
|
||||
method: run xgboost rerank with output=probability
|
||||
expected: request fails because output must be default or raw
|
||||
"""
|
||||
client = self._client()
|
||||
rows = [
|
||||
{"id": 1, "xgb_f0": 0.1, self.vector_field: [0.0, 0.0]},
|
||||
{"id": 2, "xgb_f0": 0.8, self.vector_field: [0.01, 0.0]},
|
||||
]
|
||||
collection_name = self._create_l0_xgboost_collection(
|
||||
client,
|
||||
[("xgb_f0", DataType.FLOAT, {})],
|
||||
rows,
|
||||
)
|
||||
chain = self._l0_xgboost_chain("unused_xgboost_model", ["xgb_f0"], output="probability")
|
||||
|
||||
self._assert_l0_xgboost_search_error(client, collection_name, chain, "output must be one of")
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_xgboost_feature_count_mismatch(
|
||||
self, file_resource_env, tmp_path, minio_host
|
||||
):
|
||||
"""
|
||||
target: test L0 xgboost rejects feature count mismatches
|
||||
method: use a one-feature model with two input feature columns
|
||||
expected: search fails with feature column count mismatch
|
||||
"""
|
||||
client = self._client()
|
||||
resource_name = cf.gen_unique_str("xgboost_model")
|
||||
remote_path = f"xgboost/{resource_name}.ubj"
|
||||
model_path, feature_values, _ = self._generate_xgboost_model(tmp_path)
|
||||
bucket = file_resource_env["bucket"]
|
||||
model_bytes = model_path.read_bytes()
|
||||
minio_client = self._upload_file_resource_bytes(
|
||||
client, minio_host, bucket, resource_name, remote_path, model_bytes
|
||||
)
|
||||
|
||||
try:
|
||||
rows = [
|
||||
{
|
||||
"id": idx + 1,
|
||||
"xgb_f0": value,
|
||||
"xgb_f1": value + 1.0,
|
||||
self.vector_field: [idx * 0.01, 0.0],
|
||||
}
|
||||
for idx, value in enumerate(feature_values)
|
||||
]
|
||||
collection_name = self._create_l0_xgboost_collection(
|
||||
client,
|
||||
[("xgb_f0", DataType.FLOAT, {}), ("xgb_f1", DataType.FLOAT, {})],
|
||||
rows,
|
||||
)
|
||||
chain = self._l0_xgboost_chain(resource_name, ["xgb_f0", "xgb_f1"])
|
||||
|
||||
self._assert_l0_xgboost_search_error(
|
||||
client, collection_name, chain, "expected 1 feature columns, got 2", limit=len(rows)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
client.remove_file_resource(name=resource_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
minio_client.remove_object(bucket, remote_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_xgboost_unsupported_input_type(
|
||||
self, file_resource_env, tmp_path, minio_host
|
||||
):
|
||||
"""
|
||||
target: test L0 xgboost rejects unsupported input column types
|
||||
method: pass a varchar field as an xgboost feature
|
||||
expected: search fails with unsupported input column type
|
||||
"""
|
||||
client = self._client()
|
||||
resource_name = cf.gen_unique_str("xgboost_model")
|
||||
remote_path = f"xgboost/{resource_name}.ubj"
|
||||
model_path, feature_values, _ = self._generate_xgboost_model(tmp_path)
|
||||
bucket = file_resource_env["bucket"]
|
||||
model_bytes = model_path.read_bytes()
|
||||
minio_client = self._upload_file_resource_bytes(
|
||||
client, minio_host, bucket, resource_name, remote_path, model_bytes
|
||||
)
|
||||
|
||||
try:
|
||||
rows = [
|
||||
{"id": idx + 1, "xgb_text": str(value), self.vector_field: [idx * 0.01, 0.0]}
|
||||
for idx, value in enumerate(feature_values)
|
||||
]
|
||||
collection_name = self._create_l0_xgboost_collection(
|
||||
client,
|
||||
[("xgb_text", DataType.VARCHAR, {"max_length": 64})],
|
||||
rows,
|
||||
)
|
||||
chain = self._l0_xgboost_chain(resource_name, ["xgb_text"])
|
||||
|
||||
self._assert_l0_xgboost_search_error(
|
||||
client, collection_name, chain, "unsupported input column type", limit=len(rows)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
client.remove_file_resource(name=resource_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
minio_client.remove_object(bucket, remote_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize(
|
||||
"case_name, model_data, expected_error",
|
||||
[
|
||||
("not_ubj", b'{"learner":{}}', "failed to parse UBJ model"),
|
||||
("unsupported_objective", None, "unsupported objective"),
|
||||
("unsupported_booster", None, "unsupported booster"),
|
||||
],
|
||||
)
|
||||
def test_search_rejects_l0_function_chain_xgboost_invalid_model(
|
||||
self, file_resource_env, tmp_path, minio_host, case_name, model_data, expected_error
|
||||
):
|
||||
"""
|
||||
target: test L0 xgboost rejects invalid or unsupported model artifacts
|
||||
method: register invalid UBJ content, unsupported objective, and unsupported booster models
|
||||
expected: search fails while loading the xgboost model
|
||||
"""
|
||||
client = self._client()
|
||||
resource_name = cf.gen_unique_str(f"xgboost_{case_name}")
|
||||
remote_path = f"xgboost/{resource_name}.ubj"
|
||||
if model_data is None:
|
||||
if case_name == "unsupported_objective":
|
||||
model_path = self._generate_unsupported_xgboost_model(
|
||||
tmp_path, case_name, {"objective": "rank:pairwise"}
|
||||
)
|
||||
else:
|
||||
model_path = self._generate_unsupported_xgboost_model(tmp_path, case_name, {"booster": "gblinear"})
|
||||
model_data = model_path.read_bytes()
|
||||
bucket = file_resource_env["bucket"]
|
||||
minio_client = self._upload_file_resource_bytes(
|
||||
client, minio_host, bucket, resource_name, remote_path, model_data
|
||||
)
|
||||
|
||||
try:
|
||||
rows = [
|
||||
{"id": 1, "xgb_f0": 0.1, self.vector_field: [0.0, 0.0]},
|
||||
{"id": 2, "xgb_f0": 0.8, self.vector_field: [0.01, 0.0]},
|
||||
]
|
||||
collection_name = self._create_l0_xgboost_collection(
|
||||
client,
|
||||
[("xgb_f0", DataType.FLOAT, {})],
|
||||
rows,
|
||||
)
|
||||
chain = self._l0_xgboost_chain(resource_name, ["xgb_f0"])
|
||||
|
||||
self._assert_l0_xgboost_search_error(client, collection_name, chain, expected_error, limit=len(rows))
|
||||
finally:
|
||||
try:
|
||||
client.remove_file_resource(name=resource_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
minio_client.remove_object(bucket, remote_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_with_l0_function_chain_sdk_reranks_by_scalar_field(self):
|
||||
"""
|
||||
target: test pymilvus FunctionChain SDK with L0 rerank
|
||||
method: map $score = num_combine($score, ts) at L0 stage
|
||||
expected: search succeeds and result order follows rewritten score
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
output_fields=[self.scalar_field, self.vector_field],
|
||||
function_chains=self._score_plus_ts_chain(FunctionChainStage.L0_RERANK),
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
assert [self._hit_field(hit, self.scalar_field) for hit in res[0]] == [30, 20, 10]
|
||||
expected_scores = [self._expected_l0_score(hit) for hit in res[0]]
|
||||
assert expected_scores == sorted(expected_scores, reverse=True)
|
||||
assert [pytest.approx(abs(hit["distance"]), rel=1e-5) for hit in res[0]] == expected_scores
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_with_l0_function_chain_sdk_uses_hidden_input_field(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK can use fields that are not returned
|
||||
method: rerank by ts while only requesting primary key output
|
||||
expected: search succeeds, result order follows ts, and ts is not returned
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
output_fields=["id"],
|
||||
function_chains=self._score_plus_ts_chain(FunctionChainStage.L0_RERANK),
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
assert all(self._hit_field(hit, self.scalar_field) is None for hit in res[0])
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_with_l0_function_chain_sdk_can_read_id_system_input(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK can read public system input $id
|
||||
method: map $score = num_combine($score, $id) at L0 stage
|
||||
expected: search succeeds and result order follows rewritten score
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="score_plus_id").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$score"), col("$id"), mode="sum"),
|
||||
)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
function_chains=chain,
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_sort_op(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK rejects non-map operators
|
||||
method: use sort op at L0 stage
|
||||
expected: request fails because public L0 currently only supports map op
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="bad_l0_sort").sort(
|
||||
col("$score"),
|
||||
desc=True,
|
||||
tie_break_col=col("$id"),
|
||||
)
|
||||
|
||||
self._assert_search_error(
|
||||
client, collection_name, chain, 'type "sort" is not supported by L0 rerank function chain'
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_write_readonly_system_column(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK rejects writes to read-only system columns
|
||||
method: write map output to $id
|
||||
expected: request fails because only $score is writable in public L0 chains
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="bad_l0_write_id").map(
|
||||
"$id",
|
||||
fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system output "$id" is not writable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_read_internal_system_input(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK rejects internal system input columns
|
||||
method: read $seg_offset from a map expression
|
||||
expected: request fails because public L0 only exposes $id and $score as readable system inputs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="bad_l0_seg_offset_input").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$seg_offset"), col("$score"), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system input "$seg_offset" is not readable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_read_unknown_system_input(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK rejects unknown system input columns
|
||||
method: read $tmp_score from a map expression before it is produced
|
||||
expected: request fails because users cannot invent new $-prefixed system columns
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="bad_l0_unknown_system_input").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$tmp_score"), col("$score"), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system input "$tmp_score" is not readable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_reserved_temp_output(self):
|
||||
"""
|
||||
target: test L0 FunctionChain SDK rejects user temporary columns in system namespace
|
||||
method: write a map output named $tmp_score
|
||||
expected: request fails because $ prefix is reserved for system columns
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L0_RERANK, name="bad_l0_reserved_temp_output").map(
|
||||
"$tmp_score",
|
||||
fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system output "$tmp_score" is not writable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_with_function_score(self):
|
||||
"""
|
||||
target: test search rejects ambiguous L0 rerank APIs
|
||||
method: send boost FunctionScore and L0 function chain together
|
||||
expected: request fails because function_score and function_chains are mutually exclusive
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
function = Function(
|
||||
name="boost_ts",
|
||||
function_type=FunctionType.RERANK,
|
||||
input_field_names=[],
|
||||
output_field_names=[],
|
||||
params={"reranker": "boost", "weight": "1.5"},
|
||||
)
|
||||
function_score = FunctionScore(functions=[function])
|
||||
|
||||
self._assert_search_error(
|
||||
client,
|
||||
collection_name,
|
||||
self._score_plus_ts_chain(FunctionChainStage.L0_RERANK),
|
||||
"function_chains and ranker cannot be used together",
|
||||
ranker=function_score,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_search_rejects_l0_function_chain_with_order_by(self):
|
||||
"""
|
||||
target: test search rejects order_by with L0 function rerank
|
||||
method: send order_by_fields and L0 function chain together
|
||||
expected: request fails because they define conflicting sort criteria
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
self._assert_search_error(
|
||||
client,
|
||||
collection_name,
|
||||
self._score_plus_ts_chain(FunctionChainStage.L0_RERANK),
|
||||
"order_by and function rerank cannot be used together",
|
||||
order_by_fields=[{"field": self.scalar_field, "order": "asc"}],
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_with_l2_function_chain_sdk_reranks_by_scalar_field(self):
|
||||
"""
|
||||
target: test pymilvus FunctionChain SDK with L2 rerank
|
||||
method: map $score = num_combine($score, ts), then sort by $score desc
|
||||
expected: search succeeds and result order follows rewritten score
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
output_fields=[self.scalar_field],
|
||||
function_chains=self._score_plus_ts_chain(FunctionChainStage.L2_RERANK),
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
assert [self._hit_field(hit, self.scalar_field) for hit in res[0]] == [30, 20, 10]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_with_l2_function_chain_sdk_uses_hidden_input_field(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK can use fields that are not returned
|
||||
method: rerank by ts while only requesting primary key output
|
||||
expected: search succeeds, result order follows ts, and ts is not returned
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
output_fields=["id"],
|
||||
function_chains=self._score_plus_ts_chain(FunctionChainStage.L2_RERANK),
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
assert all(self._hit_field(hit, self.scalar_field) is None for hit in res[0])
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_with_l2_function_chain_sdk_temp_column_not_returned(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK can use ordinary temporary columns
|
||||
method: write tmp_score, write it back to $score, then sort by $score desc
|
||||
expected: search succeeds, rerank order is correct, and tmp_score is not returned
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = (
|
||||
FunctionChain(FunctionChainStage.L2_RERANK, name="l2_temp_score")
|
||||
.map("tmp_score", fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"))
|
||||
.map("$score", fn.num_combine(col("tmp_score"), col("$score"), mode="sum"))
|
||||
.sort(col("$score"), desc=True, tie_break_col=col("$id"))
|
||||
)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
output_fields=[self.scalar_field],
|
||||
function_chains=chain,
|
||||
)
|
||||
|
||||
assert [hit["id"] for hit in res[0]] == [3, 2, 1]
|
||||
assert [self._hit_field(hit, self.scalar_field) for hit in res[0]] == [30, 20, 10]
|
||||
assert all(self._hit_field(hit, "tmp_score") is None for hit in res[0])
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_with_l2_function_chain_sdk_limit_op(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK supports limit operator
|
||||
method: request limit=3 and apply function chain limit op with limit=2
|
||||
expected: search succeeds and returns only function-chain-limited results
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L2_RERANK, name="l2_limit").limit(2)
|
||||
|
||||
res, _ = self.search(
|
||||
client,
|
||||
collection_name,
|
||||
data=[[0.0, 0.0]],
|
||||
anns_field=self.vector_field,
|
||||
search_params={"metric_type": "L2"},
|
||||
limit=3,
|
||||
function_chains=chain,
|
||||
)
|
||||
|
||||
assert len(res[0]) == 2
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_write_readonly_system_column(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK rejects writes to read-only system columns
|
||||
method: write map output to $id
|
||||
expected: request fails because only $score is writable in L2 rerank chains
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L2_RERANK, name="bad_l2_write_id").map(
|
||||
"$id",
|
||||
fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system output "$id" is not writable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_reserved_temp_output(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK rejects user temporary columns in system namespace
|
||||
method: write a map output named $tmp_score
|
||||
expected: request fails because $ prefix is reserved for system columns
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L2_RERANK, name="bad_l2_reserved_temp_output").map(
|
||||
"$tmp_score",
|
||||
fn.num_combine(col("$score"), col(self.scalar_field), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system output "$tmp_score" is not writable')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_read_internal_system_input(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK rejects internal system input columns
|
||||
method: read $seg_offset from a map expression
|
||||
expected: request fails because L2 only exposes selected system inputs
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L2_RERANK, name="bad_l2_seg_offset_input").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$seg_offset"), col("$score"), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system input "$seg_offset" is not supported')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_read_unknown_system_input(self):
|
||||
"""
|
||||
target: test L2 FunctionChain SDK rejects unknown system input columns
|
||||
method: read $tmp_score from a map expression before it is produced
|
||||
expected: request fails because users cannot invent new $-prefixed system columns
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
chain = FunctionChain(FunctionChainStage.L2_RERANK, name="bad_l2_unknown_system_input").map(
|
||||
"$score",
|
||||
fn.num_combine(col("$tmp_score"), col("$score"), mode="sum"),
|
||||
)
|
||||
|
||||
self._assert_search_error(client, collection_name, chain, 'system input "$tmp_score" is not supported')
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_with_function_score(self):
|
||||
"""
|
||||
target: test search rejects ambiguous L2 rerank APIs
|
||||
method: send boost FunctionScore and L2 function chain together
|
||||
expected: request fails because function chains and ranker are mutually exclusive
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
function = Function(
|
||||
name="boost_ts",
|
||||
function_type=FunctionType.RERANK,
|
||||
input_field_names=[],
|
||||
output_field_names=[],
|
||||
params={"reranker": "boost", "weight": "1.5"},
|
||||
)
|
||||
function_score = FunctionScore(functions=[function])
|
||||
|
||||
self._assert_search_error(
|
||||
client,
|
||||
collection_name,
|
||||
self._score_plus_ts_chain(FunctionChainStage.L2_RERANK),
|
||||
"function_chains and ranker cannot be used together",
|
||||
ranker=function_score,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_search_rejects_l2_function_chain_with_order_by(self):
|
||||
"""
|
||||
target: test search rejects order_by with L2 function rerank
|
||||
method: send order_by_fields and L2 function chain together
|
||||
expected: request fails because they define conflicting sort criteria
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = self._create_function_chain_collection(client)
|
||||
|
||||
self._assert_search_error(
|
||||
client,
|
||||
collection_name,
|
||||
self._score_plus_ts_chain(FunctionChainStage.L2_RERANK),
|
||||
"order_by and function rerank cannot be used together",
|
||||
order_by_fields=[{"field": self.scalar_field, "order": "asc"}],
|
||||
)
|
||||
@@ -0,0 +1,327 @@
|
||||
import pytest
|
||||
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from utils.util_log import test_log as log
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from utils.util_pymilvus import *
|
||||
from common.constants import *
|
||||
|
||||
prefix = "high_level_api"
|
||||
epsilon = ct.epsilon
|
||||
default_nb = ct.default_nb
|
||||
default_nb_medium = ct.default_nb_medium
|
||||
default_nq = ct.default_nq
|
||||
default_dim = ct.default_dim
|
||||
default_limit = ct.default_limit
|
||||
default_search_exp = "id >= 0"
|
||||
exp_res = "exp_res"
|
||||
default_search_string_exp = "varchar >= \"0\""
|
||||
default_search_mix_exp = "int64 >= 0 && varchar >= \"0\""
|
||||
default_invaild_string_exp = "varchar >= 0"
|
||||
default_json_search_exp = "json_field[\"number\"] >= 0"
|
||||
perfix_expr = 'varchar like "0%"'
|
||||
default_search_field = ct.default_float_vec_field_name
|
||||
default_search_params = ct.default_search_params
|
||||
default_primary_key_field_name = "id"
|
||||
default_vector_field_name = "vector"
|
||||
default_float_field_name = ct.default_float_field_name
|
||||
default_bool_field_name = ct.default_bool_field_name
|
||||
default_string_field_name = ct.default_string_field_name
|
||||
default_int32_array_field_name = ct.default_int32_array_field_name
|
||||
default_string_array_field_name = ct.default_string_array_field_name
|
||||
|
||||
|
||||
class TestHighLevelApi(TestMilvusClientV2Base):
|
||||
""" Test case of search interface """
|
||||
|
||||
@pytest.fixture(scope="function", params=[False, True])
|
||||
def auto_id(self, request):
|
||||
yield request.param
|
||||
|
||||
@pytest.fixture(scope="function", params=["COSINE", "L2"])
|
||||
def metric_type(self, request):
|
||||
yield request.param
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are invalid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.xfail(reason="pymilvus issue 1554")
|
||||
def test_high_level_collection_invalid_primary_field(self):
|
||||
"""
|
||||
target: test high level api: client.create_collection
|
||||
method: create collection with invalid primary field
|
||||
expected: Raise exception
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
error = {ct.err_code: 1, ct.err_msg: f"Param id_type must be int or string"}
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong",
|
||||
id_type="invalid", check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_high_level_create_same_collection_different_params(self):
|
||||
"""
|
||||
target: test high level api: client.create_collection
|
||||
method: create
|
||||
expected: 1. Successfully to create collection with same params
|
||||
2. Report errors for creating collection with same name and different params
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. create collection with same params
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
# 3. create collection with same name and different params
|
||||
error = {ct.err_code: 1, ct.err_msg: f"create duplicate collection with different parameters, "
|
||||
f"collection: {collection_name}"}
|
||||
self.create_collection(client, collection_name, default_dim + 1, consistency_level="Strong",
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_high_level_collection_invalid_metric_type(self):
|
||||
"""
|
||||
target: test high level api: client.create_collection
|
||||
method: create collection with auto id on string primary key
|
||||
expected: Raise exception
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: "float vector index does not support metric type: invalid: invalid parameter"}
|
||||
self.create_collection(client, collection_name, default_dim, metric_type="invalid",
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip("https://github.com/milvus-io/milvus/issues/29880")
|
||||
def test_high_level_search_not_consistent_metric_type(self, metric_type):
|
||||
"""
|
||||
target: test search with inconsistent metric type (default is IP) with that of index
|
||||
method: create connection, collection, insert and search with not consistent metric type
|
||||
expected: Raise exception
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. search
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
vectors_to_search = rng.random((1, 8))
|
||||
search_params = {"metric_type": metric_type}
|
||||
error = {ct.err_code: 1100,
|
||||
ct.err_msg: f"metric type not match: invalid parameter[expected=IP][actual={metric_type}]"}
|
||||
self.search(client, collection_name, vectors_to_search, limit=default_limit,
|
||||
search_params=search_params,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
"""
|
||||
******************************************************************
|
||||
# The following are valid base cases
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_high_level_search_query_default(self):
|
||||
"""
|
||||
target: test search (high level api) normal case
|
||||
method: create connection, collection, insert and search
|
||||
expected: search/query successfully
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
collections = self.list_collections(client)[0]
|
||||
assert collection_name in collections
|
||||
self.describe_collection(client, collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim, "consistency_level": 0})
|
||||
# 2. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
|
||||
# 3. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
insert_ids = [i for i in range(default_nb)]
|
||||
self.search(client, collection_name, vectors_to_search,
|
||||
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"ids": insert_ids,
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
# 4. query
|
||||
self.query(client, collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={exp_res: rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_high_level_array_insert_search(self):
|
||||
"""
|
||||
target: test search (high level api) normal case
|
||||
method: create connection, collection, insert and search
|
||||
expected: search/query successfully
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
collections = self.list_collections(client)[0]
|
||||
assert collection_name in collections
|
||||
# 2. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{
|
||||
default_primary_key_field_name: i,
|
||||
default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0,
|
||||
default_int32_array_field_name: [i, i + 1, i + 2],
|
||||
default_string_array_field_name: [str(i), str(i + 1), str(i + 2)]
|
||||
} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
|
||||
# 3. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
insert_ids = [i for i in range(default_nb)]
|
||||
self.search(client, collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"ids": insert_ids,
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip(reason="issue 25110")
|
||||
def test_high_level_search_query_string(self):
|
||||
"""
|
||||
target: test search (high level api) for string primary key
|
||||
method: create connection, collection, insert and search
|
||||
expected: search/query successfully
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, id_type="string",
|
||||
max_length=ct.default_length, consistency_level="Strong")
|
||||
self.describe_collection(client, collection_name,
|
||||
check_task=CheckTasks.check_describe_collection_property,
|
||||
check_items={"collection_name": collection_name,
|
||||
"dim": default_dim})
|
||||
# 2. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [
|
||||
{default_primary_key_field_name: str(i), default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
|
||||
# 3. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
self.search(client, collection_name, vectors_to_search,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
# 4. query
|
||||
self.query(client, collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={exp_res: rows,
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_high_level_search_different_metric_types(self, metric_type, auto_id):
|
||||
"""
|
||||
target: test search (high level api) normal case
|
||||
method: create connection, collection, insert and search
|
||||
expected: search successfully with limit(topK)
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, metric_type=metric_type,
|
||||
auto_id=auto_id, consistency_level="Strong")
|
||||
# 2. insert
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
if auto_id:
|
||||
for row in rows:
|
||||
row.pop(default_primary_key_field_name)
|
||||
self.insert(client, collection_name, rows)
|
||||
|
||||
# 3. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
search_params = {"metric_type": metric_type}
|
||||
self.search(client, collection_name, vectors_to_search, limit=default_limit,
|
||||
search_params=search_params,
|
||||
output_fields=[default_primary_key_field_name],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"limit": default_limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_high_level_delete(self):
|
||||
"""
|
||||
target: test delete (high level api)
|
||||
method: create connection, collection, insert delete, and search
|
||||
expected: search/query successfully without deleted data
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
# 1. create collection
|
||||
self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
|
||||
# 2. insert
|
||||
default_nb = 1000
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
rows = [{default_primary_key_field_name: i, default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
default_float_field_name: i * 1.0, default_string_field_name: str(i)} for i in range(default_nb)]
|
||||
self.insert(client, collection_name, rows)
|
||||
pks = [i for i in range(default_nb)]
|
||||
# 3. get first primary key
|
||||
first_pk_data = self.get(client, collection_name, ids=pks[0:1])
|
||||
# 4. delete
|
||||
delete_num = 3
|
||||
self.delete(client, collection_name, ids=pks[0:delete_num])
|
||||
# 5. search
|
||||
vectors_to_search = rng.random((1, default_dim))
|
||||
insert_ids = [i for i in range(default_nb)]
|
||||
for insert_id in pks[0:delete_num]:
|
||||
if insert_id in insert_ids:
|
||||
insert_ids.remove(insert_id)
|
||||
limit = default_nb - delete_num
|
||||
self.search(client, collection_name, vectors_to_search, limit=default_nb,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": len(vectors_to_search),
|
||||
"ids": insert_ids,
|
||||
"limit": limit,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
# 6. query
|
||||
self.query(client, collection_name, filter=default_search_exp,
|
||||
check_task=CheckTasks.check_query_results,
|
||||
check_items={exp_res: rows[delete_num:],
|
||||
"with_vec": True,
|
||||
"pk_name": default_primary_key_field_name})
|
||||
self.drop_collection(client, collection_name)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,899 @@
|
||||
from ssl import ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
|
||||
import threading
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import random
|
||||
import pytest
|
||||
from pymilvus import Index, DataType
|
||||
from pymilvus.exceptions import MilvusException
|
||||
|
||||
from base.client_base import TestcaseBase
|
||||
from utils.util_log import test_log as log
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
|
||||
prefix = "insert"
|
||||
pre_upsert = "upsert"
|
||||
exp_name = "name"
|
||||
exp_schema = "schema"
|
||||
exp_num = "num_entities"
|
||||
exp_primary = "primary"
|
||||
default_float_name = ct.default_float_field_name
|
||||
default_schema = cf.gen_default_collection_schema()
|
||||
default_binary_schema = cf.gen_default_binary_collection_schema()
|
||||
default_index_params = {"index_type": "IVF_SQ8",
|
||||
"metric_type": "L2", "params": {"nlist": 64}}
|
||||
default_binary_index_params = ct.default_binary_index
|
||||
default_search_exp = "int64 >= 0"
|
||||
|
||||
|
||||
class TestInsertParams(TestcaseBase):
|
||||
""" Test case of Insert interface """
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_insert_dataframe_data(self):
|
||||
"""
|
||||
target: test insert DataFrame data
|
||||
method: 1.create collection
|
||||
2.insert dataframe data
|
||||
expected: assert num entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
mutation_res, _ = collection_w.insert(data=df)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist()
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_insert_list_data(self):
|
||||
"""
|
||||
target: test insert list-like data
|
||||
method: 1.create 2.insert list data
|
||||
expected: assert num entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
data = cf.gen_default_list_data(ct.default_nb)
|
||||
mutation_res, _ = collection_w.insert(data=data)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == data[0].tolist()
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("data", [pd.DataFrame()])
|
||||
def test_insert_empty_dataframe(self, data):
|
||||
"""
|
||||
target: test insert empty dataFrame()
|
||||
method: insert empty
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
error = {ct.err_code: 999, ct.err_msg: "The fields don't match with schema fields"}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("data", [[[]]])
|
||||
def test_insert_empty_data(self, data):
|
||||
"""
|
||||
target: test insert empty array
|
||||
method: insert empty
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
error = {ct.err_code: 999, ct.err_msg: "The data doesn't match with schema fields"}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_dataframe_only_columns(self):
|
||||
"""
|
||||
target: test insert with dataframe just columns
|
||||
method: dataframe just have columns
|
||||
expected: num entities is zero
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
columns = [ct.default_int64_field_name,
|
||||
ct.default_float_vec_field_name]
|
||||
df = pd.DataFrame(columns=columns)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: "The fields don't match with schema fields"}
|
||||
collection_w.insert(
|
||||
data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_empty_field_name_dataframe(self):
|
||||
"""
|
||||
target: test insert empty field name df
|
||||
method: dataframe with empty column
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name, dim=32)
|
||||
df = cf.gen_default_dataframe_data(10)
|
||||
df.rename(columns={ct.default_int64_field_name: ' '}, inplace=True)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: "The name of field doesn't match, expected: int64"}
|
||||
collection_w.insert(
|
||||
data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_invalid_field_name_dataframe(self):
|
||||
"""
|
||||
target: test insert with invalid dataframe data
|
||||
method: insert with invalid field name dataframe
|
||||
expected: raise exception
|
||||
"""
|
||||
invalid_field_name = "non_existing"
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
df = cf.gen_default_dataframe_data(10)
|
||||
df.rename(
|
||||
columns={ct.default_int64_field_name: invalid_field_name}, inplace=True)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f"The name of field doesn't match, expected: int64, got {invalid_field_name}"}
|
||||
collection_w.insert(
|
||||
data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_numpy_data(self):
|
||||
"""
|
||||
target: test insert numpy.ndarray data
|
||||
method: 1.create by schema 2.insert data
|
||||
expected: assert num_entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
data = cf.gen_numpy_data(nb=nb)
|
||||
collection_w.insert(data=data)
|
||||
assert collection_w.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_binary_dataframe(self):
|
||||
"""
|
||||
target: test insert binary dataframe
|
||||
method: 1. create by schema 2. insert dataframe
|
||||
expected: assert num_entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=c_name, schema=default_binary_schema)
|
||||
df, _ = cf.gen_default_binary_dataframe_data(ct.default_nb)
|
||||
mutation_res, _ = collection_w.insert(data=df)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist()
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_insert_binary_data(self):
|
||||
"""
|
||||
target: test insert list-like binary data
|
||||
method: 1. create by schema 2. insert data
|
||||
expected: assert num_entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=c_name, schema=default_binary_schema)
|
||||
data, _ = cf.gen_default_binary_list_data(ct.default_nb)
|
||||
mutation_res, _ = collection_w.insert(data=data)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == data[0]
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_insert_single(self):
|
||||
"""
|
||||
target: test insert single
|
||||
method: insert one entity
|
||||
expected: verify num
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
data = cf.gen_default_list_data(nb=1)
|
||||
mutation_res, _ = collection_w.insert(data=data)
|
||||
assert mutation_res.insert_count == 1
|
||||
assert mutation_res.primary_keys == data[0].tolist()
|
||||
assert collection_w.num_entities == 1
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip(reason="issue #37543")
|
||||
def test_insert_dim_not_match(self):
|
||||
"""
|
||||
target: test insert with not match dim
|
||||
method: insert data dim not equal to schema dim
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
dim = 129
|
||||
df = cf.gen_default_dataframe_data(nb=20, dim=dim)
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: f'Collection field dim is {ct.default_dim}, but entities field dim is {dim}'}
|
||||
collection_w.insert(data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip(reason="Currently not check in pymilvus")
|
||||
def test_insert_field_value_not_match(self):
|
||||
"""
|
||||
target: test insert data value not match
|
||||
method: insert data value type not match schema
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
new_float_value = pd.Series(data=[float(i) for i in range(nb)], dtype="float64")
|
||||
df[df.columns[1]] = new_float_value
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: "The data type of field float doesn't match, expected: FLOAT, got DOUBLE"}
|
||||
collection_w.insert(data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_value_less(self):
|
||||
"""
|
||||
target: test insert value less than other
|
||||
method: string field value less than vec-field value
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
data = []
|
||||
for fields in collection_w.schema.fields:
|
||||
field_data = cf.gen_data_by_collection_field(fields, nb=nb)
|
||||
if fields.dtype == DataType.VARCHAR:
|
||||
field_data = field_data[:-1]
|
||||
data.append(field_data)
|
||||
error = {ct.err_code: 999, ct.err_msg: "Field data size misaligned for field [varchar] "}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_vector_value_less(self):
|
||||
"""
|
||||
target: test insert vector value less than other
|
||||
method: vec field value less than int field
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
data = []
|
||||
for fields in collection_w.schema.fields:
|
||||
field_data = cf.gen_data_by_collection_field(fields, nb=nb)
|
||||
if fields.dtype == DataType.FLOAT_VECTOR:
|
||||
field_data = field_data[:-1]
|
||||
data.append(field_data)
|
||||
error = {ct.err_code: 999, ct.err_msg: 'Field data size misaligned for field [float_vector] '}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_fields_more(self):
|
||||
"""
|
||||
target: test insert with fields more
|
||||
method: field more than schema fields
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
data = []
|
||||
for fields in collection_w.schema.fields:
|
||||
field_data = cf.gen_data_by_collection_field(fields, nb=nb)
|
||||
data.append(field_data)
|
||||
data.append([1 for _ in range(nb)])
|
||||
error = {ct.err_code: 999, ct.err_msg: "The data doesn't match with schema fields"}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_fields_less(self):
|
||||
"""
|
||||
target: test insert with fields less
|
||||
method: fields less than schema fields
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
df.drop(ct.default_float_vec_field_name, axis=1, inplace=True)
|
||||
error = {ct.err_code: 999, ct.err_msg: "The fields don't match with schema fields"}
|
||||
collection_w.insert(
|
||||
data=df, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_list_order_inconsistent_schema(self):
|
||||
"""
|
||||
target: test insert data fields order inconsistent with schema
|
||||
method: insert list data, data fields order inconsistent with schema
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 10
|
||||
data = []
|
||||
for field in collection_w.schema.fields:
|
||||
field_data = cf.gen_data_by_collection_field(field, nb=nb)
|
||||
data.append(field_data)
|
||||
tmp = data[0]
|
||||
data[0] = data[1]
|
||||
data[1] = tmp
|
||||
error = {ct.err_code: 999,
|
||||
ct.err_msg: "The Input data type is inconsistent with defined schema"}
|
||||
collection_w.insert(
|
||||
data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
|
||||
class TestInsertOperation(TestcaseBase):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test insert interface operations
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.fixture(scope="function", params=[8, 4096])
|
||||
def dim(self, request):
|
||||
yield request.param
|
||||
|
||||
@pytest.fixture(scope="function", params=[False, True])
|
||||
def auto_id(self, request):
|
||||
yield request.param
|
||||
|
||||
@pytest.fixture(scope="function", params=[ct.default_int64_field_name, ct.default_string_field_name])
|
||||
def pk_field(self, request):
|
||||
yield request.param
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_with_no_vector_field_dtype(self):
|
||||
"""
|
||||
target: test insert entities, with no vector field
|
||||
method: vector field is missing in data
|
||||
expected: error raised
|
||||
"""
|
||||
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
||||
nb = 10
|
||||
data = []
|
||||
fields = collection_w.schema.fields
|
||||
for field in fields:
|
||||
field_data = cf.gen_data_by_collection_field(field, nb=nb)
|
||||
if field.dtype != DataType.FLOAT_VECTOR:
|
||||
data.append(field_data)
|
||||
error = {ct.err_code: 999, ct.err_msg: f"The data doesn't match with schema fields, "
|
||||
f"expect {len(fields)} list, got {len(data)}"}
|
||||
collection_w.insert(data=data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_twice_auto_id_true(self, pk_field):
|
||||
"""
|
||||
target: test insert ids fields twice when auto_id=True
|
||||
method: 1.create collection with auto_id=True 2.insert twice
|
||||
expected: verify primary_keys unique
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
schema = cf.gen_default_collection_schema(
|
||||
primary_field=pk_field, auto_id=True)
|
||||
nb = 10
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=schema)
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
df.drop(pk_field, axis=1, inplace=True)
|
||||
mutation_res, _ = collection_w.insert(data=df)
|
||||
primary_keys = mutation_res.primary_keys
|
||||
assert cf._check_primary_keys(primary_keys, nb)
|
||||
mutation_res_1, _ = collection_w.insert(data=df)
|
||||
primary_keys.extend(mutation_res_1.primary_keys)
|
||||
assert cf._check_primary_keys(primary_keys, nb * 2)
|
||||
assert collection_w.num_entities == nb * 2
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_auto_id_true_list_data(self, pk_field):
|
||||
"""
|
||||
target: test insert ids fields values when auto_id=True
|
||||
method: 1.create collection with auto_id=True 2.insert list data with ids field values
|
||||
expected: assert num entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
schema = cf.gen_default_collection_schema(
|
||||
primary_field=pk_field, auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=schema)
|
||||
data = cf.gen_default_list_data()
|
||||
if pk_field == ct.default_int64_field_name:
|
||||
mutation_res, _ = collection_w.insert(data=data[1:])
|
||||
else:
|
||||
del data[2]
|
||||
mutation_res, _ = collection_w.insert(data=data)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert cf._check_primary_keys(mutation_res.primary_keys, ct.default_nb)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_auto_id_true_with_list_values(self, pk_field):
|
||||
"""
|
||||
target: test insert with auto_id=True
|
||||
method: create collection with auto_id=True
|
||||
expected: 1.verify num entities 2.verify ids
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
schema = cf.gen_default_collection_schema(primary_field=pk_field, auto_id=True)
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=schema)
|
||||
nb = 100
|
||||
data = cf.gen_column_data_by_schema(nb=nb, schema=collection_w.schema)
|
||||
|
||||
collection_w.insert(data=data)
|
||||
assert collection_w.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_auto_id_false_same_values(self):
|
||||
"""
|
||||
target: test insert same ids with auto_id false
|
||||
method: 1.create collection with auto_id=False 2.insert same int64 field values
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 100
|
||||
data = cf.gen_default_list_data(nb=nb)
|
||||
data[0] = [1 for i in range(nb)]
|
||||
mutation_res, _ = collection_w.insert(data)
|
||||
assert mutation_res.insert_count == nb
|
||||
assert mutation_res.primary_keys == data[0]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_auto_id_false_negative_values(self):
|
||||
"""
|
||||
target: test insert negative ids with auto_id false
|
||||
method: auto_id=False, primary field values is negative
|
||||
expected: verify num entities
|
||||
"""
|
||||
c_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
nb = 100
|
||||
data = cf.gen_default_list_data(nb)
|
||||
data[0] = [i for i in range(0, -nb, -1)]
|
||||
mutation_res, _ = collection_w.insert(data)
|
||||
assert mutation_res.primary_keys == data[0]
|
||||
assert collection_w.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
# @pytest.mark.xfail(reason="issue 15416")
|
||||
def test_insert_multi_threading(self):
|
||||
"""
|
||||
target: test concurrent insert
|
||||
method: multi threads insert
|
||||
expected: verify num entities
|
||||
"""
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data(ct.default_nb)
|
||||
thread_num = 4
|
||||
threads = []
|
||||
primary_keys = df[ct.default_int64_field_name].values.tolist()
|
||||
|
||||
def insert(thread_i):
|
||||
log.debug(f'In thread-{thread_i}')
|
||||
mutation_res, _ = collection_w.insert(df)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == primary_keys
|
||||
|
||||
for i in range(thread_num):
|
||||
x = threading.Thread(target=insert, args=(i,))
|
||||
threads.append(x)
|
||||
x.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
assert collection_w.num_entities == ct.default_nb * thread_num
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_multi_times(self, dim):
|
||||
"""
|
||||
target: test insert multi times
|
||||
method: insert data multi times
|
||||
expected: verify num entities
|
||||
"""
|
||||
step = 120
|
||||
nb = 12000
|
||||
collection_w = self.init_collection_general(prefix, dim=dim)[0]
|
||||
for _ in range(nb // step):
|
||||
df = cf.gen_default_dataframe_data(step, dim)
|
||||
mutation_res, _ = collection_w.insert(data=df)
|
||||
assert mutation_res.insert_count == step
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist(
|
||||
)
|
||||
|
||||
assert collection_w.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_equal_to_resource_limit(self):
|
||||
"""
|
||||
target: test insert data equal to RPC limitation 64MB (67108864)
|
||||
method: calculated critical value and insert equivalent data
|
||||
expected: raise exception
|
||||
"""
|
||||
# nb = 127583 without json field
|
||||
nb = 108993
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=collection_name)
|
||||
data = cf.gen_default_dataframe_data(nb)
|
||||
collection_w.insert(data=data)
|
||||
assert collection_w.num_entities == nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("nullable", [True, False])
|
||||
@pytest.mark.parametrize("default_value", [[], [None for i in range(ct.default_nb)]])
|
||||
def test_insert_one_field_using_default_value(self, default_value, nullable, auto_id):
|
||||
"""
|
||||
target: test insert with one field using default value
|
||||
method: 1. create a collection with one field using default value
|
||||
2. insert using default value to replace the field value []/[None]
|
||||
expected: insert successfully
|
||||
"""
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(),
|
||||
cf.gen_string_field(default_value="abc", nullable=nullable), cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields, auto_id=auto_id)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
# default value fields, [] or [None]
|
||||
data = [
|
||||
[i for i in range(ct.default_nb)],
|
||||
[np.float32(i) for i in range(ct.default_nb)],
|
||||
default_value,
|
||||
cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
]
|
||||
if auto_id:
|
||||
del data[0]
|
||||
collection_w.insert(data)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("enable_partition_key", [True, False])
|
||||
@pytest.mark.parametrize("nullable", [True, False])
|
||||
def test_insert_dataframe_using_default_data(self, enable_partition_key, nullable):
|
||||
"""
|
||||
target: test insert with dataframe
|
||||
method: insert with valid dataframe using default data
|
||||
expected: insert successfully
|
||||
"""
|
||||
if enable_partition_key is True and nullable is True:
|
||||
pytest.skip("partition key field not support nullable")
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(),
|
||||
cf.gen_string_field(default_value="abc", is_partition_key=enable_partition_key, nullable=nullable),
|
||||
cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
|
||||
df = pd.DataFrame({
|
||||
"int64": pd.Series(data=[i for i in range(ct.default_nb)]),
|
||||
"float": pd.Series(data=[float(i) for i in range(ct.default_nb)], dtype="float32"),
|
||||
"varchar": pd.Series(data=[None for _ in range(ct.default_nb)]),
|
||||
"float_vector": vectors
|
||||
})
|
||||
collection_w.insert(df)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_dataframe_using_none_data(self):
|
||||
"""
|
||||
target: test insert with dataframe
|
||||
method: insert with valid dataframe using none data
|
||||
expected: insert successfully
|
||||
"""
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(),
|
||||
cf.gen_string_field(default_value=None, nullable=True), cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
|
||||
df = pd.DataFrame({
|
||||
"int64": pd.Series(data=[i for i in range(ct.default_nb)]),
|
||||
"float": pd.Series(data=[float(i) for i in range(ct.default_nb)], dtype="float32"),
|
||||
"varchar": pd.Series(data=[None for _ in range(ct.default_nb)]),
|
||||
"float_vector": vectors
|
||||
})
|
||||
collection_w.insert(df)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
|
||||
|
||||
class TestInsertAsync(TestcaseBase):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test insert async
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_async_false(self):
|
||||
"""
|
||||
target: test insert with false async
|
||||
method: async = false
|
||||
expected: verify num entities
|
||||
"""
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data()
|
||||
mutation_res, _ = collection_w.insert(data=df, _async=False)
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist(
|
||||
)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_insert_async_callback(self):
|
||||
"""
|
||||
target: test insert with callback func
|
||||
method: insert with callback func
|
||||
expected: verify num entities
|
||||
"""
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data()
|
||||
future, _ = collection_w.insert(
|
||||
data=df, _async=True, _callback=assert_mutation_result)
|
||||
future.done()
|
||||
mutation_res = future.result()
|
||||
assert mutation_res.primary_keys == df[ct.default_int64_field_name].values.tolist(
|
||||
)
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_async_callback_timeout(self):
|
||||
"""
|
||||
target: test insert async with callback
|
||||
method: insert 10w entities with timeout=1
|
||||
expected: raise exception
|
||||
"""
|
||||
nb = 100000
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix))
|
||||
df = cf.gen_default_dataframe_data(nb)
|
||||
future, _ = collection_w.insert(
|
||||
data=df, _async=True, _callback=None, timeout=0.2)
|
||||
with pytest.raises(MilvusException):
|
||||
future.result()
|
||||
|
||||
def assert_mutation_result(mutation_res):
|
||||
assert mutation_res.insert_count == ct.default_nb
|
||||
|
||||
class TestInsertInvalid(TestcaseBase):
|
||||
"""
|
||||
******************************************************************
|
||||
The following cases are used to test insert invalid params
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_insert_with_invalid_partition_name(self):
|
||||
"""
|
||||
target: test insert with invalid scenario
|
||||
method: insert with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
collection_name = cf.gen_unique_str(prefix)
|
||||
collection_w = self.init_collection_wrap(name=collection_name)
|
||||
df = cf.gen_default_list_data(ct.default_nb)
|
||||
error = {ct.err_code: 15, 'err_msg': "partition not found"}
|
||||
mutation_res, _ = collection_w.insert(data=df, partition_name="p", check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("default_value", [[], None])
|
||||
def test_insert_tuple_using_default_value(self, default_value):
|
||||
"""
|
||||
target: test insert with tuple
|
||||
method: insert with invalid tuple
|
||||
expected: raise exception
|
||||
"""
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_vec_field(),
|
||||
cf.gen_string_field(), cf.gen_float_field(default_value=np.float32(3.14))]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
int_values = [i for i in range(0, ct.default_nb)]
|
||||
string_values = ["abc" for i in range(ct.default_nb)]
|
||||
data = (int_values, vectors, string_values, default_value)
|
||||
error = {ct.err_code: 999, ct.err_msg: "The type of data should be List, pd.DataFrame or Dict"}
|
||||
collection_w.upsert(data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
class TestUpsertValid(TestcaseBase):
|
||||
""" Valid test case of Upsert interface """
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("enable_partition_key", [True, False])
|
||||
@pytest.mark.parametrize("nullable", [True, False])
|
||||
def test_upsert_dataframe_using_default_data(self, enable_partition_key, nullable):
|
||||
"""
|
||||
target: test upsert with dataframe
|
||||
method: upsert with valid dataframe using default data
|
||||
expected: upsert successfully
|
||||
"""
|
||||
if enable_partition_key is True and nullable is True:
|
||||
pytest.skip("partition key field not support nullable")
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(),
|
||||
cf.gen_string_field(default_value="abc", is_partition_key=enable_partition_key, nullable=nullable),
|
||||
cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
collection_w.create_index(ct.default_float_vec_field_name, default_index_params)
|
||||
collection_w.load()
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
|
||||
df = pd.DataFrame({
|
||||
"int64": pd.Series(data=[i for i in range(ct.default_nb)]),
|
||||
"float": pd.Series(data=[float(i) for i in range(ct.default_nb)], dtype="float32"),
|
||||
"varchar": pd.Series(data=[None for _ in range(ct.default_nb)]),
|
||||
"float_vector": vectors
|
||||
})
|
||||
collection_w.upsert(df)
|
||||
exp = f"{ct.default_string_field_name} == 'abc'"
|
||||
res = collection_w.query(exp, output_fields=[ct.default_string_field_name])[0]
|
||||
assert len(res) == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_upsert_dataframe_using_none_data(self):
|
||||
"""
|
||||
target: test upsert with dataframe
|
||||
method: upsert with valid dataframe using none data
|
||||
expected: upsert successfully
|
||||
"""
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(),
|
||||
cf.gen_string_field(default_value=None, nullable=True),
|
||||
cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
collection_w.create_index(ct.default_float_vec_field_name, default_index_params)
|
||||
collection_w.load()
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
|
||||
df = pd.DataFrame({
|
||||
"int64": pd.Series(data=[i for i in range(ct.default_nb)]),
|
||||
"float": pd.Series(data=[float(i) for i in range(ct.default_nb)], dtype="float32"),
|
||||
"varchar": pd.Series(data=[None for _ in range(ct.default_nb)]),
|
||||
"float_vector": vectors
|
||||
})
|
||||
collection_w.upsert(df)
|
||||
exp = f"{ct.default_int64_field_name} >= 0"
|
||||
res = collection_w.query(exp, output_fields=[ct.default_string_field_name])[0]
|
||||
assert len(res) == ct.default_nb
|
||||
assert res[0][ct.default_string_field_name] is None
|
||||
exp = f"{ct.default_string_field_name} == ''"
|
||||
res = collection_w.query(exp, output_fields=[ct.default_string_field_name])[0]
|
||||
assert len(res) == 0
|
||||
|
||||
|
||||
class TestUpsertInvalid(TestcaseBase):
|
||||
""" Invalid test case of Upsert interface """
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("partition_name", ct.invalid_resource_names[4:])
|
||||
def test_upsert_partition_name_non_existing(self, partition_name):
|
||||
"""
|
||||
target: test upsert partition name invalid
|
||||
method: 1. create a collection with partitions
|
||||
2. upsert with invalid partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(pre_upsert)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
p_name = cf.gen_unique_str('partition_')
|
||||
collection_w.create_partition(p_name)
|
||||
cf.insert_data(collection_w)
|
||||
data = cf.gen_default_dataframe_data(nb=100)
|
||||
error = {ct.err_code: 999, ct.err_msg: "Invalid partition name"}
|
||||
if partition_name == "n-ame":
|
||||
error = {ct.err_code: 999, ct.err_msg: f"partition not found[partition={partition_name}]"}
|
||||
collection_w.upsert(data=data, partition_name=partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_upsert_partition_name_nonexistent(self):
|
||||
"""
|
||||
target: test upsert partition name nonexistent
|
||||
method: 1. create a collection
|
||||
2. upsert with nonexistent partition name
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(pre_upsert)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
data = cf.gen_default_dataframe_data(nb=2)
|
||||
partition_name = "partition1"
|
||||
error = {ct.err_code: 200, ct.err_msg: f"partition not found[partition={partition_name}]"}
|
||||
collection_w.upsert(data=data, partition_name=partition_name,
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip("insert and upsert have removed the [] error check")
|
||||
def test_upsert_multi_partitions(self):
|
||||
"""
|
||||
target: test upsert two partitions
|
||||
method: 1. create a collection and two partitions
|
||||
2. upsert two partitions
|
||||
expected: raise exception
|
||||
"""
|
||||
c_name = cf.gen_unique_str(pre_upsert)
|
||||
collection_w = self.init_collection_wrap(name=c_name)
|
||||
collection_w.create_partition("partition_1")
|
||||
collection_w.create_partition("partition_2")
|
||||
cf.insert_data(collection_w)
|
||||
data = cf.gen_default_dataframe_data(nb=1000)
|
||||
error = {ct.err_code: 999, ct.err_msg: "['partition_1', 'partition_2'] has type <class 'list'>, "
|
||||
"but expected one of: (<class 'bytes'>, <class 'str'>)"}
|
||||
collection_w.upsert(data=data, partition_name=["partition_1", "partition_2"],
|
||||
check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("default_value", [[], None])
|
||||
def test_upsert_tuple_using_default_value(self, default_value):
|
||||
"""
|
||||
target: test upsert with tuple
|
||||
method: upsert with invalid tuple
|
||||
expected: raise exception
|
||||
"""
|
||||
fields = [cf.gen_int64_field(is_primary=True), cf.gen_float_field(default_value=np.float32(3.14)),
|
||||
cf.gen_string_field(), cf.gen_float_vec_field()]
|
||||
schema = cf.gen_collection_schema(fields)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
vectors = cf.gen_vectors(ct.default_nb, ct.default_dim)
|
||||
int_values = [i for i in range(0, ct.default_nb)]
|
||||
string_values = ["abc" for i in range(ct.default_nb)]
|
||||
data = (int_values, default_value, string_values, vectors)
|
||||
error = {ct.err_code: 999, ct.err_msg: "The type of data should be List, pd.DataFrame or Dict"}
|
||||
collection_w.upsert(data, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
|
||||
class TestInsertArray(TestcaseBase):
|
||||
""" Test case of Insert array """
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("auto_id", [True, False])
|
||||
def test_insert_array_dataframe(self, auto_id):
|
||||
"""
|
||||
target: test insert DataFrame data
|
||||
method: Insert data in the form of dataframe
|
||||
expected: assert num entities
|
||||
"""
|
||||
schema = cf.gen_array_collection_schema(auto_id=auto_id)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
data = cf.gen_array_dataframe_data()
|
||||
if auto_id:
|
||||
data = data.drop(ct.default_int64_field_name, axis=1)
|
||||
collection_w.insert(data=data)
|
||||
collection_w.flush()
|
||||
assert collection_w.num_entities == ct.default_nb
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("auto_id", [True, False])
|
||||
def test_insert_array_list(self, auto_id):
|
||||
"""
|
||||
target: test insert list data
|
||||
method: Insert data in the form of a list
|
||||
expected: assert num entities
|
||||
"""
|
||||
schema = cf.gen_array_collection_schema(auto_id=auto_id)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
|
||||
nb = ct.default_nb
|
||||
arr_len = ct.default_max_capacity
|
||||
pk_values = [i for i in range(nb)]
|
||||
float_vec = cf.gen_vectors(nb, ct.default_dim)
|
||||
int32_values = [[np.int32(j) for j in range(i, i+arr_len)] for i in range(nb)]
|
||||
float_values = [[np.float32(j) for j in range(i, i+arr_len)] for i in range(nb)]
|
||||
string_values = [[str(j) for j in range(i, i+arr_len)] for i in range(nb)]
|
||||
|
||||
data = [pk_values, float_vec, int32_values, float_values, string_values]
|
||||
if auto_id:
|
||||
del data[0]
|
||||
# log.info(data[0][1])
|
||||
collection_w.insert(data=data)
|
||||
assert collection_w.num_entities == nb
|
||||
@@ -0,0 +1,116 @@
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from utils.util_log import test_log as log
|
||||
from base.client_base import TestcaseBase
|
||||
import random
|
||||
import pytest
|
||||
|
||||
|
||||
class TestIssues(TestcaseBase):
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("par_key_field", [ct.default_int64_field_name])
|
||||
@pytest.mark.parametrize("use_upsert", [True, False])
|
||||
def test_issue_30607(self, par_key_field, use_upsert):
|
||||
"""
|
||||
Method:
|
||||
1. create a collection with partition key on collection schema with customized num_partitions
|
||||
2. randomly check 200 entities
|
||||
2. verify partition key values are hashed into correct partitions
|
||||
"""
|
||||
self._connect()
|
||||
pk_field = cf.gen_string_field(name='pk', is_primary=True)
|
||||
int64_field = cf.gen_int64_field()
|
||||
string_field = cf.gen_string_field()
|
||||
vector_field = cf.gen_float_vec_field()
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, int64_field, string_field, vector_field],
|
||||
auto_id=False, partition_key_field=par_key_field)
|
||||
c_name = cf.gen_unique_str("par_key")
|
||||
collection_w = self.init_collection_wrap(name=c_name, schema=schema, num_partitions=9)
|
||||
|
||||
# insert
|
||||
nb = 500
|
||||
string_prefix = cf.gen_str_by_length(length=6)
|
||||
entities_per_parkey = 20
|
||||
for n in range(entities_per_parkey):
|
||||
pk_values = [str(i) for i in range(n * nb, (n+1)*nb)]
|
||||
int64_values = [i for i in range(0, nb)]
|
||||
string_values = [string_prefix + str(i) for i in range(0, nb)]
|
||||
float_vec_values = gen_vectors(nb, ct.default_dim)
|
||||
data = [pk_values, int64_values, string_values, float_vec_values]
|
||||
if use_upsert:
|
||||
collection_w.upsert(data)
|
||||
else:
|
||||
collection_w.insert(data)
|
||||
|
||||
# flush
|
||||
collection_w.flush()
|
||||
num_entities = collection_w.num_entities
|
||||
# build index
|
||||
collection_w.create_index(field_name=vector_field.name, index_params=ct.default_index)
|
||||
|
||||
for index_on_par_key_field in [False, True]:
|
||||
collection_w.release()
|
||||
if index_on_par_key_field:
|
||||
collection_w.create_index(field_name=par_key_field, index_params={})
|
||||
# load
|
||||
collection_w.load()
|
||||
|
||||
# verify the partition key values are bashed correctly
|
||||
seeds = 200
|
||||
rand_ids = random.sample(range(0, num_entities), seeds)
|
||||
rand_ids = [str(rand_ids[i]) for i in range(len(rand_ids))]
|
||||
res, _ = collection_w.query(expr=f"pk in {rand_ids}", output_fields=["pk", par_key_field])
|
||||
# verify every the random id exists
|
||||
assert len(res) == len(rand_ids)
|
||||
|
||||
dirty_count = 0
|
||||
for i in range(len(res)):
|
||||
pk = res[i].get("pk")
|
||||
parkey_value = res[i].get(par_key_field)
|
||||
res_parkey, _ = collection_w.query(expr=f"{par_key_field}=={parkey_value} and pk=='{pk}'",
|
||||
output_fields=["pk", par_key_field])
|
||||
if len(res_parkey) != 1:
|
||||
log.info(f"dirty data found: pk {pk} with parkey {parkey_value}")
|
||||
dirty_count += 1
|
||||
assert dirty_count == 0
|
||||
log.info(f"check randomly {seeds}/{num_entities}, dirty count={dirty_count}")
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_issue_32294(self):
|
||||
"""
|
||||
Method:
|
||||
1. create a collection with partition key on collection schema with customized num_partitions
|
||||
2. randomly check 200 entities
|
||||
2. verify partition key values are hashed into correct partitions
|
||||
"""
|
||||
self._connect()
|
||||
pk_field = cf.gen_int64_field(name='pk', is_primary=True)
|
||||
string_field = cf.gen_string_field(name="metadata")
|
||||
vector_field = cf.gen_float_vec_field()
|
||||
schema = cf.gen_collection_schema(fields=[pk_field, string_field, vector_field], auto_id=True)
|
||||
collection_w = self.init_collection_wrap(schema=schema)
|
||||
|
||||
# insert
|
||||
nb = 500
|
||||
string_values = [str(i) for i in range(0, nb)]
|
||||
float_vec_values = gen_vectors(nb, ct.default_dim)
|
||||
string_values[0] = ('{\n'
|
||||
'"Header 1": "Foo1?", \n'
|
||||
'"document_category": "acme", \n'
|
||||
'"type": "passage"\n'
|
||||
'}')
|
||||
string_values[1] = '{"Header 1": "Foo1?", "document_category": "acme", "type": "passage"}'
|
||||
data = [string_values, float_vec_values]
|
||||
collection_w.insert(data)
|
||||
collection_w.create_index(field_name=ct.default_float_vec_field_name, index_params=ct.default_index)
|
||||
collection_w.load()
|
||||
|
||||
expr = "metadata like '%passage%'"
|
||||
collection_w.search(float_vec_values[-2:], ct.default_float_vec_field_name, {},
|
||||
ct.default_limit, expr, output_fields=["metadata"],
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"nq": 2,
|
||||
"limit": 2})
|
||||
@@ -0,0 +1,750 @@
|
||||
"""
|
||||
Large TopK Feature E2E Tests
|
||||
Feature: collection-level property `query_mode: large_topk`
|
||||
- Backend index auto-switches to IVF RBQ2
|
||||
- Supports topk up to 1M level
|
||||
- alter/drop property requires dropping vector index first
|
||||
|
||||
Test Plan: tests/python_client/docs/test-plan-large-topk.md
|
||||
Issue: https://github.com/milvus-io/milvus/issues/48725
|
||||
"""
|
||||
import pytest
|
||||
from base.client_v2_base import TestMilvusClientV2Base
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from pymilvus import AnnSearchRequest, DataType, MilvusException, RRFRanker
|
||||
|
||||
prefix = "large_topk"
|
||||
default_nb = 3000 # > 1024 to trigger IVF index build
|
||||
default_dim = ct.default_dim # 128
|
||||
default_nq = ct.default_nq # 2
|
||||
default_limit = ct.default_limit # 10
|
||||
vec_field = ct.default_float_vec_field_name # "float_vector"
|
||||
large_topk_first = 16385 # first topk above the normal 16384 limit
|
||||
large_topk_total = 21000 # total rows in col_large_topk (> large_topk_first + default_nb for headroom)
|
||||
large_topk_max = 1_000_000 # maximum supported topk in large_topk mode
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("TestLargeTopkShared")
|
||||
class TestLargeTopkShared(TestMilvusClientV2Base):
|
||||
"""
|
||||
L0 + L1 read-only tests. Two shared collections are prepared once:
|
||||
- col_large_topk: query_mode=large_topk, 6×default_nb vectors, FLAT index
|
||||
- col_normal: no query_mode set, default_nb vectors, FLAT index
|
||||
All tests are read-only; no data modification or index changes.
|
||||
"""
|
||||
|
||||
def setup_class(self):
|
||||
super().setup_class(self)
|
||||
self.col_large_topk = "TestLargeTopkSharedLargeTopk" + cf.gen_unique_str("_")
|
||||
self.col_normal = "TestLargeTopkSharedNormal" + cf.gen_unique_str("_")
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_collections(self, request):
|
||||
client = self._client()
|
||||
|
||||
def _create(col_name, enable_large_topk, batches=1):
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
query_mode_props = {"query_mode": "large_topk"} if enable_large_topk else None
|
||||
self.create_collection(client, col_name, schema=schema,
|
||||
properties=query_mode_props, force_teardown=False)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
# FLAT: 100% recall, simplifies assertions
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col_name, index_params)
|
||||
self.load_collection(client, col_name)
|
||||
rows = [{vec_field: cf.gen_vectors(1, default_dim)[0]} for _ in range(default_nb)]
|
||||
for _ in range(batches):
|
||||
self.insert(client, col_name, rows)
|
||||
self.flush(client, col_name)
|
||||
|
||||
# large_topk_total rows total; topk tests use large_topk_total - default_nb for ~1 batch headroom
|
||||
_create(self.col_large_topk, enable_large_topk=True, batches=large_topk_total // default_nb)
|
||||
_create(self.col_normal, enable_large_topk=False)
|
||||
|
||||
def teardown():
|
||||
client.drop_collection(self.col_large_topk)
|
||||
client.drop_collection(self.col_normal)
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_create_with_large_topk_property(self):
|
||||
"""
|
||||
target: verify query_mode=large_topk property is correctly set at create time
|
||||
method:
|
||||
1. describe_collection and check properties dict contains query_mode=large_topk
|
||||
2. search with limit=100, check nq/limit/metric
|
||||
expected: property present; search returns 100 results with ascending L2 distances
|
||||
"""
|
||||
client = self._client()
|
||||
desc = client.describe_collection(self.col_large_topk)
|
||||
props = desc.get("properties", {})
|
||||
assert props.get("query_mode") == "large_topk", f"property not set: {props}"
|
||||
|
||||
vectors = cf.gen_vectors(default_nq, default_dim)
|
||||
self.search(client, self.col_large_topk, data=vectors,
|
||||
anns_field=vec_field, limit=100,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": 100,
|
||||
"metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("topk", [1, 100, 16384])
|
||||
def test_search_various_topk(self, topk):
|
||||
"""
|
||||
target: verify search with various topk values all return correct counts
|
||||
method: search col_with_prop with topk in [1, 100, 16384], check nq/limit/metric
|
||||
expected: each search returns exactly min(topk, default_nb) hits per query
|
||||
"""
|
||||
client = self._client()
|
||||
vectors = cf.gen_vectors(default_nq, default_dim)
|
||||
expected_limit = min(topk, large_topk_total)
|
||||
self.search(client, self.col_large_topk, data=vectors,
|
||||
anns_field=vec_field, limit=topk,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": expected_limit,
|
||||
"metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_search_result_consistency(self):
|
||||
"""
|
||||
target: verify repeated searches return identical results (IDs and distances)
|
||||
method: search same query vector twice, compare id list and distance list
|
||||
expected: ids and distances are identical across two calls
|
||||
"""
|
||||
client = self._client()
|
||||
vectors = cf.gen_vectors(1, default_dim)
|
||||
|
||||
res1 = client.search(self.col_large_topk, data=vectors,
|
||||
limit=50, anns_field=vec_field)
|
||||
res2 = client.search(self.col_large_topk, data=vectors,
|
||||
limit=50, anns_field=vec_field)
|
||||
|
||||
ids1 = [r["id"] for r in res1[0]]
|
||||
ids2 = [r["id"] for r in res2[0]]
|
||||
dist1 = [r["distance"] for r in res1[0]]
|
||||
dist2 = [r["distance"] for r in res2[0]]
|
||||
assert ids1 == ids2, f"Inconsistent IDs: {ids1[:5]} vs {ids2[:5]}"
|
||||
assert dist1 == dist2, f"Inconsistent distances: {dist1[:5]} vs {dist2[:5]}"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("topk", [large_topk_first, large_topk_total - default_nb])
|
||||
def test_large_topk_above_normal_limit(self, topk):
|
||||
"""
|
||||
target: verify query_mode=large_topk allows topk above 16384 without error (core MVP)
|
||||
method: search col_large_topk (large_topk_total vectors) with topk in [large_topk_first, large_topk_total - default_nb]
|
||||
expected: search completes without exception, returns results with ascending L2 distances.
|
||||
Note: exact result count is not asserted — large_topk forces IVF index which
|
||||
does not guarantee 100% recall, so returned count may be < topk.
|
||||
"""
|
||||
client = self._client()
|
||||
results = client.search(self.col_large_topk, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=topk)
|
||||
for hits in results:
|
||||
assert len(hits) > 0, "Expected non-empty results"
|
||||
distances = [h["distance"] for h in hits]
|
||||
assert distances == sorted(distances), "L2 distances should be ascending"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_topk_without_large_topk_property(self):
|
||||
"""
|
||||
target: verify topk>16384 is rejected when query_mode=large_topk is not set
|
||||
method:
|
||||
1. search col_without_prop with limit=16384 — should succeed
|
||||
2. search col_without_prop with limit=large_topk_first — should raise MilvusException
|
||||
expected: limit=16384 OK; limit=large_topk_first raises error with message about invalid topk
|
||||
"""
|
||||
client = self._client()
|
||||
vectors = cf.gen_vectors(default_nq, default_dim)
|
||||
|
||||
# Normal topk limit works fine
|
||||
self.search(client, self.col_normal, data=vectors,
|
||||
anns_field=vec_field, limit=16384,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": default_nb,
|
||||
"metric": "L2"})
|
||||
|
||||
# Above limit must be rejected
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f"topk [{large_topk_first}] is invalid, it should be in range [1, 16384]"}
|
||||
self.search(client, self.col_normal, data=vectors,
|
||||
anns_field=vec_field, limit=large_topk_first,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
# Note: search_iterator and query_iterator are NOT affected by query_mode=large_topk.
|
||||
# The SDK enforces batch_size <= 16384 client-side (ParamError, unrelated to large_topk).
|
||||
# The iterator `limit` (total result count) uses internal pagination with batch_size <= 16384,
|
||||
# so per-request topk never exceeds 16384. No large_topk-specific iterator tests are needed.
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_query_large_limit(self):
|
||||
"""
|
||||
target: verify query() with limit > 16384 works when query_mode=large_topk is set
|
||||
method: query col_large_topk with limit=large_topk_first, verify results returned
|
||||
expected: returns large_topk_first results without error
|
||||
"""
|
||||
client = self._client()
|
||||
res = client.query(
|
||||
self.col_large_topk,
|
||||
filter="",
|
||||
output_fields=["id"],
|
||||
limit=large_topk_first,
|
||||
)
|
||||
assert len(res) == large_topk_first, \
|
||||
f"Expected {large_topk_first} results, got {len(res)}"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_query_without_property_fails(self):
|
||||
"""
|
||||
target: verify query() with limit > 16384 is rejected when query_mode=large_topk is NOT set
|
||||
method: query col_normal with limit=large_topk_first
|
||||
expected: MilvusException with invalid topk message
|
||||
"""
|
||||
client = self._client()
|
||||
with pytest.raises(MilvusException) as exc_info:
|
||||
client.query(
|
||||
self.col_normal,
|
||||
filter="",
|
||||
output_fields=["id"],
|
||||
limit=large_topk_first,
|
||||
)
|
||||
assert str(large_topk_first) in str(exc_info.value), \
|
||||
f"Expected topk error, got: {exc_info.value}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Independent Tests (each test owns its own collection)
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestLargeTopkIndependent(TestMilvusClientV2Base):
|
||||
"""
|
||||
Tests that require modifying index or property — each test gets its own collection.
|
||||
force_teardown=True (default) ensures cleanup even on failure.
|
||||
"""
|
||||
|
||||
def _setup_col(self, client, enable_large_topk=True, nb=default_nb):
|
||||
"""Create collection with optional query_mode=large_topk, FLAT index, insert nb rows."""
|
||||
col = cf.gen_collection_name_by_testcase_name(module_index=2)
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
query_mode_props = {"query_mode": "large_topk"} if enable_large_topk else None
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties=query_mode_props, force_teardown=True)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
if nb > 0:
|
||||
rows = [{vec_field: cf.gen_vectors(1, default_dim)[0]} for _ in range(nb)]
|
||||
self.insert(client, col, rows)
|
||||
self.flush(client, col)
|
||||
return col
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alter_collection_add_property(self):
|
||||
"""
|
||||
target: verify alter_collection_properties correctly adds query_mode=large_topk
|
||||
method:
|
||||
1. create collection without property, build FLAT index, insert data
|
||||
2. release → drop_index → alter_collection_properties
|
||||
3. describe_collection to verify property
|
||||
4. rebuild index, load, search with limit=100
|
||||
expected: property set; search returns 100 results with ascending L2 distances
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=False)
|
||||
|
||||
self.release_collection(client, col)
|
||||
self.drop_index(client, col, vec_field)
|
||||
|
||||
self.alter_collection_properties(client, col,
|
||||
properties={"query_mode": "large_topk"})
|
||||
|
||||
desc = client.describe_collection(col)
|
||||
assert desc.get("properties", {}).get("query_mode") == "large_topk"
|
||||
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=100,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq, "limit": 100, "metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_drop_collection_property(self):
|
||||
"""
|
||||
target: verify drop_collection_properties removes query_mode and restores normal behavior
|
||||
method:
|
||||
1. create collection with property, build FLAT index, insert data
|
||||
2. release → drop_index → drop_collection_properties
|
||||
3. describe_collection to verify property absent
|
||||
4. rebuild index, load, search with limit=default_limit
|
||||
expected: property absent; normal search returns default_limit results
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=True)
|
||||
|
||||
self.release_collection(client, col)
|
||||
self.drop_index(client, col, vec_field)
|
||||
|
||||
self.drop_collection_properties(client, col, property_keys=["query_mode"])
|
||||
|
||||
desc = client.describe_collection(col)
|
||||
assert "query_mode" not in desc.get("properties", {}), \
|
||||
f"property still present: {desc.get('properties')}"
|
||||
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": default_limit,
|
||||
"metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_alter_property_without_dropping_index_fails(self):
|
||||
"""
|
||||
target: verify alter_collection_properties is rejected when vector index exists
|
||||
method: create collection with index, call alter_collection_properties directly
|
||||
expected: MilvusException with error code 702 and message containing "vector index"
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=False, nb=0)
|
||||
|
||||
error = {ct.err_code: 702,
|
||||
ct.err_msg: "can not alter query_mode if the collection already has a vector index"}
|
||||
self.alter_collection_properties(client, col,
|
||||
properties={"query_mode": "large_topk"},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_drop_property_without_dropping_index_fails(self):
|
||||
"""
|
||||
target: verify drop_collection_properties is rejected when vector index exists
|
||||
method: create collection with large_topk property and index, call drop directly
|
||||
expected: MilvusException with error code 702 and message containing "vector index"
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=True, nb=0)
|
||||
|
||||
error = {ct.err_code: 702,
|
||||
ct.err_msg: "can not alter query_mode if the collection already has a vector index"}
|
||||
self.drop_collection_properties(client, col,
|
||||
property_keys=["query_mode"],
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_empty_collection_search(self):
|
||||
"""
|
||||
target: verify search on empty large_topk collection returns 0 results
|
||||
method: create collection with property, build FLAT index, load, search without inserting
|
||||
expected: 0 results returned, no exception
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=True, nb=0)
|
||||
|
||||
res = client.search(col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
limit=default_limit, anns_field=vec_field)
|
||||
for hits in res:
|
||||
assert len(hits) == 0, f"Empty collection should return 0 results, got {len(hits)}"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_add_then_drop_property_roundtrip(self):
|
||||
"""
|
||||
target: verify adding then dropping query_mode property restores normal behavior
|
||||
method:
|
||||
1. create plain collection, build FLAT index, insert data
|
||||
2. drop index → alter_collection_properties (add) → rebuild index → search
|
||||
3. drop index → drop_collection_properties → rebuild index → search
|
||||
4. verify property absent after final drop
|
||||
expected: both searches return default_limit results; property absent after drop
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=False)
|
||||
vectors = cf.gen_vectors(default_nq, default_dim)
|
||||
|
||||
# Phase 1: add property
|
||||
self.release_collection(client, col)
|
||||
self.drop_index(client, col, vec_field)
|
||||
self.alter_collection_properties(client, col,
|
||||
properties={"query_mode": "large_topk"})
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
self.search(client, col, data=vectors, anns_field=vec_field,
|
||||
limit=default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": default_limit,
|
||||
"metric": "L2"})
|
||||
|
||||
# Phase 2: drop property
|
||||
self.release_collection(client, col)
|
||||
self.drop_index(client, col, vec_field)
|
||||
self.drop_collection_properties(client, col, property_keys=["query_mode"])
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
self.search(client, col, data=vectors, anns_field=vec_field,
|
||||
limit=default_limit,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": default_limit,
|
||||
"metric": "L2"})
|
||||
|
||||
desc = client.describe_collection(col)
|
||||
assert "query_mode" not in desc.get("properties", {})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_property_persistence_after_reload(self):
|
||||
"""
|
||||
target: verify query_mode=large_topk persists after release + load
|
||||
method:
|
||||
1. create collection with property, insert total > large_topk_first rows, flush
|
||||
2. release → load
|
||||
3. describe_collection to verify property present
|
||||
4. search with limit=large_topk_first, verify large_topk_first results returned
|
||||
expected: property present; topk=large_topk_first returns exactly large_topk_first results
|
||||
"""
|
||||
client = self._client()
|
||||
nb_total = large_topk_first + 1000
|
||||
col = self._setup_col(client, enable_large_topk=True, nb=nb_total)
|
||||
|
||||
self.release_collection(client, col)
|
||||
self.load_collection(client, col)
|
||||
|
||||
desc = client.describe_collection(col)
|
||||
assert desc.get("properties", {}).get("query_mode") == "large_topk", \
|
||||
f"property lost after reload: {desc.get('properties')}"
|
||||
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=large_topk_first,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": large_topk_first,
|
||||
"metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_create_index_after_insert_with_large_topk(self):
|
||||
"""
|
||||
target: verify create_index on a populated collection with query_mode=large_topk,
|
||||
and that large topk search (>16384) works correctly after index build
|
||||
method:
|
||||
1. create collection with query_mode=large_topk (no index yet)
|
||||
2. insert large_topk_first + default_nb rows, flush
|
||||
3. create_index on the populated collection
|
||||
4. load
|
||||
5. search with limit=large_topk_first (>16384) to verify large topk is functional
|
||||
6. search with limit > nb rows (capped to actual nb) to verify normal search works
|
||||
expected: create_index succeeds on populated collection;
|
||||
large topk search returns large_topk_first results;
|
||||
normal search returns min(limit, nb) results
|
||||
note: this catches the timeout seen when rebuilding index after alter/drop property,
|
||||
isolating whether the issue is data-at-index-build-time or the alter step itself
|
||||
"""
|
||||
client = self._client()
|
||||
nb = large_topk_first + default_nb
|
||||
col = cf.gen_collection_name_by_testcase_name()
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties={"query_mode": "large_topk"}, force_teardown=True)
|
||||
|
||||
rows = [{vec_field: cf.gen_vectors(1, default_dim)[0]} for _ in range(nb)]
|
||||
self.insert(client, col, rows)
|
||||
self.flush(client, col)
|
||||
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
|
||||
# verify large topk (>16384) is functional
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=large_topk_first,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq, "limit": large_topk_first, "metric": "L2"})
|
||||
|
||||
# verify normal search also works
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=100,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq, "limit": 100, "metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_large_topk_growing_segment(self):
|
||||
"""
|
||||
target: verify large_topk works on growing segments (before flush)
|
||||
method:
|
||||
1. create collection with property, build FLAT index, load
|
||||
2. insert default_nb rows WITHOUT flush (growing segment)
|
||||
3. search with limit=100 — should return results from growing segment
|
||||
expected: search succeeds and returns hits; no error about topk limit
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_col(client, enable_large_topk=True, nb=0)
|
||||
|
||||
# Insert without flush → growing segment
|
||||
rows = [{vec_field: cf.gen_vectors(1, default_dim)[0]} for _ in range(default_nb)]
|
||||
self.insert(client, col, rows)
|
||||
# No flush — data stays in growing segment
|
||||
|
||||
self.search(client, col, data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field, limit=100,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": default_nq,
|
||||
"limit": 100,
|
||||
"metric": "L2"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_invalid_property_value(self):
|
||||
"""
|
||||
target: verify invalid query_mode value is rejected at collection creation
|
||||
method: create_collection with properties={"query_mode": "invalid_mode"}
|
||||
expected: MilvusException with message containing valid values hint
|
||||
"""
|
||||
client = self._client()
|
||||
col = cf.gen_collection_name_by_testcase_name()
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: 'invalid query_mode value "invalid_mode", valid values: [large_topk]'}
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties={"query_mode": "invalid_mode"},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("value", ["LARGE_TOPK", "Large_TopK", "large_TOPK"])
|
||||
def test_query_mode_value_case_insensitive(self, value):
|
||||
"""
|
||||
target: verify query_mode value is case-sensitive
|
||||
method: create collection with properties={"query_mode": value} (non-lowercase value)
|
||||
expected: create collection fails with invalid query_mode value error
|
||||
"""
|
||||
client = self._client()
|
||||
col = cf.gen_collection_name_by_testcase_name()
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f'invalid query_mode value "{value}", valid values: [large_topk]'}
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties={"query_mode": value},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("key", ["QUERY_MODE", "Query_Mode", "query_MODE"])
|
||||
def test_query_mode_key_case_sensitive(self, key):
|
||||
"""
|
||||
target: verify query_mode key is case-sensitive
|
||||
method: create collection with properties={key: "large_topk"} (wrong-cased key)
|
||||
expected: create collection fails with invalid property key error
|
||||
"""
|
||||
client = self._client()
|
||||
col = cf.gen_collection_name_by_testcase_name()
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f'invalid property key "{key}", did you mean "query_mode"?'}
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties={key: "large_topk"},
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
# Note: search_iterator and query_iterator are NOT affected by query_mode=large_topk.
|
||||
# The SDK enforces batch_size <= 16384 client-side (ParamError code=1, regardless of property).
|
||||
# Iterator `limit` (total results) uses internal pagination with batch_size <= 16384 per request,
|
||||
# so per-request topk never exceeds 16384. No large_topk-specific iterator tests are needed.
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Large topk boundary tests (L3, large data volume)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L3)
|
||||
def test_large_topk_boundary_2m_rows(self):
|
||||
"""
|
||||
target: verify large_topk topk boundary values with 2M rows
|
||||
method:
|
||||
1. create collection with query_mode=large_topk
|
||||
2. insert 2,000,000 rows (128-dim) in batches, flush
|
||||
3. create_index, load
|
||||
4. search with limit=large_topk_max-1 (999,999) → should succeed
|
||||
5. search with limit=large_topk_max (1,000,000) → should succeed, return 1M results
|
||||
6. search with limit=large_topk_max+1 (1,000,001) → should fail with error
|
||||
expected: boundary limits enforced correctly; max valid topk returns 1M results
|
||||
"""
|
||||
client = self._client()
|
||||
total_nb = 2_000_000
|
||||
batch_size = 50_000
|
||||
col = cf.gen_collection_name_by_testcase_name()
|
||||
dim = 64
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=dim)
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties={"query_mode": "large_topk"}, force_teardown=True)
|
||||
|
||||
for _ in range(total_nb // batch_size):
|
||||
vecs = cf.gen_vectors(batch_size, dim)
|
||||
rows = [{vec_field: v} for v in vecs]
|
||||
self.insert(client, col, rows)
|
||||
self.flush(client, col)
|
||||
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="FLAT", metric_type="L2")
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
|
||||
# just below max → should succeed
|
||||
self.search(client, col, data=cf.gen_vectors(1, dim),
|
||||
anns_field=vec_field, limit=large_topk_max - 1,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": 1, "limit": large_topk_max - 1, "metric": "L2"})
|
||||
|
||||
# max valid large topk → should succeed, return 1M results
|
||||
self.search(client, col, data=cf.gen_vectors(1, dim),
|
||||
anns_field=vec_field, limit=large_topk_max,
|
||||
check_task=CheckTasks.check_search_results,
|
||||
check_items={"enable_milvus_client_api": True,
|
||||
"nq": 1, "limit": large_topk_max, "metric": "L2"})
|
||||
|
||||
# over max → should fail
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f"topk [{large_topk_max + 1}] is invalid, "
|
||||
f"it should be in range [1, {large_topk_max}]"}
|
||||
self.search(client, col, data=cf.gen_vectors(1, dim),
|
||||
anns_field=vec_field, limit=large_topk_max + 1,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Hybrid search interface tests
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _setup_dual_vec_col(self, client, enable_large_topk=True, nb=default_nb):
|
||||
"""Create collection with two float vector fields for hybrid search tests.
|
||||
Uses IVF_FLAT index to keep index-build time reasonable for large nb."""
|
||||
col = cf.gen_collection_name_by_testcase_name(module_index=2)
|
||||
schema = self.create_schema(client)[0]
|
||||
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
|
||||
schema.add_field(vec_field, DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
schema.add_field("vec2", DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
query_mode_props = {"query_mode": "large_topk"} if enable_large_topk else None
|
||||
self.create_collection(client, col, schema=schema,
|
||||
properties=query_mode_props, force_teardown=True)
|
||||
index_params = self.prepare_index_params(client)[0]
|
||||
index_params.add_index(vec_field, index_type="IVF_FLAT", metric_type="L2",
|
||||
params={"nlist": 64})
|
||||
index_params.add_index("vec2", index_type="IVF_FLAT", metric_type="L2",
|
||||
params={"nlist": 64})
|
||||
self.create_index(client, col, index_params)
|
||||
self.load_collection(client, col)
|
||||
if nb > 0:
|
||||
rows = [{vec_field: cf.gen_vectors(1, default_dim)[0],
|
||||
"vec2": cf.gen_vectors(1, default_dim)[0]} for _ in range(nb)]
|
||||
self.insert(client, col, rows)
|
||||
self.flush(client, col)
|
||||
return col
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_hybrid_search_large_topk(self):
|
||||
"""
|
||||
target: verify hybrid_search with limit > 16384 works when query_mode=large_topk is set
|
||||
method:
|
||||
1. create collection with two float vector fields and query_mode=large_topk
|
||||
2. insert large_topk_total rows with IVF_FLAT index
|
||||
3. hybrid_search with limit=large_topk_first using RRFRanker
|
||||
expected: hybrid_search completes without error; returns > 0 results; no error code
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_dual_vec_col(client, enable_large_topk=True, nb=large_topk_total)
|
||||
req_list = [
|
||||
AnnSearchRequest(
|
||||
data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field,
|
||||
param={"metric_type": "L2", "nprobe": 16},
|
||||
limit=large_topk_first,
|
||||
),
|
||||
AnnSearchRequest(
|
||||
data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field="vec2",
|
||||
param={"metric_type": "L2", "nprobe": 16},
|
||||
limit=large_topk_first,
|
||||
),
|
||||
]
|
||||
res, _ = self.hybrid_search(client, col,
|
||||
reqs=req_list, ranker=RRFRanker(),
|
||||
limit=large_topk_first)
|
||||
assert len(res) == default_nq, f"Expected {default_nq} query results, got {len(res)}"
|
||||
for hits in res:
|
||||
assert len(hits) > 0, "Expected non-empty hybrid search results"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_hybrid_search_without_property_fails(self):
|
||||
"""
|
||||
target: verify hybrid_search with limit > 16384 is rejected when query_mode=large_topk is NOT set
|
||||
method:
|
||||
1. create collection with two float vector fields and NO query_mode property
|
||||
2. hybrid_search with limit=large_topk_first
|
||||
expected: MilvusException with invalid topk message
|
||||
"""
|
||||
client = self._client()
|
||||
col = self._setup_dual_vec_col(client, enable_large_topk=False)
|
||||
req_list = [
|
||||
AnnSearchRequest(
|
||||
data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field=vec_field,
|
||||
param={"metric_type": "L2"},
|
||||
limit=large_topk_first,
|
||||
),
|
||||
AnnSearchRequest(
|
||||
data=cf.gen_vectors(default_nq, default_dim),
|
||||
anns_field="vec2",
|
||||
param={"metric_type": "L2"},
|
||||
limit=large_topk_first,
|
||||
),
|
||||
]
|
||||
# hybrid_search uses "invalid max query result window" (not "topk [N] is invalid")
|
||||
error = {ct.err_code: 65535,
|
||||
ct.err_msg: f"invalid max query result window, (offset+limit) should be in range [1, 16384], but got {large_topk_first}"}
|
||||
self.hybrid_search(client, col,
|
||||
reqs=req_list, ranker=RRFRanker(),
|
||||
limit=large_topk_first,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
import threading
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from base.partition_wrapper import ApiPartitionWrapper
|
||||
from base.client_base import TestcaseBase
|
||||
from common import common_func as cf
|
||||
from common import common_type as ct
|
||||
from utils.util_log import test_log as log
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common.code_mapping import PartitionErrorMessage
|
||||
|
||||
prefix = "partition_"
|
||||
|
||||
|
||||
class TestPartitionParams(TestcaseBase):
|
||||
""" Test case of partition interface in parameters"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_partition_empty_description(self):
|
||||
"""
|
||||
target: verify create a partition with empty description
|
||||
method: create a partition with empty description
|
||||
expected: create successfully
|
||||
"""
|
||||
# create collection
|
||||
collection_w = self.init_collection_wrap()
|
||||
|
||||
# init partition
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
description = ""
|
||||
self.init_partition_wrap(collection_w, partition_name,
|
||||
description=description,
|
||||
check_task=CheckTasks.check_partition_property,
|
||||
check_items={"name": partition_name, "description": description,
|
||||
"is_empty": True, "num_entities": 0}
|
||||
)
|
||||
|
||||
# check that the partition has been created
|
||||
assert collection_w.has_partition(partition_name)[0]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_partition_max_description_length(self):
|
||||
"""
|
||||
target: verify create a partition with 255 length name and 1024 length description
|
||||
method: create a partition with 255 length name and 1024 length description
|
||||
expected: create successfully
|
||||
"""
|
||||
# create collection
|
||||
collection_w = self.init_collection_wrap()
|
||||
|
||||
# init partition
|
||||
partition_name = cf.gen_str_by_length(255)
|
||||
description = cf.gen_str_by_length(2048)
|
||||
self.init_partition_wrap(collection_w, partition_name,
|
||||
description=description,
|
||||
check_task=CheckTasks.check_partition_property,
|
||||
check_items={"name": partition_name, "description": description,
|
||||
"is_empty": True}
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_partition_special_chars_description(self):
|
||||
"""
|
||||
target: verify create a partition with special characters in description
|
||||
method: create a partition with special characters in description
|
||||
expected: create successfully
|
||||
"""
|
||||
# create collection
|
||||
collection_w = self.init_collection_wrap()
|
||||
|
||||
# create partition
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
description = "!@#¥%……&*("
|
||||
self.init_partition_wrap(collection_w, partition_name,
|
||||
description=description,
|
||||
check_task=CheckTasks.check_partition_property,
|
||||
check_items={"name": partition_name, "description": description,
|
||||
"is_empty": True, "num_entities": 0}
|
||||
)
|
||||
assert collection_w.has_partition(partition_name)[0]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_partition_none_collection(self):
|
||||
"""
|
||||
target: verify create a partition with none collection
|
||||
method: create a partition with none collection
|
||||
expected: raise exception
|
||||
"""
|
||||
# create partition with collection is None
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
self.partition_wrap.init_partition(collection=None, name=partition_name,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items={ct.err_code: 1,
|
||||
ct.err_msg: "Collection must be of type pymilvus.Collection or String"})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.skip(reason="not stable")
|
||||
def test_partition_upsert(self):
|
||||
"""
|
||||
target: verify upsert entities multiple times
|
||||
method: 1. create a collection and a partition
|
||||
2. partition.upsert(data)
|
||||
3. upsert data again
|
||||
expected: upsert data successfully
|
||||
"""
|
||||
# create collection and a partition
|
||||
collection_w = self.init_collection_wrap()
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
partition_w = self.init_partition_wrap(collection_w, partition_name)
|
||||
|
||||
# insert data and load
|
||||
cf.insert_data(collection_w)
|
||||
collection_w.create_index(ct.default_float_vec_field_name, ct.default_index)
|
||||
collection_w.load()
|
||||
|
||||
# upsert data
|
||||
upsert_nb = 1000
|
||||
data, values = cf.gen_default_data_for_upsert(nb=upsert_nb, start=2000)
|
||||
partition_w.upsert(data)
|
||||
res = partition_w.query("int64 >= 2000 && int64 < 3000", [ct.default_float_field_name])[0]
|
||||
time.sleep(5)
|
||||
assert partition_w.num_entities == ct.default_nb // 2
|
||||
assert [res[i][ct.default_float_field_name] for i in range(upsert_nb)] == values.to_list()
|
||||
|
||||
# upsert data
|
||||
data, values = cf.gen_default_data_for_upsert(nb=upsert_nb, start=ct.default_nb)
|
||||
partition_w.upsert(data)
|
||||
res = partition_w.query("int64 >= 3000 && int64 < 4000", [ct.default_float_field_name])[0]
|
||||
time.sleep(5)
|
||||
assert partition_w.num_entities == upsert_nb + ct.default_nb // 2
|
||||
assert [res[i][ct.default_float_field_name] for i in range(upsert_nb)] == values.to_list()
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("data", [cf.gen_default_dataframe_data(10)])
|
||||
def test_partition_insert(self, data):
|
||||
"""
|
||||
target: verify insert entities multiple times
|
||||
method: 1. create a collection and a partition
|
||||
2. partition.insert(data)
|
||||
3. insert data again
|
||||
expected: insert data successfully
|
||||
"""
|
||||
nums = 10
|
||||
# create collection
|
||||
collection_w = self.init_collection_wrap()
|
||||
|
||||
# create partition
|
||||
partition_name = cf.gen_unique_str(prefix)
|
||||
partition_w = self.init_partition_wrap(collection_w, partition_name,
|
||||
check_task=CheckTasks.check_partition_property,
|
||||
check_items={"name": partition_name,
|
||||
"is_empty": True, "num_entities": 0}
|
||||
)
|
||||
|
||||
# insert data
|
||||
partition_w.insert(data)
|
||||
# self._connect().flush([collection_w.name]) # don't need flush for issue #5737
|
||||
assert not partition_w.is_empty
|
||||
assert partition_w.num_entities == nums
|
||||
|
||||
# insert data
|
||||
partition_w.insert(data)
|
||||
# self._connect().flush([collection_w.name])
|
||||
assert not partition_w.is_empty
|
||||
assert partition_w.num_entities == (nums + nums)
|
||||
|
||||
class TestPartitionOperations(TestcaseBase):
|
||||
""" Test case of partition interface in operations """
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("sync", [True, False])
|
||||
def test_partition_insert_sync(self, sync):
|
||||
"""
|
||||
target: verify insert sync
|
||||
method: 1. create a partition
|
||||
2. insert data in sync
|
||||
expected: insert successfully
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
from common.common_type import CaseLabel
|
||||
from common.phrase_match_generator import PhraseMatchTestGenerator
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from pymilvus import FieldSchema, CollectionSchema, DataType
|
||||
|
||||
from common.common_type import CheckTasks
|
||||
from utils.util_log import test_log as log
|
||||
from common import common_func as cf
|
||||
from base.client_base import TestcaseBase
|
||||
import time
|
||||
|
||||
prefix = "phrase_match"
|
||||
|
||||
|
||||
def init_collection_schema(
|
||||
dim: int, tokenizer: str, enable_partition_key: bool
|
||||
) -> CollectionSchema:
|
||||
"""Initialize collection schema with specified parameters"""
|
||||
analyzer_params = {"tokenizer": tokenizer}
|
||||
fields = [
|
||||
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
|
||||
FieldSchema(
|
||||
name="text",
|
||||
dtype=DataType.VARCHAR,
|
||||
max_length=65535,
|
||||
enable_analyzer=True,
|
||||
enable_match=True,
|
||||
is_partition_key=enable_partition_key,
|
||||
analyzer_params=analyzer_params,
|
||||
),
|
||||
FieldSchema(name="emb", dtype=DataType.FLOAT_VECTOR, dim=dim),
|
||||
]
|
||||
return CollectionSchema(fields=fields, description="phrase match test collection")
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
class TestQueryPhraseMatch(TestcaseBase):
|
||||
"""
|
||||
Test cases for phrase match functionality in Milvus using PhraseMatchTestGenerator.
|
||||
This class verifies the phrase matching capabilities with different configurations
|
||||
including various tokenizers, partition keys, and index settings.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("enable_partition_key", [True])
|
||||
@pytest.mark.parametrize("enable_inverted_index", [True])
|
||||
@pytest.mark.parametrize("tokenizer", ["standard", "jieba", "icu"])
|
||||
def test_query_phrase_match_with_different_tokenizer(
|
||||
self, tokenizer, enable_inverted_index, enable_partition_key
|
||||
):
|
||||
"""
|
||||
target: Verify phrase match functionality with different tokenizers (standard, jieba)
|
||||
method: 1. Generate test data using PhraseMatchTestGenerator with language-specific content
|
||||
2. Create collection with appropriate schema (primary key, text field with analyzer, vector field)
|
||||
3. Build both vector (IVF_SQ8) and inverted indexes
|
||||
4. Execute phrase match queries with various slop values
|
||||
5. Compare results against Tantivy reference implementation
|
||||
expected: Milvus phrase match results should exactly match the reference implementation
|
||||
results for all queries and slop values
|
||||
note: Test is marked to xfail for jieba tokenizer due to known issues
|
||||
"""
|
||||
# Initialize parameters
|
||||
dim = 128
|
||||
data_size = 3000
|
||||
num_queries = 10
|
||||
analyzer_params = {"tokenizer": tokenizer}
|
||||
|
||||
# Initialize generator based on tokenizer
|
||||
language = "zh" if tokenizer == "jieba" else "en"
|
||||
generator = PhraseMatchTestGenerator(language=language)
|
||||
|
||||
# Create collection
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix),
|
||||
schema=init_collection_schema(dim, tokenizer, enable_partition_key),
|
||||
consistency_level="Strong",
|
||||
)
|
||||
|
||||
# Generate test data
|
||||
test_data = generator.generate_test_data(data_size, dim)
|
||||
df = pd.DataFrame(test_data)
|
||||
log.info(f"Test data: \n{df['text']}")
|
||||
# Insert data into collection
|
||||
insert_data = [
|
||||
{"id": d["id"], "text": d["text"], "emb": d["emb"]} for d in test_data
|
||||
]
|
||||
collection_w.insert(insert_data)
|
||||
collection_w.flush()
|
||||
|
||||
# Create indexes
|
||||
collection_w.create_index(
|
||||
"emb",
|
||||
{"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}},
|
||||
)
|
||||
if enable_inverted_index:
|
||||
collection_w.create_index(
|
||||
"text", {"index_type": "INVERTED", "params": {"tokenizer": tokenizer}}
|
||||
)
|
||||
|
||||
collection_w.load()
|
||||
|
||||
# Generate and execute test queries
|
||||
test_queries = generator.generate_test_queries(num_queries)
|
||||
|
||||
for query in test_queries:
|
||||
expr = f"phrase_match(text, '{query['query']}', {query['slop']})"
|
||||
log.info(f"Testing query: {expr}")
|
||||
|
||||
# Execute query
|
||||
results, _ = collection_w.query(expr=expr, output_fields=["id", "text"])
|
||||
if tokenizer == "standard":
|
||||
# Get expected matches using Tantivy
|
||||
expected_matches = generator.get_query_results(
|
||||
query["query"], query["slop"]
|
||||
)
|
||||
# Get actual matches from Milvus
|
||||
actual_matches = [r["id"] for r in results]
|
||||
if set(actual_matches) != set(expected_matches):
|
||||
log.info(f"collection schema: {collection_w.schema}")
|
||||
for match_id in expected_matches:
|
||||
# query by id to get text
|
||||
res, _ = collection_w.query(
|
||||
expr=f"id == {match_id}", output_fields=["text"]
|
||||
)
|
||||
text = res[0]["text"]
|
||||
log.info(f"Expected match: {match_id}, text: {text}")
|
||||
|
||||
for match_id in actual_matches:
|
||||
# query by id to get text
|
||||
res, _ = collection_w.query(
|
||||
expr=f"id == {match_id}", output_fields=["text"]
|
||||
)
|
||||
text = res[0]["text"]
|
||||
log.info(f"Matched document: {match_id}, text: {text}")
|
||||
# Assert results match
|
||||
assert (
|
||||
set(actual_matches) == set(expected_matches)
|
||||
), f"Mismatch in results for query '{query['query']}' with slop {query['slop']}"
|
||||
|
||||
else:
|
||||
log.info("Tokenizer is not standard, verify phrase match results by checking all query tokens in result")
|
||||
for result in results:
|
||||
text = result["text"]
|
||||
tokens = self.get_tokens_by_analyzer(query["query"], analyzer_params)
|
||||
for token in tokens:
|
||||
if token not in text:
|
||||
log.info(f"Token {token} not in text {text}")
|
||||
assert False
|
||||
|
||||
@pytest.mark.parametrize("enable_partition_key", [True])
|
||||
@pytest.mark.parametrize("enable_inverted_index", [True])
|
||||
@pytest.mark.parametrize("tokenizer", ["standard"])
|
||||
def test_phrase_match_as_filter_in_vector_search(
|
||||
self, tokenizer, enable_inverted_index, enable_partition_key
|
||||
):
|
||||
"""
|
||||
target: Verify phrase match functionality when used as a filter in vector search
|
||||
method: 1. Generate test data with both text content and vector embeddings
|
||||
2. Create collection with vector field (128d) and text field
|
||||
3. Build both vector index (IVF_SQ8) and text inverted index
|
||||
4. Perform vector search with phrase match as a filter condition
|
||||
5. Verify the combined search results maintain accuracy
|
||||
expected: The system should correctly combine vector search with phrase match filtering
|
||||
while maintaining both search accuracy and performance
|
||||
"""
|
||||
# Initialize parameters
|
||||
dim = 128
|
||||
data_size = 3000
|
||||
num_queries = 10
|
||||
|
||||
# Initialize generator based on tokenizer
|
||||
language = "zh" if tokenizer == "jieba" else "en"
|
||||
generator = PhraseMatchTestGenerator(language=language)
|
||||
|
||||
# Create collection
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix),
|
||||
schema=init_collection_schema(dim, tokenizer, enable_partition_key),
|
||||
consistency_level="Strong",
|
||||
)
|
||||
|
||||
# Generate test data
|
||||
test_data = generator.generate_test_data(data_size, dim)
|
||||
df = pd.DataFrame(test_data)
|
||||
log.info(f"Test data: \n{df['text']}")
|
||||
# Insert data into collection
|
||||
insert_data = [
|
||||
{"id": d["id"], "text": d["text"], "emb": d["emb"]} for d in test_data
|
||||
]
|
||||
collection_w.insert(insert_data)
|
||||
collection_w.flush()
|
||||
|
||||
# Create indexes
|
||||
collection_w.create_index(
|
||||
"emb",
|
||||
{"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}},
|
||||
)
|
||||
if enable_inverted_index:
|
||||
collection_w.create_index(
|
||||
"text", {"index_type": "INVERTED", "params": {"tokenizer": tokenizer}}
|
||||
)
|
||||
|
||||
collection_w.load()
|
||||
|
||||
# Generate and execute test queries
|
||||
test_queries = generator.generate_test_queries(num_queries)
|
||||
|
||||
for query in test_queries:
|
||||
expr = f"phrase_match(text, '{query['query']}', {query['slop']})"
|
||||
log.info(f"Testing query: {expr}")
|
||||
|
||||
# Execute filter search
|
||||
data = [generator.generate_embedding(dim) for _ in range(10)]
|
||||
results, _ = collection_w.search(
|
||||
data,
|
||||
anns_field="emb",
|
||||
param={},
|
||||
limit=10,
|
||||
expr=expr,
|
||||
output_fields=["id", "text"],
|
||||
)
|
||||
|
||||
# Get expected matches using Tantivy
|
||||
expected_matches = generator.get_query_results(
|
||||
query["query"], query["slop"]
|
||||
)
|
||||
# assert results satisfy the filter
|
||||
for hits in results:
|
||||
for hit in hits:
|
||||
assert hit.id in expected_matches
|
||||
|
||||
@pytest.mark.parametrize("slop_value", [0, 1, 2, 5, 10])
|
||||
def test_slop_parameter(self, slop_value):
|
||||
"""
|
||||
target: Verify phrase matching behavior with varying slop values
|
||||
method: 1. Create collection with standard tokenizer
|
||||
2. Generate and insert data with controlled word gaps between terms
|
||||
3. Test phrase matching with specific slop values (0, 1, 2, etc.)
|
||||
4. Verify matches at different word distances
|
||||
5. Compare results with Tantivy reference implementation
|
||||
expected: Results should only match phrases where words are within the specified
|
||||
slop distance, validating the slop parameter's distance control
|
||||
"""
|
||||
dim = 128
|
||||
data_size = 3000
|
||||
num_queries = 2
|
||||
tokenizer = "standard"
|
||||
enable_partition_key = True
|
||||
# Initialize generator based on tokenizer
|
||||
language = "zh" if tokenizer == "jieba" else "en"
|
||||
generator = PhraseMatchTestGenerator(language=language)
|
||||
|
||||
# Create collection
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix),
|
||||
schema=init_collection_schema(dim, tokenizer, enable_partition_key),
|
||||
consistency_level="Strong",
|
||||
)
|
||||
|
||||
# Generate test data
|
||||
test_data = generator.generate_test_data(data_size, dim)
|
||||
df = pd.DataFrame(test_data)
|
||||
log.info(f"Test data: {df['text']}")
|
||||
# Insert data into collection
|
||||
insert_data = [
|
||||
{"id": d["id"], "text": d["text"], "emb": d["emb"]} for d in test_data
|
||||
]
|
||||
collection_w.insert(insert_data)
|
||||
collection_w.flush()
|
||||
|
||||
# Create indexes
|
||||
collection_w.create_index(
|
||||
"emb",
|
||||
{"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}},
|
||||
)
|
||||
|
||||
collection_w.create_index("text", {"index_type": "INVERTED"})
|
||||
|
||||
collection_w.load()
|
||||
|
||||
# Generate and execute test queries
|
||||
test_queries = generator.generate_test_queries(num_queries)
|
||||
|
||||
for query in test_queries:
|
||||
expr = f"phrase_match(text, '{query['query']}', {slop_value})"
|
||||
log.info(f"Testing query: {expr}")
|
||||
|
||||
# Execute query
|
||||
results, _ = collection_w.query(expr=expr, output_fields=["id", "text"])
|
||||
|
||||
# Get expected matches using Tantivy
|
||||
expected_matches = generator.get_query_results(query["query"], slop_value)
|
||||
# Get actual matches from Milvus
|
||||
actual_matches = [r["id"] for r in results]
|
||||
if set(actual_matches) != set(expected_matches):
|
||||
log.info(f"collection schema: {collection_w.schema}")
|
||||
for match_id in expected_matches:
|
||||
# query by id to get text
|
||||
res, _ = collection_w.query(
|
||||
expr=f"id == {match_id}", output_fields=["text"]
|
||||
)
|
||||
text = res[0]["text"]
|
||||
log.info(f"Expected match: {match_id}, text: {text}")
|
||||
|
||||
for match_id in actual_matches:
|
||||
# query by id to get text
|
||||
res, _ = collection_w.query(
|
||||
expr=f"id == {match_id}", output_fields=["text"]
|
||||
)
|
||||
text = res[0]["text"]
|
||||
log.info(f"Matched document: {match_id}, text: {text}")
|
||||
# Assert results match
|
||||
assert (
|
||||
set(actual_matches) == set(expected_matches)
|
||||
), f"Mismatch in results for query '{query['query']}' with slop {slop_value}"
|
||||
|
||||
def test_query_phrase_match_with_different_patterns(self):
|
||||
"""
|
||||
target: Verify phrase matching with various text patterns and complexities
|
||||
method: 1. Create collection with standard tokenizer
|
||||
2. Generate and insert data with diverse phrase patterns:
|
||||
- Exact phrases ("love swimming and running")
|
||||
- Phrases with gaps ("enjoy very basketball")
|
||||
- Complex phrases ("practice tennis seriously often")
|
||||
- Multiple term phrases ("swimming running cycling")
|
||||
3. Test each pattern with appropriate slop values
|
||||
4. Verify minimum match count for each pattern
|
||||
expected: System should correctly identify and match each pattern type
|
||||
with the specified number of matches per pattern
|
||||
"""
|
||||
dim = 128
|
||||
collection_name = f"{prefix}_patterns"
|
||||
schema = init_collection_schema(dim, "standard", False)
|
||||
collection = self.init_collection_wrap(name=collection_name, schema=schema, consistency_level="Strong")
|
||||
|
||||
# Generate data with various patterns
|
||||
generator = PhraseMatchTestGenerator(language="en")
|
||||
data = generator.generate_test_data(3000, dim)
|
||||
collection.insert(data)
|
||||
# Test various patterns
|
||||
test_patterns = [
|
||||
("love swimming and running", 0), # Exact phrase
|
||||
("enjoy very basketball", 1), # Phrase with gap
|
||||
("practice tennis seriously often", 2), # Complex phrase
|
||||
("swimming running cycling", 5), # Multiple activities
|
||||
]
|
||||
|
||||
# Generate and insert documents that match the patterns
|
||||
num_docs_per_pattern = 100
|
||||
pattern_documents = generator.generate_pattern_documents(
|
||||
test_patterns, dim, num_docs_per_pattern=num_docs_per_pattern
|
||||
)
|
||||
collection.insert(pattern_documents)
|
||||
df = pd.DataFrame(pattern_documents)[["id", "text"]]
|
||||
log.info(f"Test data:\n {df}")
|
||||
collection.flush()
|
||||
collection.create_index(
|
||||
field_name="text", index_params={"index_type": "INVERTED"}
|
||||
)
|
||||
collection.create_index(
|
||||
field_name="emb",
|
||||
index_params={
|
||||
"index_type": "IVF_SQ8",
|
||||
"metric_type": "L2",
|
||||
"params": {"nlist": 64},
|
||||
},
|
||||
)
|
||||
collection.load()
|
||||
time.sleep(1)
|
||||
|
||||
for pattern, slop in test_patterns:
|
||||
results, _ = collection.query(
|
||||
expr=f'phrase_match(text, "{pattern}", {slop})', output_fields=["text"],
|
||||
)
|
||||
log.info(
|
||||
f"Pattern '{pattern}' with slop {slop} found {len(results)} matches"
|
||||
)
|
||||
assert len(results) >= num_docs_per_pattern
|
||||
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
class TestQueryPhraseMatchNegative(TestcaseBase):
|
||||
def test_query_phrase_match_with_invalid_slop(self):
|
||||
"""
|
||||
target: Verify error handling for invalid slop values in phrase matching
|
||||
method: 1. Create collection with standard test data
|
||||
2. Test phrase matching with invalid slop values:
|
||||
- Negative slop values (-1)
|
||||
- Extremely large slop values (10^31)
|
||||
3. Verify error handling and response
|
||||
expected: System should:
|
||||
1. Reject queries with invalid slop values
|
||||
2. Return appropriate error responses
|
||||
3. Maintain system stability after invalid queries
|
||||
"""
|
||||
dim = 128
|
||||
collection_name = f"{prefix}_invalid_slop"
|
||||
schema = init_collection_schema(dim, "standard", False)
|
||||
collection = self.init_collection_wrap(name=collection_name, schema=schema, consistency_level="Strong")
|
||||
|
||||
# Insert some test data
|
||||
generator = PhraseMatchTestGenerator(language="en")
|
||||
data = generator.generate_test_data(100, dim)
|
||||
collection.insert(data)
|
||||
|
||||
collection.create_index(
|
||||
field_name="text", index_params={"index_type": "INVERTED"}
|
||||
)
|
||||
collection.create_index(
|
||||
field_name="emb",
|
||||
index_params={
|
||||
"index_type": "IVF_SQ8",
|
||||
"metric_type": "L2",
|
||||
"params": {"nlist": 64},
|
||||
},
|
||||
)
|
||||
collection.load()
|
||||
|
||||
# Test invalid inputs
|
||||
invalid_cases = [
|
||||
("valid query", -1), # Negative slop
|
||||
("valid query", 10 ** 31), # Very large slop
|
||||
]
|
||||
|
||||
for query, slop in invalid_cases:
|
||||
res, result = collection.query(
|
||||
expr=f'phrase_match(text, "{query}", {slop})',
|
||||
output_fields=["text"],
|
||||
check_task=CheckTasks.check_nothing,
|
||||
)
|
||||
log.info(f"Query: '{query[:10]}' with slop {slop} returned {res}")
|
||||
assert result is False
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,347 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from utils.util_log import test_log as log
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
from common import common_func as cf
|
||||
from base.client_base import TestcaseBase
|
||||
import pytest
|
||||
|
||||
|
||||
prefix = "query_iter_"
|
||||
|
||||
|
||||
class TestQueryIterator(TestcaseBase):
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("primary_field", [ct.default_string_field_name, ct.default_int64_field_name])
|
||||
@pytest.mark.parametrize("with_growing", [False, True])
|
||||
def test_query_iterator_normal(self, primary_field, with_growing):
|
||||
"""
|
||||
target: test query iterator normal
|
||||
method: 1. query iterator
|
||||
2. check the result, expect pk
|
||||
verify: no pk lost in interator results
|
||||
3. query iterator with checkpoint file
|
||||
4. iterator.next() for 10 times
|
||||
5. delete some entities before calling a new query iterator
|
||||
6. call a new query iterator with the same checkpoint file, with diff batch_size and output_fields
|
||||
7. iterator.next() until the end
|
||||
verify:
|
||||
1. no pk lost in interator results for the 2 iterators
|
||||
2. no dup pk in the 2 iterators
|
||||
expected: query iterators successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
nb = 4000
|
||||
batch_size = 200
|
||||
collection_w, _, _, insert_ids, _ = \
|
||||
self.init_collection_general(prefix, True, is_index=False, nb=nb, is_flush=True,
|
||||
auto_id=False, primary_field=primary_field)
|
||||
collection_w.create_index(ct.default_float_vec_field_name, {"metric_type": "L2"})
|
||||
collection_w.load()
|
||||
# 2. query iterator
|
||||
expr = "float >= 0"
|
||||
collection_w.query_iterator(batch_size, expr=expr,
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": nb,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": batch_size})
|
||||
# 3. query iterator with checkpoint file
|
||||
iterator_cp_file = f"/tmp/it_{collection_w.name}_cp"
|
||||
iterator = collection_w.query_iterator(batch_size, expr=expr, iterator_cp_file=iterator_cp_file)[0]
|
||||
iter_times = 0
|
||||
first_iter_times = nb // batch_size // 2 # only iterate half of the data for the 1st time
|
||||
pk_list1 = []
|
||||
while iter_times < first_iter_times:
|
||||
iter_times += 1
|
||||
res = iterator.next()
|
||||
if len(res) == 0:
|
||||
iterator.close()
|
||||
assert False, f"The iterator ends before {first_iter_times} times iterators: iter_times: {iter_times}"
|
||||
break
|
||||
for i in range(len(res)):
|
||||
pk_list1.append(res[i][primary_field])
|
||||
file_exist = os.path.isfile(iterator_cp_file)
|
||||
assert file_exist is True, "The checkpoint file exists without iterator close"
|
||||
|
||||
# 4. try to delete and insert some entities before calling a new query iterator
|
||||
delete_ids = random.sample(insert_ids[:nb//2], 101) + random.sample(insert_ids[nb//2:], 101)
|
||||
del_res, _ = collection_w.delete(expr=f"{primary_field} in {delete_ids}")
|
||||
assert del_res.delete_count == len(delete_ids)
|
||||
|
||||
data = cf.gen_default_list_data(nb=333, start=nb)
|
||||
collection_w.insert(data)
|
||||
if not with_growing:
|
||||
collection_w.flush()
|
||||
|
||||
# 5. call a new query iterator with the same checkpoint file to continue the first iterator
|
||||
iterator2 = collection_w.query_iterator(batch_size*2, expr=expr,
|
||||
output_fields=[primary_field, ct.default_float_field_name],
|
||||
iterator_cp_file=iterator_cp_file)[0]
|
||||
while True:
|
||||
res = iterator2.next()
|
||||
if len(res) == 0:
|
||||
iterator2.close()
|
||||
break
|
||||
for i in range(len(res)):
|
||||
pk_list1.append(res[i][primary_field])
|
||||
# 6. verify
|
||||
assert len(pk_list1) == len(set(pk_list1)) == nb
|
||||
file_exist = os.path.isfile(iterator_cp_file)
|
||||
assert file_exist is False, "The checkpoint was deleted after the iterator close"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_query_iterator_using_default_batch_size(self):
|
||||
"""
|
||||
target: test query iterator normal
|
||||
method: 1. query iterator
|
||||
2. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
collection_w = self.init_collection_general(prefix, True)[0]
|
||||
# 2. query iterator
|
||||
collection_w.query_iterator(check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": ct.default_nb,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": ct.default_batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("offset", [500, 1000, 1777])
|
||||
def test_query_iterator_with_offset(self, offset):
|
||||
"""
|
||||
target: test query iterator normal
|
||||
method: 1. query iterator
|
||||
2. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
batch_size = 300
|
||||
collection_w = self.init_collection_general(prefix, True, is_index=False)[0]
|
||||
collection_w.create_index(ct.default_float_vec_field_name, {"metric_type": "L2"})
|
||||
collection_w.load()
|
||||
# 2. search iterator
|
||||
expr = "int64 >= 0"
|
||||
collection_w.query_iterator(batch_size, expr=expr, offset=offset,
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": ct.default_nb - offset,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("vector_data_type", ct.all_dense_vector_types)
|
||||
def test_query_iterator_output_different_vector_type(self, vector_data_type):
|
||||
"""
|
||||
target: test query iterator with output fields
|
||||
method: 1. query iterator output different vector type
|
||||
2. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
batch_size = 400
|
||||
collection_w = self.init_collection_general(prefix, True,
|
||||
vector_data_type=vector_data_type)[0]
|
||||
# 2. query iterator
|
||||
expr = "int64 >= 0"
|
||||
collection_w.query_iterator(batch_size, expr=expr,
|
||||
output_fields=[ct.default_float_vec_field_name],
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": ct.default_nb,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("batch_size", [10, 777, 2000])
|
||||
def test_query_iterator_with_different_batch_size(self, batch_size):
|
||||
"""
|
||||
target: test query iterator normal
|
||||
method: 1. query iterator
|
||||
2. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
offset = 500
|
||||
collection_w = self.init_collection_general(prefix, True, is_index=False)[0]
|
||||
collection_w.create_index(ct.default_float_vec_field_name, {"metric_type": "L2"})
|
||||
collection_w.load()
|
||||
# 2. search iterator
|
||||
expr = "int64 >= 0"
|
||||
collection_w.query_iterator(batch_size=batch_size, expr=expr, offset=offset,
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": ct.default_nb - offset,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("offset", [0, 10, 1000])
|
||||
@pytest.mark.parametrize("limit", [0, 100, 10000])
|
||||
def test_query_iterator_with_different_limit(self, limit, offset):
|
||||
"""
|
||||
target: test query iterator normal
|
||||
method: 1. query iterator
|
||||
2. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
collection_w = self.init_collection_general(prefix, True)[0]
|
||||
# 2. query iterator
|
||||
Count = limit if limit + offset <= ct.default_nb else ct.default_nb - offset
|
||||
collection_w.query_iterator(limit=limit, expr="", offset=offset,
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": max(Count, 0),
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": ct.default_batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
def test_query_iterator_invalid_batch_size(self):
|
||||
"""
|
||||
target: test query iterator invalid limit and offset
|
||||
method: query iterator using invalid limit and offset
|
||||
expected: raise exception
|
||||
"""
|
||||
# 1. initialize with data
|
||||
nb = 17000 # set nb > 16384
|
||||
collection_w = self.init_collection_general(prefix, True, nb=nb)[0]
|
||||
# 2. search iterator
|
||||
expr = "int64 >= 0"
|
||||
error = {"err_code": 1, "err_msg": "batch size cannot be less than zero"}
|
||||
collection_w.query_iterator(batch_size=-1, expr=expr, check_task=CheckTasks.err_res, check_items=error)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("batch_size", [500])
|
||||
@pytest.mark.parametrize("auto_id", [False])
|
||||
def test_query_iterator_empty_expr_with_cp_file_for_times(self, auto_id, batch_size):
|
||||
"""
|
||||
target: verify 2 query iterators with/out checkpoint file works independently
|
||||
method: 1. create a collection
|
||||
2. query the 1st iterator with empty expr and checkpoint file
|
||||
3. iterator.next() for some times
|
||||
4. call a new query iterator with the same checkpoint file
|
||||
expected: verify the 2nd iterator can get the whole results
|
||||
"""
|
||||
# 0. initialize with data
|
||||
collection_w, _, _, insert_ids = self.init_collection_general(prefix, True, auto_id=auto_id)[0:4]
|
||||
|
||||
# 1. call a new query iterator and iterator for some times
|
||||
iterator_cp_file = f"/tmp/it_{collection_w.name}_cp"
|
||||
iterator = collection_w.query_iterator(batch_size=batch_size//2, iterator_cp_file=iterator_cp_file)[0]
|
||||
iter_times = 0
|
||||
first_iter_times = ct.default_nb // batch_size // 2 // 2 # only iterate half of the data for the 1st time
|
||||
while iter_times < first_iter_times:
|
||||
iter_times += 1
|
||||
res = iterator.next()
|
||||
if len(res) == 0:
|
||||
iterator.close()
|
||||
assert False, f"The iterator ends before {first_iter_times} times iterators: iter_times: {iter_times}"
|
||||
break
|
||||
|
||||
# 2. call a new query iterator to get all the results of the collection
|
||||
collection_w.query_iterator(batch_size=batch_size,
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"batch_size": batch_size,
|
||||
"count": ct.default_nb,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"exp_ids": insert_ids})
|
||||
file_exist = os.path.isfile(iterator_cp_file)
|
||||
assert file_exist is True, "The checkpoint exists if not iterator.close()"
|
||||
iterator.close()
|
||||
file_exist = os.path.isfile(iterator_cp_file)
|
||||
assert file_exist is False, "The checkpoint was deleted after the iterator close"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.parametrize("offset", [1000])
|
||||
@pytest.mark.parametrize("batch_size", [500, 1000])
|
||||
def test_query_iterator_expr_empty_with_random_pk_pagination(self, batch_size, offset):
|
||||
"""
|
||||
target: test query iterator with empty expression
|
||||
method: create a collection using random pk, query empty expression with a limit
|
||||
expected: return topK results by order
|
||||
"""
|
||||
# 1. initialize with data
|
||||
collection_w, _, _, insert_ids = self.init_collection_general(prefix, True, random_primary_key=True)[0:4]
|
||||
|
||||
# 2. query with empty expr and check the result
|
||||
exp_ids = sorted(insert_ids)
|
||||
collection_w.query_iterator(batch_size, output_fields=[ct.default_string_field_name],
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"batch_size": batch_size,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"count": ct.default_nb, "exp_ids": exp_ids})
|
||||
|
||||
# 3. query with pagination
|
||||
exp_ids = sorted(insert_ids)[offset:]
|
||||
collection_w.query_iterator(batch_size, offset=offset, output_fields=[ct.default_string_field_name],
|
||||
check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"batch_size": batch_size,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"count": ct.default_nb - offset, "exp_ids": exp_ids})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
@pytest.mark.parametrize("primary_field", [ct.default_string_field_name, ct.default_int64_field_name])
|
||||
def test_query_iterator_with_dup_pk(self, primary_field):
|
||||
"""
|
||||
target: test query iterator with duplicate pk
|
||||
method: 1. insert entities with duplicate pk
|
||||
2. query iterator
|
||||
3. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
nb = 3000
|
||||
collection_w = self.init_collection_general(prefix, insert_data=False, is_index=False,
|
||||
auto_id=False, primary_field=primary_field)[0]
|
||||
# insert entities with duplicate pk
|
||||
data = cf.gen_default_list_data(nb=nb)
|
||||
for _ in range(3):
|
||||
collection_w.insert(data)
|
||||
collection_w.flush()
|
||||
# create index
|
||||
index_type = "HNSW"
|
||||
index_params = {"index_type": index_type, "metric_type": ct.default_L0_metric,
|
||||
"params": cf.get_index_params_params(index_type)}
|
||||
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
||||
collection_w.load()
|
||||
# 2. query iterator
|
||||
collection_w.query_iterator(check_task=CheckTasks.check_query_iterator,
|
||||
check_items={"count": nb,
|
||||
"pk_name": collection_w.primary_field.name,
|
||||
"batch_size": ct.default_batch_size})
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L2)
|
||||
@pytest.mark.skip("issue #37109, need debug due to the resolution of the issue")
|
||||
def test_query_iterator_on_two_collections(self):
|
||||
"""
|
||||
target: test query iterator on two collections
|
||||
method: 1. create two collections
|
||||
2. query iterator on the first collection
|
||||
3. check the result, expect pk
|
||||
expected: query successfully
|
||||
"""
|
||||
# 1. initialize with data
|
||||
collection_w = self.init_collection_general(prefix, True)[0]
|
||||
collection_w2 = self.init_collection_general(prefix, False, primary_field=ct.default_string_field_name)[0]
|
||||
|
||||
data = cf.gen_default_list_data(nb=ct.default_nb, primary_field=ct.default_string_field_name)
|
||||
string_values = [cf.gen_str_by_length(20) for _ in range(ct.default_nb)]
|
||||
data[2] = string_values
|
||||
collection_w2.insert(data)
|
||||
|
||||
# 2. call a new query iterator and iterator for some times
|
||||
batch_size = 150
|
||||
iterator_cp_file = f"/tmp/it_{collection_w.name}_cp"
|
||||
iterator2 = collection_w2.query_iterator(batch_size=batch_size // 2, iterator_cp_file=iterator_cp_file)[0]
|
||||
iter_times = 0
|
||||
first_iter_times = ct.default_nb // batch_size // 2 // 2 # only iterate half of the data for the 1st time
|
||||
while iter_times < first_iter_times:
|
||||
iter_times += 1
|
||||
res = iterator2.next()
|
||||
if len(res) == 0:
|
||||
iterator2.close()
|
||||
assert False, f"The iterator ends before {first_iter_times} times iterators: iter_times: {iter_times}"
|
||||
break
|
||||
|
||||
# 3. query iterator on the second collection with the same checkpoint file
|
||||
|
||||
iterator = collection_w.query_iterator(batch_size=batch_size, iterator_cp_file=iterator_cp_file)[0]
|
||||
print(iterator.next())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user