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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:17 +08:00
commit 498b235461
5446 changed files with 2748612 additions and 0 deletions
@@ -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
+489
View File
@@ -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
+100
View File
@@ -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