mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
test: Add nullable vector field coverage (#49649)
issue: #47846 Related to #49337 ## What this PR does Adds release-level nullable vector field coverage in Python E2E tests: - Use nullable dense and sparse vector fields in group-by search shared collection, with inserted `None` vector rows. - Use nullable dense and sparse vector fields in pagination search shared collection, and exclude null vector rows from expected vector-search candidates. - Add embedding-list boundary coverage for `clips nullable=True`: schema creation succeeds, inserting `clips=None` is rejected. - Add BM25 nullable boundary coverage: - nullable BM25 function output sparse vector is rejected; - nullable text input accepts `None`, and BM25 search does not return the null-text row. ## Verification ```bash cd tests && make ci BASE_REF=origin/master # All checks passed! # 4 files already formatted ``` ```bash python3 -m py_compile \ tests/python_client/milvus_client/test_milvus_client_search_group_by.py \ tests/python_client/milvus_client/test_milvus_client_search_pagination.py \ tests/python_client/milvus_client/test_milvus_client_struct_array.py \ tests/python_client/testcases/test_full_text_search.py ``` ```bash cd tests/python_client ../../.venv-python-client/bin/python -m pytest \ milvus_client/test_milvus_client_search_group_by.py::TestGroupSearch::test_search_group_size \ milvus_client/test_milvus_client_search_group_by.py::TestGroupSearch::test_hybrid_search_group_size \ milvus_client/test_milvus_client_search_group_by.py::TestGroupSearch::test_search_pagination_group_by \ milvus_client/test_milvus_client_search_pagination.py::TestMilvusClientSearchPagination::test_search_float_vectors_with_pagination_default \ milvus_client/test_milvus_client_search_pagination.py::TestMilvusClientSearchPagination::test_search_sparse_with_pagination_default \ milvus_client/test_milvus_client_search_pagination.py::TestMilvusClientSearchPagination::test_search_pagination_with_expression \ milvus_client/test_milvus_client_struct_array.py::TestMilvusClientStructArrayInvalid::test_embedding_list_field_nullable_insert_none_not_supported \ testcases/test_full_text_search.py::TestCreateCollectionWithFullTextSearchNegative::test_create_collection_for_full_text_search_with_nullable_function_output \ testcases/test_full_text_search.py::TestSearchWithFullTextSearch::test_full_text_search_with_nullable_text_input \ --host 10.104.18.67 --port 19530 -q # 18 passed in 168.10s ``` Signed-off-by: yanliang567 <82361606+yanliang567@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# ruff: noqa: E712,E731,F401,F403,F405,F541,F841,I001,UP031,UP032,W291,W292,W293
|
||||
# fmt: off
|
||||
import json
|
||||
import random
|
||||
|
||||
@@ -83,7 +85,12 @@ class TestGroupSearch(TestMilvusClientV2Base):
|
||||
fields = []
|
||||
fields.append(FieldSchema(name=self.primary_field, dtype=DataType.INT64, is_primary=True, auto_id=True))
|
||||
fields.append(
|
||||
FieldSchema(name=self.float_vector_field_name, dtype=DataType.FLOAT_VECTOR, dim=self.float_vector_dim)
|
||||
FieldSchema(
|
||||
name=self.float_vector_field_name,
|
||||
dtype=DataType.FLOAT_VECTOR,
|
||||
dim=self.float_vector_dim,
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
|
||||
for data_type in ct.all_scalar_data_types:
|
||||
@@ -91,7 +98,7 @@ class TestGroupSearch(TestMilvusClientV2Base):
|
||||
|
||||
collection_schema = CollectionSchema(fields, enable_dynamic_field=self.enable_dynamic_field)
|
||||
collection_schema.add_field(self.bfloat16_vector_field_name, DataType.BFLOAT16_VECTOR, dim=self.bf16_vector_dim)
|
||||
collection_schema.add_field(self.sparse_vector_field_name, DataType.SPARSE_FLOAT_VECTOR)
|
||||
collection_schema.add_field(self.sparse_vector_field_name, DataType.SPARSE_FLOAT_VECTOR, nullable=True)
|
||||
collection_schema.add_field(self.binary_vector_field_name, DataType.BINARY_VECTOR, dim=self.binary_vector_dim)
|
||||
|
||||
collection_schema.add_field(self.inverted_string_field, DataType.VARCHAR, max_length=256, nullable=True)
|
||||
@@ -122,9 +129,9 @@ class TestGroupSearch(TestMilvusClientV2Base):
|
||||
binary_vectors = cf.gen_vectors(nb, dim=self.binary_vector_dim, vector_data_type=DataType.BINARY_VECTOR)
|
||||
for i in range(nb):
|
||||
row = {
|
||||
self.float_vector_field_name: float_vectors[i],
|
||||
self.float_vector_field_name: None if i % 10 == 9 else float_vectors[i],
|
||||
self.bfloat16_vector_field_name: bf16_vectors[i],
|
||||
self.sparse_vector_field_name: sparse_vectors[i],
|
||||
self.sparse_vector_field_name: None if i % 10 == 9 else sparse_vectors[i],
|
||||
self.binary_vector_field_name: binary_vectors[i],
|
||||
DataType.BOOL.name: bool(i % 2) if random.random() < 0.8 else None,
|
||||
DataType.INT8.name: int8_value if random.random() < 0.8 else None,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# ruff: noqa: E712,E731,F401,F403,F405,F541,F841,I001,UP031,UP032,W291,W292,W293
|
||||
# fmt: off
|
||||
from utils.util_pymilvus import *
|
||||
from common.common_type import CaseLabel, CheckTasks
|
||||
from common import common_type as ct
|
||||
@@ -54,9 +56,9 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
|
||||
# Create collection
|
||||
collection_schema = self.create_schema(client)[0]
|
||||
collection_schema.add_field(default_primary_key_field_name, DataType.INT64, is_primary=True, auto_id=False)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128)
|
||||
collection_schema.add_field(self.float_vector_field_name, DataType.FLOAT_VECTOR, dim=128, nullable=True)
|
||||
collection_schema.add_field(self.bfloat16_vector_field_name, DataType.BFLOAT16_VECTOR, dim=200)
|
||||
collection_schema.add_field(self.sparse_vector_field_name, DataType.SPARSE_FLOAT_VECTOR)
|
||||
collection_schema.add_field(self.sparse_vector_field_name, DataType.SPARSE_FLOAT_VECTOR, nullable=True)
|
||||
collection_schema.add_field(self.binary_vector_field_name, DataType.BINARY_VECTOR, dim=256)
|
||||
collection_schema.add_field(default_float_field_name, DataType.FLOAT)
|
||||
collection_schema.add_field(default_string_field_name, DataType.VARCHAR, max_length=256)
|
||||
@@ -88,9 +90,9 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
|
||||
pk = i + j * default_nb
|
||||
row = {
|
||||
default_primary_key_field_name: pk,
|
||||
self.float_vector_field_name: list(float_vectors[pk]),
|
||||
self.float_vector_field_name: None if pk % 10 == 9 else list(float_vectors[pk]),
|
||||
self.bfloat16_vector_field_name: bfloat16_vectors[pk],
|
||||
self.sparse_vector_field_name: sparse_vectors[pk],
|
||||
self.sparse_vector_field_name: None if pk % 10 == 9 else sparse_vectors[pk],
|
||||
self.binary_vector_field_name: binary_vectors[pk],
|
||||
default_float_field_name: pk * 1.0,
|
||||
default_string_field_name: str(pk),
|
||||
@@ -465,7 +467,7 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
|
||||
for i, _id in enumerate(self.primary_keys):
|
||||
int64 = total_datas[i][ct.default_int64_field_name]
|
||||
float = total_datas[i][ct.default_float_field_name]
|
||||
if not expr or eval(expr):
|
||||
if (not expr or eval(expr)) and total_datas[i][self.float_vector_field_name] is not None:
|
||||
filter_ids.append(_id)
|
||||
# 2. search
|
||||
limit = min(default_limit, len(filter_ids))
|
||||
|
||||
@@ -4433,6 +4433,57 @@ class TestMilvusClientStructArrayInvalid(TestMilvusClientV2Base):
|
||||
check_items=error,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_embedding_list_field_nullable_insert_none_not_supported(self):
|
||||
"""
|
||||
target: test embedding list field nullable boundary
|
||||
method: create struct array field with nullable=True and insert None
|
||||
expected: create collection succeeds, but None row value is not supported
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
|
||||
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
|
||||
schema.add_field(
|
||||
field_name="normal_vector",
|
||||
datatype=DataType.FLOAT_VECTOR,
|
||||
dim=default_dim,
|
||||
)
|
||||
|
||||
struct_schema = client.create_struct_field_schema()
|
||||
struct_schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=default_dim)
|
||||
struct_schema.add_field("label", DataType.VARCHAR, max_length=128)
|
||||
|
||||
schema.add_field(
|
||||
"clips",
|
||||
datatype=DataType.ARRAY,
|
||||
element_type=DataType.STRUCT,
|
||||
struct_schema=struct_schema,
|
||||
max_capacity=100,
|
||||
nullable=True,
|
||||
)
|
||||
res, check = self.create_collection(client, collection_name, schema=schema)
|
||||
assert check
|
||||
|
||||
data = [
|
||||
{
|
||||
"id": 0,
|
||||
"normal_vector": [random.random() for _ in range(default_dim)],
|
||||
"clips": None,
|
||||
}
|
||||
]
|
||||
error = {
|
||||
ct.err_code: 1,
|
||||
ct.err_msg: "Expected list, got NoneType",
|
||||
}
|
||||
self.insert(
|
||||
client,
|
||||
collection_name,
|
||||
data,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=error,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_struct_array_range_search_not_supported(self):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# ruff: noqa: E712,E731,F401,F403,F405,F541,F841,I001,UP031,UP032,W291,W292,W293
|
||||
# fmt: off
|
||||
import json
|
||||
|
||||
from pymilvus import (
|
||||
@@ -417,6 +419,54 @@ class TestCreateCollectionWithFullTextSearchNegative(TestcaseBase):
|
||||
check_items=check_items,
|
||||
)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_create_collection_for_full_text_search_with_nullable_function_output(self):
|
||||
"""
|
||||
target: test create collection with nullable BM25 function output
|
||||
method: create collection with BM25 output sparse vector field set nullable
|
||||
expected: create collection failed
|
||||
"""
|
||||
analyzer_params = {
|
||||
"tokenizer": "standard",
|
||||
}
|
||||
dim = 128
|
||||
fields = [
|
||||
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
|
||||
FieldSchema(
|
||||
name="text",
|
||||
dtype=DataType.VARCHAR,
|
||||
max_length=65535,
|
||||
nullable=True,
|
||||
enable_analyzer=True,
|
||||
analyzer_params=analyzer_params,
|
||||
),
|
||||
FieldSchema(name="emb", dtype=DataType.FLOAT_VECTOR, dim=dim),
|
||||
FieldSchema(
|
||||
name="text_sparse_emb",
|
||||
dtype=DataType.SPARSE_FLOAT_VECTOR,
|
||||
nullable=True,
|
||||
),
|
||||
]
|
||||
schema = CollectionSchema(fields=fields, description="test collection")
|
||||
bm25_function = Function(
|
||||
name="text_bm25_emb",
|
||||
function_type=FunctionType.BM25,
|
||||
input_field_names=["text"],
|
||||
output_field_names=["text_sparse_emb"],
|
||||
params={},
|
||||
)
|
||||
schema.add_function(bm25_function)
|
||||
check_items = {
|
||||
ct.err_code: 65535,
|
||||
ct.err_msg: "function output field cannot be nullable",
|
||||
}
|
||||
self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix),
|
||||
schema=schema,
|
||||
check_task=CheckTasks.err_res,
|
||||
check_items=check_items,
|
||||
)
|
||||
|
||||
|
||||
# @pytest.mark.skip("skip")
|
||||
class TestInsertWithFullTextSearch(TestcaseBase):
|
||||
@@ -2222,6 +2272,83 @@ class TestSearchWithFullTextSearch(TestcaseBase):
|
||||
******************************************************************
|
||||
"""
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_full_text_search_with_nullable_text_input(self):
|
||||
"""
|
||||
target: test full text search with nullable function input
|
||||
method: insert data with None text and search on BM25 sparse field
|
||||
expected: nullable input rows are accepted and not matched by BM25 search
|
||||
"""
|
||||
analyzer_params = {
|
||||
"tokenizer": "standard",
|
||||
}
|
||||
dim = 8
|
||||
fields = [
|
||||
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
|
||||
FieldSchema(
|
||||
name="text",
|
||||
dtype=DataType.VARCHAR,
|
||||
max_length=65535,
|
||||
nullable=True,
|
||||
enable_analyzer=True,
|
||||
enable_match=True,
|
||||
analyzer_params=analyzer_params,
|
||||
),
|
||||
FieldSchema(name="emb", dtype=DataType.FLOAT_VECTOR, dim=dim),
|
||||
FieldSchema(name="text_sparse_emb", dtype=DataType.SPARSE_FLOAT_VECTOR),
|
||||
]
|
||||
schema = CollectionSchema(fields=fields, description="test collection")
|
||||
bm25_function = Function(
|
||||
name="text_bm25_emb",
|
||||
function_type=FunctionType.BM25,
|
||||
input_field_names=["text"],
|
||||
output_field_names=["text_sparse_emb"],
|
||||
params={},
|
||||
)
|
||||
schema.add_function(bm25_function)
|
||||
collection_w = self.init_collection_wrap(
|
||||
name=cf.gen_unique_str(prefix), schema=schema
|
||||
)
|
||||
data = [
|
||||
{"id": 0, "text": "milvus vector database", "emb": [0.1] * dim},
|
||||
{"id": 1, "text": None, "emb": [0.2] * dim},
|
||||
{"id": 2, "text": "", "emb": [0.3] * dim},
|
||||
{"id": 3, "text": "vector search", "emb": [0.4] * dim},
|
||||
]
|
||||
collection_w.insert(data)
|
||||
collection_w.flush()
|
||||
collection_w.create_index(
|
||||
"emb",
|
||||
{
|
||||
"index_type": "FLAT",
|
||||
"metric_type": "L2",
|
||||
},
|
||||
)
|
||||
collection_w.create_index(
|
||||
"text_sparse_emb",
|
||||
{
|
||||
"index_type": "SPARSE_INVERTED_INDEX",
|
||||
"metric_type": "BM25",
|
||||
},
|
||||
)
|
||||
collection_w.create_index("text", {"index_type": "INVERTED"})
|
||||
collection_w.load()
|
||||
|
||||
query_res, _ = collection_w.query(
|
||||
expr="id in [1]", output_fields=["id", "text"]
|
||||
)
|
||||
assert query_res == [{"id": 1, "text": None}]
|
||||
search_res, _ = collection_w.search(
|
||||
data=["vector"],
|
||||
anns_field="text_sparse_emb",
|
||||
param={},
|
||||
limit=4,
|
||||
output_fields=["id", "text"],
|
||||
)
|
||||
search_ids = [hit.id for hit in search_res[0]]
|
||||
assert len(search_ids) > 0
|
||||
assert 1 not in search_ids
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
@pytest.mark.parametrize("nq", [2])
|
||||
@pytest.mark.parametrize("empty_percent", [0.5])
|
||||
|
||||
Reference in New Issue
Block a user