// 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 "db/doc.h" #include "db/index/column/vector_column/vector_column_indexer.h" #include #include #include #include #include "db/index/column/vector_column/vector_column_params.h" #include "tests/test_util.h" #include "zvec/ailego/utility/float_helper.h" #include "zvec/db/doc.h" #include "zvec/db/index_params.h" #if defined(__GNUC__) || defined(__GNUG__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-result" #endif using namespace zvec; std::string print_dense_vector(const void *vector, size_t dim, DataType data_type) { std::stringstream ss; switch (data_type) { case DataType::VECTOR_FP32: { const float *data = reinterpret_cast(vector); for (size_t i = 0; i < dim; ++i) { ss << data[i] << " "; } } break; case DataType::VECTOR_FP16: { const zvec::float16_t *data = reinterpret_cast(vector); for (size_t i = 0; i < dim; ++i) { ss << data[i] << " "; } } break; default: LOG_ERROR("Unsupported data type: %d", static_cast(data_type)); break; } return ss.str(); } TEST(VectorColumnIndexerTest, General) { auto func = [&](const IndexParams::Ptr index_params, const QueryParams::Ptr query_params) { const std::string index_file_path = "test_indexer.index"; constexpr idx_t kDocId = 2345; zvec::test_util::RemoveTestFiles(index_file_path); // 1. create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 4, false, index_params)); ASSERT_TRUE(indexer); // 2. open ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); { // can't use `DenseVector{std::vector{1.0f, 2.0f, 3.0f}.data()}}`, // which will be destroyed immediately auto vector = std::vector{1.0f, 2.0f, 3.0f, 0}; // 3. add data auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); } { auto vector = std::vector{1.0f, 2000.0f, 3.0f, 0}; // 1 * 1 + 2 * 2000 + 3 * 3 = 12006 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } { // add_with_id() won't check duplication, overwrite last one auto vector = std::vector{1.0f, 0, 3.0f, 0}; // 1 * 1 + 2 * 0 + 3 * 3 = 10 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } // 5. fetch auto fetched_data = indexer->Fetch(kDocId); ASSERT_TRUE(fetched_data); const float *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(dense_vector[0], 1.0, 0.1); ASSERT_NEAR(dense_vector[1], 2.0, 0.1); ASSERT_NEAR(dense_vector[2], 3.0, 0.1); ASSERT_NEAR(dense_vector[3], 0, 0.1); // 4. search auto query_vector = std::vector{1.0f, 2.0f, 3.0f, 0}; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams indexer_query_params; indexer_query_params.topk = 10; indexer_query_params.filter = nullptr; indexer_query_params.fetch_vector = true; indexer_query_params.query_params = query_params; auto results = indexer->Search(query, indexer_query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); { int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 2); } { // top1 doc auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId); if (iter->score() > 14) { ASSERT_NEAR(iter->score(), 14.0, 0.1); } // top2 iter->next(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId + 10); ASSERT_NEAR(iter->score(), 10.0, 0.1); } auto vector_index_params = reinterpret_cast(index_params.get()); if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) { ASSERT_TRUE(vector_results->docs().size() == 2); ASSERT_TRUE(vector_results->reverted_vector_list().size() == 2); ASSERT_TRUE(vector_results->reverted_sparse_values_list().empty()); } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::IP), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100), std::make_shared(300)); func(std::make_shared(MetricType::IP), std::make_shared(10)); func(std::make_shared(MetricType::IP, QuantizeType::FP16), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16), std::make_shared(300)); func(std::make_shared(MetricType::IP, 1024, 10, false, QuantizeType::FP16), std::make_shared(10)); func(std::make_shared(MetricType::IP, QuantizeType::INT8), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::INT8), std::make_shared(300)); func(std::make_shared(MetricType::IP, 1024, 10, false, QuantizeType::INT8), std::make_shared(10)); func(std::make_shared(MetricType::IP, QuantizeType::INT4), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::INT4), std::make_shared(300)); } TEST(VectorColumnIndexerTest, DenseDataTypeFP16) { auto func = [&](const IndexParams::Ptr index_params, const QueryParams::Ptr query_params) { const std::string index_file_path = "test_indexer.index"; constexpr idx_t kDocId = 2345; constexpr int dimension = 4; zvec::test_util::RemoveTestFiles(index_file_path); // 1. create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP16, dimension, false, index_params)); ASSERT_TRUE(indexer); // 2. open ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); { // can't use `DenseVector{std::vector{1.0f, 2.0f, 3.0f}.data()}}`, // which will be destroyed immediately auto origin_vector = std::vector{1.0f, 2.0f, 3.0f, 0}; std::vector buffer(dimension); ailego::FloatHelper::ToFP16((float *)origin_vector.data(), dimension, buffer.data()); auto vector = buffer; // 3. add data auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); } { auto origin_vector = std::vector{1.0f, 2000.0f, 3.0f, 0}; std::vector buffer(dimension); ailego::FloatHelper::ToFP16((float *)origin_vector.data(), dimension, buffer.data()); auto vector = buffer; // 1 * 1 + 2 * 2000 + 3 * 3 = 12006 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } { // add_with_id() won't check duplication, overwrite last one auto origin_vector = std::vector{1.0f, 0, 3.0f, 0}; std::vector buffer(dimension); ailego::FloatHelper::ToFP16((float *)origin_vector.data(), dimension, buffer.data()); auto vector = buffer; // 1 * 1 + 2 * 0 + 3 * 3 = 10 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } // 5. fetch { auto fetched_data = indexer->Fetch(kDocId); ASSERT_TRUE(fetched_data); const uint16_t *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[0]), 1.0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[1]), 2.0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[2]), 3.0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[3]), 0, 0.1); } { auto fetched_data = indexer->Fetch(kDocId + 10); ASSERT_TRUE(fetched_data); const uint16_t *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[0]), 1.0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[1]), 0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[2]), 3.0, 0.1); ASSERT_NEAR(ailego::FloatHelper::ToFP32(dense_vector[3]), 0, 0.1); } // 4. search // https://stackoverflow.com/questions/69009389/how-to-get-away-with-using-designated-initializers-in-c17-or-why-is-it-seemi auto origin_query_vector = std::vector{1.0f, 2.0f, 3.0f, 0}; std::vector buffer(dimension); ailego::FloatHelper::ToFP16((float *)origin_query_vector.data(), dimension, buffer.data()); auto query_vector = buffer; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams indexer_query_params; indexer_query_params.topk = 10; indexer_query_params.filter = nullptr; indexer_query_params.fetch_vector = true; indexer_query_params.query_params = query_params; auto results = indexer->Search(query, indexer_query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); { int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 2); } { // top1 doc auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId); if (iter->score() > 14) { ASSERT_NEAR(iter->score(), 14.0, 0.1); } // top2 iter->next(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId + 10); ASSERT_NEAR(iter->score(), 10.0, 0.1); } auto vector_index_params = reinterpret_cast(index_params.get()); if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) { ASSERT_TRUE(vector_results->docs().size() == 2); ASSERT_TRUE(vector_results->reverted_vector_list().size() == 2); ASSERT_TRUE(vector_results->reverted_sparse_values_list().empty()); } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::IP), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100), std::make_shared(300)); } TEST(VectorColumnIndexerTest, DenseDataTypeINT8) { auto func = [&](const IndexParams::Ptr index_params, const QueryParams::Ptr query_params) { const std::string index_file_path = "test_indexer.index"; constexpr idx_t kDocId = 2345; constexpr int dimension = 4; zvec::test_util::RemoveTestFiles(index_file_path); // 1. create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_INT8, dimension, false, index_params)); ASSERT_TRUE(indexer); // 2. open ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); { // can't use `DenseVector{std::vector{1.0f, 2.0f, 3.0f}.data()}}`, // which will be destroyed immediately auto vector = std::vector{1, 2, 3, 0}; // 3. add data auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); } { auto vector = std::vector{1, 200, 3, 0}; // 1 * 1 + 2 * 2000 + 3 * 3 = 12006 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } { // add_with_id() won't check duplication, overwrite last one auto vector = std::vector{1, 0, 3, 0}; // 1 * 1 + 2 * 0 + 3 * 3 = 10 ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}, kDocId + 10) .ok()); } // 5. fetch { auto fetched_data = indexer->Fetch(kDocId); ASSERT_TRUE(fetched_data); const uint8_t *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(dense_vector[0], 1.0, 0.1); ASSERT_NEAR(dense_vector[1], 2.0, 0.1); ASSERT_NEAR(dense_vector[2], 3.0, 0.1); ASSERT_NEAR(dense_vector[3], 0, 0.1); } { auto fetched_data = indexer->Fetch(kDocId + 10); ASSERT_TRUE(fetched_data); const uint8_t *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(dense_vector[0], 1.0, 0.1); ASSERT_NEAR(dense_vector[1], 0, 0.1); ASSERT_NEAR(dense_vector[2], 3.0, 0.1); ASSERT_NEAR(dense_vector[3], 0, 0.1); } // 4. search auto query_vector = std::vector{1, 2, 3, 0}; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams indexer_query_params; indexer_query_params.topk = 10; indexer_query_params.filter = nullptr; indexer_query_params.fetch_vector = true; indexer_query_params.query_params = query_params; auto results = indexer->Search(query, indexer_query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); { int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 2); } { // top1 doc auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId); if (iter->score() > 14) { ASSERT_NEAR(iter->score(), 14.0, 0.1); } // top2 iter->next(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId + 10); ASSERT_NEAR(iter->score(), 10.0, 0.1); } auto vector_index_params = reinterpret_cast(index_params.get()); if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) { ASSERT_TRUE(vector_results->docs().size() == 2); ASSERT_TRUE(vector_results->reverted_vector_list().size() == 2); ASSERT_TRUE(vector_results->reverted_sparse_values_list().empty()); } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::IP), std::make_shared(IndexType::FLAT)); func(std::make_shared(MetricType::IP, 10, 100), std::make_shared(300)); } TEST(VectorColumnIndexerTest, SparseGeneral) { constexpr uint32_t kSparseCount = 3; auto func = [&](const IndexParams::Ptr index_params) { const std::string index_file_path = "test_indexer.index"; constexpr idx_t kDocId = 2345; zvec::test_util::RemoveTestFiles(index_file_path); // create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::SPARSE_VECTOR_FP32, false, index_params)); ASSERT_TRUE(indexer); // open if (auto ret = indexer->Open(vector_column_params::ReadOptions{true, true}); !ret.ok()) { std::cout << ret.message() << std::endl; ASSERT_TRUE(false); } std::vector indices(kSparseCount); std::vector values(kSparseCount); for (uint32_t i = 0; i < kSparseCount; ++i) { indices[i] = i; values[i] = i; } vector_column_params::SparseVector vector{kSparseCount, indices.data(), values.data()}; ASSERT_TRUE( indexer->Insert(vector_column_params::VectorData{vector}, kDocId).ok()); // fetch auto fetched_data = indexer->Fetch(kDocId); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data.value().vector_buffer); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast(fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); ASSERT_FLOAT_EQ(i, fetched_values[i]); } // search auto query = vector_column_params::VectorData{vector_column_params::SparseVector{ kSparseCount, indices.data(), values.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 1); { int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 1); } { auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId); ASSERT_FLOAT_EQ(iter->score(), 5.0); auto vector = iter->vector(); auto sparse_vector = std::get(vector.vector); auto indices = reinterpret_cast(sparse_vector.indices); auto values = reinterpret_cast(sparse_vector.values); ASSERT_EQ(sparse_vector.count, kSparseCount); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, indices[i]); ASSERT_FLOAT_EQ(i, values[i]); } auto vector_index_params = reinterpret_cast(index_params.get()); if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) { ASSERT_TRUE(vector_results->docs().size() == 1); ASSERT_TRUE(vector_results->reverted_sparse_values_list().size() == 1); ASSERT_TRUE(vector_results->reverted_vector_list().empty()); } } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::IP)); func(std::make_shared(MetricType::IP, 10, 100)); func(std::make_shared(MetricType::IP, QuantizeType::FP16)); func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16)); } TEST(VectorColumnIndexerTest, SparseDataTypeFP16) { constexpr uint32_t kSparseCount = 3; auto func = [&](const IndexParams::Ptr index_params) { const std::string index_file_path = "test_indexer.index"; constexpr idx_t kDocId = 2345; zvec::test_util::RemoveTestFiles(index_file_path); // create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::SPARSE_VECTOR_FP16, false, index_params)); ASSERT_TRUE(indexer); // open if (auto ret = indexer->Open(vector_column_params::ReadOptions{true, true}); !ret.ok()) { std::cout << ret.message() << std::endl; ASSERT_TRUE(false); } std::vector indices(kSparseCount); std::vector origin_values(kSparseCount); for (uint32_t i = 0; i < kSparseCount; ++i) { indices[i] = i; origin_values[i] = i; } std::vector buffer1(kSparseCount); ailego::FloatHelper::ToFP16((float *)origin_values.data(), kSparseCount, buffer1.data()); auto values = buffer1; vector_column_params::SparseVector vector{kSparseCount, indices.data(), values.data()}; ASSERT_TRUE( indexer->Insert(vector_column_params::VectorData{vector}, kDocId).ok()); // fetch auto fetched_data = indexer->Fetch(kDocId); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data.value().vector_buffer); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast(fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); ASSERT_FLOAT_EQ(i, ailego::FloatHelper::ToFP32(fetched_values[i])); } // search auto query = vector_column_params::VectorData{vector_column_params::SparseVector{ kSparseCount, indices.data(), values.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 1); { int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 1); } { auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), kDocId); ASSERT_FLOAT_EQ(iter->score(), 5.0); auto vector = iter->vector(); auto sparse_vector = std::get(vector.vector); auto indices = reinterpret_cast(sparse_vector.indices); auto values = reinterpret_cast(sparse_vector.values); ASSERT_EQ(sparse_vector.count, kSparseCount); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, indices[i]); ASSERT_FLOAT_EQ(i, ailego::FloatHelper::ToFP32(values[i])); } auto vector_index_params = reinterpret_cast(index_params.get()); if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) { ASSERT_TRUE(vector_results->docs().size() == 1); ASSERT_TRUE(vector_results->reverted_sparse_values_list().size() == 1); ASSERT_TRUE(vector_results->reverted_vector_list().empty()); } } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::IP)); func(std::make_shared(MetricType::IP, 10, 100)); } TEST(VectorColumnIndexerTest, Merge) { constexpr uint32_t kDimension = 64; const std::string index_name{"test_indexer.index"}; auto del_index_file_func = [](const std::string &file_name) { zvec::test_util::RemoveTestFiles(file_name); }; auto create_indexer_func = [&](const IndexParams::Ptr &index_params, const std::string &index_name) -> VectorColumnIndexer::Ptr { del_index_file_func(index_name); auto indexer = std::make_shared( index_name, FieldSchema("test", DataType::VECTOR_FP32, kDimension, false, index_params)); if (indexer == nullptr || !indexer->Open(vector_column_params::ReadOptions{true, true}).ok()) { return nullptr; } return indexer; }; auto func = [&](const IndexParams::Ptr ¶m1, const IndexParams::Ptr ¶m2, const IndexParams::Ptr ¶m3) { auto indexer1 = create_indexer_func(param1, index_name + "1"); ASSERT_NE(nullptr, indexer1); auto indexer2 = create_indexer_func(param2, index_name + "2"); ASSERT_NE(nullptr, indexer2); std::vector vector(kDimension); vector[1] = 1.0f; vector[2] = 123.0f; auto vector_data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer1->Insert(vector_data, 0).ok()); vector[1] = 2.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 0).ok()); vector[1] = 3.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 1).ok()); { auto fetched_data = indexer1->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(1.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } { auto fetched_data = indexer2->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(2.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } { auto fetched_data = indexer2->Fetch(1); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(3.0f, fetched_vector[1], 0.1); ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]); } { // test reduce auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, nullptr).ok()); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(1.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } { auto fetched_data = indexer3->Fetch(1); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(2.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } indexer3->Close(); del_index_file_func(index_name + "3"); } { // test reduce with filter auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); auto filter = std::make_shared( [](uint64_t key) { return key == 0; }); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter).ok()); // 0.0 -> x ; 1.0 -> 0 ; 1.1 -> 1 ASSERT_TRUE(indexer3->doc_count() == 2); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(2.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } { // search with fetch vector auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer2->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); { ASSERT_TRUE(iter->valid()); auto doc_id = iter->doc_id(); LOG_DEBUG("topk1 pk: %zu", (size_t)doc_id); LOG_DEBUG("topk1 score: %.10f", iter->score()); LOG_DEBUG( "topk1 fetched_vector:%s", print_dense_vector(std::get( iter->vector().vector) .data, 3, DataType::VECTOR_FP32) .c_str()); { auto fetched_vector = vector_results->docs()[0].vector(); LOG_DEBUG( "topk1 fetched_vector - original:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP16) .c_str()); } if (!vector_results->reverted_vector_list().empty()) { auto fetched_vector = vector_results->reverted_vector_list()[0].data(); LOG_DEBUG( "topk1 fetched_vector - reverted:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP32) .c_str()); } // ASSERT_TRUE(iter->score() < 2.01); // ASSERT_TRUE(iter->score() > -0.01); } } indexer3->Close(); del_index_file_func(index_name + "3"); } { // test reduce with filter in parallel auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); auto filter = std::make_shared( [](uint64_t key) { return key == 0; }); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter, {3}).ok()); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); ASSERT_NEAR(2.0f, fetched_vector[1], 0.1); ASSERT_NEAR(123.0f, fetched_vector[2], 0.1); } indexer3->Close(); del_index_file_func(index_name + "3"); } indexer1->Close(); indexer2->Close(); del_index_file_func(index_name + "1"); del_index_file_func(index_name + "2"); }; // same index with different quantize type auto test_different_quantize_type = [&](MetricType metric_type, QuantizeType quantize_type) { LOG_INFO( "Merge test_different_quantize_type(): with metric type %s and " "quantize type %s", MetricTypeCodeBook::AsString(metric_type).c_str(), QuantizeTypeCodeBook::AsString(quantize_type).c_str()); auto param_flat = std::make_shared(metric_type); auto param_flat_fp16 = std::make_shared(metric_type, quantize_type); auto param_hnsw = std::make_shared(metric_type, 10, 100); auto param_hnsw_fp16 = std::make_shared(metric_type, 10, 100, quantize_type); func(param_flat, param_flat, param_hnsw_fp16); std::vector fp32_params = {param_flat, param_hnsw}; std::vector fp16_params = {param_flat_fp16, param_hnsw_fp16}; // can't mix for (auto param_target : fp32_params) { func(param_flat_fp16, param_hnsw_fp16, param_target); // for (auto param1 : fp16_params) { // for (auto param2 : fp16_params) { // func(param1, param2, param_target); // } // } func(param_hnsw, param_flat, param_target); // for (auto param1 : fp32_params) { // for (auto param2 : fp32_params) { // func(param1, param2, param_target); // } // } } for (auto param_target : fp16_params) { func(param_flat_fp16, param_hnsw_fp16, param_target); // for (auto param1 : fp16_params) { // for (auto param2 : fp16_params) { // func(param1, param2, param_target); // } // } func(param_hnsw, param_flat, param_target); // for (auto param1 : fp32_params) { // for (auto param2 : fp32_params) { // func(param1, param2, param_target); // } // } } }; test_different_quantize_type(MetricType::L2, QuantizeType::UNDEFINED); test_different_quantize_type(MetricType::L2, QuantizeType::FP16); test_different_quantize_type(MetricType::IP, QuantizeType::FP16); test_different_quantize_type(MetricType::L2, QuantizeType::INT8); // test_different_quantize_type(MetricType::IP, QuantizeType::INT8); // The quantization error is toooooo large for INT4 =_= // test_different_quantize_type(MetricType::L2, QuantizeType::INT4); // test_different_quantize_type(MetricType::IP, QuantizeType::INT4); // test_different_quantize_type(MetricType::COSINE); } TEST(VectorColumnIndexerTest, SparseMerge) { constexpr uint32_t kSparseCount = 3; constexpr uint32_t kUnitSize = sizeof(float); // VECTOR_FP32 const std::string index_name{"test_indexer.index"}; auto del_index_file_func = [](const std::string &file_name) { zvec::test_util::RemoveTestFiles(file_name); }; auto create_indexer_func = [&](const IndexParams::Ptr &index_params, const std::string &index_name) -> VectorColumnIndexer::Ptr { del_index_file_func(index_name); auto indexer = std::make_shared( index_name, FieldSchema("test", DataType::SPARSE_VECTOR_FP32, false, index_params)); if (indexer == nullptr || !indexer->Open(vector_column_params::ReadOptions{true, true}).ok()) { return nullptr; } return indexer; }; auto func = [&](const IndexParams::Ptr ¶m1, const IndexParams::Ptr ¶m2, const IndexParams::Ptr ¶m3) { auto indexer1 = create_indexer_func(param1, index_name + "1"); ASSERT_NE(nullptr, indexer1); auto indexer2 = create_indexer_func(param2, index_name + "2"); ASSERT_NE(nullptr, indexer2); std::vector indices(kSparseCount); std::vector values(kSparseCount); for (uint32_t i = 0; i < kSparseCount; ++i) { indices[i] = i; values[i] = (float)i; } vector_column_params::SparseVector vector{kSparseCount, indices.data(), values.data()}; auto vector_data = vector_column_params::VectorData{vector}; ASSERT_TRUE(indexer1->Insert(vector_data, 0).ok()); values[1] = 2.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 0).ok()); values[1] = 3.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 1).ok()); { auto fetched_data = indexer1->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast(fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(1.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } { auto fetched_data = indexer2->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast(fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(2.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } { auto fetched_data = indexer2->Fetch(1); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast(fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(3.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } { // test reduce auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, nullptr).ok()); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast( fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(1.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } { auto fetched_data = indexer3->Fetch(1); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast( fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(2.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } indexer3->Close(); del_index_file_func(index_name + "3"); } { // test reduce with filter auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); auto filter = std::make_shared( [](uint64_t key) { return key == 0; }); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter).ok()); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast( fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(2.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } indexer3->Close(); del_index_file_func(index_name + "3"); } { // test reduce with filter in parallel auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); auto filter = std::make_shared( [](uint64_t key) { return key == 0; }); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter, {3}).ok()); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); auto fetched_sparse_vector = std::get( fetched_data->vector_buffer); ASSERT_EQ(kSparseCount, fetched_sparse_vector.indices.size() / sizeof(uint32_t)); ASSERT_EQ(kSparseCount, fetched_sparse_vector.values.size() / kUnitSize); auto fetched_indices = reinterpret_cast( fetched_sparse_vector.indices.data()); auto fetched_values = reinterpret_cast( fetched_sparse_vector.values.data()); for (uint32_t i = 0; i < kSparseCount; ++i) { ASSERT_EQ(i, fetched_indices[i]); } ASSERT_EQ(0.0f, fetched_values[0]); ASSERT_EQ(2.0f, fetched_values[1]); ASSERT_EQ(2.0f, fetched_values[2]); } indexer3->Close(); del_index_file_func(index_name + "3"); } indexer1->Close(); indexer2->Close(); del_index_file_func(index_name + "1"); del_index_file_func(index_name + "2"); }; //=============================================== // Fp32 //=============================================== { auto param_flat = std::make_shared(MetricType::IP); auto param_hnsw = std::make_shared(MetricType::IP, 10, 100); LOG_INFO("SparseMerge: param_flat, param_flat, param_flat"); func(param_flat, param_flat, param_flat); LOG_INFO("SparseMerge: param_hnsw, param_hnsw, param_hnsw"); func(param_hnsw, param_hnsw, param_hnsw); LOG_INFO("SparseMerge: param_flat, param_hnsw, param_hnsw"); func(param_flat, param_hnsw, param_hnsw); LOG_INFO("SparseMerge: param_hnsw, param_flat, param_flat"); func(param_hnsw, param_flat, param_flat); LOG_INFO("SparseMerge: param_flat, param_hnsw, param_flat"); func(param_flat, param_hnsw, param_flat); LOG_INFO("SparseMerge: param_hnsw, param_flat, param_hnsw"); func(param_hnsw, param_flat, param_hnsw); } //=============================================== // Fp16 fp32 //=============================================== { auto param_flat = std::make_shared(MetricType::IP); auto param_hnsw = std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16); LOG_INFO("SparseMerge - fp16: param_flat, param_flat -> param_flat"); func(param_flat, param_flat, param_flat); LOG_INFO("SparseMerge - fp16: param_hnsw, param_hnsw -> param_hnsw"); func(param_hnsw, param_hnsw, param_hnsw); LOG_INFO("SparseMerge - fp16: param_hnsw, param_hnsw -> param_flat"); func(param_hnsw, param_hnsw, param_flat); LOG_INFO("SparseMerge - fp16: param_flat, param_flat -> param_hnsw"); func(param_flat, param_flat, param_hnsw); } } TEST(VectorColumnIndexerTest, BfPks) { auto func = [&](const IndexParams::Ptr index_params) { const std::string index_file_path = "test_indexer.index"; zvec::test_util::RemoveTestFiles(index_file_path); // 1. create indexer auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, index_params)); ASSERT_TRUE(indexer); // 2. open ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); auto vector1 = std::vector{1.0f, 2.0f, 3.0f}; auto vector2 = std::vector{4.0f, 5.0f, 6.0f}; // 3. add data auto data1 = vector_column_params::VectorData{ vector_column_params::DenseVector{vector1.data()}}; ASSERT_TRUE(indexer->Insert(data1, 1).ok()); auto data2 = vector_column_params::VectorData{ vector_column_params::DenseVector{vector2.data()}}; ASSERT_TRUE(indexer->Insert(data2, 2).ok()); { auto bf_pks = std::vector{1}; auto query_vec = std::vector{1.0f, 2.0f, 3.0f}; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vec.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; query_params.bf_pks = {bf_pks}; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 1); auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), 1); auto fetched_vector = std::get(iter->vector().vector); const float *fetched_vector_data = reinterpret_cast(fetched_vector.data); for (int i = 0; i < 3; ++i) { ASSERT_FLOAT_EQ(fetched_vector_data[i], vector1[i]); } } { auto bf_pks = std::vector{1, 2}; auto query_vec = std::vector{1.0f, 2.0f, 3.0f}; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vec.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; query_params.bf_pks = {bf_pks}; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), 1); auto fetched_vector = std::get(iter->vector().vector); const float *fetched_vector_data = reinterpret_cast(fetched_vector.data); for (int i = 0; i < 3; ++i) { ASSERT_FLOAT_EQ(fetched_vector_data[i], vector1[i]); } } { auto bf_pks = std::vector{2}; auto query_vec = std::vector{1.0f, 2.0f, 3.0f}; auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vec.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; query_params.bf_pks = {bf_pks}; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 1); auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); ASSERT_EQ(iter->doc_id(), 2); auto fetched_vector = std::get(iter->vector().vector); const float *fetched_vector_data = reinterpret_cast(fetched_vector.data); for (int i = 0; i < 3; ++i) { ASSERT_FLOAT_EQ(fetched_vector_data[i], vector2[i]); } } indexer->Close(); zvec::test_util::RemoveTestFiles(index_file_path); }; func(std::make_shared(MetricType::COSINE)); func(std::make_shared(MetricType::COSINE, 10, 100)); } using DenseVectorDataBuffer = vector_column_params::DenseVectorBuffer; using SparseVectorBuffer = vector_column_params::SparseVectorBuffer; DenseVectorDataBuffer create_dense_vector(int dim, DataType data_type, int pk, size_t count, float float_offset = 0.1f) { count += 1; switch (data_type) { case DataType::VECTOR_FP32: { std::string ret; ret.resize(dim * sizeof(float)); float *data = reinterpret_cast(ret.data()); for (int i = 0; i < dim; ++i) { data[i] = pk + i + float_offset; } return DenseVectorDataBuffer{std::move(ret)}; } case DataType::VECTOR_FP16: { std::string ret; ret.resize(dim * sizeof(zvec::float16_t)); zvec::float16_t *data = reinterpret_cast(ret.data()); for (int i = 0; i < dim; ++i) { data[i] = pk + i + float_offset; } return DenseVectorDataBuffer{std::move(ret)}; } case DataType::VECTOR_INT8: { std::string ret; ret.resize(dim * sizeof(int8_t)); int8_t *data = reinterpret_cast(ret.data()); for (int i = 0; i < dim; ++i) { data[i] = pk + i; } return DenseVectorDataBuffer{std::move(ret)}; } case DataType::VECTOR_INT16: { std::string ret; ret.resize(dim * sizeof(int16_t)); int16_t *data = reinterpret_cast(ret.data()); for (int i = 0; i < dim; ++i) { data[i] = pk + i; } return DenseVectorDataBuffer{std::move(ret)}; } case DataType::VECTOR_BINARY32: case DataType::VECTOR_BINARY64: { std::string ret; ret.resize(dim / 8); uint8_t *data = reinterpret_cast(ret.data()); for (int i = 0; i < dim; ++i) { data[i / 8] |= ((pk + i) % 2) << (i % 8); } return DenseVectorDataBuffer{std::move(ret)}; } default: LOG_ERROR("Unsupported data type: %d", static_cast(data_type)); return DenseVectorDataBuffer{}; } } SparseVectorBuffer create_sparse_vector(int dim, DataType data_type, int pk, float float_offset = 0.1f) { SparseVectorBuffer ret; switch (data_type) { case DataType::SPARSE_VECTOR_FP32: { std::vector values(dim); for (int i = 0; i < dim; ++i) { values[i] = pk * 100 + i + float_offset; } ret.values = std::string(reinterpret_cast(values.data()), values.size() * sizeof(float)); } break; case DataType::SPARSE_VECTOR_FP16: { std::vector values(dim); for (int i = 0; i < dim; ++i) { values[i] = pk * 100 + i + float_offset; } ret.values = std::string(reinterpret_cast(values.data()), values.size() * sizeof(zvec::float16_t)); } break; default: LOG_ERROR("Unsupported data type: %d", static_cast(data_type)); return SparseVectorBuffer{}; } std::vector indices(dim); for (int i = 0; i < dim; ++i) { indices[i] = i; } ret.indices = std::string(reinterpret_cast(indices.data()), indices.size() * sizeof(uint32_t)); return ret; } bool compare_dense_vector(const DenseVectorDataBuffer &lhs, const void *rhs, DataType data_type) { switch (data_type) { case DataType::VECTOR_FP32: { size_t dim = lhs.data.size() / sizeof(float); auto rhs_data = reinterpret_cast(rhs); auto lhs_data = reinterpret_cast(lhs.data.data()); for (size_t i = 0; i < dim; ++i) { if (std::abs(lhs_data[i] - rhs_data[i]) > 1) { // reformer LOG_ERROR("lhs_data[%zu] = %f, rhs_data[%zu] = %f", i, (float)lhs_data[i], i, (float)rhs_data[i]); return false; } } return true; }; case DataType::VECTOR_FP16: { size_t dim = lhs.data.size() / sizeof(zvec::float16_t); auto rhs_data = reinterpret_cast(rhs); auto lhs_data = reinterpret_cast(lhs.data.data()); for (size_t i = 0; i < dim; ++i) { if (std::abs(lhs_data[i] - rhs_data[i]) > 1e-2) { // reformer LOG_ERROR("lhs_data[%zu] = %f, rhs_data[%zu] = %f", i, (float)lhs_data[i], i, (float)rhs_data[i]); return false; } } return true; } default: return memcmp(lhs.data.data(), rhs, lhs.data.size()) == 0; } } bool compare_sparse_vector(const SparseVectorBuffer &lhs, const void *rhs_indices, const void *rhs_values, DataType data_type) { if (memcmp(lhs.indices.data(), rhs_indices, lhs.indices.size()) != 0) { return false; } size_t dim = lhs.indices.size() / sizeof(uint32_t); switch (data_type) { case DataType::SPARSE_VECTOR_FP32: { auto rhs_values_data = reinterpret_cast(rhs_values); auto lhs_values_data = reinterpret_cast(lhs.values.data()); for (size_t i = 0; i < dim; ++i) { if (std::abs(lhs_values_data[i] - rhs_values_data[i]) > 1e-2) { // reformer LOG_ERROR("lhs_values_data[%zu] = %f, rhs_values_data[%zu] = %f", i, (float)lhs_values_data[i], i, (float)rhs_values_data[i]); return false; } } return true; } case DataType::SPARSE_VECTOR_FP16: { auto rhs_values_data = reinterpret_cast(rhs_values); auto lhs_values_data = reinterpret_cast(lhs.values.data()); for (size_t i = 0; i < dim; ++i) { if (std::abs(lhs_values_data[i] - rhs_values_data[i]) > 1e-2) { // reformer LOG_ERROR("lhs_values_data[%zu] = %f, rhs_values_data[%zu] = %f", i, (float)lhs_values_data[i], i, (float)rhs_values_data[i]); return false; } } return true; } default: return memcmp(lhs.values.data(), rhs_values, lhs.values.size()) == 0; } } TEST(VectorColumnIndexerTest, CosineGeneral) { const std::string index_file_path = "test_indexer.index"; const int kDim = 20; const int kCount = 20; // can't set too large, or the qunatization error // will be too large due to float's precision const uint32_t kTopk = 10; zvec::test_util::RemoveTestFiles(index_file_path); auto func = [&](const IndexParams::Ptr index_params, DataType data_type) { zvec::test_util::RemoveTestFiles(index_file_path); auto indexer = std::make_shared( index_file_path, FieldSchema("test", data_type, kDim, false, index_params)); ASSERT_TRUE(indexer); if (auto ret = indexer->Open(vector_column_params::ReadOptions{true, true}); !ret.ok()) { LOG_ERROR("Failed to open indexer: %s", ret.message().c_str()); return; } // insert for (int i = 0; i < kCount; ++i) { auto buffer = create_dense_vector(kDim, data_type, i, kCount, 0.1f); // print_dense_vector(buffer.data.data(), kDim, data_type); auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{buffer.data.data()}}; ASSERT_TRUE(indexer->Insert(data, i).ok()); } // fetch for (int i = 0; i < kCount; ++i) { auto fetched_data = indexer->Fetch(i); ASSERT_TRUE(fetched_data); ASSERT_TRUE(compare_dense_vector( create_dense_vector(kDim, data_type, i, kCount, 0.1f), std::get(fetched_data->vector_buffer) .data.data(), data_type)); } // query for (int i = 0; i < kCount; ++i) { auto buffer = create_dense_vector(kDim, data_type, i, kCount, 0.3f); auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{buffer.data.data()}}; auto _t = std::make_shared(100); _t->set_is_linear(true); vector_column_params::QueryParams query_params; query_params.topk = kTopk; query_params.filter = nullptr; query_params.fetch_vector = true; query_params.query_params = _t; auto results = indexer->Search(data, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), kTopk); auto iter = vector_results->create_iterator(); LOG_INFO("===query pk: %d", i); LOG_INFO("query_vector:%s", print_dense_vector(buffer.data.data(), kDim, data_type).c_str()); { // topk1 ASSERT_TRUE(iter->valid()); LOG_INFO("topk1 pk:%zu", (size_t)iter->doc_id()); LOG_INFO("topk1 score:%.10f", iter->score()); if (!(iter->score() > -0.01 && iter->score() < 2.01)) { ASSERT_TRUE(iter->score() < 2.01); } ASSERT_TRUE(iter->score() < 2.01); ASSERT_TRUE(iter->score() > -0.01); auto fetched_vector = std::get(iter->vector().vector); LOG_INFO( "topk1 fetched_vector:%s", print_dense_vector(fetched_vector.data, kDim, data_type).c_str()); // ASSERT_EQ(iter->doc_id(), i); ASSERT_TRUE(compare_dense_vector( create_dense_vector(kDim, data_type, iter->doc_id(), kCount, 0.1f), fetched_vector.data, data_type)); } } indexer->Destroy(); }; LOG_INFO("Test FlatIndexParams(MetricType::COSINE), VECTOR_FP32"); func(std::make_shared(MetricType::COSINE), DataType::VECTOR_FP32); LOG_INFO("Test HnswIndexParams(MetricType::COSINE), VECTOR_FP32"); func(std::make_shared(MetricType::COSINE, 10, 100), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::FP16"); func( std::make_shared(MetricType::COSINE, QuantizeType::FP16), DataType::VECTOR_FP32); LOG_INFO( "Test HnswIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::FP16"); func(std::make_shared(MetricType::COSINE, 10, 100, QuantizeType::FP16), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::INT8"); func( std::make_shared(MetricType::COSINE, QuantizeType::INT8), DataType::VECTOR_FP32); LOG_INFO( "Test HnswIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::INT8"); func(std::make_shared(MetricType::COSINE, 10, 100, QuantizeType::INT8), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::INT4"); func( std::make_shared(MetricType::COSINE, QuantizeType::INT4), DataType::VECTOR_FP32); LOG_INFO( "Test HnswIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::INT4"); func(std::make_shared(MetricType::COSINE, 10, 100, QuantizeType::INT4), DataType::VECTOR_FP32); // cosine doesn't support int8/int4 datatype, but support int8/int4 quantizer // LOG_INFO("Test FlatIndexParams(MetricType::COSINE), VECTOR_FP16"); // func( // std::make_shared(MetricType::COSINE, // QuantizeType::FP16), DataType::VECTOR_FP16); // LOG_INFO("Test HnswIndexParams(MetricType::COSINE), VECTOR_FP16"); // func(std::make_shared(MetricType::COSINE, 10, 100, // QuantizeType::FP16), // DataType::VECTOR_FP16); } TEST(VectorColumnIndexerTest, Score) { const std::string index_file_path = "test_indexer.index"; const uint32_t kTopk = 10; constexpr idx_t kDocId1 = 2345; constexpr idx_t kDocId2 = 5432; auto vector1 = std::vector{3.0f, 4.0f, 5.0f}; auto vector2 = std::vector{1.0f, 20.0f, 3.0f}; auto vector_id_map = std::unordered_map>{ {kDocId1, vector1}, {kDocId2, vector2}, }; auto sparse_indices = std::vector{0, 1, 2}; auto query_vector = std::vector{1.0f, 2.0f, 3.0f}; zvec::test_util::RemoveTestFiles(index_file_path); auto check_score = [&](VectorIndexResults *vector_results, MetricType metric_type) { ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); auto inner_produce_score_func = [&](const std::vector &v1, const std::vector &v2) { return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; }; auto cosine_score_func = [&](const std::vector &v1, const std::vector &v2) { return 1 - inner_produce_score_func(v1, v2) / (std::sqrt(inner_produce_score_func(v1, v1)) * std::sqrt(inner_produce_score_func(v2, v2))); }; // SquaredEuclidean auto l2_score_func = [&](const std::vector &v1, const std::vector &v2) { assert(v1.size() == 3); assert(v2.size() == 3); float ret = 0.0f; for (size_t i = 0; i < v1.size(); ++i) { ret += (v1[i] - v2[i]) * (v1[i] - v2[i]); } return ret; }; std::function &, const std::vector &)> score_func; switch (metric_type) { case MetricType::IP: score_func = inner_produce_score_func; break; case MetricType::COSINE: score_func = cosine_score_func; break; case MetricType::L2: score_func = l2_score_func; break; default: ASSERT_TRUE(false); } auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); printf("iter->score() top1: %f\n", iter->score()); printf("score_func(vector_id_map[iter->doc_id()], query_vector): %f\n", score_func(vector_id_map[iter->doc_id()], query_vector)); ASSERT_TRUE( std::abs(iter->score() - score_func(vector_id_map[iter->doc_id()], query_vector)) < 1e-2); iter->next(); ASSERT_TRUE(iter->valid()); printf("iter->score() top2: %f\n", iter->score()); printf("score_func(vector_id_map[iter->doc_id()], query_vector): %f\n", score_func(vector_id_map[iter->doc_id()], query_vector)); ASSERT_TRUE( std::abs(iter->score() - score_func(vector_id_map[iter->doc_id()], query_vector)) < 1e-2); }; auto dense_func = [&](const std::shared_ptr &index_params) { auto metric_type = index_params->metric_type(); auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, index_params)); ASSERT_TRUE(indexer); if (auto ret = indexer->Open(vector_column_params::ReadOptions{true, true}); !ret.ok()) { LOG_ERROR("Failed to open indexer: %s", ret.message().c_str()); ASSERT_TRUE(false); } ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector1.data()}}, kDocId1) .ok()); ASSERT_TRUE(indexer ->Insert( vector_column_params::VectorData{ vector_column_params::DenseVector{vector2.data()}}, kDocId2) .ok()); auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams query_params; query_params.topk = kTopk; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); check_score(dynamic_cast(results.value().get()), metric_type); indexer->Destroy(); }; auto sparse_func = [&](const std::shared_ptr &index_params) { auto metric_type = index_params->metric_type(); auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::SPARSE_VECTOR_FP32, false, index_params)); ASSERT_TRUE(indexer); if (auto ret = indexer->Open(vector_column_params::ReadOptions{true, true}); !ret.ok()) { LOG_ERROR("Failed to open indexer: %s", ret.message().c_str()); ASSERT_TRUE(false); } ASSERT_TRUE( indexer ->Insert( vector_column_params::VectorData{ vector_column_params::SparseVector{ 3, reinterpret_cast(sparse_indices.data()), vector1.data()}}, kDocId1) .ok()); ASSERT_TRUE( indexer ->Insert( vector_column_params::VectorData{ vector_column_params::SparseVector{ 3, reinterpret_cast(sparse_indices.data()), vector2.data()}}, kDocId2) .ok()); auto query = vector_column_params::VectorData{vector_column_params::SparseVector{ 3, reinterpret_cast(sparse_indices.data()), query_vector.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); check_score(dynamic_cast(results.value().get()), metric_type); indexer->Destroy(); }; LOG_INFO("Test DenseVector, MetricType::IP"); dense_func(std::make_shared(MetricType::IP)); dense_func(std::make_shared(MetricType::IP, 10, 100)); LOG_INFO("Test DenseVector, MetricType::IP, QuantizeType::FP16"); dense_func( std::make_shared(MetricType::IP, QuantizeType::FP16)); dense_func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16)); LOG_INFO("Test DenseVector, MetricType::COSINE"); dense_func(std::make_shared(MetricType::COSINE)); dense_func(std::make_shared(MetricType::COSINE, 10, 100)); LOG_INFO("Test DenseVector, MetricType::COSINE, QuantizeType::FP16"); dense_func(std::make_shared(MetricType::COSINE, QuantizeType::FP16)); dense_func(std::make_shared(MetricType::COSINE, 10, 100, QuantizeType::FP16)); LOG_INFO("Test DenseVector, MetricType::L2"); dense_func(std::make_shared(MetricType::L2)); dense_func(std::make_shared(MetricType::L2, 10, 100)); LOG_INFO("Test DenseVector, MetricType::L2, QuantizeType::FP16"); dense_func( std::make_shared(MetricType::L2, QuantizeType::FP16)); dense_func(std::make_shared(MetricType::L2, 10, 100, QuantizeType::FP16)); LOG_INFO("Test SparseVector, MetricType::IP"); sparse_func(std::make_shared(MetricType::IP)); sparse_func(std::make_shared(MetricType::IP, 10, 100)); LOG_INFO("Test SparseVector, MetricType::IP, QuantizeType::FP16"); sparse_func( std::make_shared(MetricType::IP, QuantizeType::FP16)); sparse_func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16)); } TEST(VectorColumnIndexerTest, Failure) { const std::string index_file_path = "test_indexer_failure.index"; constexpr idx_t kDocId = 1234; auto vector = std::vector{1.0f, 2.0f, 3.0f}; zvec::test_util::RemoveTestFiles(index_file_path); // Test case 1: Operations on unopened indexer { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); // Test Flush on unopened indexer auto flush_result = indexer->Flush(); ASSERT_FALSE(flush_result.ok()); ASSERT_EQ(flush_result.message(), "Index not opened"); // Test Close on unopened indexer auto close_result = indexer->Close(); ASSERT_FALSE(close_result.ok()); ASSERT_EQ(close_result.message(), "Index not opened"); // Test Destroy on unopened indexer auto destroy_result = indexer->Destroy(); ASSERT_FALSE(destroy_result.ok()); ASSERT_EQ(destroy_result.message(), "Index not opened"); // Test Insert on unopened indexer auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; auto insert_result = indexer->Insert(data, kDocId); ASSERT_FALSE(insert_result.ok()); ASSERT_EQ(insert_result.message(), "Index not opened"); // Test Fetch on unopened indexer auto fetch_result = indexer->Fetch(kDocId); ASSERT_FALSE(fetch_result.has_value()); ASSERT_EQ(fetch_result.error().message(), "Index not opened"); // Test Search on unopened indexer auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = false; auto search_result = indexer->Search(query, query_params); ASSERT_FALSE(search_result.has_value()); ASSERT_EQ(search_result.error().message(), "Index not opened"); // Test Merge on unopened indexer auto merge_result = indexer->Merge({}, nullptr); ASSERT_FALSE(merge_result.ok()); ASSERT_EQ(merge_result.message(), "Index not opened"); } // Test case 2: Unsupported engine name { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP)), "unsupported_engine"); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result.ok()); ASSERT_EQ(open_result.message(), "Engine name not supported"); } // Test case 3: Invalid field schema (nullptr index_params) { FieldSchema invalid_schema("test", DataType::VECTOR_FP32, 3, false, nullptr); auto indexer = std::make_shared(index_file_path, invalid_schema); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result.ok()); ASSERT_EQ(open_result.message(), "field_schema.index_params nullptr"); } // Test case 4: Unsupported data type in engine helper { // Create a mock index params with unsupported data type // We'll use a data type that's not supported by convert_to_engine_data_type FieldSchema unsupported_schema( "test", DataType::UNDEFINED, 3, false, std::make_shared(MetricType::IP)); auto indexer = std::make_shared(index_file_path, unsupported_schema); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result.ok()); ASSERT_EQ(open_result.message(), "failed to build index param: unsupported data type"); } // Test case 5: Unsupported metric type in engine helper { FieldSchema unsupported_schema( "test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::UNDEFINED)); auto indexer = std::make_shared(index_file_path, unsupported_schema); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result.ok()); ASSERT_EQ(open_result.message(), "failed to build index param: unsupported metric type"); } // Test case 6: Unsupported quantize type in engine helper { auto index_params = std::make_shared(MetricType::IP); index_params->set_quantize_type(static_cast(999)); FieldSchema unsupported_schema("test", DataType::VECTOR_FP32, 3, false, index_params); auto indexer = std::make_shared(index_file_path, unsupported_schema); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result.ok()); ASSERT_EQ(open_result.message(), "failed to build index param: unsupported quantize type"); } // // Test case 7: Unsupported index type in engine helper // { // // Create a custom index params with unsupported index type // class UnsupportedIndexTypeParams : public FlatIndexParams { // public: // UnsupportedIndexTypeParams() : FlatIndexParams(MetricType::IP) {} // void mock() { // type_ = static_cast(999); // } // }; // auto index_params = std::make_shared(); // index_params->mock(); // FieldSchema unsupported_schema("test", DataType::VECTOR_FP32, 3, false, // index_params); // auto indexer = std::make_shared(index_file_path, // unsupported_schema); // ASSERT_TRUE(indexer); // // auto open_result = // indexer->Open(vector_column_params::ReadOptions{true, true}); // ASSERT_FALSE(open_result.ok()); // ASSERT_EQ(open_result.message(), "not supported"); // } // Test case 8: bf_pks size > 1 error { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); // Insert some data first auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); // Test search with bf_pks size > 1 auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; auto bf_pks1 = std::vector{1, 2}; auto bf_pks2 = std::vector{3, 4}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = false; query_params.bf_pks = {bf_pks1, bf_pks2}; auto search_result = indexer->Search(query, query_params); ASSERT_FALSE(search_result.has_value()); ASSERT_EQ(search_result.error().message(), "bf_pks size > 1 is not supported"); indexer->Destroy(); } // Test case 9: Invalid field schema for query param conversion { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, nullptr)); ASSERT_TRUE(indexer); ASSERT_FALSE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); } // Test case 10: use_mmap = false { zvec::ailego::MemoryLimitPool::get_instance().init(10 * 1024UL * 1024UL); auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true, false}) .ok()); // Insert some data first auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); ASSERT_TRUE(indexer->Flush().ok()); ASSERT_TRUE(indexer->Close().ok()); { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); auto open_result = indexer->Open(vector_column_params::ReadOptions{false, false, true}); ASSERT_TRUE(open_result.ok()); indexer->Destroy(); } } // Test case 11: Index already opened error { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); // First open should succeed auto open_result1 = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_TRUE(open_result1.ok()); // Second open should fail auto open_result2 = indexer->Open(vector_column_params::ReadOptions{true, true}); ASSERT_FALSE(open_result2.ok()); ASSERT_EQ(open_result2.message(), "Index already opened"); indexer->Destroy(); } // Test case 12: Test doc_count() on unopened indexer { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); // doc_count() should return -1 for unopened indexer ASSERT_EQ(indexer->doc_count(), static_cast(-1)); } // Test case 13: Test Merge with empty indexers list { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); // Merge with empty indexers list should succeed auto merge_result = indexer->Merge({}, nullptr); ASSERT_TRUE(merge_result.ok()); indexer->Destroy(); } // Test case 14: Test Merge with same index file path (should be skipped) { auto indexer1 = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer1); ASSERT_TRUE( indexer1->Open(vector_column_params::ReadOptions{true, true}).ok()); // Insert some data auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer1->Insert(data, kDocId).ok()); // Merge with itself (same index file path) should succeed (skipped) auto merge_result = indexer1->Merge({indexer1}, nullptr); ASSERT_TRUE(merge_result.ok()); indexer1->Destroy(); } // Test case 15: Test Fetch with non-existent doc_id { auto indexer = std::make_shared( index_file_path, FieldSchema("test", DataType::VECTOR_FP32, 3, false, std::make_shared(MetricType::IP))); ASSERT_TRUE(indexer); ASSERT_TRUE( indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); // Fetch non-existent doc_id should fail auto fetch_result = indexer->Fetch(99999); ASSERT_FALSE(fetch_result.has_value()); ASSERT_EQ(fetch_result.error().message(), "Failed to fetch vector from index"); indexer->Destroy(); } // // Test case 16: Test Search with invalid query params (unsupported index // // type) // { // // Create a custom index params with unsupported index type for query // class UnsupportedQueryIndexParams : public IndexParams { // public: // IndexType type() const override { // return static_cast(999); // } // MetricType metric_type() const override { // return MetricType::IP; // } // QuantizeType quantize_type() const override { // return QuantizeType::UNDEFINED; // } // IndexParams::Ptr clone() const override { // return std::make_shared(); // } // }; // // FieldSchema unsupported_schema( // "test", DataType::VECTOR_FP32, 3, false, // std::make_shared()); // auto indexer = std::make_shared(index_file_path, // unsupported_schema); // ASSERT_TRUE(indexer); // // ASSERT_TRUE( // indexer->Open(vector_column_params::ReadOptions{true, true}).ok()); // // // Insert some data first // auto data = vector_column_params::VectorData{ // vector_column_params::DenseVector{vector.data()}}; // ASSERT_TRUE(indexer->Insert(data, kDocId).ok()); // // // Test search with unsupported index type // auto query = vector_column_params::VectorData{ // vector_column_params::DenseVector{vector.data()}}; // vector_column_params::QueryParams query_params; // query_params.topk = 10; // query_params.filter = nullptr; // query_params.fetch_vector = false; // // auto search_result = indexer->Search(query, query_params); // ASSERT_FALSE(search_result.has_value()); // ASSERT_EQ(search_result.error().message(), "not supported"); // // indexer->Close(); // } zvec::test_util::RemoveTestFiles(index_file_path); } TEST(VectorColumnIndexerTest, CosineMerge) { constexpr uint32_t kDimension = 64; const std::string index_name{"test_indexer.index"}; auto del_index_file_func = [](const std::string &file_name) { zvec::test_util::RemoveTestFiles(file_name); }; auto create_indexer_func = [&](const IndexParams::Ptr &index_params, const std::string &index_name) -> VectorColumnIndexer::Ptr { del_index_file_func(index_name); auto indexer = std::make_shared( index_name, FieldSchema("test", DataType::VECTOR_FP32, kDimension, false, index_params)); if (indexer == nullptr || !indexer->Open(vector_column_params::ReadOptions{true, true}).ok()) { return nullptr; } return indexer; }; auto func = [&](const IndexParams::Ptr ¶m1, const IndexParams::Ptr ¶m2, const IndexParams::Ptr ¶m3) { auto indexer1 = create_indexer_func(param1, index_name + "1"); ASSERT_NE(nullptr, indexer1); auto indexer2 = create_indexer_func(param2, index_name + "2"); ASSERT_NE(nullptr, indexer2); std::vector vector(kDimension); vector[1] = 1.0f; vector[2] = 123.0f; auto vector_data = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; ASSERT_TRUE(indexer1->Insert(vector_data, 0).ok()); vector[1] = 2.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 0).ok()); vector[1] = 3.0f; ASSERT_TRUE(indexer2->Insert(vector_data, 1).ok()); { auto fetched_data = indexer1->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); LOG_INFO( "indexer1 fetched_vector doc_id:0:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP32).c_str()); ASSERT_TRUE(fetched_vector[1] - 1.0f < 1e-2); ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); } { auto fetched_data = indexer2->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); LOG_INFO( "indexer2 fetched_vector doc_id:0:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP32).c_str()); ASSERT_TRUE(fetched_vector[1] - 2.0f < 1e-2); ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); } { auto fetched_data = indexer2->Fetch(1); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); LOG_INFO( "indexer2 fetched_vector doc_id:1:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP32).c_str()); ASSERT_TRUE(fetched_vector[1] - 3.0f < 1e-2); ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); } // { // test reduce // auto indexer3 = create_indexer_func(param3, index_name + "3"); // ASSERT_NE(nullptr, indexer3); // ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, nullptr).ok()); // { // auto fetched_data = indexer3->Fetch(0); // ASSERT_TRUE(fetched_data.has_value()); // const float *fetched_vector = reinterpret_cast( // std::get( // fetched_data->vector_buffer) // .data.data()); // LOG_INFO("indexer3 fetched_vector doc_id:0:%s", // print_dense_vector(fetched_vector, 3, // DataType::VECTOR_FP32) // .c_str()); // ASSERT_TRUE(fetched_vector[1] - 1.0f < 1e-2); // ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); // } // { // auto fetched_data = indexer3->Fetch(1); // ASSERT_TRUE(fetched_data.has_value()); // const float *fetched_vector = reinterpret_cast( // std::get( // fetched_data->vector_buffer) // .data.data()); // LOG_INFO("indexer3 fetched_vector doc_id:1:%s", // print_dense_vector(fetched_vector, 3, // DataType::VECTOR_FP32) // .c_str()); // ASSERT_TRUE(fetched_vector[1] - 2.0f < 1e-2); // ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); // } // indexer3->Close(); // del_index_file_func(index_name + "3"); // } // { // test reduce with filter auto indexer3 = create_indexer_func(param3, index_name + "3"); ASSERT_NE(nullptr, indexer3); auto filter = std::make_shared( [](uint64_t key) { return key == 0; }); ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter).ok()); // 0.0 -> x ; 1.0 -> 0 ; 1.1 -> 1 ASSERT_TRUE(indexer3->doc_count() == 2); { auto fetched_data = indexer3->Fetch(0); ASSERT_TRUE(fetched_data.has_value()); const float *fetched_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); LOG_INFO("indexer3 fetched_vector doc_id:0:%s", print_dense_vector(fetched_vector, 3, DataType::VECTOR_FP32) .c_str()); ASSERT_TRUE(fetched_vector[1] - 2.0f < 1e-2); ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); } { vector[1] = 3.0f; // search with fetch vector auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{vector.data()}}; vector_column_params::QueryParams query_params; query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = true; auto results = indexer2->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 2); auto iter = vector_results->create_iterator(); ASSERT_TRUE(iter->valid()); { int doc_idx = 0; auto query_results_doc = vector_results->docs()[doc_idx]; LOG_INFO("topk%d pk: %zu", doc_idx, (size_t)query_results_doc.key()); LOG_INFO("topk%d score: %.10f", doc_idx, query_results_doc.score()); LOG_INFO("topk%d fetched_vector - reverted:%s", doc_idx, print_dense_vector( vector_results->reverted_vector_list()[doc_idx].data(), kDimension, DataType::VECTOR_FP32) .c_str()); LOG_INFO("topk%d fetched_vector - original:%s", doc_idx, print_dense_vector(query_results_doc.vector(), kDimension, DataType::VECTOR_FP16) .c_str()); ASSERT_TRUE(query_results_doc.score() < 2.01); ASSERT_TRUE(query_results_doc.score() > -0.01); } { int doc_idx = 1; auto query_results_doc = vector_results->docs()[doc_idx]; LOG_INFO("topk%d pk: %zu", doc_idx, (size_t)query_results_doc.key()); LOG_INFO("topk%d score: %.10f", doc_idx, query_results_doc.score()); LOG_INFO("topk%d fetched_vector - reverted:%s", doc_idx, print_dense_vector( vector_results->reverted_vector_list()[doc_idx].data(), kDimension, DataType::VECTOR_FP32) .c_str()); LOG_INFO("topk%d fetched_vector - original:%s", doc_idx, print_dense_vector(query_results_doc.vector(), kDimension, DataType::VECTOR_FP16) .c_str()); ASSERT_TRUE(query_results_doc.score() < 2.01); ASSERT_TRUE(query_results_doc.score() > -0.01); } // ASSERT_TRUE(vector_results->docs()[0].key() == 1); } indexer3->Close(); del_index_file_func(index_name + "3"); } // // { // test reduce with filter in parallel // auto indexer3 = create_indexer_func(param3, index_name + "3"); // ASSERT_NE(nullptr, indexer3); // auto filter = std::make_shared( // [](uint64_t key) { return key == 0; }); // ASSERT_TRUE(indexer3->Merge({indexer1, indexer2}, filter, {3}).ok()); // // { // auto fetched_data = indexer3->Fetch(0); // ASSERT_TRUE(fetched_data.has_value()); // const float *fetched_vector = reinterpret_cast( // std::get( // fetched_data->vector_buffer) // .data.data()); // LOG_INFO("indexer3 fetched_vector doc_id:0:%s", // print_dense_vector(fetched_vector, 3, // DataType::VECTOR_FP32) // .c_str()); // ASSERT_TRUE(fetched_vector[1] - 2.0f < 1e-2); // ASSERT_TRUE(fetched_vector[2] - 123.0f < 1); // } // indexer3->Close(); // del_index_file_func(index_name + "3"); // } indexer1->Close(); indexer2->Close(); del_index_file_func(index_name + "1"); del_index_file_func(index_name + "2"); }; // same index with different quantize type { LOG_INFO("Merge: same index - FlatIndex with different quantize type"); auto metric_type = MetricType::COSINE; auto param_flat = std::make_shared(metric_type); auto param_flat_fp16 = std::make_shared(metric_type, QuantizeType::FP16); auto param_hnsw = std::make_shared(metric_type, 10, 100); auto param_hnsw_fp16 = std::make_shared( metric_type, 10, 100, QuantizeType::FP16); // func(param, param_fp16, param_fp16); // func(param, param_fp16, param); // func(param_fp16, param, param_fp16); // func(param_fp16, param, param); // func(param_fp16, param_fp16, param_fp16); func(param_hnsw_fp16, param_flat_fp16, param_flat_fp16); } } TEST(VectorColumnIndexerTest, Refiner) { const std::string kIndexFilePath = "test_indexer.index"; const int kDim = 20; const int kCount = 20; // can't set too large, or the qunatization error // will be too large due to float's precision const uint32_t kTopk = 10; auto del_index_file_func = [](const std::string &file_name) { zvec::test_util::RemoveTestFiles(file_name); }; auto create_indexer_func = [&](const IndexParams::Ptr &index_params, const std::string &index_file_path, DataType data_type) -> VectorColumnIndexer::Ptr { del_index_file_func(index_file_path); auto indexer = std::make_shared( index_file_path, FieldSchema("test", data_type, kDim, false, index_params)); if (indexer == nullptr || !indexer->Open(vector_column_params::ReadOptions{true, true}).ok()) { return nullptr; } return indexer; }; auto func = [&](const IndexParams::Ptr &index_params, const IndexParams::Ptr &reference_index_params, DataType data_type) { auto indexer = create_indexer_func(index_params, kIndexFilePath, data_type); if (indexer == nullptr) { return; } auto reference_indexer = create_indexer_func( reference_index_params, kIndexFilePath + "_reference", data_type); if (reference_indexer == nullptr) { return; } // insert for (int i = 0; i < kCount; ++i) { auto buffer = create_dense_vector(kDim, data_type, i, kCount, 0.1f); // print_dense_vector(buffer.data.data(), kDim, data_type); auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{buffer.data.data()}}; ASSERT_TRUE(indexer->Insert(data, i).ok()); ASSERT_TRUE(reference_indexer->Insert(data, i).ok()); } // query for (int i = 0; i < kCount; ++i) { auto buffer = create_dense_vector(kDim, data_type, i, kCount, 0.3f); auto data = vector_column_params::VectorData{ vector_column_params::DenseVector{buffer.data.data()}}; ; vector_column_params::QueryParams query_params; query_params.topk = kTopk; query_params.filter = nullptr; query_params.fetch_vector = true; query_params.query_params = std::make_shared(100); query_params.refiner_param = std::make_shared( vector_column_params::RefinerParam{10, reference_indexer}); auto results = indexer->Search(data, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), kTopk); auto iter = vector_results->create_iterator(); LOG_INFO("===query pk: %d", i); LOG_INFO("query_vector:%s", print_dense_vector(buffer.data.data(), kDim, data_type).c_str()); } indexer->Destroy(); }; LOG_INFO( "Test FlatIndexParams(MetricType::IP), VECTOR_FP32, " "QuantizeType::FP16"); func(std::make_shared(MetricType::IP, 10, 100, QuantizeType::FP16), std::make_shared(MetricType::IP), DataType::VECTOR_FP32); func(std::make_shared(MetricType::IP, QuantizeType::FP16), std::make_shared(MetricType::IP), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::MIPSL2), VECTOR_FP32, " "QuantizeType::FP16"); func(std::make_shared(MetricType::MIPSL2, 10, 100, QuantizeType::FP16), std::make_shared(MetricType::IP), DataType::VECTOR_FP32); func( std::make_shared(MetricType::MIPSL2, QuantizeType::FP16), std::make_shared(MetricType::IP), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::COSINE), VECTOR_FP32, " "QuantizeType::FP16"); func( std::make_shared(MetricType::COSINE, QuantizeType::FP16), std::make_shared(MetricType::COSINE), DataType::VECTOR_FP32); LOG_INFO( "Test FlatIndexParams(MetricType::L2), VECTOR_FP32, " "QuantizeType::Int8"); func(std::make_shared(MetricType::L2, QuantizeType::INT8), std::make_shared(MetricType::L2), DataType::VECTOR_FP32); } #if defined(__GNUC__) || defined(__GNUG__) #pragma GCC diagnostic pop #endif