1024 lines
34 KiB
C++
1024 lines
34 KiB
C++
// 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.
|
|
|
|
#include "zvec/db/schema.h"
|
|
#include <gtest/gtest.h>
|
|
#include "zvec/db/index_params.h"
|
|
#include "zvec/db/status.h"
|
|
|
|
using namespace zvec;
|
|
|
|
TEST(FieldSchemaTest, DefaultConstructor) {
|
|
FieldSchema field;
|
|
EXPECT_EQ(field.name(), "");
|
|
EXPECT_EQ(field.data_type(), DataType::UNDEFINED);
|
|
EXPECT_FALSE(field.nullable());
|
|
EXPECT_EQ(field.dimension(), 0u);
|
|
EXPECT_EQ(field.index_params(), nullptr);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, ConstructorWithParameters) {
|
|
auto index_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
FieldSchema field("test_field", DataType::VECTOR_FP32, 128, true,
|
|
index_params);
|
|
|
|
EXPECT_EQ(field.name(), "test_field");
|
|
EXPECT_EQ(field.data_type(), DataType::VECTOR_FP32);
|
|
EXPECT_TRUE(field.nullable());
|
|
EXPECT_EQ(field.dimension(), 128u);
|
|
EXPECT_NE(field.index_params(), nullptr);
|
|
EXPECT_EQ(field.index_params()->type(), IndexType::HNSW);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, SettersAndGetters) {
|
|
FieldSchema field;
|
|
|
|
field.set_name("new_field");
|
|
EXPECT_EQ(field.name(), "new_field");
|
|
|
|
field.set_data_type(DataType::STRING);
|
|
EXPECT_EQ(field.data_type(), DataType::STRING);
|
|
|
|
field.set_nullable(true);
|
|
EXPECT_TRUE(field.nullable());
|
|
|
|
field.set_dimension(256);
|
|
EXPECT_EQ(field.dimension(), 256u);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, ElementDataType) {
|
|
FieldSchema array_field;
|
|
array_field.set_data_type(DataType::ARRAY_BINARY);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::BINARY);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_STRING);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::STRING);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_BOOL);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::BOOL);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_INT32);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::INT32);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_INT64);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::INT64);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_UINT32);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::UINT32);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_UINT64);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::UINT64);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_FLOAT);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::FLOAT);
|
|
|
|
array_field.set_data_type(DataType::ARRAY_DOUBLE);
|
|
EXPECT_EQ(array_field.element_data_type(), DataType::DOUBLE);
|
|
|
|
// Non-array types should return the same type
|
|
FieldSchema non_array_field;
|
|
non_array_field.set_data_type(DataType::STRING);
|
|
EXPECT_EQ(non_array_field.element_data_type(), DataType::STRING);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, VectorFieldDetection) {
|
|
FieldSchema field;
|
|
|
|
// Test dense vector field detection
|
|
field.set_data_type(DataType::VECTOR_BINARY32);
|
|
EXPECT_TRUE(field.is_vector_field());
|
|
EXPECT_TRUE(field.is_dense_vector());
|
|
EXPECT_FALSE(field.is_sparse_vector());
|
|
|
|
field.set_data_type(DataType::VECTOR_FP32);
|
|
EXPECT_TRUE(field.is_vector_field());
|
|
EXPECT_TRUE(field.is_dense_vector());
|
|
EXPECT_FALSE(field.is_sparse_vector());
|
|
|
|
field.set_data_type(DataType::VECTOR_INT16);
|
|
EXPECT_TRUE(field.is_vector_field());
|
|
EXPECT_TRUE(field.is_dense_vector());
|
|
EXPECT_FALSE(field.is_sparse_vector());
|
|
|
|
// Test sparse vector field detection
|
|
field.set_data_type(DataType::SPARSE_VECTOR_FP32);
|
|
EXPECT_TRUE(field.is_vector_field());
|
|
EXPECT_FALSE(field.is_dense_vector());
|
|
EXPECT_TRUE(field.is_sparse_vector());
|
|
|
|
// Test non-vector field
|
|
field.set_data_type(DataType::STRING);
|
|
EXPECT_FALSE(field.is_vector_field());
|
|
EXPECT_FALSE(field.is_dense_vector());
|
|
EXPECT_FALSE(field.is_sparse_vector());
|
|
|
|
// Test static methods
|
|
EXPECT_TRUE(FieldSchema::is_dense_vector_field(DataType::VECTOR_FP32));
|
|
EXPECT_FALSE(FieldSchema::is_dense_vector_field(DataType::STRING));
|
|
|
|
EXPECT_TRUE(
|
|
FieldSchema::is_sparse_vector_field(DataType::SPARSE_VECTOR_FP32));
|
|
EXPECT_FALSE(FieldSchema::is_sparse_vector_field(DataType::VECTOR_FP32));
|
|
|
|
EXPECT_TRUE(FieldSchema::is_vector_field(DataType::VECTOR_FP32));
|
|
EXPECT_TRUE(FieldSchema::is_vector_field(DataType::SPARSE_VECTOR_FP32));
|
|
EXPECT_FALSE(FieldSchema::is_vector_field(DataType::STRING));
|
|
}
|
|
|
|
TEST(FieldSchemaTest, ArrayTypeDetection) {
|
|
FieldSchema field;
|
|
|
|
field.set_data_type(DataType::ARRAY_BINARY);
|
|
EXPECT_TRUE(field.is_array_type());
|
|
|
|
field.set_data_type(DataType::ARRAY_STRING);
|
|
EXPECT_TRUE(field.is_array_type());
|
|
|
|
field.set_data_type(DataType::ARRAY_DOUBLE);
|
|
EXPECT_TRUE(field.is_array_type());
|
|
|
|
field.set_data_type(DataType::STRING);
|
|
EXPECT_FALSE(field.is_array_type());
|
|
|
|
field.set_data_type(DataType::VECTOR_FP32);
|
|
EXPECT_FALSE(field.is_array_type());
|
|
}
|
|
|
|
TEST(FieldSchemaTest, IndexTypeAndParams) {
|
|
FieldSchema field;
|
|
EXPECT_EQ(field.index_type(), IndexType::UNDEFINED);
|
|
EXPECT_EQ(field.index_params(), nullptr);
|
|
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(MetricType::IP, 32, 200);
|
|
field.set_index_params(hnsw_params);
|
|
EXPECT_EQ(field.index_type(), IndexType::HNSW);
|
|
EXPECT_NE(field.index_params(), nullptr);
|
|
|
|
// Test setting with nullptr
|
|
field.set_index_params(nullptr);
|
|
EXPECT_EQ(field.index_type(), IndexType::UNDEFINED);
|
|
EXPECT_EQ(field.index_params(), nullptr);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, CopyConstructorAndAssignment) {
|
|
auto index_params = std::make_shared<FlatIndexParams>(MetricType::L2);
|
|
FieldSchema original("original", DataType::STRING, 100, true, index_params);
|
|
|
|
// Test copy constructor
|
|
FieldSchema copy(original);
|
|
EXPECT_EQ(copy.name(), "original");
|
|
EXPECT_EQ(copy.data_type(), DataType::STRING);
|
|
EXPECT_TRUE(copy.nullable());
|
|
EXPECT_EQ(copy.dimension(), 100u);
|
|
EXPECT_NE(copy.index_params(), nullptr);
|
|
EXPECT_EQ(copy.index_params()->type(), IndexType::FLAT);
|
|
|
|
// Test copy assignment
|
|
FieldSchema assigned;
|
|
assigned = original;
|
|
EXPECT_EQ(assigned.name(), "original");
|
|
EXPECT_EQ(assigned.data_type(), DataType::STRING);
|
|
EXPECT_TRUE(assigned.nullable());
|
|
EXPECT_EQ(assigned.dimension(), 100u);
|
|
EXPECT_NE(assigned.index_params(), nullptr);
|
|
EXPECT_EQ(assigned.index_params()->type(), IndexType::FLAT);
|
|
|
|
// Verify deep copy - modifying original shouldn't affect copy
|
|
original.set_name("modified");
|
|
EXPECT_EQ(copy.name(), "original"); // Copy should be unchanged
|
|
EXPECT_EQ(assigned.name(), "original"); // Assigned should be unchanged
|
|
}
|
|
|
|
TEST(FieldSchemaTest, MoveConstructorAndAssignment) {
|
|
auto index_params = std::make_shared<IVFIndexParams>(MetricType::COSINE, 128);
|
|
FieldSchema original("move_test", DataType::VECTOR_FP32, 256, false,
|
|
index_params);
|
|
|
|
// Test move constructor
|
|
FieldSchema moved(std::move(original));
|
|
EXPECT_EQ(moved.name(), "move_test");
|
|
EXPECT_EQ(moved.data_type(), DataType::VECTOR_FP32);
|
|
EXPECT_FALSE(moved.nullable());
|
|
EXPECT_EQ(moved.dimension(), 256u);
|
|
EXPECT_NE(moved.index_params(), nullptr);
|
|
EXPECT_EQ(moved.index_params()->type(), IndexType::IVF);
|
|
|
|
// After move, original should be in valid but unspecified state
|
|
// Note: In practice, the name would likely be moved, but we don't test that
|
|
// as it's implementation-dependent
|
|
}
|
|
|
|
TEST(FieldSchemaTest, ComparisonOperators) {
|
|
auto index_params1 =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
auto index_params2 =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
auto index_params3 = std::make_shared<FlatIndexParams>(MetricType::IP);
|
|
|
|
FieldSchema field1("field", DataType::STRING, 100, false, index_params1);
|
|
FieldSchema field2("field", DataType::STRING, 100, false, index_params2);
|
|
FieldSchema field3("field", DataType::STRING, 100, false, index_params3);
|
|
FieldSchema field4("field", DataType::STRING, 100, true, index_params1);
|
|
FieldSchema field5("different", DataType::STRING, 100, false, index_params1);
|
|
|
|
// Equal fields
|
|
EXPECT_TRUE(field1 == field2);
|
|
EXPECT_FALSE(field1 != field2);
|
|
|
|
// Different index params
|
|
EXPECT_FALSE(field1 == field3);
|
|
EXPECT_TRUE(field1 != field3);
|
|
|
|
// Different nullable
|
|
EXPECT_FALSE(field1 == field4);
|
|
EXPECT_TRUE(field1 != field4);
|
|
|
|
// Different name
|
|
EXPECT_FALSE(field1 == field5);
|
|
EXPECT_TRUE(field1 != field5);
|
|
}
|
|
|
|
TEST(FieldSchemaTest, Validate) {
|
|
{
|
|
FieldSchema field("", DataType::UNDEFINED);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
FieldSchema field("", DataType::STRING);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 0,
|
|
false); // Zero dimension
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
FieldSchema field("dense_vector", DataType::VECTOR_FP32, 20001,
|
|
false); // Zero dimension
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
auto ivf_params = std::make_shared<IVFIndexParams>(MetricType::IP, 128);
|
|
FieldSchema field("sparse_field", DataType::SPARSE_VECTOR_FP32, 0, false,
|
|
ivf_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
auto hnsw_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
FieldSchema field("sparse_field", DataType::SPARSE_VECTOR_FP32, 0, false,
|
|
hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
auto invalid_params = std::make_shared<InvertIndexParams>(false);
|
|
FieldSchema field("dense_field", DataType::VECTOR_FP32, 128, false,
|
|
invalid_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
auto hnsw_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
FieldSchema field("scalar_field", DataType::STRING, 0, false, hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
{
|
|
auto hnsw_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 128, false,
|
|
hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
auto flat_params = std::make_shared<FlatIndexParams>(MetricType::IP);
|
|
FieldSchema field("sparse_field", DataType::SPARSE_VECTOR_FP32, 0, false,
|
|
flat_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
auto invert_params = std::make_shared<InvertIndexParams>(false);
|
|
FieldSchema field("scalar_field", DataType::STRING, 0, false,
|
|
invert_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
auto fts_params = std::make_shared<FtsIndexParams>(
|
|
"standard", std::vector<std::string>{"lowercase", "stemmer"},
|
|
R"({"stemmer_lang":"english"})");
|
|
FieldSchema field("fts_field", DataType::STRING, false, fts_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
auto fts_params = std::make_shared<FtsIndexParams>(
|
|
"standard", std::vector<std::string>{"lowercase", "stemmer"},
|
|
R"({"stemmer_lang":"nonexistent_lang"})");
|
|
FieldSchema field("fts_field", DataType::STRING, false, fts_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
EXPECT_NE(status.message().find("invalid FTS index params"),
|
|
std::string::npos);
|
|
}
|
|
|
|
{
|
|
FieldSchema field("simple_field", DataType::STRING);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok()); // Scalar fields without index params are valid
|
|
|
|
FieldSchema vector_field("vector_field", DataType::VECTOR_FP32, 128, false);
|
|
status = vector_field.validate();
|
|
EXPECT_TRUE(
|
|
status.ok()); // Vector fields without index params are also valid
|
|
}
|
|
|
|
{
|
|
// Test that VECTOR_FP32 with FP16 quantize type is valid
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(
|
|
MetricType::L2, 16, 100, QuantizeType::FP16);
|
|
FieldSchema field("fp32_vector", DataType::VECTOR_FP32, 128, false,
|
|
hnsw_params);
|
|
auto status = field.validate();
|
|
if (!status.ok()) {
|
|
std::cout << "status: " << status.message() << std::endl;
|
|
}
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
// Test that VECTOR_FP32 with UNDEFINED quantize type is valid
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(
|
|
MetricType::L2, 16, 100, QuantizeType::UNDEFINED);
|
|
FieldSchema field("fp32_vector_no_quantize", DataType::VECTOR_FP32, 128,
|
|
false, hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
// Test that SPARSE_VECTOR_FP32 with FP16 quantize type should fail
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(
|
|
MetricType::IP, 16, 100, QuantizeType::FP16);
|
|
FieldSchema field("sparse_fp32_vector", DataType::SPARSE_VECTOR_FP32, 0,
|
|
false, hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
|
|
{
|
|
// Test that VECTOR_FP64 with FP16 quantize type is valid
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(
|
|
MetricType::L2, 16, 100, QuantizeType::FP16);
|
|
FieldSchema field("fp64_vector", DataType::VECTOR_FP64, 128, false,
|
|
hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
}
|
|
|
|
{
|
|
// already support int8/int4 quantizer
|
|
// Test that VECTOR_FP32 with INT8 quantize type should succeed
|
|
auto hnsw_params = std::make_shared<HnswIndexParams>(
|
|
MetricType::L2, 16, 100, QuantizeType::INT8);
|
|
FieldSchema field("fp32_vector_int8_quantize", DataType::VECTOR_FP32, 128,
|
|
false, hnsw_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok());
|
|
|
|
auto flat_params =
|
|
std::make_shared<FlatIndexParams>(MetricType::L2, QuantizeType::INT4);
|
|
FieldSchema flat_field("fp32_vector_int4_quantize", DataType::VECTOR_FP32,
|
|
128, false, flat_params);
|
|
EXPECT_TRUE(field.validate().ok());
|
|
}
|
|
|
|
{
|
|
std::vector<std::string> valid_names = {
|
|
"a", // min len = 1
|
|
"A",
|
|
"0",
|
|
"_",
|
|
"-", // single allowed char
|
|
"abc",
|
|
"ABC",
|
|
"a1_",
|
|
"user_name",
|
|
"test-123",
|
|
"aBc123_-",
|
|
std::string(32, 'a'), // max len = 32
|
|
"a_b-c1",
|
|
"__test__",
|
|
"123_test"};
|
|
for (auto name : valid_names) {
|
|
FieldSchema field(name, DataType::STRING);
|
|
auto status = field.validate();
|
|
if (!status.ok()) {
|
|
std::cout << "status: " << status.message() << std::endl;
|
|
}
|
|
EXPECT_TRUE(status.ok());
|
|
}
|
|
}
|
|
|
|
{
|
|
std::vector<std::string> invalid_names = {
|
|
"", // empty — len < 1
|
|
std::string(33, 'a'), // len > 32
|
|
"a b", // space
|
|
"a.b",
|
|
"a@b",
|
|
"a#b", // illegal chars: . @ #
|
|
"a$b",
|
|
"a%",
|
|
"a&", // $ % & etc.
|
|
"中文",
|
|
"用户", // non-ASCII
|
|
"a..b",
|
|
"a__b?", // ? not allowed
|
|
};
|
|
for (auto name : invalid_names) {
|
|
FieldSchema field(name, DataType::STRING);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, DefaultConstructor) {
|
|
CollectionSchema schema;
|
|
EXPECT_EQ(schema.name(), "");
|
|
EXPECT_EQ(schema.fields().size(), 0);
|
|
EXPECT_EQ(schema.max_doc_count_per_segment(), MAX_DOC_COUNT_PER_SEGMENT);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, ConstructorWithParameters) {
|
|
FieldSchemaPtrList fields;
|
|
auto field1 = std::make_shared<FieldSchema>("field1", DataType::STRING);
|
|
auto field2 = std::make_shared<FieldSchema>("field2", DataType::VECTOR_FP32);
|
|
fields.push_back(field1);
|
|
fields.push_back(field2);
|
|
|
|
CollectionSchema schema("test_collection", fields);
|
|
EXPECT_EQ(schema.name(), "test_collection");
|
|
EXPECT_EQ(schema.fields().size(), 2);
|
|
EXPECT_TRUE(schema.has_field("field1"));
|
|
EXPECT_TRUE(schema.has_field("field2"));
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, NameManagement) {
|
|
CollectionSchema schema;
|
|
EXPECT_EQ(schema.name(), "");
|
|
|
|
schema.set_name("new_name");
|
|
EXPECT_EQ(schema.name(), "new_name");
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, MaxDocCountPerSegment) {
|
|
CollectionSchema schema;
|
|
EXPECT_EQ(schema.max_doc_count_per_segment(), MAX_DOC_COUNT_PER_SEGMENT);
|
|
|
|
schema.set_max_doc_count_per_segment(500000);
|
|
EXPECT_EQ(schema.max_doc_count_per_segment(), 500000u);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, AddField) {
|
|
CollectionSchema schema;
|
|
auto field = std::make_shared<FieldSchema>("test_field", DataType::STRING);
|
|
|
|
auto status = schema.add_field(field);
|
|
EXPECT_TRUE(status.ok());
|
|
EXPECT_TRUE(schema.has_field("test_field"));
|
|
EXPECT_EQ(schema.fields().size(), 1);
|
|
|
|
// Try to add the same field again
|
|
auto status2 = schema.add_field(field);
|
|
EXPECT_FALSE(status2.ok());
|
|
EXPECT_EQ(status2.code(), StatusCode::ALREADY_EXISTS);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, DropField) {
|
|
CollectionSchema schema;
|
|
auto field1 = std::make_shared<FieldSchema>("field1", DataType::STRING);
|
|
auto field2 = std::make_shared<FieldSchema>("field2", DataType::VECTOR_FP32);
|
|
|
|
schema.add_field(field1);
|
|
schema.add_field(field2);
|
|
EXPECT_EQ(schema.fields().size(), 2);
|
|
|
|
// Drop existing field
|
|
auto status = schema.drop_field("field1");
|
|
EXPECT_TRUE(status.ok());
|
|
EXPECT_FALSE(schema.has_field("field1"));
|
|
EXPECT_TRUE(schema.has_field("field2"));
|
|
EXPECT_EQ(schema.fields().size(), 1);
|
|
|
|
// Try to drop non-existing field
|
|
auto status2 = schema.drop_field("nonexistent");
|
|
EXPECT_FALSE(status2.ok());
|
|
EXPECT_EQ(status2.code(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, AlterField) {
|
|
CollectionSchema schema;
|
|
auto original_field =
|
|
std::make_shared<FieldSchema>("field", DataType::STRING);
|
|
schema.add_field(original_field);
|
|
|
|
auto new_field =
|
|
std::make_shared<FieldSchema>("field", DataType::VECTOR_FP32);
|
|
auto status = schema.alter_field("field", new_field);
|
|
EXPECT_TRUE(status.ok());
|
|
|
|
auto *field = schema.get_field("field");
|
|
EXPECT_NE(field, nullptr);
|
|
EXPECT_EQ(field->data_type(), DataType::VECTOR_FP32);
|
|
|
|
// Try to alter non-existing field
|
|
auto status2 = schema.alter_field("nonexistent", new_field);
|
|
EXPECT_FALSE(status2.ok());
|
|
EXPECT_EQ(status2.code(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, FieldRetrieval) {
|
|
CollectionSchema schema;
|
|
auto string_field =
|
|
std::make_shared<FieldSchema>("string_field", DataType::STRING);
|
|
auto vector_field =
|
|
std::make_shared<FieldSchema>("vector_field", DataType::VECTOR_FP32);
|
|
|
|
schema.add_field(string_field);
|
|
schema.add_field(vector_field);
|
|
|
|
// Test get_field
|
|
const auto *const_string_field = schema.get_field("string_field");
|
|
EXPECT_NE(const_string_field, nullptr);
|
|
EXPECT_EQ(const_string_field->data_type(), DataType::STRING);
|
|
|
|
auto *mutable_string_field = schema.get_field("string_field");
|
|
EXPECT_NE(mutable_string_field, nullptr);
|
|
EXPECT_EQ(mutable_string_field->data_type(), DataType::STRING);
|
|
|
|
// Test get_forward_field
|
|
const auto *const_forward_field = schema.get_forward_field("string_field");
|
|
EXPECT_NE(const_forward_field, nullptr);
|
|
EXPECT_EQ(const_forward_field->data_type(), DataType::STRING);
|
|
|
|
auto *mutable_forward_field = schema.get_forward_field("string_field");
|
|
EXPECT_NE(mutable_forward_field, nullptr);
|
|
EXPECT_EQ(mutable_forward_field->data_type(), DataType::STRING);
|
|
|
|
// Forward field should return nullptr for vector fields
|
|
EXPECT_EQ(schema.get_forward_field("vector_field"), nullptr);
|
|
|
|
// Test get_vector_field
|
|
const auto *const_vector_field = schema.get_vector_field("vector_field");
|
|
EXPECT_NE(const_vector_field, nullptr);
|
|
EXPECT_EQ(const_vector_field->data_type(), DataType::VECTOR_FP32);
|
|
|
|
auto *mutable_vector_field = schema.get_vector_field("vector_field");
|
|
EXPECT_NE(mutable_vector_field, nullptr);
|
|
EXPECT_EQ(mutable_vector_field->data_type(), DataType::VECTOR_FP32);
|
|
|
|
// Vector field should return nullptr for string fields
|
|
EXPECT_EQ(schema.get_vector_field("string_field"), nullptr);
|
|
|
|
// Test non-existing field
|
|
EXPECT_EQ(schema.get_field("nonexistent"), nullptr);
|
|
EXPECT_EQ(schema.get_forward_field("nonexistent"), nullptr);
|
|
EXPECT_EQ(schema.get_vector_field("nonexistent"), nullptr);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, FieldLists) {
|
|
CollectionSchema schema;
|
|
auto string_field =
|
|
std::make_shared<FieldSchema>("string_field", DataType::STRING);
|
|
auto vector_field =
|
|
std::make_shared<FieldSchema>("vector_field", DataType::VECTOR_FP32);
|
|
auto array_field =
|
|
std::make_shared<FieldSchema>("array_field", DataType::ARRAY_INT32);
|
|
|
|
schema.add_field(string_field);
|
|
schema.add_field(vector_field);
|
|
schema.add_field(array_field);
|
|
|
|
// Test fields()
|
|
auto all_fields = schema.fields();
|
|
EXPECT_EQ(all_fields.size(), 3);
|
|
|
|
// Test forward_fields()
|
|
auto forward_fields = schema.forward_fields();
|
|
EXPECT_EQ(forward_fields.size(), 2); // string_field and array_field
|
|
|
|
// Test forward_field_names()
|
|
auto forward_field_names = schema.forward_field_names();
|
|
EXPECT_EQ(forward_field_names.size(), 2);
|
|
EXPECT_TRUE(std::find(forward_field_names.begin(), forward_field_names.end(),
|
|
"string_field") != forward_field_names.end());
|
|
EXPECT_TRUE(std::find(forward_field_names.begin(), forward_field_names.end(),
|
|
"array_field") != forward_field_names.end());
|
|
|
|
// Test vector_fields()
|
|
auto vector_fields = schema.vector_fields();
|
|
EXPECT_EQ(vector_fields.size(), 1);
|
|
EXPECT_EQ(vector_fields[0]->name(), "vector_field");
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, IndexManagement) {
|
|
CollectionSchema schema;
|
|
auto field =
|
|
std::make_shared<FieldSchema>("indexed_field", DataType::VECTOR_FP32);
|
|
schema.add_field(field);
|
|
|
|
auto forward_field =
|
|
std::make_shared<FieldSchema>("forward_field", DataType::STRING);
|
|
schema.add_field(forward_field);
|
|
|
|
// Test has_index on field without index
|
|
EXPECT_FALSE(schema.has_index("indexed_field"));
|
|
EXPECT_FALSE(schema.has_index("forward_field"));
|
|
|
|
// Add index
|
|
auto index_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, 16, 100);
|
|
auto status = schema.add_index("indexed_field", index_params);
|
|
EXPECT_TRUE(status.ok());
|
|
EXPECT_TRUE(schema.has_index("indexed_field"));
|
|
|
|
// Try to add index to non-existing field
|
|
auto status2 = schema.add_index("nonexistent", index_params);
|
|
EXPECT_FALSE(status2.ok());
|
|
EXPECT_EQ(status2.code(), StatusCode::NOT_FOUND);
|
|
|
|
// Drop index
|
|
auto status3 = schema.drop_index("indexed_field");
|
|
EXPECT_TRUE(status3.ok());
|
|
EXPECT_FALSE(schema.has_index("indexed_field"));
|
|
|
|
// Try to drop index from non-existing field
|
|
auto status4 = schema.drop_index("nonexistent");
|
|
EXPECT_FALSE(status4.ok());
|
|
EXPECT_EQ(status4.code(), StatusCode::NOT_FOUND);
|
|
|
|
auto forward_index_params = std::make_shared<InvertIndexParams>(false);
|
|
auto status5 = schema.add_index("forward_field", forward_index_params);
|
|
EXPECT_TRUE(status5.ok());
|
|
EXPECT_TRUE(schema.has_index("forward_field"));
|
|
|
|
auto status6 = schema.drop_index("forward_field");
|
|
EXPECT_TRUE(status5.ok());
|
|
EXPECT_FALSE(schema.has_index("forward_field"));
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, CopyConstructor) {
|
|
CollectionSchema original("original_schema", {});
|
|
auto field = std::make_shared<FieldSchema>("field", DataType::STRING);
|
|
original.add_field(field);
|
|
original.set_max_doc_count_per_segment(100000);
|
|
|
|
CollectionSchema copy(original);
|
|
EXPECT_EQ(copy.name(), "original_schema");
|
|
EXPECT_EQ(copy.fields().size(), 1);
|
|
EXPECT_TRUE(copy.has_field("field"));
|
|
EXPECT_EQ(copy.max_doc_count_per_segment(), 100000u);
|
|
}
|
|
|
|
TEST(CollectionSchemaTest, Validate) {
|
|
CollectionSchema original("original_schema", {});
|
|
auto field =
|
|
std::make_shared<FieldSchema>("sparse", DataType::SPARSE_VECTOR_FP32);
|
|
original.add_field(field);
|
|
original.set_max_doc_count_per_segment(100000);
|
|
|
|
ASSERT_TRUE(original.validate().ok());
|
|
|
|
CollectionSchema c1;
|
|
auto s = c1.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
CollectionSchema c2("c2", {});
|
|
s = c1.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
auto f1 = std::make_shared<FieldSchema>();
|
|
CollectionSchema c3("c3", {f1});
|
|
s = c3.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
auto f2 = std::make_shared<FieldSchema>("f2", DataType::INT32);
|
|
CollectionSchema c4("c4", {f2});
|
|
s = c4.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
auto f3 = std::make_shared<FieldSchema>("f3", DataType::VECTOR_FP16);
|
|
CollectionSchema c5("c5", {f3});
|
|
s = c5.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
// validate collection name regex "^[a-zA-Z0-9_-]{3,32}$"
|
|
{
|
|
std::vector<std::string> invalid_names = {
|
|
"", // empty
|
|
"ab", // too short (<3)
|
|
std::string(65, 'a'), // too long (>64)
|
|
"a b", // space not allowed
|
|
"a.b", // dot not allowed
|
|
"a$b", // $ not allowed
|
|
"中文", // non-ASCII
|
|
"a\nb", // newline not allowed
|
|
"a\tb", // tab not allowed
|
|
"a\rb", // carriage return not allowed
|
|
};
|
|
|
|
for (const auto &name : invalid_names) {
|
|
CollectionSchema c(name, {field});
|
|
s = c.validate();
|
|
if (!s.ok()) {
|
|
std::cout << "Invalid name: " << name << std::endl;
|
|
}
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
|
|
std::vector<std::string> valid_names = {
|
|
"test_collection_supported_vectors",
|
|
std::string(64, 'a'),
|
|
"a_b", // underscore allowed
|
|
"a-b", // dash allowed
|
|
"a_1", // underscore and digit allowed
|
|
"a-1", // dash and digit allowed
|
|
"a_1b", // underscore, digit and letter allowed
|
|
"a-1b", // dash, digit and letter allowed
|
|
"-start", // allowed! (regex permits leading -/_)
|
|
"_start", // also allowed
|
|
"end-",
|
|
"end_", // trailing -/_ allowed
|
|
"a--b",
|
|
"__b",
|
|
"a__b" // consecutive allowed
|
|
};
|
|
for (const auto &name : valid_names) {
|
|
CollectionSchema c(name, {field});
|
|
s = c.validate();
|
|
ASSERT_TRUE(s.ok());
|
|
}
|
|
}
|
|
|
|
// validate vector/scalar field size
|
|
{
|
|
std::vector<FieldSchema::Ptr> fields;
|
|
for (int i = 0; i < 1025; ++i) {
|
|
auto f = std::make_shared<FieldSchema>("f" + std::to_string(i),
|
|
DataType::VECTOR_FP32, 1024);
|
|
fields.emplace_back(f);
|
|
}
|
|
CollectionSchema c5("c5", fields);
|
|
s = c5.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
|
|
std::vector<FieldSchema::Ptr> vectors;
|
|
for (int i = 0; i < 5; ++i) {
|
|
auto f = std::make_shared<FieldSchema>(
|
|
"f" + std::to_string(i), DataType::VECTOR_FP32, 1024, false);
|
|
fields.emplace_back(f);
|
|
}
|
|
CollectionSchema c6("c6", fields);
|
|
s = c6.validate();
|
|
ASSERT_FALSE(s.ok());
|
|
ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT);
|
|
}
|
|
}
|
|
|
|
#if RABITQ_SUPPORTED
|
|
TEST(FieldSchemaTest, HnswRabitqIndexValidationMetricTypes) {
|
|
// Test supported combinations: FP32 + (L2/IP/COSINE)
|
|
|
|
// FP32 + L2
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok())
|
|
<< "FP32 + L2 should be supported, but got error: " << status.message();
|
|
}
|
|
|
|
// FP32 + IP
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::IP, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok())
|
|
<< "FP32 + IP should be supported, but got error: " << status.message();
|
|
}
|
|
|
|
// FP32 + COSINE
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::COSINE, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok())
|
|
<< "FP32 + COSINE should be supported, but got error: "
|
|
<< status.message();
|
|
}
|
|
|
|
// FP32 + MIPSL2
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::MIPSL2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "FP32 + MIPSL2 should not be supported, but got error: "
|
|
<< status.message();
|
|
}
|
|
}
|
|
|
|
|
|
TEST(FieldSchemaTest, HnswRabitqIndexValidation_Dimension) {
|
|
// Dimension less than 64 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 63, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "Dimension 63 should not be supported with HNSW_RABITQ";
|
|
EXPECT_NE(
|
|
status.message().find("HNSW_RABITQ index only support dimension in"),
|
|
std::string::npos)
|
|
<< "Error message should mention dimension range, got: "
|
|
<< status.message();
|
|
}
|
|
|
|
// Dimension equal to 1 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 1, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "Dimension 1 should not be supported with HNSW_RABITQ";
|
|
}
|
|
|
|
// Dimension greater than 4095 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 4096, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "Dimension 4096 should not be supported with HNSW_RABITQ";
|
|
EXPECT_NE(
|
|
status.message().find("HNSW_RABITQ index only support dimension in"),
|
|
std::string::npos)
|
|
<< "Error message should mention dimension range, got: "
|
|
<< status.message();
|
|
}
|
|
|
|
// Boundary: dimension 64 should be supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 64, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok())
|
|
<< "Dimension 64 should be supported, but got error: "
|
|
<< status.message();
|
|
}
|
|
|
|
// Boundary: dimension 4095 should be supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP32, 4095, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_TRUE(status.ok())
|
|
<< "Dimension 4095 should be supported, but got error: "
|
|
<< status.message();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
TEST(FieldSchemaTest, HnswRabitqIndexValidation_UnsupportedDataTypes) {
|
|
// Test unsupported data types with HNSW_RABITQ index
|
|
|
|
// FP16 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP16, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "FP16 should not be supported with HNSW_RABITQ";
|
|
EXPECT_NE(
|
|
status.message().find("HNSW_RABITQ index only support FP32 data type"),
|
|
std::string::npos)
|
|
<< "Error message should mention FP32 support only, got: "
|
|
<< status.message();
|
|
}
|
|
|
|
// INT8 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_INT8, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "INT8 should not be supported with HNSW_RABITQ";
|
|
EXPECT_NE(
|
|
status.message().find("HNSW_RABITQ index only support FP32 data type"),
|
|
std::string::npos)
|
|
<< "Error message should mention FP32 support only, got: "
|
|
<< status.message();
|
|
}
|
|
|
|
// FP64 is not supported
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::L2, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::VECTOR_FP64, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "FP64 should not be supported with HNSW_RABITQ";
|
|
}
|
|
|
|
// Sparse vector is not supported with HNSW_RABITQ
|
|
{
|
|
auto index_params = std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::IP, 7, 256, 16, 200, 0);
|
|
FieldSchema field("vector_field", DataType::SPARSE_VECTOR_FP32, 128, false,
|
|
index_params);
|
|
auto status = field.validate();
|
|
EXPECT_FALSE(status.ok())
|
|
<< "Sparse vector should not be supported with HNSW_RABITQ";
|
|
EXPECT_NE(
|
|
status.message().find("sparse_vector's index_params only support"),
|
|
std::string::npos)
|
|
<< "Error message should mention sparse vector index support, got: "
|
|
<< status.message();
|
|
}
|
|
}
|