1721 lines
62 KiB
Python
1721 lines
62 KiB
Python
# Copyright 2025-present the zvec project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from distance_helper import *
|
|
from fixture_helper import *
|
|
from doc_helper import *
|
|
from params_helper import *
|
|
|
|
|
|
class TestDDL:
|
|
def test_collection_stats(self, basic_collection: Collection):
|
|
assert basic_collection.stats is not None
|
|
stats = basic_collection.stats
|
|
assert stats.doc_count == 0
|
|
assert len(stats.index_completeness) == 2
|
|
assert stats.index_completeness["dense"] == 1
|
|
assert stats.index_completeness["sparse"] == 1
|
|
|
|
def test_collection_destroy(
|
|
self, basic_collection: Collection, collection_temp_dir, collection_option
|
|
):
|
|
doc = generate_doc(1, basic_collection.schema)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
basic_collection.destroy()
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
stats = basic_collection.stats
|
|
assert ACCESS_DESTROYED_COLLECTION_ERROR_MSG in str(exc_info.value)
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
zvec.open(path=collection_temp_dir, option=collection_option)
|
|
assert COLLECTION_PATH_NOT_EXIST_ERROR_MSG in str(exc_info.value)
|
|
|
|
def test_collection_flush(self, basic_collection: Collection):
|
|
doc = generate_doc(1, basic_collection.schema)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
|
|
basic_collection.flush()
|
|
|
|
fetched_docs = basic_collection.fetch(["1"])
|
|
assert "1" in fetched_docs
|
|
assert fetched_docs["1"].id == "1"
|
|
|
|
|
|
class TestIndexDDL:
|
|
@pytest.mark.parametrize("field_name", DEFAULT_SCALAR_FIELD_NAME.values())
|
|
@pytest.mark.parametrize("index_type", SUPPORT_SCALAR_INDEX_TYPES)
|
|
def test_scalar_index_operation(
|
|
self,
|
|
full_collection: Collection,
|
|
field_name: str,
|
|
index_type: IndexType,
|
|
):
|
|
# INSERT 0~5 Doc
|
|
docs = [generate_doc(i, full_collection.schema) for i in range(5)]
|
|
|
|
result = full_collection.insert(docs)
|
|
assert len(result) == 5
|
|
for item in result:
|
|
assert item.ok()
|
|
|
|
stats = full_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 5
|
|
|
|
if field_name in ["bool_field"]:
|
|
query_filter = f"{field_name} = true"
|
|
elif field_name in ["double_field", "float_field"]:
|
|
query_filter = f"{field_name} >= 3.0"
|
|
elif field_name in [
|
|
"int32_field",
|
|
"int64_field",
|
|
"uint32_field",
|
|
"uint64_field",
|
|
]:
|
|
query_filter = f"{field_name} >= 30"
|
|
elif field_name in ["string_field"]:
|
|
query_filter = f"{field_name} >= 'test_3'"
|
|
elif field_name in ["array_bool_field"]:
|
|
query_filter = f"{field_name} contain_any (false)"
|
|
elif field_name in ["array_double_field", "array_float_field"]:
|
|
query_filter = f"{field_name} contain_any (3.0, 4.0)"
|
|
elif field_name in [
|
|
"array_int64_field",
|
|
"array_int32_field",
|
|
"array_uint64_field",
|
|
"array_uint32_field",
|
|
]:
|
|
query_filter = f"{field_name} contain_any (3, 4)"
|
|
elif field_name == "array_string_field":
|
|
query_filter = f"{field_name} contain_any ('test_3', 'test_4')"
|
|
else:
|
|
assert False, f"Unsupported field type for index creation: {field_name}"
|
|
|
|
query_result_before = full_collection.query(filter=query_filter, topk=10)
|
|
|
|
if index_type not in DEFAULT_INDEX_PARAMS:
|
|
pytest.fail(f"Unsupported index type for index creation: {index_type}")
|
|
index_param = DEFAULT_INDEX_PARAMS[index_type]
|
|
|
|
full_collection.create_index(
|
|
field_name=field_name, index_param=index_param, option=IndexOption()
|
|
)
|
|
stats_after_create = full_collection.stats
|
|
assert stats_after_create is not None
|
|
assert stats_after_create.doc_count == 5
|
|
|
|
query_result_after = full_collection.query(filter=query_filter, topk=10)
|
|
|
|
assert len(query_result_before) == len(query_result_after), (
|
|
f"Query result count mismatch for {field_name} with index type {index_type}: before={len(query_result_before)}, after={len(query_result_after)}"
|
|
)
|
|
|
|
before_ids = set(doc.id for doc in query_result_before)
|
|
after_ids = set(doc.id for doc in query_result_after)
|
|
assert before_ids == after_ids, (
|
|
f"Query result IDs mismatch for {field_name} with index type {index_type}: before={before_ids}, after={after_ids}"
|
|
)
|
|
|
|
# INSERT 5~8 Doc
|
|
new_docs = [generate_doc(i, full_collection.schema) for i in range(5, 8)]
|
|
|
|
result = full_collection.insert(new_docs)
|
|
assert len(result) == 3
|
|
for item in result:
|
|
assert item.ok()
|
|
|
|
stats_after_insert1 = full_collection.stats
|
|
assert stats_after_insert1 is not None
|
|
assert stats_after_insert1.doc_count == 8
|
|
|
|
fetched_docs = full_collection.fetch([f"{i}" for i in range(5, 8)])
|
|
assert len(fetched_docs) == 3
|
|
|
|
for i in range(5, 8):
|
|
doc_id = f"{i}"
|
|
assert doc_id in fetched_docs
|
|
|
|
query_result = full_collection.query(filter=query_filter, topk=20)
|
|
assert len(query_result) >= len(query_result_before)
|
|
|
|
full_collection.drop_index(field_name=field_name)
|
|
|
|
# Insert 8~10 Doc
|
|
more_docs = [generate_doc(i, full_collection.schema) for i in range(8, 10)]
|
|
|
|
result = full_collection.insert(more_docs)
|
|
assert len(result) == 2
|
|
for item in result:
|
|
assert item.ok()
|
|
|
|
stats_after_insert2 = full_collection.stats
|
|
assert stats_after_insert2 is not None
|
|
assert stats_after_insert2.doc_count == 10
|
|
|
|
fetched_docs = full_collection.fetch([f"{i}" for i in range(8, 10)])
|
|
assert len(fetched_docs) == 2
|
|
|
|
for i in range(8, 10):
|
|
doc_id = f"{i}"
|
|
assert doc_id in fetched_docs
|
|
|
|
query_result = full_collection.query(filter=query_filter, topk=20)
|
|
assert len(query_result) >= len(query_result_before)
|
|
|
|
final_stats = full_collection.stats
|
|
assert final_stats is not None
|
|
assert final_stats.doc_count == 10
|
|
full_collection.destroy()
|
|
|
|
@pytest.mark.parametrize("field_name", DEFAULT_SCALAR_FIELD_NAME.values())
|
|
@pytest.mark.parametrize("index_type", SUPPORT_SCALAR_INDEX_TYPES)
|
|
def test_duplicate_create_index(
|
|
self, full_collection: Collection, field_name: str, index_type: IndexType
|
|
):
|
|
docs = [generate_doc(i, full_collection.schema) for i in range(10)]
|
|
|
|
result = full_collection.insert(docs)
|
|
assert bool(result)
|
|
for item in result:
|
|
assert item.ok()
|
|
|
|
stats = full_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 10
|
|
|
|
if field_name in ["bool_field"]:
|
|
query_filter = f"{field_name} = true"
|
|
elif field_name in ["double_field", "float_field"]:
|
|
query_filter = f"{field_name} >= 3.0"
|
|
elif field_name in [
|
|
"int32_field",
|
|
"int64_field",
|
|
"uint32_field",
|
|
"uint64_field",
|
|
]:
|
|
query_filter = f"{field_name} >= 30"
|
|
elif field_name in ["string_field"]:
|
|
query_filter = f"{field_name} >= 'test_3'"
|
|
elif field_name in ["array_bool_field"]:
|
|
query_filter = f"{field_name} contain_any (false)"
|
|
elif field_name in ["array_double_field", "array_float_field"]:
|
|
query_filter = f"{field_name} contain_any (3.0, 4.0)"
|
|
elif field_name in [
|
|
"array_int64_field",
|
|
"array_int32_field",
|
|
"array_uint64_field",
|
|
"array_uint32_field",
|
|
]:
|
|
query_filter = f"{field_name} contain_any (3, 4)"
|
|
elif field_name == "array_string_field":
|
|
query_filter = f"{field_name} contain_any ('test_3', 'test_4')"
|
|
else:
|
|
assert False, f"Unsupported field type for index creation: {field_name}"
|
|
|
|
query_result_before = full_collection.query(filter=query_filter, topk=5)
|
|
|
|
if index_type not in DEFAULT_INDEX_PARAMS:
|
|
pytest.fail(f"Unsupported index type for index creation: {index_type}")
|
|
index_param = DEFAULT_INDEX_PARAMS[index_type]
|
|
|
|
full_collection.create_index(
|
|
field_name=field_name, index_param=index_param, option=IndexOption()
|
|
)
|
|
|
|
query_result_after = full_collection.query(filter=query_filter, topk=5)
|
|
|
|
assert len(query_result_before) == len(query_result_after), (
|
|
f"Query result count mismatch: before={len(query_result_before)}, after={len(query_result_after)}"
|
|
)
|
|
|
|
before_ids = set(doc.id for doc in query_result_before)
|
|
after_ids = set(doc.id for doc in query_result_after)
|
|
assert before_ids == after_ids, (
|
|
f"Query result IDs mismatch: before={before_ids}, after={after_ids}"
|
|
)
|
|
|
|
full_collection.create_index(
|
|
field_name=field_name, index_param=index_param, option=IndexOption()
|
|
)
|
|
|
|
def test_optimize(self, full_collection: Collection):
|
|
docs = [generate_doc(i, full_collection.schema) for i in range(10)]
|
|
|
|
result = full_collection.insert(docs)
|
|
assert bool(result)
|
|
for item in result:
|
|
assert item.ok()
|
|
|
|
stats = full_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 10
|
|
|
|
full_collection.optimize(option=OptimizeOption())
|
|
|
|
fetched_docs = full_collection.fetch(["1"])
|
|
assert "1" in fetched_docs
|
|
assert fetched_docs["1"].id == "1"
|
|
|
|
@pytest.mark.parametrize(
|
|
"vector_type, index_type", SUPPORT_VECTOR_DATA_TYPE_INDEX_MAP_PARAMS
|
|
)
|
|
def test_vector_index_operation(
|
|
self,
|
|
full_collection: Collection,
|
|
vector_type: DataType,
|
|
index_type: IndexType,
|
|
):
|
|
vector_field_name = DEFAULT_VECTOR_FIELD_NAME[vector_type]
|
|
|
|
docs = [generate_doc(i, full_collection.schema) for i in range(5)]
|
|
|
|
result = full_collection.insert(docs)
|
|
assert len(result) == 5, (
|
|
f"Expected 5 insertion results, got {len(result)} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
for i, item in enumerate(result):
|
|
assert item.ok(), (
|
|
f"Before create_index,result={result},Insertion result {i} is not OK for vector type {vector_type} and index type {index_type} and result={result}"
|
|
)
|
|
|
|
stats = full_collection.stats
|
|
assert stats is not None, (
|
|
f"stats is None for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert stats.doc_count == 5, (
|
|
f"doc_count!=5 for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
if index_type not in DEFAULT_INDEX_PARAMS:
|
|
pytest.fail(
|
|
f"Unsupported index type {index_type} for vector type {vector_type} in test_vector_all_data_types_index_create_drop_validation"
|
|
)
|
|
index_param = DEFAULT_INDEX_PARAMS[index_type]
|
|
|
|
full_collection.create_index(
|
|
field_name=vector_field_name,
|
|
index_param=index_param,
|
|
option=IndexOption(),
|
|
)
|
|
|
|
stats_after_create = full_collection.stats
|
|
assert stats_after_create is not None, (
|
|
f"stats_after_create_index is None for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
new_docs = [generate_doc(i, full_collection.schema) for i in range(5, 8)]
|
|
|
|
result = full_collection.insert(new_docs)
|
|
assert len(result) == 3, (
|
|
f"Expected 3 insertion results, got {len(result)} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
for i, item in enumerate(result):
|
|
assert item.ok(), (
|
|
f"Before drop_index,result={result},BInsertion result {i} is not OK for vector type {vector_type} and index type {index_type} and "
|
|
)
|
|
|
|
stats_after_insert1 = full_collection.stats
|
|
assert stats_after_insert1 is not None, (
|
|
f"stats_after_insert1 is None for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert stats_after_insert1.doc_count == 8, (
|
|
f"Expected 8 documents, got {stats_after_insert1.doc_count} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
fetched_docs = full_collection.fetch([f"{i}" for i in range(5, 8)])
|
|
assert len(fetched_docs) == 3, (
|
|
f"Expected 3 fetched documents, got {len(fetched_docs)} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
for i in range(5, 8):
|
|
doc_id = f"{i}"
|
|
assert doc_id in fetched_docs, (
|
|
f"Document ID {doc_id} not found in fetched results for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert fetched_docs[doc_id].id == doc_id, (
|
|
f"Document {doc_id} has incorrect ID field value for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
full_collection.drop_index(field_name=vector_field_name)
|
|
|
|
more_docs = [generate_doc(i, full_collection.schema) for i in range(8, 10)]
|
|
result = full_collection.insert(more_docs)
|
|
assert len(result) == 2, (
|
|
f"Expected 2 insertion results, got {len(result)} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
for i, item in enumerate(result):
|
|
assert item.ok(), (
|
|
f"After drop_index,Insertion result {i} is not OK for vector type {vector_type} and index type {index_type} and result={result}"
|
|
)
|
|
|
|
# Verify document count after second insertion
|
|
stats_after_insert2 = full_collection.stats
|
|
assert stats_after_insert2 is not None, (
|
|
f"stats_after_insert2 is None for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert stats_after_insert2.doc_count == 10, (
|
|
f"Expected 10 documents, got {stats_after_insert2.doc_count} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
# Fetch data
|
|
fetched_docs = full_collection.fetch([f"{i}" for i in range(8, 10)])
|
|
assert len(fetched_docs) == 2, (
|
|
f"Expected 2 fetched documents, got {len(fetched_docs)} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
# Verify fetched documents have correct data
|
|
for i in range(8, 10):
|
|
doc_id = f"{i}"
|
|
assert doc_id in fetched_docs, (
|
|
f"Document ID {doc_id} not found in fetched results for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert fetched_docs[doc_id].id == doc_id, (
|
|
f"Document {doc_id} has incorrect ID field value for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
|
|
# Final verification
|
|
final_stats = full_collection.stats
|
|
assert final_stats is not None, (
|
|
f"final_stats is None for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
assert final_stats.doc_count == 10, (
|
|
f"Expected 10 documents, got {final_stats.doc_count} for vector type {vector_type} and index type {index_type}"
|
|
)
|
|
full_collection.destroy()
|
|
|
|
@staticmethod
|
|
def create_collection(
|
|
collection_path, collection_option: CollectionOption
|
|
) -> Collection:
|
|
schema = CollectionSchema(
|
|
name="test_collection_invalid_vector_index",
|
|
fields=[
|
|
FieldSchema(
|
|
"id",
|
|
DataType.INT64,
|
|
nullable=False,
|
|
index_param=InvertIndexParam(enable_range_optimization=True),
|
|
),
|
|
FieldSchema(
|
|
"name",
|
|
DataType.STRING,
|
|
nullable=True,
|
|
index_param=InvertIndexParam(),
|
|
),
|
|
],
|
|
vectors=[
|
|
VectorSchema(
|
|
"dense",
|
|
DataType.VECTOR_FP32,
|
|
dimension=128,
|
|
index_param=HnswIndexParam(),
|
|
),
|
|
],
|
|
)
|
|
coll = zvec.create_and_open(
|
|
path=collection_path, schema=schema, option=collection_option
|
|
)
|
|
assert coll is not None, "Failed to create and open collection"
|
|
return coll
|
|
|
|
@staticmethod
|
|
def check_error_message(exc_info, invalid_name):
|
|
if type(invalid_name) is str:
|
|
assert INDEX_NON_EXISTENT_COLUMN_ERROR_MSG in str(exc_info.value), (
|
|
"Error message is unreasonable: e=" + str(exc_info.value)
|
|
)
|
|
else:
|
|
assert INCOMPATIBLE_FUNCTION_ERROR_MSG in str(exc_info.value), (
|
|
"Error message is unreasonable: e=" + str(exc_info.value)
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_field_name,invalid_vector_name",
|
|
[
|
|
("", ""), # Empty string
|
|
(" ", " "), # Space only
|
|
("v" * 33, "v" * 33), # Too long (33 characters, exceeds 32)
|
|
("vector name", "vector_name"), # Contains space
|
|
("vector@name", "vector@name"), # Contains special character
|
|
("vector/name", "vector/name"), # Contains slash
|
|
("vector\\name", "vector\\name"), # Contains backslash
|
|
("vector.name", "vector.name"), # Contains dot
|
|
("vector$data", "vector$data"), # Contains dollar sign
|
|
("vector+name", "vector+name"), # Contains plus sign
|
|
("vector=name", "vector=name"), # Contains equals sign
|
|
(None, None), # None value,
|
|
(1, 1),
|
|
(1.1, 1.1),
|
|
],
|
|
)
|
|
def test_invalid_field_and_vector_name(
|
|
self,
|
|
collection_temp_dir,
|
|
collection_option: CollectionOption,
|
|
invalid_field_name: Any,
|
|
invalid_vector_name: Any,
|
|
):
|
|
coll = self.create_collection(collection_temp_dir, collection_option)
|
|
with pytest.raises(Exception) as exc_info:
|
|
coll.create_index(
|
|
field_name=invalid_vector_name,
|
|
index_param=HnswIndexParam(),
|
|
option=IndexOption(),
|
|
)
|
|
self.check_error_message(exc_info, invalid_vector_name)
|
|
with pytest.raises(Exception) as exc_info:
|
|
coll.create_index(
|
|
field_name=invalid_field_name,
|
|
index_param=InvertIndexParam(),
|
|
option=IndexOption(),
|
|
)
|
|
self.check_error_message(exc_info, invalid_field_name)
|
|
coll.destroy()
|
|
coll = self.create_collection(collection_temp_dir, collection_option)
|
|
with pytest.raises(Exception) as exc_info:
|
|
coll.drop_index(field_name=invalid_vector_name)
|
|
self.check_error_message(exc_info, invalid_vector_name)
|
|
with pytest.raises(Exception) as exc_info:
|
|
coll.drop_index(field_name=invalid_field_name)
|
|
self.check_error_message(exc_info, invalid_field_name)
|
|
coll.destroy()
|
|
|
|
@pytest.mark.parametrize(
|
|
"field_name,vector_name",
|
|
[
|
|
("2", "3"),
|
|
("col", "co1"),
|
|
("ID", "IM"),
|
|
("name-1", "name2"),
|
|
("Weigt_12", "Weigt_13"),
|
|
("123age", "123agl"),
|
|
],
|
|
)
|
|
def test_valid_field_and_vector_name(
|
|
self,
|
|
collection_temp_dir,
|
|
collection_option: CollectionOption,
|
|
field_name: str,
|
|
vector_name: str,
|
|
):
|
|
schema = zvec.CollectionSchema(
|
|
name="test_index_names",
|
|
fields=[
|
|
FieldSchema(
|
|
"id",
|
|
DataType.INT64,
|
|
nullable=False,
|
|
index_param=InvertIndexParam(enable_range_optimization=True),
|
|
),
|
|
FieldSchema(field_name, DataType.STRING, nullable=True),
|
|
],
|
|
vectors=[
|
|
VectorSchema(
|
|
vector_name,
|
|
DataType.VECTOR_FP32,
|
|
dimension=128,
|
|
index_param=HnswIndexParam(),
|
|
)
|
|
],
|
|
)
|
|
|
|
coll = zvec.create_and_open(
|
|
path=collection_temp_dir, schema=schema, option=collection_option
|
|
)
|
|
|
|
assert coll is not None, (
|
|
f"Failed to create and open collection with field_name={field_name}, vector_name={vector_name}"
|
|
)
|
|
|
|
# Insert some data
|
|
docs = [
|
|
Doc(
|
|
id=f"{i}",
|
|
fields={"id": i, field_name: f"value_{i}"},
|
|
vectors={vector_name: [float(j % 10) for j in range(128)]},
|
|
)
|
|
for i in range(5)
|
|
]
|
|
|
|
result = coll.insert(docs)
|
|
assert len(result) == 5, (
|
|
f"Expected 5 insertion results, got {len(result)} for field_name={field_name}, vector_name={vector_name}"
|
|
)
|
|
for item in result:
|
|
assert item.ok(), (
|
|
f"Insertion failed for field_name={field_name}, vector_name={vector_name}: {item}"
|
|
)
|
|
|
|
# Create index on field
|
|
coll.create_index(
|
|
field_name=field_name,
|
|
index_param=InvertIndexParam(),
|
|
option=IndexOption(),
|
|
)
|
|
|
|
# Create index on vector
|
|
coll.create_index(
|
|
field_name=vector_name,
|
|
index_param=HnswIndexParam(),
|
|
option=IndexOption(),
|
|
)
|
|
|
|
# Verify indexes were created successfully
|
|
stats = coll.stats
|
|
assert stats is not None, (
|
|
f"Stats is None for field_name={field_name}, vector_name={vector_name}"
|
|
)
|
|
|
|
coll.destroy()
|
|
|
|
def test_compicated_workflow(
|
|
self,
|
|
collection_temp_dir,
|
|
basic_schema: CollectionSchema,
|
|
collection_option: CollectionOption,
|
|
):
|
|
"""
|
|
Test the complete workflow:
|
|
1. Create collection
|
|
2. Create index
|
|
3. Insert doc
|
|
4. Upsert
|
|
5. Update doc
|
|
6. Fetch doc
|
|
7. Query doc
|
|
8. Drop index
|
|
9. Insert doc
|
|
10. Update doc
|
|
11. Upsert doc
|
|
12. Fetch doc
|
|
13. Query doc
|
|
14. Flush
|
|
15. Destroy
|
|
"""
|
|
# Step 1: Create collection
|
|
coll = zvec.create_and_open(
|
|
path=collection_temp_dir,
|
|
schema=basic_schema,
|
|
option=collection_option,
|
|
)
|
|
|
|
assert coll is not None, "Failed to create and open collection"
|
|
assert coll.path == collection_temp_dir
|
|
assert coll.schema.name == basic_schema.name
|
|
assert coll.stats.doc_count == 0
|
|
|
|
# Step 2: Create index
|
|
coll.create_index(
|
|
field_name="name", index_param=InvertIndexParam(), option=IndexOption()
|
|
)
|
|
# Verify index was created
|
|
stats = coll.stats
|
|
assert stats is not None, "coll.stats is None!"
|
|
|
|
# Step 3: Insert doc
|
|
doc1 = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test1", "weight": 80.5},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = coll.insert(doc1)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 1
|
|
|
|
# Step 4: Upsert (existing doc)
|
|
doc1_updated = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test1_updated", "weight": 85.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.5, 2: 2.5},
|
|
},
|
|
)
|
|
|
|
result = coll.upsert(doc1_updated)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 1
|
|
|
|
# Step 5: Update doc
|
|
doc2 = Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "test2", "weight": 90.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 3.0, 2: 4.0},
|
|
},
|
|
)
|
|
|
|
# First insert doc2
|
|
result = coll.insert(doc2)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 2
|
|
|
|
# Then update it
|
|
doc2_updated = Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "test2_updated", "weight": 95.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 3.5, 2: 4.5},
|
|
},
|
|
)
|
|
|
|
result = coll.update(doc2_updated)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 2
|
|
|
|
# Step 6: Fetch doc
|
|
fetched_docs = coll.fetch(["1", "2"])
|
|
assert len(fetched_docs) == 2
|
|
assert "1" in fetched_docs
|
|
assert "2" in fetched_docs
|
|
assert fetched_docs["1"].field("name") == "test1_updated"
|
|
assert fetched_docs["2"].field("name") == "test2_updated"
|
|
|
|
# Step 7: Query doc
|
|
query_result = coll.query(filter="id >= 1", topk=10)
|
|
assert len(query_result) == 2
|
|
|
|
# Step 8: Drop index
|
|
coll.drop_index(field_name="name")
|
|
|
|
# Step 9: Insert doc
|
|
doc3 = Doc(
|
|
id="3",
|
|
fields={"id": 3, "name": "test3", "weight": 100.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 5.0, 2: 6.0},
|
|
},
|
|
)
|
|
|
|
result = coll.insert(doc3)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 3
|
|
|
|
# Step 10: Update doc
|
|
doc3_updated = Doc(
|
|
id="3",
|
|
fields={"id": 3, "name": "test3_updated", "weight": 105.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 5.5, 2: 6.5},
|
|
},
|
|
)
|
|
|
|
result = coll.update(doc3_updated)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 3
|
|
|
|
# Step 11: Upsert doc
|
|
doc4 = Doc(
|
|
id="4",
|
|
fields={"id": 4, "name": "test4", "weight": 110.0},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 7.0, 2: 8.0},
|
|
},
|
|
)
|
|
|
|
result = coll.upsert(doc4)
|
|
assert bool(result)
|
|
assert result.ok()
|
|
assert coll.stats.doc_count == 4
|
|
|
|
# Step 12: Fetch doc
|
|
fetched_docs = coll.fetch(["3", "4"])
|
|
assert len(fetched_docs) == 2
|
|
assert "3" in fetched_docs
|
|
assert "4" in fetched_docs
|
|
assert fetched_docs["3"].field("name") == "test3_updated"
|
|
assert fetched_docs["4"].field("name") == "test4"
|
|
|
|
# Step 13: Query doc
|
|
query_result = coll.query(filter="id >= 3", topk=10)
|
|
assert len(query_result) == 2
|
|
|
|
# Step 14: Flush
|
|
coll.flush()
|
|
|
|
# Verify data is still accessible after flush
|
|
fetched_docs = coll.fetch(["1", "2", "3", "4"])
|
|
assert len(fetched_docs) == 4
|
|
|
|
# Step 15: Destroy
|
|
coll.destroy()
|
|
|
|
@pytest.mark.parametrize(
|
|
"data_type, index_param", VALID_VECTOR_DATA_TYPE_INDEX_PARAM_MAP_PARAMS
|
|
)
|
|
def test_vector_index_params(
|
|
self,
|
|
collection_temp_dir,
|
|
collection_option: CollectionOption,
|
|
data_type: DataType,
|
|
index_param,
|
|
single_vector_schema,
|
|
):
|
|
vector_name = DEFAULT_VECTOR_FIELD_NAME[data_type]
|
|
dimension = DEFAULT_VECTOR_DIMENSION
|
|
|
|
coll = zvec.create_and_open(
|
|
path=collection_temp_dir,
|
|
schema=single_vector_schema,
|
|
option=collection_option,
|
|
)
|
|
|
|
assert coll is not None, (
|
|
f"Failed to create and open collection, {data_type}, {index_param}"
|
|
)
|
|
|
|
docs = {str(i): generate_doc(i, single_vector_schema) for i in range(5)}
|
|
result = coll.insert(docs.values())
|
|
assert len(result) == len(docs), (
|
|
f"Expected 5 results, got {len(result)}, {data_type}, {index_param}"
|
|
)
|
|
for item in result:
|
|
assert item.ok(), f"Insertion failed for, {data_type}, {index_param}"
|
|
|
|
def check_result(
|
|
label: str, metric_type: MetricType, quantize_type: QuantizeType
|
|
):
|
|
query_vector = [1] * dimension
|
|
if data_type in [DataType.SPARSE_VECTOR_FP16, DataType.SPARSE_VECTOR_FP32]:
|
|
query_vector = {1: 1}
|
|
|
|
fetch_result = coll.fetch([str(i) for i in range(len(docs))])
|
|
assert len(fetch_result) == len(docs), (
|
|
f"{label}, Expected 5 fetched docs, got {len(fetch_result)}, {data_type}, {index_param}"
|
|
)
|
|
for i in range(len(docs)):
|
|
doc_id = str(i)
|
|
assert doc_id in fetch_result, (
|
|
f"{label}, Document ID '{doc_id}' not found, {data_type}, {index_param}"
|
|
)
|
|
fetched_doc = fetch_result[doc_id]
|
|
# Verify doc equal
|
|
assert is_doc_equal(fetched_doc, docs[doc_id], single_vector_schema), (
|
|
f"{label}, doc not equal, insert: {docs[doc_id]}, fetched: {fetched_doc}, {data_type}, {index_param}"
|
|
)
|
|
|
|
query_result: list[Doc] = coll.query(
|
|
Query(field_name=vector_name, vector=query_vector),
|
|
include_vector=False,
|
|
topk=len(docs),
|
|
)
|
|
assert len(query_result) == len(docs), (
|
|
f"{label}, Expected {len(docs)} result, got {len(query_result)}, {data_type}, {index_param}"
|
|
)
|
|
inserted_ids = [str(i) for i in range(len(docs))]
|
|
queried_ids = [doc.id for doc in query_result]
|
|
assert set(inserted_ids) == set(queried_ids), (
|
|
f"{label}, inserted_ids != queried_ids, insert: {inserted_ids}, query: {queried_ids}, {data_type}, {index_param}"
|
|
)
|
|
|
|
last_score = None
|
|
for i, doc in enumerate(query_result):
|
|
# Get the document's vector for comparison
|
|
expect_doc = generate_doc(int(doc.id), single_vector_schema)
|
|
doc_vector = expect_doc.vector(vector_name)
|
|
expected_score = distance(
|
|
doc_vector,
|
|
query_vector,
|
|
metric_type,
|
|
data_type,
|
|
quantize_type,
|
|
)
|
|
print(f"query: {doc}, expect_core: {expected_score}")
|
|
if quantize_type is QuantizeType.UNDEFINED:
|
|
assert is_float_equal(doc.score, expected_score), (
|
|
f"{label} top{i} pk{doc.id} score {doc.score:6f} expected:{expected_score:6f}, {data_type}, {index_param}"
|
|
)
|
|
if last_score is not None:
|
|
if metric_type == MetricType.IP:
|
|
assert last_score >= doc.score, (
|
|
f"{label}, score not sorted, last_score: {last_score}, current_score: {doc.score}, {data_type}, {index_param}"
|
|
)
|
|
else:
|
|
assert last_score <= doc.score, (
|
|
f"{label}, score not sorted, last_score: {last_score}, current_score: {doc.score}, {data_type}, {index_param}"
|
|
)
|
|
last_score = doc.score
|
|
|
|
# default metric_type=IP, quantize_type=None
|
|
check_result("pre_create_index", MetricType.IP, QuantizeType.UNDEFINED)
|
|
|
|
# create index
|
|
coll.create_index(
|
|
field_name=vector_name,
|
|
index_param=index_param,
|
|
option=IndexOption(),
|
|
)
|
|
check_result(
|
|
"post_create_index", index_param.metric_type, index_param.quantize_type
|
|
)
|
|
|
|
coll.drop_index(field_name=vector_name)
|
|
check_result("post_drop_index", MetricType.IP, QuantizeType.UNDEFINED)
|
|
|
|
new_docs = {str(i): generate_doc(i, single_vector_schema) for i in range(5, 8)}
|
|
new_result = coll.insert(new_docs.values())
|
|
assert len(new_result) == len(new_docs), (
|
|
f"Expected {len(new_docs)} insertion results for new docs, got {len(new_result)} for vector {vector_name}"
|
|
)
|
|
for item in new_result:
|
|
assert item.ok(), (
|
|
f"New document insertion failed for vector {vector_name}: {item}"
|
|
)
|
|
docs |= new_docs
|
|
coll.create_index(
|
|
field_name=vector_name,
|
|
index_param=index_param,
|
|
option=IndexOption(),
|
|
)
|
|
|
|
check_result(
|
|
"post_create_index2", index_param.metric_type, index_param.quantize_type
|
|
)
|
|
coll.destroy()
|
|
|
|
|
|
class TestColumnDDL:
|
|
def test_add_column(self, basic_collection: Collection):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("income", DataType.INT32),
|
|
expression="'weight' * 2", # Simple expression
|
|
)
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, "income": 1},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
def test_add_column_with_default_option(self, basic_collection: Collection):
|
|
# Add a new column with default option
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("test_column_default", DataType.INT32),
|
|
expression="100",
|
|
option=AddColumnOption(), # Default option
|
|
)
|
|
# Verify column was added by inserting data
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, "test_column_default": 1},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize("concurrency", [0, 1, 4, 8])
|
|
def test_add_column_with_various_concurrency_options(
|
|
self, basic_collection: Collection, concurrency
|
|
):
|
|
field_name = f"test_column_concurrent_{concurrency}"
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, DataType.INT32),
|
|
expression="100",
|
|
option=AddColumnOption(concurrency=concurrency),
|
|
)
|
|
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize("data_type", SUPPORT_ADD_COLUMN_DATA_TYPE)
|
|
def test_add_column_valid_data_types(self, basic_collection: Collection, data_type):
|
|
field_name = f"test_field_{data_type.name.lower()}"
|
|
|
|
# Add a new column with specific data type
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, data_type),
|
|
expression="1" if data_type != DataType.STRING else "'test'",
|
|
)
|
|
|
|
# Verify column was added by inserting data
|
|
if data_type == DataType.STRING:
|
|
field_value = "test_value"
|
|
elif data_type in [DataType.ARRAY_STRING]:
|
|
field_value = ["test_value"]
|
|
elif data_type in [DataType.ARRAY_INT32, DataType.ARRAY_INT64]:
|
|
field_value = [1, 2, 3]
|
|
elif data_type in [DataType.ARRAY_FLOAT, DataType.ARRAY_DOUBLE]:
|
|
field_value = [1.1, 2.2, 3.3]
|
|
elif data_type == DataType.ARRAY_BOOL:
|
|
field_value = [True, False]
|
|
elif data_type in [DataType.FLOAT, DataType.DOUBLE]:
|
|
field_value = 1.5
|
|
elif data_type in [DataType.INT32, DataType.INT64]:
|
|
field_value = 100
|
|
elif data_type == DataType.BOOL:
|
|
field_value = True
|
|
else:
|
|
field_value = 1
|
|
|
|
doc = Doc(
|
|
id="1",
|
|
fields={
|
|
"id": 1,
|
|
"name": "test",
|
|
"weight": 80.5,
|
|
field_name: field_value,
|
|
},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize("data_type", NOT_SUPPORT_ADD_COLUMN_DATA_TYPE)
|
|
def test_add_column_invalid_data_types(
|
|
self, basic_collection: Collection, data_type
|
|
):
|
|
with pytest.raises(Exception) as exc_info:
|
|
field_name = f"test_field_{data_type.name.lower()}"
|
|
|
|
# Add a new column with specific data type
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, data_type),
|
|
expression="1" if data_type != DataType.STRING else "'test'",
|
|
)
|
|
|
|
assert NOT_SUPPORT_ADD_COLUMN_ERROR_MSG in str(exc_info.value)
|
|
|
|
@pytest.mark.parametrize("nullable", [True, False])
|
|
def test_add_column_with_nullable_options(
|
|
self, basic_collection: Collection, nullable
|
|
):
|
|
field_name = f"test_field_nullable_{str(nullable).lower()}"
|
|
|
|
# Add a new column with specific nullable option
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, DataType.INT32, nullable=nullable),
|
|
expression="100",
|
|
)
|
|
|
|
# Verify column was added by inserting data
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
# Verify column was added by inserting data
|
|
doc = Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "test", "weight": 80.5, field_name: None},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
if nullable:
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
else:
|
|
with pytest.raises(ValueError) as e:
|
|
basic_collection.insert(doc)
|
|
assert (
|
|
"Field 'test_field_nullable_false': expected non-nullable type"
|
|
in str(e.value)
|
|
)
|
|
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
if nullable:
|
|
assert stats.doc_count == 2
|
|
else:
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"expression",
|
|
[
|
|
"1", # Constant integer
|
|
"1.5", # Constant float
|
|
"'test'", # Constant string
|
|
"id", # Reference to existing field
|
|
"weight * 2", # Simple arithmetic
|
|
"weight + id", # Complex arithmetic
|
|
"CASE WHEN weight > 50 THEN 1 ELSE 0 END", # Conditional expression
|
|
],
|
|
)
|
|
def test_add_column_with_different_expressions(
|
|
self, basic_collection: Collection, expression
|
|
):
|
|
field_name = f"test_field_expr_{abs(hash(expression)) % 1000}"
|
|
|
|
# Add a new column with specific expression
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, DataType.INT32),
|
|
expression=expression,
|
|
)
|
|
|
|
# Verify column was added by inserting data
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
def test_add_column_with_index_param(self, basic_collection: Collection):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(
|
|
"indexed_field",
|
|
DataType.INT32,
|
|
index_param=InvertIndexParam(enable_range_optimization=True),
|
|
),
|
|
expression="id * 2",
|
|
)
|
|
|
|
# Verify column was added by inserting data
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, "indexed_field": 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
# Verify document was inserted
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"field_name",
|
|
[
|
|
"a", # Minimum length
|
|
"a" * 32, # Maximum length (32 characters)
|
|
"valid_field_name_123", # Alphanumeric with underscore
|
|
"Valid-Field-Name", # With hyphens
|
|
"_underscore_start", # Starting with underscore
|
|
"field_name_with_123_numbers", # Numbers in middle
|
|
"FIELD_NAME_UPPERCASE", # Uppercase
|
|
# "field_with_nums_123_and_hyphens-456", # Complex valid name within limit
|
|
],
|
|
)
|
|
def test_add_column_with_valid_field_names(
|
|
self, basic_collection: Collection, field_name
|
|
):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, DataType.INT32), expression="200"
|
|
)
|
|
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, field_name: 300},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_field_name",
|
|
[
|
|
"", # Empty string
|
|
" ", # Space only
|
|
"a" * 33, # Too long (33 characters, exceeds 32)
|
|
"field name", # Contains space
|
|
"field.name", # Contains dot
|
|
"field@name", # Contains special character
|
|
"field/name", # Contains slash
|
|
"field\\name", # Contains backslash
|
|
"field$name", # Contains dollar sign
|
|
"field+name", # Contains plus sign
|
|
"field=name", # Contains equals sign
|
|
None, # None value
|
|
],
|
|
)
|
|
def test_add_column_with_invalid_field_names(
|
|
self, basic_collection: Collection, invalid_field_name
|
|
):
|
|
with pytest.raises(Exception) as exc_info:
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(invalid_field_name, DataType.INT32),
|
|
expression="100",
|
|
)
|
|
|
|
if invalid_field_name is None:
|
|
assert "validate failed" in str(exc_info.value), (
|
|
"Error message is unreasonable: e=" + str(exc_info.value)
|
|
)
|
|
else:
|
|
assert (
|
|
"invalid" in str(exc_info.value).lower()
|
|
or "name" in str(exc_info.value).lower()
|
|
)
|
|
|
|
def test_alter_column_rename(self, basic_collection: Collection):
|
|
basic_collection.alter_column(
|
|
old_name="weight",
|
|
new_name="mass",
|
|
option=AlterColumnOption(),
|
|
)
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "mass": 80.5}, # Use new name
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
def test_alter_column_non_exist(self, basic_collection: Collection):
|
|
with pytest.raises(Exception) as exc_info:
|
|
basic_collection.alter_column(
|
|
old_name="non_existing",
|
|
new_name="new_name",
|
|
field_schema=FieldSchema("new_name", DataType.STRING),
|
|
)
|
|
assert "column non_existing not found" in str(exc_info.value), (
|
|
"Error message is unreasonable: e=" + str(exc_info.value)
|
|
)
|
|
|
|
def test_alter_column_with_default_option(self, basic_collection: Collection):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("original_field", DataType.INT32), expression="100"
|
|
)
|
|
|
|
basic_collection.alter_column(
|
|
old_name="original_field",
|
|
new_name="renamed_field",
|
|
option=AlterColumnOption(),
|
|
)
|
|
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, "renamed_field": 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize("concurrency", [0, 1, 4, 8])
|
|
def test_alter_column_with_various_concurrency_options(
|
|
self, basic_collection: Collection, concurrency
|
|
):
|
|
old_field_name = f"orig_field_{concurrency}"
|
|
new_field_name = f"modified_field_{concurrency}"
|
|
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(old_field_name, DataType.INT32),
|
|
expression="100",
|
|
)
|
|
|
|
basic_collection.alter_column(
|
|
old_name=old_field_name,
|
|
new_name=new_field_name,
|
|
option=AlterColumnOption(concurrency=concurrency),
|
|
)
|
|
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, new_field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"old_field_name,new_field_name",
|
|
[
|
|
("a", "new_a"), # Minimum length
|
|
(
|
|
"abcdefghijklmnopqrstuvwxyz123456",
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
), # Maximum length (32 characters)
|
|
("valid_field_name_123", "new_valid_field"), # Alphanumeric with underscore
|
|
("Valid-Field-Name", "New-Field-Name"), # With hyphens
|
|
("_underscore_start", "new_underscore"), # Starting with underscore
|
|
("field_name_with_123_numbers", "new_with_nums"), # Numbers in middle
|
|
("FIELD_NAME_UPPERCASE", "new_uppercase"), # Uppercase
|
|
(
|
|
"field_with_nums_3_and_hyphens-6",
|
|
"new_field_hyphens",
|
|
), # Complex valid name
|
|
],
|
|
)
|
|
def test_alter_column_field_name_valid(
|
|
self, basic_collection: Collection, old_field_name, new_field_name
|
|
):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(old_field_name, DataType.INT32),
|
|
expression="100",
|
|
)
|
|
basic_collection.alter_column(
|
|
old_name=old_field_name,
|
|
new_name=new_field_name,
|
|
option=AlterColumnOption(),
|
|
)
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, new_field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"valid_old_name,invalid_new_name",
|
|
[
|
|
("temp_field", ""), # Empty new name
|
|
("temp_field", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), # Too long new name
|
|
("temp_field", "field name"), # New name with space
|
|
("temp_field", "field.name"), # New name with dot
|
|
("temp_field", "field@name"), # New name with special character
|
|
("temp_field", "field/name"), # New name with slash
|
|
("temp_field", "field\\name"), # New name with backslash
|
|
("temp_field", "field$name"), # New name with dollar sign
|
|
("temp_field", "field+name"), # New name with plus sign
|
|
("temp_field", "field=name"), # New name with equals sign
|
|
("temp_field", None), # None new name
|
|
],
|
|
)
|
|
def test_alter_column_with_invalid_field_names(
|
|
self, basic_collection: Collection, valid_old_name, invalid_new_name
|
|
):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("temp_field", DataType.INT32), expression="100"
|
|
)
|
|
with pytest.raises(Exception) as exc_info:
|
|
basic_collection.alter_column(
|
|
old_name=valid_old_name,
|
|
new_name=invalid_new_name if invalid_new_name is not None else "",
|
|
field_schema=FieldSchema(
|
|
invalid_new_name if invalid_new_name is not None else "",
|
|
DataType.INT32,
|
|
),
|
|
)
|
|
|
|
assert (
|
|
"invalid" in str(exc_info.value).lower()
|
|
or "name" in str(exc_info.value).lower()
|
|
or "incompatible" in str(exc_info.value).lower()
|
|
)
|
|
|
|
def test_drop_column_exist(self, basic_collection: Collection):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("temp_field", DataType.INT32), expression="100"
|
|
)
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, "temp_field": 1},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
basic_collection.drop_column("temp_field")
|
|
doc = Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "test", "weight": 80.5, "temp_field": 1},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
with pytest.raises(Exception) as exc_info:
|
|
result = basic_collection.insert(doc)
|
|
|
|
assert SCHEMA_VALIDATE_ERROR_MSG in str(exc_info.value)
|
|
|
|
def test_drop_column_non_exist(self, basic_collection: Collection):
|
|
with pytest.raises(Exception) as exc_info:
|
|
basic_collection.drop_column("non_existing_column")
|
|
assert NOT_EXIST_COLUMN_TO_DROP_ERROR_MSG in str(exc_info.value)
|
|
|
|
@pytest.mark.parametrize(
|
|
"field_name",
|
|
[
|
|
"a", # Minimum length
|
|
"a" * 32, # Maximum length (32 characters)
|
|
"valid_field_name_123", # Alphanumeric with underscore
|
|
"Valid-Field-Name", # With hyphens
|
|
"_underscore_start", # Starting with underscore
|
|
"field_name_with_123_numbers", # Numbers in middle
|
|
"FIELD_NAME_UPPERCASE", # Uppercase
|
|
"field_with_nums_3_and_hyphens-6", # Complex valid name within limit
|
|
],
|
|
)
|
|
def test_drop_column_field_name_valid(
|
|
self, basic_collection: Collection, field_name
|
|
):
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema(field_name, DataType.INT32), expression="100"
|
|
)
|
|
doc = Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "test", "weight": 80.5, field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
|
|
result = basic_collection.insert(doc)
|
|
assert bool(result), f"Expected 1 result, but got {len(result)}"
|
|
assert result.ok(), (
|
|
f"result={result},Insert operation failed with code = {result.code()}"
|
|
)
|
|
|
|
stats = basic_collection.stats
|
|
assert stats is not None
|
|
assert stats.doc_count == 1
|
|
|
|
basic_collection.drop_column(field_name)
|
|
|
|
doc = Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "test", "weight": 80.5, field_name: 200},
|
|
vectors={
|
|
"dense": np.random.random(128).tolist(),
|
|
"sparse": {1: 1.0, 2: 2.0},
|
|
},
|
|
)
|
|
with pytest.raises(Exception) as exc_info:
|
|
result = basic_collection.insert(doc)
|
|
|
|
assert SCHEMA_VALIDATE_ERROR_MSG in str(exc_info.value)
|
|
|
|
def test_add_column_then_query_returns_new_field(
|
|
self, basic_collection: Collection
|
|
):
|
|
"""Regression test for issue #426: query() should return fields added via add_column()."""
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("score", DataType.INT64, nullable=True),
|
|
)
|
|
|
|
docs = [
|
|
Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "alice", "weight": 60.0, "score": 100},
|
|
vectors={
|
|
"dense": generate_constant_vector(1, 128),
|
|
"sparse": generate_sparse_vector(1),
|
|
},
|
|
),
|
|
Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "bob", "weight": 70.0, "score": 200},
|
|
vectors={
|
|
"dense": generate_constant_vector(2, 128),
|
|
"sparse": generate_sparse_vector(2),
|
|
},
|
|
),
|
|
]
|
|
result = basic_collection.insert(docs)
|
|
assert all(r.ok() for r in result)
|
|
|
|
# Query with explicit output_fields
|
|
query_result = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
output_fields=["score"],
|
|
)
|
|
assert len(query_result) == 2
|
|
for doc in query_result:
|
|
assert "score" in doc.fields, (
|
|
f"Doc {doc.id} missing 'score' field after add_column (output_fields explicit)"
|
|
)
|
|
assert doc.fields["score"] in (100, 200)
|
|
|
|
# Query with select-all (no output_fields)
|
|
query_result_all = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
)
|
|
assert len(query_result_all) == 2
|
|
for doc in query_result_all:
|
|
assert "score" in doc.fields, (
|
|
f"Doc {doc.id} missing 'score' field after add_column (select all)"
|
|
)
|
|
|
|
def test_alter_column_rename_then_query_returns_new_name(
|
|
self, basic_collection: Collection
|
|
):
|
|
"""Regression test: query() should use the new field name after alter_column rename."""
|
|
docs = [
|
|
Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "alice", "weight": 60.0},
|
|
vectors={
|
|
"dense": generate_constant_vector(1, 128),
|
|
"sparse": generate_sparse_vector(1),
|
|
},
|
|
),
|
|
Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "bob", "weight": 70.0},
|
|
vectors={
|
|
"dense": generate_constant_vector(2, 128),
|
|
"sparse": generate_sparse_vector(2),
|
|
},
|
|
),
|
|
]
|
|
result = basic_collection.insert(docs)
|
|
assert all(r.ok() for r in result)
|
|
|
|
# Rename 'weight' -> 'mass'
|
|
basic_collection.alter_column("weight", new_name="mass")
|
|
|
|
# Query with explicit output_fields using new name
|
|
query_result = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
output_fields=["mass"],
|
|
)
|
|
assert len(query_result) == 2
|
|
for doc in query_result:
|
|
assert "mass" in doc.fields, (
|
|
f"Doc {doc.id} missing 'mass' field after alter_column rename"
|
|
)
|
|
assert "weight" not in doc.fields, (
|
|
f"Doc {doc.id} still has old name 'weight' after rename"
|
|
)
|
|
|
|
# Query with select-all
|
|
query_result_all = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
)
|
|
assert len(query_result_all) == 2
|
|
for doc in query_result_all:
|
|
assert "mass" in doc.fields, (
|
|
f"Doc {doc.id} missing 'mass' in select-all after alter_column rename"
|
|
)
|
|
assert "weight" not in doc.fields, (
|
|
f"Doc {doc.id} still has old name 'weight' in select-all after rename"
|
|
)
|
|
|
|
def test_drop_column_then_query_excludes_dropped_field(
|
|
self, basic_collection: Collection
|
|
):
|
|
"""Regression test: query() should not return fields removed via drop_column()."""
|
|
basic_collection.add_column(
|
|
field_schema=FieldSchema("score", DataType.INT64, nullable=True),
|
|
)
|
|
|
|
docs = [
|
|
Doc(
|
|
id="1",
|
|
fields={"id": 1, "name": "alice", "weight": 60.0, "score": 100},
|
|
vectors={
|
|
"dense": generate_constant_vector(1, 128),
|
|
"sparse": generate_sparse_vector(1),
|
|
},
|
|
),
|
|
Doc(
|
|
id="2",
|
|
fields={"id": 2, "name": "bob", "weight": 70.0, "score": 200},
|
|
vectors={
|
|
"dense": generate_constant_vector(2, 128),
|
|
"sparse": generate_sparse_vector(2),
|
|
},
|
|
),
|
|
]
|
|
result = basic_collection.insert(docs)
|
|
assert all(r.ok() for r in result)
|
|
|
|
# Verify field exists before drop
|
|
query_before = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
)
|
|
assert all("score" in doc.fields for doc in query_before)
|
|
|
|
# Drop the column
|
|
basic_collection.drop_column("score")
|
|
|
|
# Query after drop - 'score' should not appear
|
|
query_after = basic_collection.query(
|
|
Query(field_name="dense", vector=generate_constant_vector(1, 128)),
|
|
topk=2,
|
|
)
|
|
assert len(query_after) == 2
|
|
for doc in query_after:
|
|
assert "score" not in doc.fields, (
|
|
f"Doc {doc.id} still has 'score' after drop_column"
|
|
)
|
|
assert "name" in doc.fields, (
|
|
f"Doc {doc.id} missing 'name' - other fields should still be present"
|
|
)
|