2431 lines
87 KiB
C++
2431 lines
87 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 <cassert>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <unordered_map>
|
|
#include <gtest/gtest.h>
|
|
#include "tests/test_util.h"
|
|
#if RABITQ_SUPPORTED
|
|
#include "core/algorithm/hnsw_rabitq/rabitq_converter.h"
|
|
#include "zvec/core/framework/index_provider.h"
|
|
#endif
|
|
#include <zvec/ailego/buffer/block_eviction_queue.h>
|
|
#include "zvec/core/interface/index.h"
|
|
#include "zvec/core/interface/index_factory.h"
|
|
#include "zvec/core/interface/index_param.h"
|
|
#include "zvec/core/interface/index_param_builders.h"
|
|
|
|
#if defined(__GNUC__) || defined(__GNUG__)
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wunused-result"
|
|
#endif
|
|
|
|
using namespace zvec::core_interface;
|
|
|
|
TEST(IndexInterface, General) {
|
|
constexpr uint32_t kDimension = 64;
|
|
const std::string index_name{"test.index"};
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(kDimension);
|
|
vector[1] = 1.0f;
|
|
vector[2] = 2.0f;
|
|
VectorData vector_data;
|
|
vector_data.vector = DenseVector{vector.data()};
|
|
ASSERT_TRUE(0 == index->Add(vector_data, 233));
|
|
ASSERT_TRUE(0 == index->Train());
|
|
|
|
SearchResult result;
|
|
VectorData query;
|
|
query.vector = DenseVector{vector.data()};
|
|
index->Search(query, query_param, &result);
|
|
ASSERT_EQ(1, result.doc_list_.size());
|
|
ASSERT_EQ(233, result.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(5.0f, result.doc_list_[0].score());
|
|
if (query_param->fetch_vector) {
|
|
auto &doc = result.doc_list_[0];
|
|
if (result.reverted_vector_list_.size() != 0) {
|
|
// cosine metric or bf16 quantizer
|
|
ASSERT_EQ(1, result.reverted_vector_list_.size());
|
|
auto reverted_vector = reinterpret_cast<const float *>(
|
|
result.reverted_vector_list_[0].data());
|
|
ASSERT_FLOAT_EQ(1.0f, reverted_vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, reverted_vector[2]);
|
|
} else {
|
|
auto vector = reinterpret_cast<const float *>(doc.vector());
|
|
ASSERT_FLOAT_EQ(1.0f, vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, vector[2]);
|
|
}
|
|
}
|
|
|
|
vector[1] = 0;
|
|
vector[2] = 0;
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index->Fetch(233, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, fetched_vector[2]);
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
};
|
|
|
|
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
func(param,
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
func(FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
func(IVFIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithNList(10)
|
|
.Build(),
|
|
IVFQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
func(IVFIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithNList(10)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
IVFQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
|
|
func(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(50)
|
|
.build());
|
|
|
|
// Vamana with topk > ef_search to exercise _get_coarse_search_topk branch
|
|
// that picks max(topk, ef_search).
|
|
func(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(100)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(10)
|
|
.build());
|
|
}
|
|
|
|
TEST(IndexInterface, CopyOnWrite) {
|
|
constexpr uint32_t kDimension = 64;
|
|
constexpr uint32_t kNumVectors = 50;
|
|
const std::string index_name{"test_cow.index"};
|
|
|
|
auto make_vec = [&](uint32_t seed) {
|
|
std::vector<float> v(kDimension, 0.0f);
|
|
v[seed % kDimension] = 1.0f;
|
|
return v;
|
|
};
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
|
|
// Phase 1: build the index with shared mmap (writeable shared mapping)
|
|
// since the COW mode isn't used as the initial ingest path here.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/true, /*read_only=*/false,
|
|
/*copy_on_write=*/false}));
|
|
|
|
std::vector<std::vector<float>> vecs;
|
|
vecs.reserve(kNumVectors);
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
vecs.emplace_back(make_vec(i));
|
|
VectorData vd;
|
|
vd.vector = DenseVector{vecs.back().data()};
|
|
ASSERT_EQ(0, index->Add(vd, /*key=*/100 + i));
|
|
}
|
|
ASSERT_EQ(0, index->Train());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 2: reopen with COW mmap. Search and Fetch must succeed against
|
|
// the persisted file.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/true}));
|
|
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
auto target = make_vec(i);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(100u + i, result.doc_list_[0].key());
|
|
|
|
VectorDataBuffer fetched;
|
|
ASSERT_EQ(0, index->Fetch(100 + i, &fetched));
|
|
auto *fetched_ptr = reinterpret_cast<const float *>(
|
|
std::get<DenseVectorBuffer>(fetched.vector_buffer).data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_ptr[i % kDimension]);
|
|
}
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 3: reopen with shared mmap to confirm the file is intact after
|
|
// the COW session.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/false}));
|
|
|
|
auto target = make_vec(13);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(113u, result.doc_list_[0].key());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 4: repeated open/close under COW mmap must not lose entries.
|
|
for (int cycle = 0; cycle < 3; ++cycle) {
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/true}));
|
|
uint32_t i = static_cast<uint32_t>(cycle * 5 + 2);
|
|
auto target = make_vec(i);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(100u + i, result.doc_list_[0].key());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 5: open in COW mmap (writable MAP_PRIVATE with forced flush).
|
|
// Without performing writes the close path still exercises the pwrite
|
|
// branch with no dirty pages, which must not corrupt the file.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/true}));
|
|
|
|
auto target = make_vec(21);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(121u, result.doc_list_[0].key());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 6: reopen with shared mmap to confirm Phase 5's open/close left
|
|
// the file intact.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(
|
|
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/false}));
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
auto target = make_vec(i);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(100u + i, result.doc_list_[0].key());
|
|
}
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
};
|
|
|
|
func(FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(5).with_fetch_vector(false).build());
|
|
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(5)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(20)
|
|
.build());
|
|
|
|
func(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(64)
|
|
.WithAlpha(1.2f)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(5)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(32)
|
|
.build());
|
|
|
|
// Flat-only durability check for COW mmap: writes performed under
|
|
// MAP_PRIVATE must be pwrite-flushed back and visible after a shared-mmap
|
|
// reopen. Flat is used because Add/Flush against a previously-built file is
|
|
// straightforward to reason about for this storage layer.
|
|
{
|
|
const std::string persist_index{"test_cow_persist.index"};
|
|
zvec::test_util::RemoveTestFiles(persist_index);
|
|
auto persist_param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto persist_query =
|
|
FlatQueryParamBuilder().with_topk(5).with_fetch_vector(false).build();
|
|
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*persist_param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(0, index->Open(persist_index,
|
|
{StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/true, /*read_only=*/false,
|
|
/*copy_on_write=*/false}));
|
|
auto v0 = make_vec(0);
|
|
VectorData vd;
|
|
vd.vector = DenseVector{v0.data()};
|
|
ASSERT_EQ(0, index->Add(vd, /*key=*/500));
|
|
ASSERT_EQ(0, index->Train());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Add a new vector through COW mmap and explicitly Flush so
|
|
// dirty private pages are written back to the file.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*persist_param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(0, index->Open(persist_index,
|
|
{StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/false,
|
|
/*copy_on_write=*/true}));
|
|
auto v1 = make_vec(1);
|
|
VectorData vd;
|
|
vd.vector = DenseVector{v1.data()};
|
|
ASSERT_EQ(0, index->Add(vd, /*key=*/501));
|
|
ASSERT_EQ(0, index->Flush());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Reopen with shared mmap: the entry written in COW mode must be durable
|
|
// on disk.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*persist_param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(0, index->Open(persist_index,
|
|
{StorageOptions::StorageType::kMMAP,
|
|
/*create_new=*/false, /*read_only=*/true,
|
|
/*copy_on_write=*/false}));
|
|
auto target = make_vec(1);
|
|
VectorData query;
|
|
query.vector = DenseVector{target.data()};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, persist_query, &result));
|
|
ASSERT_FALSE(result.doc_list_.empty());
|
|
ASSERT_EQ(501u, result.doc_list_[0].key());
|
|
|
|
VectorDataBuffer fetched;
|
|
ASSERT_EQ(0, index->Fetch(501, &fetched));
|
|
auto *fetched_ptr = reinterpret_cast<const float *>(
|
|
std::get<DenseVectorBuffer>(fetched.vector_buffer).data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_ptr[1 % kDimension]);
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
zvec::test_util::RemoveTestFiles(persist_index);
|
|
}
|
|
}
|
|
|
|
TEST(IndexInterface, BufferGeneral) {
|
|
zvec::ailego::MemoryLimitPool::get_instance().init(100 * 1024 * 1024);
|
|
constexpr uint32_t kDimension = 64;
|
|
const std::string index_name{"test.index"};
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
std::string real_index_name = index_name;
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
auto write_index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, write_index);
|
|
|
|
write_index->Open(real_index_name,
|
|
{StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(kDimension);
|
|
vector[1] = 1.0f;
|
|
vector[2] = 2.0f;
|
|
VectorData vector_data;
|
|
vector_data.vector = DenseVector{vector.data()};
|
|
ASSERT_TRUE(0 == write_index->Add(vector_data, 233));
|
|
write_index->Close();
|
|
|
|
auto read_index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, read_index);
|
|
read_index->Open(real_index_name,
|
|
{StorageOptions::StorageType::kBufferPool, false});
|
|
|
|
SearchResult result;
|
|
VectorData query;
|
|
query.vector = DenseVector{vector.data()};
|
|
read_index->Search(query, query_param, &result);
|
|
ASSERT_EQ(1, result.doc_list_.size());
|
|
ASSERT_EQ(233, result.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(5.0f, result.doc_list_[0].score());
|
|
if (query_param->fetch_vector) {
|
|
auto &doc = result.doc_list_[0];
|
|
if (result.reverted_vector_list_.size() != 0) {
|
|
// cosine metric or bf16 quantizer
|
|
ASSERT_EQ(1, result.reverted_vector_list_.size());
|
|
auto reverted_vector = reinterpret_cast<const float *>(
|
|
result.reverted_vector_list_[0].data());
|
|
ASSERT_FLOAT_EQ(1.0f, reverted_vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, reverted_vector[2]);
|
|
} else {
|
|
auto vector = reinterpret_cast<const float *>(doc.vector());
|
|
ASSERT_FLOAT_EQ(1.0f, vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, vector[2]);
|
|
}
|
|
}
|
|
|
|
vector[1] = 0;
|
|
vector[2] = 0;
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == read_index->Fetch(233, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(2.0f, fetched_vector[2]);
|
|
result.doc_list_.clear();
|
|
read_index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
};
|
|
|
|
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
func(param,
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
func(FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
}
|
|
|
|
|
|
TEST(IndexInterface, SparseGeneral) {
|
|
constexpr uint32_t kSparseCount = 3;
|
|
const std::string index_name{"test.index"};
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<uint32_t> indices(kSparseCount);
|
|
std::vector<float> values(kSparseCount);
|
|
for (uint32_t i = 0; i < kSparseCount; ++i) {
|
|
indices[i] = i;
|
|
values[i] = i;
|
|
}
|
|
|
|
VectorData vector_data{
|
|
SparseVector{kSparseCount, indices.data(), values.data()}};
|
|
ASSERT_TRUE(0 == index->Add(vector_data, 233));
|
|
|
|
|
|
SearchResult result;
|
|
VectorData query = {
|
|
SparseVector{kSparseCount, indices.data(), values.data()}};
|
|
index->Search(query, query_param, &result);
|
|
ASSERT_EQ(1, result.doc_list_.size());
|
|
ASSERT_EQ(233, result.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(5.0f, result.doc_list_[0].score());
|
|
|
|
if (query_param->fetch_vector) {
|
|
auto &sparse_doc = result.doc_list_[0].sparse_doc();
|
|
auto sparse_indices = reinterpret_cast<const uint32_t *>(
|
|
sparse_doc.sparse_indices().data());
|
|
for (uint32_t i = 0; i < kSparseCount; ++i) {
|
|
ASSERT_EQ(i, sparse_indices[i]);
|
|
}
|
|
if (!result.reverted_sparse_values_list_.empty()) {
|
|
ASSERT_EQ(1, result.reverted_sparse_values_list_.size());
|
|
auto reverted_sparse_values = reinterpret_cast<const float *>(
|
|
result.reverted_sparse_values_list_[0].data());
|
|
for (uint32_t i = 0; i < kSparseCount; ++i) {
|
|
ASSERT_EQ(i, reverted_sparse_values[i]);
|
|
}
|
|
} else {
|
|
auto sparse_values =
|
|
reinterpret_cast<const float *>(sparse_doc.sparse_values().data());
|
|
for (uint32_t i = 0; i < kSparseCount; ++i) {
|
|
ASSERT_EQ(i, sparse_values[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
values[1] = 0;
|
|
values[2] = 0;
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index->Fetch(233, &fetched_vector_data));
|
|
const SparseVectorBuffer &sparse_vector_buffer =
|
|
std::get<SparseVectorBuffer>(fetched_vector_data.vector_buffer);
|
|
const uint32_t *fetched_indices =
|
|
reinterpret_cast<const uint32_t *>(sparse_vector_buffer.indices.data());
|
|
const float *fetched_values =
|
|
reinterpret_cast<const float *>(sparse_vector_buffer.values.data());
|
|
ASSERT_EQ(kSparseCount, sparse_vector_buffer.count);
|
|
for (uint32_t i = 0; i < kSparseCount; ++i) {
|
|
ASSERT_EQ(i, fetched_indices[i]);
|
|
ASSERT_EQ(i, fetched_values[i]);
|
|
}
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
};
|
|
|
|
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.Build();
|
|
// func(param, FlatQueryParam{{.topk = 10, .fetch_vector = true}});
|
|
func(FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build());
|
|
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build());
|
|
}
|
|
|
|
|
|
TEST(IndexInterface, Merge) {
|
|
constexpr uint32_t kDimension = 64;
|
|
const std::string index_name{"test.index"};
|
|
|
|
auto del_index_file_func = [&](const std::string file_name) {
|
|
zvec::test_util::RemoveTestFiles(file_name);
|
|
};
|
|
|
|
auto create_index_func =
|
|
[&](const BaseIndexParam::Pointer ¶m,
|
|
const std::string &index_name) -> Index::Pointer {
|
|
del_index_file_func(index_name);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
if (index == nullptr ||
|
|
0 != index->Open(index_name,
|
|
{StorageOptions::StorageType::kMMAP, true})) {
|
|
return nullptr;
|
|
}
|
|
return index;
|
|
};
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m_target,
|
|
const BaseIndexParam::Pointer ¶m_source) {
|
|
auto index1 = create_index_func(param_source, index_name + "1");
|
|
ASSERT_NE(nullptr, index1);
|
|
auto index2 = create_index_func(param_source, index_name + "2");
|
|
ASSERT_NE(nullptr, index2);
|
|
|
|
|
|
std::vector<float> vector(kDimension);
|
|
vector[1] = 1.0f;
|
|
vector[2] = 123.0f;
|
|
VectorData vector_data{DenseVector{vector.data()}};
|
|
ASSERT_TRUE(0 == index1->Add(vector_data, 0));
|
|
|
|
vector[1] = 2.0f;
|
|
ASSERT_TRUE(0 == index2->Add(vector_data, 0));
|
|
vector[1] = 3.0f;
|
|
ASSERT_TRUE(0 == index2->Add(vector_data, 1));
|
|
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index1->Fetch(0, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index2->Fetch(0, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(2.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index2->Fetch(1, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(3.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
|
|
{ // test reduce
|
|
auto index3 = create_index_func(param_target, index_name + "3");
|
|
ASSERT_NE(nullptr, index3);
|
|
ASSERT_TRUE(0 == index3->Merge({index1, index2}, IndexFilter()));
|
|
ASSERT_TRUE(3 == index3->GetDocCount());
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index3->Fetch(0, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(1.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index3->Fetch(1, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(2.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
index3->Close();
|
|
del_index_file_func(index_name + "3");
|
|
}
|
|
|
|
{ // test reduce with filter
|
|
auto index3 = create_index_func(param_target, index_name + "3");
|
|
ASSERT_NE(nullptr, index3);
|
|
auto filter = IndexFilter();
|
|
filter.set([](uint64_t key) { return key == 0; }); // TODO: uint32?
|
|
ASSERT_TRUE(0 == index3->Merge({index1, index2}, filter));
|
|
ASSERT_TRUE(2 == index3->GetDocCount());
|
|
{
|
|
VectorDataBuffer fetched_vector_data;
|
|
ASSERT_TRUE(0 == index3->Fetch(0, &fetched_vector_data));
|
|
float *fetched_vector = reinterpret_cast<float *>(
|
|
std::get<DenseVectorBuffer>(fetched_vector_data.vector_buffer)
|
|
.data.data());
|
|
ASSERT_FLOAT_EQ(2.0f, fetched_vector[1]);
|
|
ASSERT_FLOAT_EQ(123.0f, fetched_vector[2]);
|
|
}
|
|
index3->Close();
|
|
del_index_file_func(index_name + "3");
|
|
}
|
|
|
|
index1->Close();
|
|
index2->Close();
|
|
del_index_file_func(index_name + "1");
|
|
del_index_file_func(index_name + "2");
|
|
};
|
|
|
|
// same index
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
func(param, param);
|
|
}
|
|
{
|
|
auto param = HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
func(param, param);
|
|
}
|
|
|
|
// different index
|
|
{
|
|
auto param_flat = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto param_hnsw = HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
func(param_flat, param_hnsw);
|
|
func(param_hnsw, param_flat);
|
|
}
|
|
}
|
|
|
|
|
|
TEST(IndexInterface, Serialize) {
|
|
{
|
|
std::cout << "\n\n----flat index----" << std::endl;
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam{QuantizerType::kFP16})
|
|
.Build();
|
|
|
|
std::cout << "flat index -- omit=true: " << param->SerializeToJson(true)
|
|
<< std::endl;
|
|
std::cout << "omit=false: " << param->SerializeToJson() << std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::DeserializeIndexParamFromJson(param->SerializeToJson());
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< deserialized_param->SerializeToJson() << std::endl;
|
|
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson() ==
|
|
param->SerializeToJson());
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson(true) ==
|
|
param->SerializeToJson(true));
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----hnsw index----" << std::endl;
|
|
auto param = HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam{QuantizerType::kFP16})
|
|
.Build();
|
|
|
|
std::cout << "hnsw index -- omit=true: " << param->SerializeToJson(true)
|
|
<< std::endl;
|
|
std::cout << "hnsw index -- omit=false: " << param->SerializeToJson()
|
|
<< std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::DeserializeIndexParamFromJson(param->SerializeToJson());
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< deserialized_param->SerializeToJson() << std::endl;
|
|
|
|
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson() ==
|
|
param->SerializeToJson());
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson(true) ==
|
|
param->SerializeToJson(true));
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----flat query----" << std::endl;
|
|
auto param =
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(true).build();
|
|
std::cout << "flat query -- omit=true: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param, true)
|
|
<< std::endl;
|
|
std::cout << "flat query -- omit=false: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param) << std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::QueryParamDeserializeFromJson<FlatQueryParam>(
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< IndexFactory::QueryParamSerializeToJson(*deserialized_param)
|
|
<< std::endl;
|
|
|
|
ASSERT_TRUE(IndexFactory::QueryParamSerializeToJson(*deserialized_param) ==
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----hnsw query----" << std::endl;
|
|
auto param = HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build();
|
|
std::cout << "hnsw query -- omit=true: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param, true)
|
|
<< std::endl;
|
|
std::cout << "hnsw query -- omit=false: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param, false)
|
|
<< std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::QueryParamDeserializeFromJson<HNSWQueryParam>(
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< IndexFactory::QueryParamSerializeToJson(*deserialized_param)
|
|
<< std::endl;
|
|
|
|
ASSERT_TRUE(IndexFactory::QueryParamSerializeToJson(*deserialized_param) ==
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----vamana index----" << std::endl;
|
|
auto param = VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build();
|
|
|
|
std::cout << "vamana index -- omit=true: " << param->SerializeToJson(true)
|
|
<< std::endl;
|
|
std::cout << "vamana index -- omit=false: " << param->SerializeToJson()
|
|
<< std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::DeserializeIndexParamFromJson(param->SerializeToJson());
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< deserialized_param->SerializeToJson() << std::endl;
|
|
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson() ==
|
|
param->SerializeToJson());
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson(true) ==
|
|
param->SerializeToJson(true));
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----hnsw index with use_contiguous_memory----"
|
|
<< std::endl;
|
|
auto param = std::make_shared<HNSWIndexParam>();
|
|
param->metric_type = MetricType::kL2sq;
|
|
param->data_type = DataType::DT_FP32;
|
|
param->dimension = 64;
|
|
param->use_contiguous_memory = true;
|
|
|
|
auto json_str = param->SerializeToJson();
|
|
std::cout << "hnsw contiguous -- json: " << json_str << std::endl;
|
|
ASSERT_TRUE(json_str.find("use_contiguous_memory") != std::string::npos);
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::DeserializeIndexParamFromJson(json_str);
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
auto hnsw_param =
|
|
std::dynamic_pointer_cast<HNSWIndexParam>(deserialized_param);
|
|
ASSERT_NE(nullptr, hnsw_param.get());
|
|
ASSERT_TRUE(hnsw_param->use_contiguous_memory);
|
|
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson() == json_str);
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----vamana index with use_contiguous_memory----"
|
|
<< std::endl;
|
|
auto param = std::make_shared<VamanaIndexParam>();
|
|
param->metric_type = MetricType::kL2sq;
|
|
param->data_type = DataType::DT_FP32;
|
|
param->dimension = 64;
|
|
param->max_degree = 48;
|
|
param->search_list_size = 200;
|
|
param->alpha = 1.5f;
|
|
param->use_contiguous_memory = true;
|
|
|
|
auto json_str = param->SerializeToJson();
|
|
std::cout << "vamana contiguous -- json: " << json_str << std::endl;
|
|
ASSERT_TRUE(json_str.find("use_contiguous_memory") != std::string::npos);
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::DeserializeIndexParamFromJson(json_str);
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
auto vamana_param =
|
|
std::dynamic_pointer_cast<VamanaIndexParam>(deserialized_param);
|
|
ASSERT_NE(nullptr, vamana_param.get());
|
|
ASSERT_TRUE(vamana_param->use_contiguous_memory);
|
|
ASSERT_EQ(48, vamana_param->max_degree);
|
|
ASSERT_EQ(200, vamana_param->search_list_size);
|
|
ASSERT_FLOAT_EQ(1.5f, vamana_param->alpha);
|
|
|
|
ASSERT_TRUE(deserialized_param->SerializeToJson() == json_str);
|
|
}
|
|
|
|
{
|
|
std::cout << "\n\n----vamana query----" << std::endl;
|
|
auto param = VamanaQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(50)
|
|
.build();
|
|
std::cout << "vamana query -- omit=true: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param, true)
|
|
<< std::endl;
|
|
std::cout << "vamana query -- omit=false: "
|
|
<< IndexFactory::QueryParamSerializeToJson(*param) << std::endl;
|
|
|
|
auto deserialized_param =
|
|
IndexFactory::QueryParamDeserializeFromJson<VamanaQueryParam>(
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
ASSERT_NE(nullptr, deserialized_param.get());
|
|
|
|
std::cout << "serialize then de then se:"
|
|
<< IndexFactory::QueryParamSerializeToJson(*deserialized_param)
|
|
<< std::endl;
|
|
|
|
ASSERT_TRUE(IndexFactory::QueryParamSerializeToJson(*deserialized_param) ==
|
|
IndexFactory::QueryParamSerializeToJson(*param));
|
|
}
|
|
}
|
|
|
|
TEST(IndexInterface, Failure) {
|
|
// Test unsupported index type
|
|
{
|
|
auto param = std::make_shared<BaseIndexParam>(IndexType::kIVF);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_EQ(nullptr, index);
|
|
}
|
|
|
|
// Test unsupported metric type
|
|
{
|
|
auto param =
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kNone) // L2 not supported for sparse
|
|
.WithDataType(DataType::DT_FP32)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_EQ(nullptr, index);
|
|
}
|
|
|
|
// Test unsupported metric type for sparse index
|
|
{
|
|
auto param =
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq) // L2 not supported for sparse
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_EQ(nullptr, index);
|
|
}
|
|
|
|
// // Test unsupported quantizer type
|
|
// {
|
|
// auto param = FlatIndexParamBuilder()
|
|
// .WithMetricType(MetricType::kInnerProduct)
|
|
// .WithDataType(DataType::DT_INT4)
|
|
// .WithDimension(64)
|
|
// .WithIsSparse(false)
|
|
// .WithQuantizerParam(
|
|
// QuantizerParam(QuantizerType::kInt8)) //
|
|
// Unsupported
|
|
// .Build();
|
|
// auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
// ASSERT_EQ(nullptr, index);
|
|
// }
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(true)
|
|
.WithQuantizerParam(
|
|
QuantizerParam(QuantizerType::kInt8)) // Unsupported
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_EQ(nullptr, index);
|
|
}
|
|
|
|
// Test unsupported data type for cosine metric
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kCosine)
|
|
.WithDataType(DataType::DT_INT8) // Unsupported for cosine
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_EQ(nullptr, index);
|
|
}
|
|
|
|
// Test invalid storage type
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
StorageOptions invalid_storage;
|
|
invalid_storage.type = StorageOptions::StorageType::kNone; // Unsupported
|
|
int ret = index->Open("test.index", invalid_storage);
|
|
ASSERT_NE(0, ret);
|
|
}
|
|
|
|
// Test invalid vector data type for dense operations
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
// Try to add sparse vector to dense index
|
|
std::vector<uint32_t> indices = {0, 1, 2};
|
|
std::vector<float> values = {1.0f, 2.0f, 3.0f};
|
|
VectorData sparse_vector_data{
|
|
SparseVector{3, indices.data(), values.data()}};
|
|
|
|
int ret = index->Add(sparse_vector_data, 1);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test invalid vector data type for sparse operations
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
// Try to add dense vector to sparse index
|
|
std::vector<float> vector(64, 1.0f);
|
|
VectorData dense_vector_data{DenseVector{vector.data()}};
|
|
|
|
int ret = index->Add(dense_vector_data, 1);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test fetch non-existent document
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
VectorDataBuffer fetched_vector_data;
|
|
int ret = index->Fetch(999, &fetched_vector_data); // Non-existent doc_id
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test search with invalid vector data
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
// Add a vector first
|
|
std::vector<float> vector(64, 1.0f);
|
|
VectorData vector_data{DenseVector{vector.data()}};
|
|
ASSERT_EQ(0, index->Add(vector_data, 1));
|
|
|
|
// Try to search with sparse vector in dense index
|
|
std::vector<uint32_t> indices = {0, 1, 2};
|
|
std::vector<float> values = {1.0f, 2.0f, 3.0f};
|
|
VectorData sparse_query{SparseVector{3, indices.data(), values.data()}};
|
|
|
|
SearchResult result;
|
|
FlatQueryParam::Pointer query_param =
|
|
FlatQueryParamBuilder().with_topk(10).with_fetch_vector(false).build();
|
|
int ret = index->Search(sparse_query, query_param, &result);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test merge with invalid write concurrency
|
|
{
|
|
auto param1 = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index1 = IndexFactory::CreateAndInitIndex(*param1);
|
|
ASSERT_NE(nullptr, index1);
|
|
index1->Open("test1.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
auto param2 = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index2 = IndexFactory::CreateAndInitIndex(*param2);
|
|
ASSERT_NE(nullptr, index2);
|
|
index2->Open("test2.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
auto param3 = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index3 = IndexFactory::CreateAndInitIndex(*param3);
|
|
ASSERT_NE(nullptr, index3);
|
|
index3->Open("test3.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
MergeOptions invalid_options;
|
|
invalid_options.write_concurrency = 0; // Invalid: must be > 0
|
|
|
|
int ret = index3->Merge({index1, index2}, IndexFilter(), invalid_options);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index1->Close();
|
|
index2->Close();
|
|
index3->Close();
|
|
zvec::test_util::RemoveTestFiles("test1.index");
|
|
zvec::test_util::RemoveTestFiles("test2.index");
|
|
zvec::test_util::RemoveTestFiles("test3.index");
|
|
}
|
|
|
|
// Test Vamana search with ef_search == 0 (invalid, ef_search must be > 0)
|
|
{
|
|
auto param = VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(64, 1.0f);
|
|
VectorData vector_data{DenseVector{vector.data()}};
|
|
ASSERT_EQ(0, index->Add(vector_data, 1));
|
|
|
|
VectorData query{DenseVector{vector.data()}};
|
|
auto query_param = VamanaQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(0)
|
|
.build();
|
|
SearchResult result;
|
|
int ret = index->Search(query, query_param, &result);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test Vamana search with ef_search > 2048 (invalid upper bound)
|
|
{
|
|
auto param = VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(64, 1.0f);
|
|
VectorData vector_data{DenseVector{vector.data()}};
|
|
ASSERT_EQ(0, index->Add(vector_data, 1));
|
|
|
|
VectorData query{DenseVector{vector.data()}};
|
|
auto query_param = VamanaQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(4096)
|
|
.build();
|
|
SearchResult result;
|
|
int ret = index->Search(query, query_param, &result);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
|
|
// Test Vamana search with wrong query param type (HNSWQueryParam instead of
|
|
// VamanaQueryParam)
|
|
{
|
|
auto param = VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(64)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open("test.index", {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(64, 1.0f);
|
|
VectorData vector_data{DenseVector{vector.data()}};
|
|
ASSERT_EQ(0, index->Add(vector_data, 1));
|
|
|
|
VectorData query{DenseVector{vector.data()}};
|
|
// Intentionally pass an HNSWQueryParam to a Vamana index
|
|
auto wrong_query_param = HNSWQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build();
|
|
SearchResult result;
|
|
int ret = index->Search(query, wrong_query_param, &result);
|
|
ASSERT_NE(0, ret);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles("test.index");
|
|
}
|
|
}
|
|
|
|
TEST(IndexInterface, SerializeFailure) {
|
|
// Test invalid JSON deserialization
|
|
{
|
|
std::string invalid_json = "invalid json string";
|
|
auto param = IndexFactory::DeserializeIndexParamFromJson(invalid_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test JSON with invalid enum value
|
|
{
|
|
std::string invalid_enum_json = R"({
|
|
"index_type": "kInvalidType",
|
|
"metric_type": "kL2",
|
|
"dimension": 64,
|
|
"is_sparse": false,
|
|
"data_type": "DT_FP32"
|
|
})";
|
|
auto param = IndexFactory::DeserializeIndexParamFromJson(invalid_enum_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test JSON with invalid field type
|
|
{
|
|
std::string invalid_type_json = R"({
|
|
"index_type": "kFlat",
|
|
"metric_type": "kL2",
|
|
"dimension": "not_a_number",
|
|
"is_sparse": false,
|
|
"data_type": "DT_FP32"
|
|
})";
|
|
auto param = IndexFactory::DeserializeIndexParamFromJson(invalid_type_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test JSON with invalid field type
|
|
{
|
|
std::string invalid_type_json = R"({
|
|
"index_type": "kHNSW",
|
|
"metric_type": "kL2",
|
|
"dimension": 1,
|
|
"is_sparse": "false",
|
|
"data_type": "DT_FP32"
|
|
})";
|
|
auto param = IndexFactory::DeserializeIndexParamFromJson(invalid_type_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test unsupported index_type
|
|
{
|
|
std::string wrong_type_json = R"({
|
|
"index_type": "kNone",
|
|
"metric_type": "kL2",
|
|
"dimension": 64,
|
|
"is_sparse": false,
|
|
"data_type": "DT_FP32"
|
|
})";
|
|
auto param = IndexFactory::DeserializeIndexParamFromJson(wrong_type_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test QueryParam deserialization with invalid JSON
|
|
{
|
|
std::string invalid_json = "invalid json";
|
|
auto param = IndexFactory::QueryParamDeserializeFromJson<FlatQueryParam>(
|
|
invalid_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test QueryParam deserialization with invalid enum
|
|
{
|
|
std::string invalid_enum_json = R"({
|
|
"index_type": "kInvalidType",
|
|
"topk": 10,
|
|
"fetch_vector": false,
|
|
"radius": 0.0,
|
|
"is_linear": false
|
|
})";
|
|
auto param = IndexFactory::QueryParamDeserializeFromJson<FlatQueryParam>(
|
|
invalid_enum_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test QueryParam deserialization with invalid field type
|
|
{
|
|
std::string invalid_type_json = R"({
|
|
"index_type": "kFlat",
|
|
"topk": "not_a_number",
|
|
"fetch_vector": false,
|
|
"radius": 0.0,
|
|
"is_linear": false
|
|
})";
|
|
auto param = IndexFactory::QueryParamDeserializeFromJson<FlatQueryParam>(
|
|
invalid_type_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
|
|
// Test HNSWQueryParam deserialization with invalid field type
|
|
{
|
|
std::string invalid_type_json = R"({
|
|
"index_type": "kHNSW",
|
|
"topk": 10,
|
|
"fetch_vector": false,
|
|
"radius": 0.0,
|
|
"is_linear": false,
|
|
"ef_search": "not_a_number"
|
|
})";
|
|
auto param = IndexFactory::QueryParamDeserializeFromJson<HNSWQueryParam>(
|
|
invalid_type_json);
|
|
ASSERT_EQ(nullptr, param);
|
|
}
|
|
}
|
|
|
|
TEST(IndexInterface, Score) {
|
|
const std::string index_file_path = "test_indexer.index";
|
|
const int kTopk = 10;
|
|
constexpr uint32_t kDocId1 = 2345;
|
|
constexpr uint32_t kDocId2 = 5432;
|
|
auto vector1 = std::vector<float>{3.0f, 4.0f, 5.0f};
|
|
auto vector2 = std::vector<float>{1.0f, 20.0f, 3.0f};
|
|
auto vector_id_map = std::unordered_map<uint32_t, std::vector<float>>{
|
|
{kDocId1, vector1},
|
|
{kDocId2, vector2},
|
|
};
|
|
auto sparse_indices = std::vector<uint32_t>{0, 1, 2};
|
|
auto query_vector = std::vector<float>{1.0f, 2.0f, 3.0f};
|
|
|
|
zvec::test_util::RemoveTestFiles(index_file_path);
|
|
|
|
auto check_score = [&](const SearchResult &result, MetricType metric_type) {
|
|
ASSERT_EQ(result.doc_list_.size(), 2);
|
|
|
|
auto inner_produce_score_func = [&](const std::vector<float> &v1,
|
|
const std::vector<float> &v2) {
|
|
return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
|
|
};
|
|
|
|
auto cosine_score_func = [&](const std::vector<float> &v1,
|
|
const std::vector<float> &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<float> &v1,
|
|
const std::vector<float> &v2) {
|
|
assert(v1.size() == 3);
|
|
assert(v2.size() == 3);
|
|
float ret = 0.0f;
|
|
for (int i = 0; i < v1.size(); ++i) {
|
|
ret += (v1[i] - v2[i]) * (v1[i] - v2[i]);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
std::function<float(const std::vector<float> &, const std::vector<float> &)>
|
|
score_func;
|
|
|
|
switch (metric_type) {
|
|
case MetricType::kInnerProduct:
|
|
score_func = inner_produce_score_func;
|
|
break;
|
|
case MetricType::kCosine:
|
|
score_func = cosine_score_func;
|
|
break;
|
|
case MetricType::kL2sq:
|
|
score_func = l2_score_func;
|
|
break;
|
|
default:
|
|
ASSERT_TRUE(false);
|
|
}
|
|
|
|
// Iterate over doc_list_ and check scores
|
|
ASSERT_GE(result.doc_list_.size(), 2);
|
|
printf("result.doc_list_[0].score() top1: %f\n",
|
|
result.doc_list_[0].score());
|
|
printf(
|
|
"score_func(vector_id_map[result.doc_list_[0].key()], query_vector): "
|
|
"%f\n",
|
|
score_func(vector_id_map[result.doc_list_[0].key()], query_vector));
|
|
ASSERT_TRUE(std::abs(result.doc_list_[0].score() -
|
|
score_func(vector_id_map[result.doc_list_[0].key()],
|
|
query_vector)) < 1e-2);
|
|
printf("result.doc_list_[1].score() top2: %f\n",
|
|
result.doc_list_[1].score());
|
|
printf(
|
|
"score_func(vector_id_map[result.doc_list_[1].key()], query_vector): "
|
|
"%f\n",
|
|
score_func(vector_id_map[result.doc_list_[1].key()], query_vector));
|
|
ASSERT_TRUE(std::abs(result.doc_list_[1].score() -
|
|
score_func(vector_id_map[result.doc_list_[1].key()],
|
|
query_vector)) < 1e-2);
|
|
};
|
|
|
|
auto dense_func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer query_param,
|
|
MetricType metric_type) {
|
|
zvec::test_util::RemoveTestFiles(index_file_path);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open(index_file_path, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
VectorData vector_data1;
|
|
vector_data1.vector = DenseVector{vector1.data()};
|
|
ASSERT_EQ(0, index->Add(vector_data1, kDocId1));
|
|
|
|
VectorData vector_data2;
|
|
vector_data2.vector = DenseVector{vector2.data()};
|
|
ASSERT_EQ(0, index->Add(vector_data2, kDocId2));
|
|
|
|
SearchResult result;
|
|
VectorData query;
|
|
query.vector = DenseVector{query_vector.data()};
|
|
index->Search(query, query_param, &result);
|
|
|
|
check_score(result, metric_type);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_file_path);
|
|
};
|
|
|
|
auto sparse_func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer query_param,
|
|
MetricType metric_type) {
|
|
zvec::test_util::RemoveTestFiles(index_file_path);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open(index_file_path, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
VectorData vector_data1;
|
|
vector_data1.vector =
|
|
SparseVector{3, reinterpret_cast<const void *>(sparse_indices.data()),
|
|
vector1.data()};
|
|
ASSERT_EQ(0, index->Add(vector_data1, kDocId1));
|
|
|
|
VectorData vector_data2;
|
|
vector_data2.vector =
|
|
SparseVector{3, reinterpret_cast<const void *>(sparse_indices.data()),
|
|
vector2.data()};
|
|
ASSERT_EQ(0, index->Add(vector_data2, kDocId2));
|
|
|
|
SearchResult result;
|
|
VectorData query;
|
|
query.vector =
|
|
SparseVector{3, reinterpret_cast<const void *>(sparse_indices.data()),
|
|
query_vector.data()};
|
|
index->Search(query, query_param, &result);
|
|
|
|
check_score(result, metric_type);
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_file_path);
|
|
};
|
|
|
|
constexpr uint32_t kDimension = 3;
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kInnerProduct");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kInnerProduct);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kInnerProduct);
|
|
|
|
dense_func(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(50)
|
|
.build(),
|
|
MetricType::kInnerProduct);
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kInnerProduct, QuantizerType::kFP16");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kInnerProduct);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kInnerProduct);
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kCosine");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kCosine)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kCosine);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kCosine)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kCosine);
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kCosine, QuantizerType::kFP16");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kCosine)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kCosine);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kCosine)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kCosine);
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kL2sq");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kL2sq);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kL2sq);
|
|
|
|
LOG_INFO("Test DenseVector, MetricType::kL2sq, QuantizerType::kFP16");
|
|
dense_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kL2sq);
|
|
dense_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kL2sq);
|
|
|
|
LOG_INFO("Test SparseVector, MetricType::kInnerProduct");
|
|
sparse_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kInnerProduct);
|
|
sparse_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithEFConstruction(100)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kInnerProduct);
|
|
|
|
LOG_INFO(
|
|
"Test SparseVector, MetricType::kInnerProduct, QuantizerType::kFP16");
|
|
sparse_func(
|
|
FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
FlatQueryParamBuilder().with_topk(kTopk).with_fetch_vector(true).build(),
|
|
MetricType::kInnerProduct);
|
|
sparse_func(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithEFConstruction(100)
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(true)
|
|
.with_ef_search(20)
|
|
.build(),
|
|
MetricType::kInnerProduct);
|
|
}
|
|
|
|
#if RABITQ_SUPPORTED
|
|
TEST(IndexInterface, HNSWRabitqGeneral) {
|
|
constexpr uint32_t kDimension = 64;
|
|
const std::string index_name{"test_rabitq.index"};
|
|
const std::string cleanup_pattern = index_name + "*";
|
|
|
|
auto func = [&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
zvec::test_util::RemoveTestFiles(cleanup_pattern);
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
std::vector<float> vector(kDimension);
|
|
vector[1] = 1.0f;
|
|
vector[2] = 2.0f;
|
|
VectorData vector_data;
|
|
vector_data.vector = DenseVector{vector.data()};
|
|
ASSERT_TRUE(0 == index->Add(vector_data, 233));
|
|
ASSERT_TRUE(0 == index->Train());
|
|
|
|
SearchResult result;
|
|
VectorData query;
|
|
query.vector = DenseVector{vector.data()};
|
|
index->Search(query, query_param, &result);
|
|
ASSERT_EQ(1, result.doc_list_.size());
|
|
ASSERT_EQ(233, result.doc_list_[0].key());
|
|
|
|
// Fetch is meaningless for HNSWRabitq
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(cleanup_pattern);
|
|
};
|
|
|
|
using namespace zvec::core;
|
|
using namespace zvec::ailego;
|
|
auto holder = std::make_shared<
|
|
zvec::core::MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(
|
|
kDimension);
|
|
size_t doc_cnt = 500UL;
|
|
for (size_t i = 0; i < doc_cnt; i++) {
|
|
NumericalVector<float> vec(kDimension);
|
|
for (size_t j = 0; j < kDimension; ++j) {
|
|
vec[j] = static_cast<float>(i);
|
|
}
|
|
ASSERT_TRUE(holder->emplace(i, vec));
|
|
}
|
|
std::shared_ptr<IndexMeta> index_meta_ptr_;
|
|
index_meta_ptr_.reset(
|
|
new (std::nothrow) IndexMeta(IndexMeta::DataType::DT_FP32, kDimension));
|
|
index_meta_ptr_->set_metric("SquaredEuclidean", 0, Params());
|
|
|
|
RabitqConverter converter;
|
|
converter.init(*index_meta_ptr_, Params());
|
|
ASSERT_EQ(converter.train(holder), 0);
|
|
std::shared_ptr<IndexReformer> index_reformer;
|
|
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
|
|
|
|
// HNSWRabitq with default total_bits
|
|
func(HNSWRabitqIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithProvider(holder)
|
|
.WithReformer(index_reformer)
|
|
.Build(),
|
|
HNSWRabitqQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build());
|
|
|
|
// HNSWRabitq with InnerProduct metric
|
|
func(HNSWRabitqIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithProvider(holder)
|
|
.WithReformer(index_reformer)
|
|
.Build(),
|
|
HNSWRabitqQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build());
|
|
|
|
// HNSWRabitq with custom total_bits
|
|
// Reformer must be re-created with matching total_bits to keep ex_bits
|
|
// consistent between reformer and entity.
|
|
RabitqConverter converter2;
|
|
Params converter2_params;
|
|
converter2_params.set(PARAM_RABITQ_TOTAL_BITS, 2u);
|
|
converter2.init(*index_meta_ptr_, converter2_params);
|
|
ASSERT_EQ(converter2.train(holder), 0);
|
|
std::shared_ptr<IndexReformer> index_reformer2;
|
|
ASSERT_EQ(converter2.to_reformer(&index_reformer2), 0);
|
|
|
|
func(HNSWRabitqIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithTotalBits(2)
|
|
.WithProvider(holder)
|
|
.WithReformer(index_reformer2)
|
|
.Build(),
|
|
HNSWRabitqQueryParamBuilder()
|
|
.with_topk(10)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build());
|
|
}
|
|
#endif
|
|
|
|
// Verify that enabling use_contiguous_memory on HNSW / Vamana index params at
|
|
// the interface layer is correctly propagated to the underlying streamer and
|
|
// yields a working build -> close -> reopen-for-search pipeline. This guards
|
|
// the interface -> streamer param binding introduced for contiguous memory
|
|
// mode.
|
|
TEST(IndexInterface, ContiguousMemoryEndToEnd) {
|
|
constexpr uint32_t kDimension = 32;
|
|
constexpr uint32_t kNumDocs = 500;
|
|
constexpr int kTopk = 10;
|
|
const std::string index_name{"test_contiguous.index"};
|
|
|
|
// build_then_search builds an index from scratch (with use_contiguous_memory
|
|
// possibly enabled), closes it, then reopens with the same params and runs a
|
|
// search for each inserted vector, asserting top-1 is itself.
|
|
auto build_then_search =
|
|
[&](const BaseIndexParam::Pointer ¶m,
|
|
const BaseIndexQueryParam::Pointer &query_param) {
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
|
|
// Phase 1: build & persist.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(0, index->Open(index_name,
|
|
{StorageOptions::StorageType::kMMAP, true}));
|
|
|
|
std::vector<float> vec(kDimension);
|
|
for (uint32_t i = 0; i < kNumDocs; ++i) {
|
|
for (uint32_t d = 0; d < kDimension; ++d) {
|
|
vec[d] = static_cast<float>(i);
|
|
}
|
|
VectorData data{DenseVector{vec.data()}};
|
|
ASSERT_EQ(0, index->Add(data, i));
|
|
}
|
|
ASSERT_EQ(0, index->Train());
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
// Phase 2: reopen with same params (contiguous memory takes effect
|
|
// here) and search.
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_EQ(0,
|
|
index->Open(index_name,
|
|
{StorageOptions::StorageType::kMMAP, false}));
|
|
|
|
std::vector<float> q(kDimension);
|
|
for (uint32_t i = 0; i < kNumDocs; i += 50) {
|
|
for (uint32_t d = 0; d < kDimension; ++d) {
|
|
q[d] = static_cast<float>(i);
|
|
}
|
|
VectorData query{DenseVector{q.data()}};
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->Search(query, query_param, &result));
|
|
ASSERT_GT(result.doc_list_.size(), 0UL);
|
|
ASSERT_EQ(i, result.doc_list_[0].key());
|
|
}
|
|
ASSERT_EQ(0, index->Close());
|
|
}
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
};
|
|
|
|
// HNSW + use_contiguous_memory=true
|
|
build_then_search(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithM(16)
|
|
.WithEFConstruction(64)
|
|
.WithUseContiguousMemory(true)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(64)
|
|
.build());
|
|
|
|
// HNSW + use_contiguous_memory=false (baseline, same harness)
|
|
build_then_search(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithM(16)
|
|
.WithEFConstruction(64)
|
|
.WithUseContiguousMemory(false)
|
|
.Build(),
|
|
HNSWQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(64)
|
|
.build());
|
|
|
|
// Vamana + use_contiguous_memory=true
|
|
build_then_search(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.WithUseContiguousMemory(true)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(64)
|
|
.build());
|
|
|
|
// Vamana + use_contiguous_memory=false (baseline, same harness)
|
|
build_then_search(VamanaIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithMaxDegree(32)
|
|
.WithSearchListSize(100)
|
|
.WithAlpha(1.2f)
|
|
.WithUseContiguousMemory(false)
|
|
.Build(),
|
|
VamanaQueryParamBuilder()
|
|
.with_topk(kTopk)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(64)
|
|
.build());
|
|
}
|
|
|
|
class TestVectorSource : public zvec::core::VectorSource {
|
|
public:
|
|
TestVectorSource(const float *base, uint32_t dim) : base_(base), dim_(dim) {}
|
|
|
|
const void *get_vector(uint32_t node_id) const override {
|
|
return base_ + static_cast<size_t>(node_id) * dim_;
|
|
}
|
|
|
|
private:
|
|
const float *base_;
|
|
uint32_t dim_;
|
|
};
|
|
|
|
TEST(IndexInterface, ExternalVectorEndToEnd) {
|
|
constexpr uint32_t kDimension = 64;
|
|
constexpr uint32_t kNumVectors = 100;
|
|
const std::string index_name{"test_external.index"};
|
|
|
|
std::vector<float> all_vectors(kDimension * kNumVectors);
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
for (uint32_t d = 0; d < kDimension; ++d) {
|
|
all_vectors[i * kDimension + d] =
|
|
static_cast<float>(i * kDimension + d) * 0.01f;
|
|
}
|
|
}
|
|
|
|
TestVectorSource source(all_vectors.data(), kDimension);
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
|
|
auto param = HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithUseExternalVector(true)
|
|
.Build();
|
|
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
VectorData vector_data;
|
|
vector_data.vector = DenseVector{all_vectors.data() + i * kDimension};
|
|
int ret = index->AddWithSource(vector_data, i, source);
|
|
ASSERT_EQ(0, ret) << "AddWithSource failed for doc_id=" << i;
|
|
}
|
|
|
|
auto query_param = HNSWQueryParamBuilder()
|
|
.with_topk(5)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build();
|
|
|
|
VectorData query;
|
|
query.vector = DenseVector{all_vectors.data()};
|
|
SearchResult result;
|
|
int ret = index->SearchWithSource(query, query_param, source, &result);
|
|
ASSERT_EQ(0, ret);
|
|
ASSERT_GE(result.doc_list_.size(), 1u);
|
|
ASSERT_EQ(0u, result.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(0.0f, result.doc_list_[0].score());
|
|
|
|
VectorData query2;
|
|
query2.vector = DenseVector{all_vectors.data() + 50 * kDimension};
|
|
SearchResult result2;
|
|
ret = index->SearchWithSource(query2, query_param, source, &result2);
|
|
ASSERT_EQ(0, ret);
|
|
ASSERT_GE(result2.doc_list_.size(), 1u);
|
|
ASSERT_EQ(50u, result2.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(0.0f, result2.doc_list_[0].score());
|
|
|
|
index->Close();
|
|
|
|
auto index2 = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index2);
|
|
index2->Open(index_name, {StorageOptions::StorageType::kMMAP, false});
|
|
|
|
SearchResult result3;
|
|
ret = index2->SearchWithSource(query, query_param, source, &result3);
|
|
ASSERT_EQ(0, ret);
|
|
ASSERT_GE(result3.doc_list_.size(), 1u);
|
|
ASSERT_EQ(0u, result3.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(0.0f, result3.doc_list_[0].score());
|
|
|
|
index2->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
}
|
|
|
|
TEST(IndexInterface, ExternalVectorInnerProduct) {
|
|
constexpr uint32_t kDimension = 16;
|
|
constexpr uint32_t kNumVectors = 10;
|
|
const std::string index_name{"test_external_ip.index"};
|
|
|
|
std::vector<float> all_vectors(kDimension * kNumVectors, 0.0f);
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
all_vectors[i * kDimension + i % kDimension] = static_cast<float>(i + 1);
|
|
}
|
|
|
|
TestVectorSource source(all_vectors.data(), kDimension);
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
|
|
auto param = HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.WithUseExternalVector(true)
|
|
.Build();
|
|
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
|
|
for (uint32_t i = 0; i < kNumVectors; ++i) {
|
|
VectorData vector_data;
|
|
vector_data.vector = DenseVector{all_vectors.data() + i * kDimension};
|
|
ASSERT_EQ(0, index->AddWithSource(vector_data, i, source));
|
|
}
|
|
|
|
std::vector<float> query_vec(kDimension, 0.0f);
|
|
query_vec[0] = 1.0f;
|
|
VectorData query;
|
|
query.vector = DenseVector{query_vec.data()};
|
|
|
|
auto query_param = HNSWQueryParamBuilder()
|
|
.with_topk(1)
|
|
.with_fetch_vector(false)
|
|
.with_ef_search(50)
|
|
.build();
|
|
|
|
SearchResult result;
|
|
ASSERT_EQ(0, index->SearchWithSource(query, query_param, source, &result));
|
|
ASSERT_EQ(1u, result.doc_list_.size());
|
|
ASSERT_EQ(0u, result.doc_list_[0].key());
|
|
ASSERT_FLOAT_EQ(1.0f, result.doc_list_[0].score());
|
|
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name + "*");
|
|
}
|
|
TEST(IndexInterface, IsDirty) {
|
|
constexpr uint32_t kDimension = 16;
|
|
const std::string index_name{"test_is_dirty.index"};
|
|
|
|
auto test = [&](const BaseIndexParam::Pointer ¶m) {
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
|
|
// Before open: not dirty (no storage)
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
ASSERT_FALSE(index->IsDirty());
|
|
}
|
|
|
|
// Create the index file: dirty from initial metadata writes
|
|
{
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
ASSERT_TRUE(index->IsDirty());
|
|
ASSERT_EQ(0, index->Flush());
|
|
ASSERT_FALSE(index->IsDirty());
|
|
index->Close();
|
|
}
|
|
|
|
// Reopen existing file: should be clean
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, false});
|
|
ASSERT_FALSE(index->IsDirty());
|
|
|
|
// Add a vector: should become dirty
|
|
std::vector<float> vec(kDimension, 1.0f);
|
|
VectorData vd;
|
|
vd.vector = DenseVector{vec.data()};
|
|
ASSERT_EQ(0, index->Add(vd, 1));
|
|
ASSERT_TRUE(index->IsDirty());
|
|
|
|
// Flush: should become clean
|
|
ASSERT_EQ(0, index->Flush());
|
|
ASSERT_FALSE(index->IsDirty());
|
|
|
|
// Add another vector: dirty again
|
|
ASSERT_EQ(0, index->Add(vd, 2));
|
|
ASSERT_TRUE(index->IsDirty());
|
|
|
|
// Close flushes implicitly, verify no crash
|
|
index->Close();
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
};
|
|
|
|
test(FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build());
|
|
|
|
test(HNSWIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.WithEFConstruction(100)
|
|
.Build());
|
|
}
|
|
|
|
TEST(IndexInterface, IsDirtyBufferPool) {
|
|
constexpr uint32_t kDimension = 16;
|
|
const std::string index_name{"test_is_dirty_bp.index"};
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
|
|
// First create and populate the index with MMAP storage
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
index->Open(index_name, {StorageOptions::StorageType::kMMAP, true});
|
|
std::vector<float> vec(kDimension, 1.0f);
|
|
VectorData vd;
|
|
vd.vector = DenseVector{vec.data()};
|
|
ASSERT_EQ(0, index->Add(vd, 1));
|
|
index->Close();
|
|
}
|
|
|
|
// Reopen with BufferPool storage in writable mode
|
|
{
|
|
auto param = FlatIndexParamBuilder()
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithDimension(kDimension)
|
|
.WithIsSparse(false)
|
|
.Build();
|
|
auto index = IndexFactory::CreateAndInitIndex(*param);
|
|
ASSERT_NE(nullptr, index);
|
|
index->Open(index_name, {StorageOptions::StorageType::kBufferPool, true});
|
|
|
|
ASSERT_FALSE(index->IsDirty());
|
|
|
|
std::vector<float> vec(kDimension, 2.0f);
|
|
VectorData vd;
|
|
vd.vector = DenseVector{vec.data()};
|
|
ASSERT_EQ(0, index->Add(vd, 2));
|
|
ASSERT_TRUE(index->IsDirty());
|
|
|
|
ASSERT_EQ(0, index->Flush());
|
|
ASSERT_FALSE(index->IsDirty());
|
|
|
|
index->Close();
|
|
}
|
|
|
|
zvec::test_util::RemoveTestFiles(index_name);
|
|
}
|
|
|
|
TEST(IndexInterface, BuilderSetsAllBaseFields) {
|
|
auto param =
|
|
FlatIndexParamBuilder()
|
|
.WithVersion(42)
|
|
.WithIndexType(IndexType::kFlat)
|
|
.WithMetricType(MetricType::kInnerProduct)
|
|
.WithDimension(128)
|
|
.WithDataType(DataType::DT_FP32)
|
|
.WithIsSparse(true)
|
|
.WithUseIDMap(false)
|
|
.WithUseExternalVector(true)
|
|
.WithPreprocessParam(PreprocessorParam(PreprocessorType::kPCA))
|
|
.WithQuantizerParam(QuantizerParam(QuantizerType::kFP16))
|
|
.Build();
|
|
|
|
ASSERT_NE(nullptr, param);
|
|
EXPECT_EQ(42, param->version);
|
|
EXPECT_EQ(IndexType::kFlat, param->index_type);
|
|
EXPECT_EQ(MetricType::kInnerProduct, param->metric_type);
|
|
EXPECT_EQ(128, param->dimension);
|
|
EXPECT_EQ(DataType::DT_FP32, param->data_type);
|
|
EXPECT_TRUE(param->is_sparse);
|
|
EXPECT_FALSE(param->use_id_map);
|
|
EXPECT_TRUE(param->use_external_vector);
|
|
EXPECT_EQ(PreprocessorType::kPCA, param->preprocess_param.type);
|
|
EXPECT_EQ(QuantizerType::kFP16, param->quantizer_param.type);
|
|
}
|
|
|
|
TEST(IndexInterface, BuilderChainingReturnsCorrectType) {
|
|
HNSWIndexParamBuilder builder;
|
|
auto &ref = builder.WithVersion(1)
|
|
.WithMetricType(MetricType::kL2sq)
|
|
.WithDimension(64)
|
|
.WithM(16)
|
|
.WithEFConstruction(200);
|
|
auto param = ref.Build();
|
|
|
|
ASSERT_NE(nullptr, param);
|
|
EXPECT_EQ(1, param->version);
|
|
EXPECT_EQ(MetricType::kL2sq, param->metric_type);
|
|
EXPECT_EQ(64, param->dimension);
|
|
EXPECT_EQ(16, param->m);
|
|
EXPECT_EQ(200, param->ef_construction);
|
|
}
|
|
|
|
#if defined(__GNUC__) || defined(__GNUG__)
|
|
#pragma GCC diagnostic pop
|
|
#endif
|