chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:47:42 +08:00
commit be3ef883e1
1214 changed files with 431743 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
include(${PROJECT_ROOT_DIR}/cmake/option.cmake)
cc_directories(algorithm)
cc_directories(framework)
cc_directories(metric)
cc_directories(utility)
cc_directories(interface)
cc_directories(quantizer)
+21
View File
@@ -0,0 +1,21 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
include(${PROJECT_ROOT_DIR}/cmake/option.cmake)
cc_directories(cluster)
cc_directories(flat)
cc_directories(flat_sparse)
cc_directories(ivf)
cc_directories(hnsw)
cc_directories(hnsw_sparse)
cc_directories(vamana)
if(DISKANN_SUPPORTED)
message(STATUS "build diskann tests")
cc_directory(diskann)
else()
message(STATUS "skip diskann tests (unsupported platform)")
endif()
if(RABITQ_SUPPORTED)
cc_directories(hnsw_rabitq)
endif()
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_knn_cluster
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm
)
endforeach()
@@ -0,0 +1,106 @@
// 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 <cmath>
#include <random>
#include <gtest/gtest.h>
#include <zvec/ailego/container/params.h>
#include "zvec/core/framework/index_framework.h"
#include "zvec/core/framework/index_meta.h"
using namespace zvec::core;
using namespace zvec::ailego;
TEST(KmeansCluster, General) {
// Prepare index data
const uint32_t count = 5000u;
const uint32_t dimension = 33u;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
index_meta.set_metric("SquaredEuclidean", 0, zvec::ailego::Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0, 5.0);
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
// IndexCluster::Pointer cluster = std::make_shared<KmeansCluster>();
IndexCluster::Pointer cluster = IndexFactory::CreateCluster("KmeansCluster");
ASSERT_TRUE(!!cluster);
zvec::ailego::Params params;
params.set("zvec.general.cluster.count", 1);
params.set("zvec.kmeans.cluster.count", 56);
ASSERT_EQ(0, cluster->init(index_meta, params));
ASSERT_EQ(0, cluster->mount(features));
cluster->suggest(64u);
auto threads = std::make_shared<SingleQueueIndexThreads>();
std::cout << "---------- FIRST ----------\n";
std::vector<IndexCluster::Centroid> centroids;
std::vector<uint32_t> labels;
ASSERT_NE(0, cluster->classify(threads, centroids));
ASSERT_NE(0, cluster->label(threads, centroids, &labels));
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- SECOND ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- THIRD ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
ASSERT_EQ(0, cluster->classify(threads, centroids));
ASSERT_EQ(0, cluster->label(threads, centroids, &labels));
}
@@ -0,0 +1,221 @@
// 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 "cluster/multi_chunk_cluster.h"
#include <cmath>
#include <random>
#include <ailego/algorithm/kmeans.h>
#include <gtest/gtest.h>
#include <zvec/ailego/container/params.h>
#include "zvec/core/framework/index_framework.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace zvec::ailego;
TEST(MultiChunkCluster, General) {
const uint32_t count = 10000u;
const uint32_t dimension = 960u;
const uint32_t chunk_count = 480u;
const uint32_t cluster_count = 256u;
// const uint32_t thread_count = 4;
const uint32_t thread_count = 16;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
index_meta.set_metric("SquaredEuclidean", 0, Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0, 5.0);
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
MultiChunkCluster cluster = MultiChunkCluster();
Params params;
params.set("zvec.cluster.multi_chunk_cluster.count", cluster_count);
params.set("zvec.cluster.multi_chunk_cluster.chunk_count", chunk_count);
params.set("zvec.cluster.multi_chunk_cluster.thread_count", thread_count);
ASSERT_EQ(0, cluster.init(index_meta, params));
ASSERT_EQ(0, cluster.mount(features));
IndexCluster::CentroidList centroids;
std::vector<uint32_t> labels;
ASSERT_EQ(0, cluster.cluster(nullptr, centroids));
for (size_t chunk = 0; chunk < chunk_count; ++chunk) {
for (size_t cluster = 0; cluster < cluster_count; ++cluster) {
size_t idx = chunk * cluster_count + cluster;
const auto &cent = centroids[idx];
const auto &vec = cent.vector<float>();
std::cout << "chunk: " << chunk << ", cluster: " << cluster
<< ", dim: " << vec.size() << ", count: " << cent.follows()
<< " (" << cent.score() << ") { " << vec[0] << "," << vec[1]
<< " }" << std::endl;
ASSERT_EQ(0u, cent.similars().size());
}
}
ASSERT_EQ(0, cluster.label(nullptr, centroids, &labels));
}
TEST(MultiChunkCluster, TestChunk) {
const uint32_t count = 10000u;
const uint32_t dimension = 95;
const uint32_t chunk_count = 20u;
const uint32_t cluster_count = 256u;
// const uint32_t thread_count = 4;
const uint32_t thread_count = 16;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
index_meta.set_metric("SquaredEuclidean", 0, Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0, 5.0);
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
MultiChunkCluster cluster = MultiChunkCluster();
Params params;
params.set("zvec.cluster.multi_chunk_cluster.count", cluster_count);
params.set("zvec.cluster.multi_chunk_cluster.chunk_count", chunk_count);
params.set("zvec.cluster.multi_chunk_cluster.thread_count", thread_count);
ASSERT_EQ(0, cluster.init(index_meta, params));
ASSERT_EQ(0, cluster.mount(features));
IndexCluster::CentroidList centroids;
std::vector<uint32_t> labels;
ASSERT_EQ(0, cluster.cluster(nullptr, centroids));
for (size_t chunk = 0; chunk < chunk_count; ++chunk) {
for (size_t cluster = 0; cluster < cluster_count; ++cluster) {
size_t idx = chunk * cluster_count + cluster;
const auto &cent = centroids[idx];
const auto &vec = cent.vector<float>();
std::cout << "chunk: " << chunk << ", cluster: " << cluster
<< ", dim: " << vec.size() << ", count: " << cent.follows()
<< " (" << cent.score() << ") { " << vec[0] << "," << vec[1]
<< " }" << std::endl;
ASSERT_EQ(0u, cent.similars().size());
}
}
ASSERT_EQ(0, cluster.label(nullptr, centroids, &labels));
}
TEST(MultiChunkCluster, General_InnerProduct) {
const uint32_t count = 50000u;
const uint32_t dimension = 96u;
const uint32_t chunk_count = 12u;
const uint32_t cluster_count = 16u;
const uint32_t chain_length = 0;
const uint32_t thread_count = 16;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
index_meta.set_metric("InnerProduct", 0, Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
// do normalize
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
float norm = 0;
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
norm += vec[j] * vec[j];
}
norm = sqrt(norm);
for (size_t j = 0; j < dimension; ++j) {
vec[j] /= norm;
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
MultiChunkCluster cluster = MultiChunkCluster();
Params params;
params.set("zvec.cluster.multi_chunk_cluster.count", cluster_count);
params.set("zvec.cluster.multi_chunk_cluster.chunk_count", chunk_count);
params.set("zvec.cluster.multi_chunk_cluster.thread_count", thread_count);
params.set("zvec.cluster.multi_chunk_cluster.markov_chain_length",
chain_length);
ASSERT_EQ(0, cluster.init(index_meta, params));
ASSERT_EQ(0, cluster.mount(features));
IndexCluster::CentroidList centroids;
std::vector<uint32_t> labels;
ASSERT_EQ(0, cluster.cluster(nullptr, centroids));
for (size_t chunk = 0; chunk < chunk_count; ++chunk) {
for (size_t cluster = 0; cluster < cluster_count; ++cluster) {
size_t idx = chunk * cluster_count + cluster;
const auto &cent = centroids[idx];
const auto &vec = cent.vector<float>();
std::cout << "chunk: " << chunk << ", cluster: " << cluster
<< ", dim: " << vec.size() << ", count: " << cent.follows()
<< " (" << cent.score() << ") { " << vec[0] << ", " << vec[1]
<< ", " << vec[2] << ", ... , " << vec[vec.size() - 2] << ", "
<< vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, cent.similars().size());
}
}
ASSERT_EQ(0, cluster.label(nullptr, centroids, &labels));
}
@@ -0,0 +1,502 @@
// 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 <cmath>
#include <random>
#include <ailego/algorithm/kmeans.h>
#include <gtest/gtest.h>
#include <zvec/ailego/container/params.h>
#include "zvec/core/framework/index_framework.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace zvec::ailego;
TEST(OptKmeansCluster, General) {
// Prepare index data
const uint32_t count = 5000u;
const uint32_t dimension = 33u;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0, 5.0);
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
IndexCluster::Pointer cluster =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster);
Params params;
params.set("zvec.general.cluster.count", 1);
params.set("zvec.optkmeans.cluster.count", 56);
ASSERT_EQ(0, cluster->init(index_meta, params));
ASSERT_EQ(0, cluster->mount(features));
cluster->suggest(64u);
auto threads = std::make_shared<SingleQueueIndexThreads>();
std::cout << "---------- FIRST ----------\n";
std::vector<IndexCluster::Centroid> centroids;
std::vector<uint32_t> labels;
ASSERT_NE(0, cluster->classify(threads, centroids));
ASSERT_NE(0, cluster->label(threads, centroids, &labels));
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- SECOND ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- THIRD ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
ASSERT_EQ(0, cluster->classify(threads, centroids));
ASSERT_EQ(0, cluster->label(threads, centroids, &labels));
}
// TEST(OptKmeansCluster, NoEmptyCentroids) {
// // Prepare index data
// const uint32_t count = 500u;
// const uint32_t dimension = 8u;
// IndexMeta index_meta;
// index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
// index_meta.set_metric("SquaredEuclidean", 0, Params());
// std::shared_ptr<CompactIndexFeatures> features(
// new CompactIndexFeatures(index_meta));
// std::random_device rd;
// std::mt19937 gen(rd());
// std::uniform_real_distribution<float> dist(0.0, 5.0);
// for (uint32_t i = 0; i < count; ++i) {
// std::vector<float> vec(dimension);
// for (size_t j = 0; j < dimension; ++j) {
// vec[j] = dist(gen);
// }
// features->emplace(vec.data());
// }
// // Create a Kmeans cluster
// IndexCluster::Pointer cluster =
// IndexFactory::CreateCluster("OptKmeansCluster");
// ASSERT_TRUE(!!cluster);
// Params params;
// ASSERT_EQ(0, cluster->init(index_meta, params));
// ASSERT_EQ(0, cluster->mount(features));
// cluster->suggest(20u);
// auto threads = std::make_shared<SingleQueueIndexThreads>();
// std::vector<IndexCluster::Centroid> centroids;
// for (uint32_t i = 0; i < 3; ++i) {
// std::vector<float> vec(dimension);
// for (size_t j = 0; j < dimension; ++j) {
// vec[j] = NAN;
// }
// centroids.emplace_back(vec.data(), vec.size() * sizeof(float));
// }
// ASSERT_EQ(0, cluster->cluster(threads, centroids));
// ASSERT_EQ(3u, centroids.size());
// for (uint32_t i = 0; i < 3; ++i) {
// std::vector<float> vec(dimension);
// for (size_t j = 0; j < dimension; ++j) {
// vec[j] = dist(gen);
// }
// centroids.emplace_back(vec.data(), vec.size() * sizeof(float));
// }
// ASSERT_EQ(0, cluster->cluster(threads, centroids));
// ASSERT_EQ(6u, centroids.size());
// for (uint32_t i = 0; i < 3; ++i) {
// std::vector<float> vec(dimension);
// for (size_t j = 0; j < dimension; ++j) {
// vec[j] = NAN;
// }
// centroids.emplace_back(vec.data(), vec.size() * sizeof(float));
// }
// ASSERT_EQ(0, cluster->cluster(threads, centroids));
// ASSERT_EQ(9u, centroids.size());
// for (uint32_t i = 0; i < 3; ++i) {
// std::vector<float> vec(dimension);
// for (size_t j = 0; j < dimension; ++j) {
// vec[j] = dist(gen);
// }
// centroids.emplace_back(vec.data(), vec.size() * sizeof(float));
// }
// ASSERT_EQ(0, cluster->cluster(threads, centroids));
// ASSERT_EQ(12u, centroids.size());
// for (const auto &it : centroids) {
// const auto &vec = it.vector<float>();
// std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ",
// "
// << vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() -
// 2]
// << ", " << vec[vec.size() - 1] << " }" << std::endl;
// }
// params.set("zvec.optkmeans.cluster.purge_empty", true);
// cluster->update(params);
// ASSERT_EQ(12u, centroids.size());
// ASSERT_EQ(0, cluster->cluster(threads, centroids));
// ASSERT_EQ(7u, centroids.size());
// for (const auto &it : centroids) {
// const auto &vec = it.vector<float>();
// std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ",
// "
// << vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() -
// 2]
// << ", " << vec[vec.size() - 1] << " }" << std::endl;
// }
// }
TEST(OptKmeansCluster, IN4General) {
// Prepare index data
const uint32_t count = 5000u;
const uint32_t dimension = 64u;
const uint32_t dimension_wrong = 66u;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_INT4, dimension);
index_meta.set_metric("SquaredEuclidean", 0, Params());
IndexMeta index_meta_wrong;
index_meta_wrong.set_meta(IndexMeta::DataType::DT_INT4, dimension_wrong);
index_meta_wrong.set_metric("SquaredEuclidean", 0, Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::shared_ptr<CompactIndexFeatures> features_wrong(
new CompactIndexFeatures(index_meta_wrong));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<unsigned short> dist(0, UINT8_MAX);
for (uint32_t i = 0; i < count; ++i) {
std::vector<uint8_t> vec(dimension / 2);
std::vector<uint8_t> vec_wrong(dimension_wrong / 2);
for (size_t j = 0; j < dimension / 2; ++j) {
vec[j] = dist(gen);
}
for (size_t j = 0; j < dimension_wrong / 2; ++j) {
vec_wrong[j] = dist(gen);
}
features->emplace(vec.data());
features_wrong->emplace(vec_wrong.data());
}
// Create a OptKmeans cluster
IndexCluster::Pointer cluster =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster);
Params params;
ASSERT_EQ(0, cluster->init(index_meta_wrong, params));
ASSERT_NE(0, cluster->mount(features_wrong));
params.set("zvec.general.cluster.count", 1);
params.set("zvec.optkmeans.cluster.count", 56);
ASSERT_EQ(0, cluster->init(index_meta, params));
ASSERT_EQ(0, cluster->mount(features));
cluster->suggest(64u);
auto threads = std::make_shared<SingleQueueIndexThreads>();
std::cout << "---------- FIRST ----------\n";
std::vector<IndexCluster::Centroid> centroids;
std::vector<uint32_t> labels;
ASSERT_NE(0, cluster->classify(threads, centroids));
ASSERT_NE(0, cluster->label(threads, centroids, &labels));
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- SECOND ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- THIRD ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
ASSERT_EQ(0, cluster->classify(threads, centroids));
ASSERT_EQ(0, cluster->label(threads, centroids, &labels));
}
TEST(OptKmeansCluster, IN4Correctness) {
// Prepare index data
const uint32_t count = 5000u;
const uint32_t dimension = 64u;
IndexMeta index_meta1;
index_meta1.set_meta(IndexMeta::DataType::DT_INT8, dimension);
index_meta1.set_metric("SquaredEuclidean", 0, Params());
IndexMeta index_meta2;
index_meta2.set_meta(IndexMeta::DataType::DT_INT4, dimension);
index_meta2.set_metric("SquaredEuclidean", 0, Params());
std::shared_ptr<CompactIndexFeatures> features1(
new CompactIndexFeatures(index_meta1));
std::shared_ptr<CompactIndexFeatures> features2(
new CompactIndexFeatures(index_meta2));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(-8, 7);
// Generate features
for (size_t i = 0; i < count; ++i) {
NumericalVector<int8_t> vec1(dimension);
NibbleVector<int32_t> vec2(dimension);
for (size_t j = 0; j < dimension; ++j) {
int8_t val = (int8_t)dist(gen);
vec1[j] = val;
vec2.set(j, val);
}
features1->emplace(vec1.data());
features2->emplace(vec2.data());
}
// Create a OptKmeans cluster of int8, and cluster only once
IndexCluster::Pointer cluster_once =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster_once);
Params params_once;
params_once.set("zvec.general.cluster.count", 65);
params_once.set("zvec.optkmeans.cluster.count", 63);
params_once.set("zvec.optkmeans.cluster.max_iterations", 1);
// Use KMC2 to init centroids
params_once.set("zvec.optkmeans.cluster.markov_chain_length", 20);
ASSERT_EQ(0, cluster_once->init(index_meta1, params_once));
ASSERT_EQ(0, cluster_once->mount(features1));
cluster_once->suggest(63);
auto threads = std::make_shared<SingleQueueIndexThreads>();
// Cluster once and get centroids
std::vector<IndexCluster::Centroid> centroids1;
ASSERT_EQ(0, cluster_once->cluster(threads, centroids1));
// Use centroids_one as init centroids to both int8 and int4 cluster
// Create a int8 cluster
IndexCluster::Pointer cluster_int8 =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster_int8);
Params params_int8;
params_int8.set("zvec.general.cluster.count", 65);
params_int8.set("zvec.optkmeans.cluster.count", 63);
ASSERT_EQ(0, cluster_int8->init(index_meta1, params_int8));
ASSERT_EQ(0, cluster_int8->mount(features1));
cluster_int8->suggest(63u);
// Create a int4 cluster
IndexCluster::Pointer cluster_int4 =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster_int4);
Params params_int4;
params_int4.set("zvec.general.cluster.count", 65);
params_int4.set("zvec.optkmeans.cluster.count", 63);
ASSERT_EQ(0, cluster_int4->init(index_meta2, params_int4));
ASSERT_EQ(0, cluster_int4->mount(features2));
cluster_int4->suggest(63u);
std::vector<IndexCluster::Centroid> centroids2;
// Use centroids of int8 to init centroids of int4
for (size_t i = 0; i < centroids1.size(); ++i) {
NibbleVector<int8_t> nvec;
nvec.assign(reinterpret_cast<const int8_t *>(centroids1[i].feature()),
dimension);
IndexCluster::Centroid curr_centroid;
curr_centroid.set_score(centroids1[i].score());
curr_centroid.set_follows(centroids1[i].follows());
curr_centroid.set_feature(nvec.data(), nvec.dimension() >> 1);
centroids2.push_back(curr_centroid);
}
ASSERT_EQ(0, cluster_int8->cluster(threads, centroids1));
ASSERT_EQ(0, cluster_int4->cluster(threads, centroids2));
EXPECT_EQ(centroids1.size(), centroids2.size());
for (size_t i = 0; i < centroids1.size(); ++i) {
EXPECT_EQ(centroids1[i].follows(), centroids2[i].follows());
EXPECT_DOUBLE_EQ(centroids1[i].score(), centroids2[i].score());
}
}
TEST(OptKmeansCluster, InnerProduct) {
// Prepare index data
const uint32_t count = 5000u;
const uint32_t dimension = 33u;
IndexMeta index_meta;
index_meta.set_meta(IndexMeta::DataType::DT_FP32, dimension);
index_meta.set_metric("InnerProduct", 0, Params());
std::shared_ptr<CompactIndexFeatures> features(
new CompactIndexFeatures(index_meta));
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
for (uint32_t i = 0; i < count; ++i) {
std::vector<float> vec(dimension);
for (size_t j = 0; j < dimension; ++j) {
vec[j] = dist(gen);
}
features->emplace(vec.data());
}
// Create a Kmeans cluster
IndexCluster::Pointer cluster =
IndexFactory::CreateCluster("OptKmeansCluster");
ASSERT_TRUE(!!cluster);
Params params;
params.set("zvec.general.cluster.count", 1);
params.set("zvec.optkmeans.cluster.count", 56);
ASSERT_EQ(0, cluster->init(index_meta, params));
ASSERT_EQ(0, cluster->mount(features));
cluster->suggest(64u);
auto threads = std::make_shared<SingleQueueIndexThreads>();
std::cout << "---------- FIRST ----------\n";
std::vector<IndexCluster::Centroid> centroids;
std::vector<uint32_t> labels;
ASSERT_NE(0, cluster->classify(threads, centroids));
ASSERT_NE(0, cluster->label(threads, centroids, &labels));
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- SECOND ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
std::cout << "---------- THIRD ----------\n";
ASSERT_EQ(0, cluster->cluster(threads, centroids));
for (const auto &it : centroids) {
const auto &vec = it.vector<float>();
std::cout << it.follows() << " (" << it.score() << ") { " << vec[0] << ", "
<< vec[1] << ", " << vec[2] << ", ... , " << vec[vec.size() - 2]
<< ", " << vec[vec.size() - 1] << " }" << std::endl;
ASSERT_EQ(0u, it.similars().size());
}
ASSERT_EQ(0, cluster->classify(threads, centroids));
ASSERT_EQ(0, cluster->label(threads, centroids, &labels));
}
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_cluster core_plugin core_knn_diskann
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/diskann
)
endforeach()
@@ -0,0 +1,169 @@
// 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 "diskann_builder.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <chrono>
#include <future>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include <zvec/core/framework/index_framework.h>
#include "diskann_holder.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
constexpr size_t static dim = 64;
class DiskAnnBuilderTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
static std::string _dir;
static shared_ptr<IndexMeta> _index_meta_ptr;
};
std::string DiskAnnBuilderTest::_dir("DiskAnnBuilderTest");
shared_ptr<IndexMeta> DiskAnnBuilderTest::_index_meta_ptr;
void DiskAnnBuilderTest::SetUp(void) {
LoggerBroker::SetLevel(Logger::LEVEL_INFO);
_index_meta_ptr.reset(new (nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
_index_meta_ptr->set_metric("SquaredEuclidean", 0, Params());
}
void DiskAnnBuilderTest::TearDown(void) {
char cmdBuf[100];
snprintf(cmdBuf, 100, "rm -rf %s", _dir.c_str());
system(cmdBuf);
}
TEST_F(DiskAnnBuilderTest, TestGeneral) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 50);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestGeneral";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
}
// Regression test: building a small DiskAnn index must complete quickly.
// A lost-wakeup bug in the condition-variable progress loops previously caused
// 1530 second stalls during train/build on small datasets because
// notify_one() was either missing or racing against a wrong predicate.
TEST_F(DiskAnnBuilderTest, SmallDatasetBuildTime) {
constexpr size_t kSmallDim = 4;
constexpr size_t kSmallDocCnt = 12;
auto meta = make_shared<IndexMeta>(IndexMeta::DataType::DT_FP32, kSmallDim);
meta->set_metric("SquaredEuclidean", 0, Params());
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder = make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
kSmallDim);
for (size_t i = 0; i < kSmallDocCnt; ++i) {
NumericalVector<float> vec(kSmallDim, static_cast<float>(i));
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 50);
params.set("zvec.diskann.builder.max_pq_chunk_num", 2);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*meta, params));
auto t0 = std::chrono::steady_clock::now();
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto t1 = std::chrono::steady_clock::now();
auto elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
// Before the fix, this took 1530 seconds. After the fix, it should
// complete in well under 5 seconds even on slow CI machines.
EXPECT_LT(elapsed_ms, 5000)
<< "DiskAnn build with " << kSmallDocCnt << " vectors took " << elapsed_ms
<< " ms — likely a lost-wakeup regression in progress loops.";
}
// DiskAnn is now exposed implicitly: no caller ever invokes a
// ``LoadDiskAnnPlugin`` / ``IsLibAioAvailable`` API (those were removed from
// the public surface together with ``zvec.load_diskann_plugin()`` in Python).
// The only contract this test validates is the UX guarantee: once the DiskAnn
// module has been linked into the hosting binary (here, directly into the
// test via the ``core_knn_diskann`` target), its factory entries are
// registered automatically and the global ``IndexFactory`` can hand out a
// ``DiskAnnBuilder`` without any explicit setup step. On hosts missing
// libaio, DiskAnn would fail at the index-creation layer with a clear error
// while other index types (HNSW/IVF/Flat/Vamana) remain unaffected; that
// runtime branch lives in ``DiskAnnIndex::CreateAndInitStreamer`` and is
// covered by the higher-level interface tests.
TEST_F(DiskAnnBuilderTest, TestImplicitFactoryRegistration) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr)
<< "DiskAnnBuilder factory entry missing: DiskAnn must be available "
"without any manual plugin load step.";
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("DiskAnnStreamer");
ASSERT_NE(streamer, nullptr)
<< "DiskAnnStreamer factory entry missing: DiskAnn must be available "
"without any manual plugin load step.";
}
@@ -0,0 +1,816 @@
// 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 "diskann_searcher.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <ailego/math/distance.h>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include <zvec/core/framework/index_framework.h>
#include "diskann_holder.h"
#include "diskann_params.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
constexpr size_t static dim = 64;
class DiskAnnSearcherTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
static std::string _dir;
static shared_ptr<IndexMeta> _index_meta_ptr;
};
std::string DiskAnnSearcherTest::_dir("DiskAnnSearcherTest/");
shared_ptr<IndexMeta> DiskAnnSearcherTest::_index_meta_ptr;
void DiskAnnSearcherTest::SetUp(void) {
LoggerBroker::SetLevel(Logger::LEVEL_INFO);
_index_meta_ptr.reset(new (nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
_index_meta_ptr->set_metric("SquaredEuclidean", 0, Params());
}
void DiskAnnSearcherTest::TearDown(void) {
char cmdBuf[100];
snprintf(cmdBuf, 100, "rm -rf %s", _dir.c_str());
system(cmdBuf);
}
TEST_F(DiskAnnSearcherTest, TestGeneral) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestGeneral";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
auto ctx = searcher->create_context();
ASSERT_TRUE(!!ctx);
auto linearCtx = searcher->create_context();
auto linearByPKeysCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
ASSERT_TRUE(!!linearCtx);
ASSERT_TRUE(!!linearByPKeysCtx);
ASSERT_TRUE(!!knnCtx);
NumericalVector<float> vec(dim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t topk = 200;
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
linearCtx->set_topk(topk);
linearByPKeysCtx->set_topk(topk);
knnCtx->set_topk(topk);
// do linear search test
{
float query[dim];
for (size_t i = 0; i < dim; ++i) {
query[i] = 3.1f;
}
ASSERT_EQ(0, searcher->search_bf_impl(query, qmeta, linearCtx));
auto &linearResult = linearCtx->result();
ASSERT_EQ(3UL, linearResult[0].key());
ASSERT_EQ(4UL, linearResult[1].key());
ASSERT_EQ(2UL, linearResult[2].key());
ASSERT_EQ(5UL, linearResult[3].key());
ASSERT_EQ(1UL, linearResult[4].key());
ASSERT_EQ(6UL, linearResult[5].key());
ASSERT_EQ(0UL, linearResult[6].key());
ASSERT_EQ(7UL, linearResult[7].key());
for (size_t i = 8; i < topk; ++i) {
ASSERT_EQ(i, linearResult[i].key());
}
}
// do linear search by p_keys test
std::vector<std::vector<uint64_t>> p_keys;
p_keys.resize(1);
p_keys[0] = {8, 9, 10, 11, 3, 2, 1, 0};
{
float query[dim];
for (size_t i = 0; i < dim; ++i) {
query[i] = 3.1f;
}
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(query, p_keys, qmeta,
linearByPKeysCtx));
auto &linearByPKeysResult = linearByPKeysCtx->result();
ASSERT_EQ(8, linearByPKeysResult.size());
ASSERT_EQ(3UL, linearByPKeysResult[0].key());
ASSERT_EQ(2UL, linearByPKeysResult[1].key());
ASSERT_EQ(1UL, linearByPKeysResult[2].key());
ASSERT_EQ(0UL, linearByPKeysResult[3].key());
ASSERT_EQ(8UL, linearByPKeysResult[4].key());
ASSERT_EQ(9UL, linearByPKeysResult[5].key());
ASSERT_EQ(10UL, linearByPKeysResult[6].key());
ASSERT_EQ(11UL, linearByPKeysResult[7].key());
}
size_t step = 500;
for (size_t i = 0; i < doc_cnt; i += step) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
auto t1 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
auto t2 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto t3 = Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
auto &knnResult = knnCtx->result();
// TODO: check
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * step * step * 1.0f / totalCnts;
float topk1Recall = topk1Hits * step * 1.0f / doc_cnt;
float cost = linearTotalTime * 1.0f / knnTotalTime;
EXPECT_GT(recall, 0.90f);
EXPECT_GT(topk1Recall, 0.80f);
EXPECT_GT(cost, 2.0f);
}
TEST_F(DiskAnnSearcherTest, TestNodeCache) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestNodeCache";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.cache_node_num", 32);
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
auto ctx = searcher->create_context();
ASSERT_TRUE(!!ctx);
auto linearCtx = searcher->create_context();
auto linearByPKeysCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
ASSERT_TRUE(!!linearCtx);
ASSERT_TRUE(!!linearByPKeysCtx);
ASSERT_TRUE(!!knnCtx);
NumericalVector<float> vec(dim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t topk = 200;
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
linearCtx->set_topk(topk);
linearByPKeysCtx->set_topk(topk);
knnCtx->set_topk(topk);
size_t step = 500;
for (size_t i = 0; i < doc_cnt; i += step) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
auto t1 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
auto t2 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto t3 = Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
auto &knnResult = knnCtx->result();
// TODO: check
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * step * step * 1.0f / totalCnts;
float topk1Recall = topk1Hits * step * 1.0f / doc_cnt;
float cost = linearTotalTime * 1.0f / knnTotalTime;
EXPECT_GT(recall, 0.90f);
EXPECT_GT(topk1Recall, 0.80f);
EXPECT_GT(cost, 2.0f);
}
TEST_F(DiskAnnSearcherTest, TestFilter) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestFilter";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.cache_node_num", 32);
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
auto ctx = searcher->create_context();
ASSERT_TRUE(!!ctx);
auto linearCtx = searcher->create_context();
auto linearByPKeysCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
ASSERT_TRUE(!!linearCtx);
ASSERT_TRUE(!!linearByPKeysCtx);
ASSERT_TRUE(!!knnCtx);
NumericalVector<float> vec(dim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t topk = 200;
linearCtx->set_topk(topk);
linearByPKeysCtx->set_topk(topk);
knnCtx->set_topk(topk);
size_t key = 50;
for (size_t j = 0; j < dim; ++j) {
vec[j] = key + 0.1f;
}
// no filter
{
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
ASSERT_EQ(50UL, knnResult[0].key());
ASSERT_EQ(51UL, knnResult[1].key());
ASSERT_EQ(49UL, knnResult[2].key());
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(50UL, linearResult[0].key());
ASSERT_EQ(51UL, linearResult[1].key());
ASSERT_EQ(49UL, linearResult[2].key());
}
// with filter
{
auto filterFunc = [](uint64_t key) {
if (key == 50UL || key == 51UL || key == 49UL) {
return true;
}
return false;
};
knnCtx->set_filter(filterFunc);
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
ASSERT_EQ(52UL, knnResult[0].key());
ASSERT_EQ(48UL, knnResult[1].key());
ASSERT_EQ(53UL, knnResult[2].key());
linearCtx->set_filter(filterFunc);
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(52UL, linearResult[0].key());
ASSERT_EQ(48UL, linearResult[1].key());
ASSERT_EQ(53UL, linearResult[2].key());
}
}
TEST_F(DiskAnnSearcherTest, TestGroup) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i / 10.0;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestGroup";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
auto ctx = searcher->create_context();
ASSERT_TRUE(!!ctx);
NumericalVector<float> vec(dim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t group_topk = 20;
uint64_t total_time = 0;
auto groupbyFunc = [](uint64_t key) {
uint32_t group_id = key / 10 % 10;
// std::cout << "key: " << key << ", group id: " << group_id << std::endl;
return std::string("g_") + std::to_string(group_id);
};
size_t group_num = 5;
ctx->set_group_params(group_num, group_topk);
ctx->set_group_by(groupbyFunc);
size_t query_value = doc_cnt / 2;
for (size_t j = 0; j < dim; ++j) {
vec[j] = query_value / 10 + 0.1f;
}
auto t1 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, ctx));
auto t2 = Realtime::MicroSeconds();
total_time += t2 - t1;
auto &group_result = ctx->group_result();
for (uint32_t i = 0; i < group_result.size(); ++i) {
const std::string &group_id = group_result[i].group_id();
auto &result = group_result[i].docs();
ASSERT_GT(result.size(), 0);
std::cout << "Group ID: " << group_id << std::endl;
for (uint32_t j = 0; j < result.size(); ++j) {
std::cout << "\tKey: " << result[j].key() << std::fixed
<< std::setprecision(3) << ", Score: " << result[j].score()
<< std::endl;
}
}
#if 0
// do linear search by p_keys test
auto groupbyFuncLinear = [](uint64_t key) {
uint32_t group_id = key % 10;
return std::string("g_") + std::to_string(group_id);
};
auto linear_pk_ctx = searcher->create_context();
linear_pk_ctx->set_group_params(group_num, group_topk);
linear_pk_ctx->set_group_by(groupbyFuncLinear);
std::vector<std::vector<uint64_t>> p_keys;
p_keys.resize(1);
p_keys[0] = {4, 3, 2, 1, 5, 6, 7, 8, 9, 10};
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(vec.data(), p_keys, qmeta,
linear_pk_ctx));
auto &linear_by_pkeys_group_result = linear_pk_ctx->group_result();
ASSERT_EQ(linear_by_pkeys_group_result.size(), group_num);
for (uint32_t i = 0; i < linear_by_pkeys_group_result.size(); ++i) {
const std::string &group_id = linear_by_pkeys_group_result[i].group_id();
auto &result = linear_by_pkeys_group_result[i].docs();
ASSERT_GT(result.size(), 0);
std::cout << "Group ID: " << group_id << std::endl;
for (uint32_t j = 0; j < result.size(); ++j) {
std::cout << "\tKey: " << result[j].key() << std::fixed
<< std::setprecision(3) << ", Score: " << result[j].score()
<< std::endl;
}
ASSERT_EQ(10 - i, result[0].key());
}
#endif
}
TEST_F(DiskAnnSearcherTest, TestFetchVector) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestFetchVector";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
size_t query_cnt = 20U;
auto linearCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
auto linearByPKeysCtx = searcher->create_context();
knnCtx->set_fetch_vector(true);
for (size_t i = 0; i < doc_cnt; i += doc_cnt / 10) {
std::string vec_value;
ASSERT_EQ(0, searcher->get_vector(i, linearCtx, vec_value));
float vector_value = *(const float *)(vec_value.data());
ASSERT_EQ(vector_value, i);
}
size_t topk = 200;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t i = 0; i < query_cnt; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
auto t1 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
auto t2 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto t3 = Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
ASSERT_NE(knnResult[0].vector_string(), "");
float vector_value = *((float *)(knnResult[0].vector_string().data()));
ASSERT_EQ(vector_value, i);
}
}
TEST_F(DiskAnnSearcherTest, TestRnnSearch) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("DiskAnnBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 10000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set("zvec.diskann.builder.max_degree", 32);
params.set("zvec.diskann.builder.list_size", 300);
params.set("zvec.diskann.builder.max_pq_chunk_num", 32);
params.set("zvec.diskann.builder.threads", 4);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "/TestRnnSearch";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(doc_cnt, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_GT(stats.trained_costtime(), 0UL);
ASSERT_GT(stats.built_costtime(), 0UL);
// test searcher
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("DiskAnnSearcher");
ASSERT_TRUE(searcher != nullptr);
Params search_params;
search_params.set("zvec.diskann.searcher.list_size", 500);
ASSERT_EQ(0, searcher->init(search_params));
auto storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, storage->open(path, false));
ASSERT_EQ(0, searcher->load(storage, IndexMetric::Pointer()));
auto ctx = searcher->create_context();
ASSERT_TRUE(!!ctx);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 0.0;
}
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t topk = 50;
ctx->set_topk(topk);
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, ctx));
auto &results = ctx->result();
ASSERT_EQ(topk, results.size());
float radius = results[topk / 2].score();
ctx->set_threshold(radius);
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, ctx));
ASSERT_GT(topk, results.size());
for (size_t k = 0; k < results.size(); ++k) {
ASSERT_GE(radius, results[k].score());
}
// Test Reset Threshold
ctx->reset_threshold();
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, ctx));
ASSERT_EQ(topk, results.size());
ASSERT_LT(radius, results[topk - 1].score());
}
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_flat
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm
)
endforeach()
@@ -0,0 +1,334 @@
// 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 "flat/flat_builder.h"
#include <future>
#include <iostream>
#include <vector>
#include <gtest/gtest.h>
#include "tests/test_util.h"
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
static inline size_t RandomDimension(void) {
std::mt19937 gen((std::random_device())());
return (std::uniform_int_distribution<size_t>(1, 129))(gen);
}
static size_t DIMENSION = RandomDimension();
class FlatBuilderTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
public:
static std::string dir_;
static IndexMeta meta_;
};
std::string FlatBuilderTest ::dir_("flat_builder_test/");
IndexMeta FlatBuilderTest::meta_;
void FlatBuilderTest::SetUp(void) {
meta_.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
}
//! self-check column-major and row-major search.
void FlatBuilderTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
void build_process(IndexBuilder::Pointer &builder,
IndexHolder::Pointer holder) {
Params params;
ASSERT_EQ(0, builder->init(FlatBuilderTest::meta_, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
std::string path = FlatBuilderTest::dir_ + "TestGeneral";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(0UL, stats.discarded_count());
}
TEST_F(FlatBuilderTest, TestInitSuccess) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
}
TEST_F(FlatBuilderTest, TestInitFailedWithInvalidMeasure) {
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
meta_.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
meta_.set_metric("invalid", 0, Params());
Params params;
int ret = builder->init(meta_, params);
EXPECT_EQ(IndexError_InvalidArgument, ret);
}
TEST_F(FlatBuilderTest, TestInt8InvalidColumnMajor) {
size_t dim = (DIMENSION + 3) / 4 * 4;
meta_.set_meta(IndexMeta::DataType::DT_INT8, dim + 2);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
ASSERT_EQ(IndexMeta::MO_COLUMN, meta_.major_order());
Params params;
ASSERT_NE(0, builder->init(meta_, params));
}
TEST_F(FlatBuilderTest, TestInt8WithRandomDimension) {
size_t dim = DIMENSION;
meta_.set_meta(IndexMeta::DataType::DT_INT8, dim);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_UNDEFINED);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
}
TEST_F(FlatBuilderTest, TestBuildWithRowMajor) {
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_ROW);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_FP32>>(DIMENSION);
size_t doc_cnt = 2000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
int ret = builder->train(holder);
EXPECT_EQ(0, ret);
ret = builder->build(holder);
EXPECT_EQ(0, ret);
}
TEST_F(FlatBuilderTest, TestInt8BuildWithRowMajor) {
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_meta(IndexMeta::DT_INT8, DIMENSION);
meta_.set_major_order(IndexMeta::MO_ROW);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_INT8>>(DIMENSION);
size_t doc_cnt = 128UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<int8_t> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = (int8_t)(i % 128);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
int ret = builder->train(holder);
EXPECT_EQ(0, ret);
ret = builder->build(holder);
EXPECT_EQ(0, ret);
}
TEST_F(FlatBuilderTest, TestBuildWithColumnMajor) {
meta_.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_FP32>>(DIMENSION);
size_t doc_cnt = 2000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
int ret = builder->train(holder);
EXPECT_EQ(0, ret);
ret = builder->build(holder);
EXPECT_EQ(0, ret);
}
TEST_F(FlatBuilderTest, TestInt8BuildWithColumnMajor) {
size_t dim = (DIMENSION + 3) / 4 * 4;
meta_.set_meta(IndexMeta::DataType::DT_INT8, dim);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
ASSERT_EQ(0, builder->init(meta_, params));
std::string path = dir_ + "TestGeneral";
auto holder = std::make_shared<OnePassIndexHolder<IndexMeta::DT_INT8>>(dim);
size_t doc_cnt = 128UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<int8_t> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = (int8_t)(i % 128);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
int ret = builder->train(holder);
EXPECT_EQ(0, ret);
ret = builder->build(holder);
EXPECT_EQ(0, ret);
}
TEST_F(FlatBuilderTest, TestWithRowMajor) {
meta_.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_ROW);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_FP32>>(DIMENSION);
size_t doc_cnt = 2000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
build_process(builder, holder);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
}
TEST_F(FlatBuilderTest, TestInt8WithRowMajor) {
meta_.set_meta(IndexMeta::DataType::DT_INT8, DIMENSION);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_ROW);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_INT8>>(DIMENSION);
size_t doc_cnt = 128UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<int8_t> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = (int8_t)(i % 128);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
build_process(builder, holder);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
}
TEST_F(FlatBuilderTest, TestWithColumnMajor) {
meta_.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
std::string path = dir_ + "TestGeneral";
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DT_FP32>>(DIMENSION);
size_t doc_cnt = 2000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
build_process(builder, holder);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
}
TEST_F(FlatBuilderTest, TestInt8WithColumnMajor) {
size_t dim = (DIMENSION + 3) / 4 * 4;
meta_.set_meta(IndexMeta::DataType::DT_INT8, dim);
meta_.set_metric("SquaredEuclidean", 0, Params());
meta_.set_major_order(IndexMeta::MO_COLUMN);
IndexBuilder::Pointer builder = IndexFactory::CreateBuilder("FlatBuilder");
ASSERT_NE(builder, nullptr);
Params params;
std::string path = dir_ + "TestGeneral";
auto holder = std::make_shared<OnePassIndexHolder<IndexMeta::DT_INT8>>(dim);
size_t doc_cnt = 128UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<int8_t> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = (int8_t)(i % 128);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
build_process(builder, holder);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,623 @@
#include <future>
#include <string>
#include <vector>
#include <ailego/utility/math_helper.h>
#include <ailego/utility/memory_helper.h>
#include <gtest/gtest.h>
#include <zvec/core/framework/index_framework.h>
#include <zvec/core/framework/index_streamer.h>
#include "tests/test_util.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
constexpr size_t static dim = 16;
class FlatStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
void hybrid_scale(std::vector<float> &dense_value,
std::vector<float> &sparse_value, float alpha_scale);
static std::string dir_;
static std::shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string FlatStreamerTest::dir_("flat_streamer_buffer_test_dir/");
std::shared_ptr<IndexMeta> FlatStreamerTest::index_meta_ptr_;
void FlatStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (std::nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
index_meta_ptr_->set_metric("SquaredEuclidean", 0, Params());
zvec::test_util::RemoveTestPath(dir_);
}
void FlatStreamerTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
TEST_F(FlatStreamerTest, TestLinearSearch) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/LinearSearch", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/LinearSearch", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
read_streamer->close();
read_streamer.reset();
}
TEST_F(FlatStreamerTest, TestLinearSearchBuffer) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/LinearSearchBuffer", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/LinearSearchBuffer", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
read_streamer->close();
read_streamer.reset();
}
TEST_F(FlatStreamerTest, TestLinearSearchBufferMMap) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/LinearSearchBuffer", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/LinearSearchBuffer", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
read_streamer->close();
read_streamer.reset();
}
TEST_F(FlatStreamerTest, TestLinearSearchWithLRU) {
MemoryLimitPool::get_instance().init(100 * 1024UL * 1024UL);
#ifdef __ANDROID__
GTEST_SKIP()
<< "Skipped on Android: requires ~6GB memory/disk (emulator limit)";
#endif
constexpr size_t static dim = 1600;
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
IndexMeta meta = IndexMeta(IndexMeta::DataType::DT_FP32, dim);
meta.set_metric("SquaredEuclidean", 0, Params());
ASSERT_EQ(0, write_streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/Test/LinearSearchWithLRU", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 50000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(meta, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "/Test/LinearSearchWithLRU", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
ElapsedTime elapsed_time;
for (size_t i = 0; i < 10; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
read_streamer->close();
read_streamer.reset();
}
TEST_F(FlatStreamerTest, TestLinearSearchMMap) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/LinearSearchMMap", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/LinearSearchMMap", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
for (size_t j = 0; j < dim; ++j) {
const float *data = (float *)provider->get_vector(result1[0].key());
EXPECT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
read_streamer->close();
read_streamer.reset();
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
@@ -0,0 +1,231 @@
#include <future>
#include <string>
#include <vector>
#include <ailego/utility/math_helper.h>
#include <ailego/utility/memory_helper.h>
#include <gtest/gtest.h>
#include <zvec/ailego/utility/file_helper.h>
#include <zvec/core/framework/index_framework.h>
#include <zvec/core/framework/index_streamer.h>
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
constexpr size_t static dim = 128;
class FlatStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
void hybrid_scale(std::vector<float> &dense_value,
std::vector<float> &sparse_value, float alpha_scale);
static std::string dir_;
static std::shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string FlatStreamerTest::dir_("streamer_test/");
std::shared_ptr<IndexMeta> FlatStreamerTest::index_meta_ptr_;
void FlatStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (std::nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
index_meta_ptr_->set_metric("SquaredEuclidean", 0, Params());
zvec::ailego::FileHelper::RemovePath(dir_.c_str());
}
void FlatStreamerTest::TearDown(void) {
zvec::ailego::FileHelper::RemovePath(dir_.c_str());
}
TEST_F(FlatStreamerTest, TestLinearSearchMMap) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/Test/LinearSearchMMap", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t data_cnt = 300000UL, cnt = 500UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < data_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "/Test/LinearSearchMMap", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 30;
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result1 = ctx->result();
// ASSERT_EQ(topk, result1.size());
// ASSERT_EQ(i, result1[0].key());
// for (size_t j = 0; j < dim; ++j) {
// vec[j] = i + 0.1f;
// }
// ctx->set_topk(topk);
// ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result2 = ctx->result();
// ASSERT_EQ(topk, result2.size());
// ASSERT_EQ(i, result2[0].key());
// ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
// ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.micro_seconds() << " us" << endl;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result1 = ctx->result();
// ASSERT_EQ(topk, result1.size());
// ASSERT_EQ(i, result1[0].key());
// for (size_t j = 0; j < dim; ++j) {
// vec[j] = i + 0.1f;
// }
// ctx->set_topk(topk);
// ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result2 = ctx->result();
// ASSERT_EQ(topk, result2.size());
// ASSERT_EQ(i, result2[0].key());
// ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
// ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.micro_seconds() << " us" << endl;
read_streamer->close();
read_streamer.reset();
}
TEST_F(FlatStreamerTest, TestLinearSearchBuffer) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/Test/LinearSearchBuffer", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t data_cnt = 300000UL, cnt = 500UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < data_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "/Test/LinearSearchBuffer", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 30;
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result1 = ctx->result();
// ASSERT_EQ(topk, result1.size());
// ASSERT_EQ(i, result1[0].key());
// for (size_t j = 0; j < dim; ++j) {
// vec[j] = i + 0.1f;
// }
// ctx->set_topk(topk);
// ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result2 = ctx->result();
// ASSERT_EQ(topk, result2.size());
// ASSERT_EQ(i, result2[0].key());
// ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
// ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.micro_seconds() << " us" << endl;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result1 = ctx->result();
// ASSERT_EQ(topk, result1.size());
// ASSERT_EQ(i, result1[0].key());
// for (size_t j = 0; j < dim; ++j) {
// vec[j] = i + 0.1f;
// }
// ctx->set_topk(topk);
// ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
// auto &result2 = ctx->result();
// ASSERT_EQ(topk, result2.size());
// ASSERT_EQ(i, result2[0].key());
// ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
// ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
cout << "Elapsed time: " << elapsed_time.micro_seconds() << " us" << endl;
read_streamer->close();
read_streamer.reset();
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_flat_sparse
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm
)
endforeach()
@@ -0,0 +1,300 @@
// 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 "flat_sparse/flat_sparse_builder.h"
#include <future>
#include <iostream>
#include <vector>
#include <gtest/gtest.h>
#include "tests/test_util.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
class FlatSparseBuilderTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
static std::string _dir;
static shared_ptr<IndexMeta> _index_meta_ptr;
};
std::string FlatSparseBuilderTest::_dir("FlatSparseBuilderTest/");
shared_ptr<IndexMeta> FlatSparseBuilderTest::_index_meta_ptr;
void FlatSparseBuilderTest::SetUp(void) {
_index_meta_ptr.reset(new (nothrow) IndexMeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32));
_index_meta_ptr->set_metric("InnerProductSparse", 0, Params());
}
void FlatSparseBuilderTest::TearDown(void) {
zvec::test_util::RemoveTestPath(_dir);
}
TEST_F(FlatSparseBuilderTest, TestGeneral) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("FlatSparseBuilder");
ASSERT_NE(builder, nullptr);
auto holder = make_shared<OnePassIndexSparseHolder<IndexMeta::DT_FP32>>();
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestGeneral";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_EQ(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
auto holder2 = make_shared<MultiPassIndexSparseHolder<IndexMeta::DT_FP32>>();
size_t doc_cnt2 = 2000UL;
for (size_t i = 0; i < doc_cnt2; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder2->emplace(i, vec));
}
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder2));
ASSERT_EQ(0, builder->build(holder2));
auto dumper2 = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper2, nullptr);
ASSERT_EQ(0, dumper2->create(path));
ASSERT_EQ(0, builder->dump(dumper2));
ASSERT_EQ(0, dumper2->close());
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt2, stats.built_count());
ASSERT_EQ(doc_cnt2, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_EQ(stats.built_costtime(), 0UL);
}
TEST_F(FlatSparseBuilderTest, TestIndexThreads) {
IndexBuilder::Pointer builder1 =
IndexFactory::CreateBuilder("FlatSparseBuilder");
ASSERT_NE(builder1, nullptr);
IndexBuilder::Pointer builder2 =
IndexFactory::CreateBuilder("FlatSparseBuilder");
ASSERT_NE(builder2, nullptr);
auto holder = make_shared<MultiPassIndexSparseHolder<IndexMeta::DT_FP32>>();
size_t doc_cnt = 1000UL;
uint32_t sparse_count = 32;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
std::srand(Realtime::MilliSeconds());
auto threads =
std::make_shared<SingleQueueIndexThreads>(std::rand() % 4, false);
ASSERT_EQ(0, builder1->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder2->init(*_index_meta_ptr, params));
auto build_index1 = [&]() {
ASSERT_EQ(0, builder1->train(threads, holder));
ASSERT_EQ(0, builder1->build(threads, holder));
};
auto build_index2 = [&]() {
ASSERT_EQ(0, builder2->train(threads, holder));
ASSERT_EQ(0, builder2->build(threads, holder));
};
auto t1 = std::async(std::launch::async, build_index1);
auto t2 = std::async(std::launch::async, build_index2);
t1.wait();
t2.wait();
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestIndexThreads";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder1->dump(dumper));
ASSERT_EQ(0, dumper->close());
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder2->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats1 = builder1->stats();
ASSERT_EQ(doc_cnt, stats1.built_count());
auto &stats2 = builder2->stats();
ASSERT_EQ(doc_cnt, stats2.built_count());
}
TEST_F(FlatSparseBuilderTest, TestHalfFloatConverter) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("FlatSparseBuilder");
ASSERT_NE(builder, nullptr);
auto holder = make_shared<OnePassIndexSparseHolder<IndexMeta::DT_FP32>>();
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
Params converter_params;
auto converter = IndexFactory::CreateConverter("HalfFloatSparseConverter");
converter->init(*_index_meta_ptr, converter_params);
IndexMeta index_meta = converter->meta();
converter->transform(holder);
auto converted_holder = converter->sparse_result();
Params params;
ASSERT_EQ(0, builder->init(index_meta, converter_params));
ASSERT_EQ(0, builder->train(converted_holder));
ASSERT_EQ(0, builder->build(converted_holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestHalFloatConverter";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_EQ(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
auto holder2 = make_shared<MultiPassIndexSparseHolder<IndexMeta::DT_FP32>>();
size_t doc_cnt2 = 2000UL;
for (size_t i = 0; i < doc_cnt2; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder2->emplace(i, vec));
}
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder2));
ASSERT_EQ(0, builder->build(holder2));
auto dumper2 = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper2, nullptr);
ASSERT_EQ(0, dumper2->create(path));
ASSERT_EQ(0, builder->dump(dumper2));
ASSERT_EQ(0, dumper2->close());
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt2, stats.built_count());
ASSERT_EQ(doc_cnt2, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_EQ(stats.built_costtime(), 0UL);
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
@@ -0,0 +1,819 @@
// 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 <future>
#include <iostream>
#include <random>
#include <vector>
#include <ailego/math/norm2_matrix.h>
#include <ailego/utility/math_helper.h>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include "tests/test_util.h"
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_meta.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
constexpr size_t static sparse_dim_count = 16;
class FlatSparseSearcherTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
void generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm);
static std::string dir_;
static std::shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string FlatSparseSearcherTest::dir_("searcher_test/");
std::shared_ptr<IndexMeta> FlatSparseSearcherTest::index_meta_ptr_;
void FlatSparseSearcherTest::generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
for (size_t i = 0; i < cnt; ++i) {
// prepare sparse
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_vec(sparse_dim_count);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_vec[j] = dist(gen);
}
float norm;
Norm2Matrix<float, 1>::Compute(sparse_vec.data(), sparse_dim_count, &norm);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_vec[j] = sparse_vec[j] / norm;
}
sparse_indices_list.push_back(sparse_indices);
sparse_vec_list.push_back(sparse_vec);
}
}
void FlatSparseSearcherTest::SetUp(void) {
IndexLoggerBroker::SetLevel(2);
index_meta_ptr_.reset(new IndexMeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32));
index_meta_ptr_->set_metric("InnerProductSparse", 0, Params());
zvec::test_util::RemoveTestPath(dir_);
}
void FlatSparseSearcherTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
TEST_F(FlatSparseSearcherTest, TestGeneral) {
// init storage
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_TRUE(storage != nullptr);
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestGeneral", true));
// init streamer
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(streamer != nullptr);
IndexMeta index_meta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
index_meta.set_metric("InnerProductSparse", 0, Params());
Params params;
ASSERT_EQ(0, streamer->init(index_meta, params));
ASSERT_EQ(0, streamer->open(storage));
// generate sparse data
size_t sparse_dim_count = 32;
size_t cnt = 100U;
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
// test add data
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, streamer->add_impl(i, sparse_dim_count,
sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
// test get data
uint32_t sparse_count;
std::string sparse_indices_buffer;
std::string sparse_values_buffer;
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(
0, streamer->get_sparse_vector(i, &sparse_count, &sparse_indices_buffer,
&sparse_values_buffer));
ASSERT_EQ(sparse_dim_count, sparse_count);
const uint32_t *sparse_indices_ptr =
reinterpret_cast<const uint32_t *>(sparse_indices_buffer.data());
const float *sparse_values_ptr =
reinterpret_cast<const float *>(sparse_values_buffer.data());
for (size_t j = 0; j < sparse_count; ++j) {
ASSERT_EQ(sparse_indices_ptr[j], sparse_indices_list[i][j]);
ASSERT_FLOAT_EQ(sparse_values_ptr[j], sparse_vec_list[i][j]);
// std::cout << "1: " << sparse_values_ptr[j]
// << " 2: " << sparse_vec_list[i][j] << std::endl;
}
// must clear ^_^
sparse_indices_buffer.clear();
sparse_values_buffer.clear();
}
// test dump
auto path = dir_ + "TestGeneral_dump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher get vector
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_TRUE(read_storage != nullptr);
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_EQ(0, searcher->init(Params()));
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
// test searcher get data
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(
0, searcher->get_sparse_vector(i, &sparse_count, &sparse_indices_buffer,
&sparse_values_buffer));
ASSERT_EQ(sparse_dim_count, sparse_count);
const uint32_t *sparse_indices_ptr =
reinterpret_cast<const uint32_t *>(sparse_indices_buffer.data());
const float *sparse_values_ptr =
reinterpret_cast<const float *>(sparse_values_buffer.data());
for (size_t j = 0; j < sparse_count; ++j) {
ASSERT_EQ(sparse_indices_ptr[j], sparse_indices_list[i][j]);
ASSERT_FLOAT_EQ(sparse_values_ptr[j], sparse_vec_list[i][j]);
// std::cout << "1: " << sparse_values_ptr[j]
// << " 2: " << sparse_vec_list[i][j] << std::endl;
}
// must clear ^_^
sparse_indices_buffer.clear();
sparse_values_buffer.clear();
}
}
TEST_F(FlatSparseSearcherTest, TestStreamerDump) {
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_NE(streamer, nullptr);
Params params;
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestStreamerDump.index", true));
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 10000U;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, streamer->add_impl(i, sparse_dim_count,
sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
auto path = dir_ + "TestStreamerDump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher knn
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, searcher->init(Params()));
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
auto linearCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
size_t topk = 200;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
for (size_t i = 0; i < cnt; i += 50) {
const auto &sparse_indices = sparse_indices_list[i];
const auto &sparse_vec = sparse_vec_list[i];
auto t1 = Realtime::MicroSeconds();
ASSERT_EQ(0, searcher->search_impl(sparse_dim_count, sparse_indices.data(),
sparse_vec.data(), qmeta, knnCtx));
auto t2 = Realtime::MicroSeconds();
ASSERT_EQ(0,
searcher->search_bf_impl(sparse_dim_count, sparse_indices.data(),
sparse_vec.data(), qmeta, linearCtx));
auto t3 = Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
auto &knnResult = knnCtx->result();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(topk, knnResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
ASSERT_EQ(linearResult[k].key(), knnResult[k].key());
}
}
printf("linear: %zu, knn: %zu\n", (size_t)linearTotalTime,
(size_t)knnTotalTime);
}
TEST_F(FlatSparseSearcherTest, TestLoadClose) {
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_TRUE(storage != nullptr);
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestGeneral", true));
// init streamer
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(streamer != nullptr);
IndexMeta index_meta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
index_meta.set_metric("InnerProductSparse", 0, Params());
Params params;
ASSERT_EQ(0, streamer->init(index_meta, params));
ASSERT_EQ(0, streamer->open(storage));
// generate sparse data
size_t sparse_dim_count = 32;
size_t cnt = 100U;
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
// test add data
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, streamer->add_impl(i, sparse_dim_count,
sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
// test get data
uint32_t sparse_count;
std::string sparse_indices_buffer;
std::string sparse_values_buffer;
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(
0, streamer->get_sparse_vector(i, &sparse_count, &sparse_indices_buffer,
&sparse_values_buffer));
ASSERT_EQ(sparse_dim_count, sparse_count);
const uint32_t *sparse_indices_ptr =
reinterpret_cast<const uint32_t *>(sparse_indices_buffer.data());
const float *sparse_values_ptr =
reinterpret_cast<const float *>(sparse_values_buffer.data());
for (size_t j = 0; j < sparse_count; ++j) {
ASSERT_EQ(sparse_indices_ptr[j], sparse_indices_list[i][j]);
ASSERT_FLOAT_EQ(sparse_values_ptr[j], sparse_vec_list[i][j]);
// std::cout << "1: " << sparse_values_ptr[j]
// << " 2: " << sparse_vec_list[i][j] << std::endl;
}
// must clear ^_^
sparse_indices_buffer.clear();
sparse_values_buffer.clear();
}
// test dump
auto path = dir_ + "TestGeneral_dump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher get vector
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_TRUE(read_storage != nullptr);
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_EQ(0, searcher->init(Params()));
uint32_t loop = 5;
while (loop--) {
std::cout << "loop: " << loop << std::endl;
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
// test searcher get data
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, searcher->get_sparse_vector(i, &sparse_count,
&sparse_indices_buffer,
&sparse_values_buffer));
ASSERT_EQ(sparse_dim_count, sparse_count);
const uint32_t *sparse_indices_ptr =
reinterpret_cast<const uint32_t *>(sparse_indices_buffer.data());
const float *sparse_values_ptr =
reinterpret_cast<const float *>(sparse_values_buffer.data());
for (size_t j = 0; j < sparse_count; ++j) {
ASSERT_EQ(sparse_indices_ptr[j], sparse_indices_list[i][j]);
ASSERT_FLOAT_EQ(sparse_values_ptr[j], sparse_vec_list[i][j]);
// std::cout << "1: " << sparse_values_ptr[j]
// << " 2: " << sparse_vec_list[i][j] << std::endl;
}
// must clear ^_^
sparse_indices_buffer.clear();
sparse_values_buffer.clear();
}
ASSERT_EQ(searcher->unload(), 0);
}
}
TEST_F(FlatSparseSearcherTest, TestSearch) {
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(streamer != nullptr);
Params params;
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestLinearSearch.index", true));
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 5000UL;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_velues(sparse_dim_count);
for (size_t i = 0; i < cnt; ++i) {
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = -1.0 * i - 1.0f;
}
ASSERT_EQ(0, streamer->add_impl(i, sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
}
// test dump
auto path = dir_ + "TestGeneral_dump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher get vector
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_TRUE(read_storage != nullptr);
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_EQ(0, searcher->init(Params()));
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
size_t step = 50;
for (size_t i = 0; i < cnt; i += step) {
// std::cout << "search " << i << std::endl;
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = i + 1.0f;
}
ctx->set_topk(1U);
ASSERT_EQ(0,
searcher->search_bf_impl(sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(1UL, result1.size());
ASSERT_EQ(0, result1[0].key());
// std::cout << result1[0].key() << " " << result1[0].score() << std::endl;
ctx->set_topk(3U);
ASSERT_EQ(0,
searcher->search_bf_impl(sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(3UL, result2.size());
for (size_t i = 0; i < 3UL; ++i) {
// std::cout << result2[i].key() << " " << result2[i].score() <<
// std::endl;
ASSERT_EQ(i, result2[i].key());
}
}
ctx->set_topk(100U);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = 10.1f;
}
ASSERT_EQ(0, searcher->search_bf_impl(sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
for (size_t i = 0; i < 100; ++i) {
ASSERT_EQ(i, result[i].key());
}
}
TEST_F(FlatSparseSearcherTest, TestSearchPKeys) {
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(streamer != nullptr);
Params params;
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestLinearSearchByKeys.index", true));
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 5000UL;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
std::vector<std::vector<uint64_t>> p_keys;
p_keys.resize(1);
p_keys[0].resize(cnt);
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_velues(sparse_dim_count);
for (size_t i = 0; i < cnt; ++i) {
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = -1.0 * i - 1.0f;
}
ASSERT_EQ(0, streamer->add_impl(i, sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
p_keys[0][i] = i;
}
// test dump
auto path = dir_ + "TestGeneral_dump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher get vector
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("FileReadStorage");
ASSERT_TRUE(read_storage != nullptr);
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_EQ(0, searcher->init(Params()));
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
size_t topk = 3;
for (size_t i = 0; i < cnt; i += 50) {
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = i + 1.0f;
}
ctx->set_topk(1U);
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(1UL, result1.size());
ASSERT_EQ(0, result1[0].key());
ctx->set_topk(topk);
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(0, result2[0].key());
ASSERT_EQ(1, result2[1].key());
ASSERT_EQ(2, result2[2].key());
}
{
ctx->set_topk(100U);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = 1.0f;
}
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(0, result[0].key());
ASSERT_EQ(1, result[1].key());
ASSERT_EQ(10, result[10].key());
ASSERT_EQ(20, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
}
{
ctx->set_topk(100U);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = 10.0f;
}
p_keys[0] = {{cnt + 1, 10, 1, 15, cnt + 2}};
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(3U, result.size());
ASSERT_EQ(1, result[0].key());
ASSERT_EQ(10, result[1].key());
ASSERT_EQ(15, result[2].key());
}
{
ctx->set_topk(100U);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = 9.0f;
}
p_keys[0].clear();
for (size_t j = 0; j < cnt; j += 10) {
p_keys[0].push_back((uint64_t)j);
}
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(0, result[0].key());
ASSERT_EQ(10, result[1].key());
ASSERT_EQ(100, result[10].key());
ASSERT_EQ(200, result[20].key());
ASSERT_EQ(300, result[30].key());
ASSERT_EQ(350, result[35].key());
ASSERT_EQ(990, result[99].key());
}
}
TEST_F(FlatSparseSearcherTest, TestMultiThread) {
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(streamer != nullptr);
Params params;
constexpr size_t static sparse_dim_count = 32;
IndexMeta meta(IndexMeta::MetaType::MT_SPARSE, IndexMeta::DataType::DT_FP32);
meta.set_metric("InnerProductSparse", 0, Params());
ASSERT_EQ(0, streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TessKnnMultiThread", true));
ASSERT_EQ(0, streamer->open(storage));
auto addVector = [&streamer](int baseKey, size_t addCnt) {
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
size_t succAdd = 0;
auto ctx = streamer->create_context();
for (size_t i = 0; i < addCnt; i++) {
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_velues(sparse_dim_count);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = (float)i + baseKey;
}
succAdd += !streamer->add_impl(baseKey + i, sparse_dim_count,
sparse_indices.data(),
sparse_velues.data(), qmeta, ctx);
}
streamer->flush(0UL);
return succAdd;
};
auto t2 = std::async(std::launch::async, addVector, 1000, 1000);
auto t3 = std::async(std::launch::async, addVector, 2000, 1000);
auto t1 = std::async(std::launch::async, addVector, 0, 1000);
ASSERT_EQ(1000U, t1.get());
ASSERT_EQ(1000U, t2.get());
ASSERT_EQ(1000U, t3.get());
streamer->close();
// checking data
ASSERT_EQ(0, streamer->open(storage));
auto provider = streamer->create_sparse_provider();
auto iter = provider->create_iterator();
ASSERT_TRUE(!!iter);
size_t total = 0;
uint64_t min = 1000;
uint64_t max = 0;
std::set<uint64_t> keys;
while (iter->is_valid()) {
const uint32_t sparse_count = iter->sparse_count();
ASSERT_EQ(sparse_count, sparse_dim_count);
const float *data = reinterpret_cast<const float *>(iter->sparse_data());
for (size_t j = 0; j < sparse_dim_count; ++j) {
ASSERT_EQ((float)iter->key(), data[j]);
}
total++;
min = std::min(min, iter->key());
max = std::max(max, iter->key());
keys.insert(iter->key());
iter->next();
}
ASSERT_EQ(3000, keys.size());
ASSERT_EQ(3000, total);
ASSERT_EQ(0, min);
ASSERT_EQ(2999, max);
// test dump
auto path = dir_ + "TestGeneral_dump";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, streamer->dump(dumper));
ASSERT_EQ(0, streamer->close());
ASSERT_EQ(0, dumper->close());
// do searcher get vector
IndexSearcher::Pointer searcher =
IndexFactory::CreateSearcher("FlatSparseSearcher");
auto read_storage = IndexFactory::CreateStorage("MMapFileReadStorage");
ASSERT_TRUE(read_storage != nullptr);
ASSERT_TRUE(searcher != nullptr);
ASSERT_EQ(0, read_storage->open(path, false));
ASSERT_EQ(0, searcher->init(Params()));
ASSERT_EQ(0, searcher->load(read_storage, IndexMetric::Pointer()));
// ====== multi thread search
size_t topk = 10;
size_t cnt = 3000;
auto knnSearch = [&]() {
auto linearCtx = searcher->create_context();
auto linearByPkeysCtx = searcher->create_context();
auto ctx = searcher->create_context();
IndexQueryMeta qmeta(IndexMeta::DT_FP32);
linearCtx->set_topk(topk);
linearByPkeysCtx->set_topk(topk);
ctx->set_topk(topk);
size_t totalCnts = 0;
size_t totalHits = 0;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_velues(sparse_dim_count);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_velues[j] = ((float)i + 1.1f);
}
ASSERT_EQ(0,
searcher->search_impl(sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, ctx));
ASSERT_EQ(
0, searcher->search_bf_impl(sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), qmeta, linearCtx));
std::vector<std::vector<uint64_t>> p_keys = {{cnt - 1, cnt - 2, cnt - 3}};
ASSERT_EQ(0, searcher->search_bf_by_p_keys_impl(
sparse_dim_count, sparse_indices.data(),
sparse_velues.data(), p_keys, qmeta, linearByPkeysCtx));
auto &r1 = ctx->result();
ASSERT_EQ(topk, r1.size());
// std::cout << "r1 top1: " << r1[0].key() << ", score: " << r1[0].score()
// << std::endl;
ASSERT_EQ(cnt - 1, r1[0].key());
auto &r2 = linearCtx->result();
ASSERT_EQ(topk, r2.size());
// std::cout << "r2 top1: " << r2[0].key() << ", score: " << r2[0].score()
// << std::endl;
ASSERT_EQ(cnt - 1, r2[0].key());
auto &r3 = linearByPkeysCtx->result();
ASSERT_EQ(std::min(topk, p_keys[0].size()), r3.size());
#if 0
printf("linear: %zd => %zd %zd %zd %zd %zd\n", i, r2[0].key,
r2[1].key, r2[2].key, r2[3].key, r2[4].key);
printf("knn: %zd => %zd %zd %zd %zd %zd\n", i, r1[0].key, r1[1].key,
r1[2].key, r1[3].key, r1[4].key);
#endif
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (r2[j].key() == r1[k].key()) {
totalHits++;
break;
}
}
}
}
printf("%f\n", totalHits * 1.0f / totalCnts);
ASSERT_FLOAT_EQ(1.0f, totalHits * 1.0f / totalCnts);
};
auto s1 = std::async(std::launch::async, knnSearch);
auto s2 = std::async(std::launch::async, knnSearch);
auto s3 = std::async(std::launch::async, knnSearch);
s1.wait();
s2.wait();
s3.wait();
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
@@ -0,0 +1,230 @@
// 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 <string>
#include <vector>
#include <ailego/math/norm2_matrix.h>
#include <ailego/utility/math_helper.h>
#include <ailego/utility/memory_helper.h>
#include <algorithm/flat_sparse/flat_sparse_utility.h>
#include <gtest/gtest.h>
#include <zvec/core/framework/index_framework.h>
#include <zvec/core/framework/index_streamer.h>
#include "tests/test_util.h"
using namespace std;
using namespace testing;
using namespace zvec::ailego;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
namespace zvec {
namespace core {
class FlatSparseStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
void generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm);
static std::string dir_;
static shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string FlatSparseStreamerTest::dir_("FlatSparseStreamerTest/");
shared_ptr<IndexMeta> FlatSparseStreamerTest::index_meta_ptr_;
void FlatSparseStreamerTest::generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
for (size_t i = 0; i < cnt; ++i) {
// prepare sparse
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_vec(sparse_dim_count);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_vec[j] = dist(gen);
}
float norm;
ailego::Norm2Matrix<float, 1>::Compute(sparse_vec.data(), sparse_dim_count,
&norm);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_vec[j] = sparse_vec[j] / norm;
}
sparse_indices_list.push_back(sparse_indices);
sparse_vec_list.push_back(sparse_vec);
}
}
void FlatSparseStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (nothrow) IndexMeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32));
index_meta_ptr_->set_metric("InnerProductSparse", 0, ailego::Params());
zvec::test_util::RemoveTestPath(dir_);
}
void FlatSparseStreamerTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
TEST_F(FlatSparseStreamerTest, TestGeneral) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_TRUE(write_streamer != nullptr);
size_t sparse_dim_count = 32;
IndexMeta index_meta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
index_meta.set_metric("InnerProductSparse", 0, ailego::Params());
ailego::Params params;
ailego::Params stg_params;
auto write_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, write_storage->init(stg_params));
ASSERT_EQ(0, write_storage->open(dir_ + "Test/FlatSparseSearch", true));
ASSERT_EQ(0, write_streamer->init(index_meta, params));
ASSERT_EQ(0, write_streamer->open(write_storage));
size_t cnt = 20000U;
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
IndexQueryMeta qmeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, write_streamer->add_impl(
i, sparse_dim_count, sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
write_storage->close();
write_storage.reset();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("FlatSparseStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/FlatSparseSearch", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
auto linearCtx = read_streamer->create_context();
ASSERT_TRUE(!!linearCtx);
auto knnCtx = read_streamer->create_context();
ASSERT_TRUE(!!knnCtx);
// streamer->print_debug_info();
size_t topk = 200;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
for (size_t i = 0; i < cnt; i += 100) {
const auto &sparse_indices = sparse_indices_list[i];
const auto &sparse_vec = sparse_vec_list[i];
auto t1 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(
0, read_streamer->search_impl(sparse_dim_count, sparse_indices.data(),
sparse_vec.data(), qmeta, knnCtx));
auto t2 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(0, read_streamer->search_bf_impl(
sparse_dim_count, sparse_indices.data(), sparse_vec.data(),
qmeta, linearCtx));
auto t3 = ailego::Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
// std::cout << "i: " << i << std::endl;
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * 1.0f / totalCnts;
float topk1Recall = topk1Hits * 100.0f / cnt;
// float cost = linearTotalTime * 1.0f / knnTotalTime;
std::cout << "knnTotalTime=" << knnTotalTime
<< " linearTotalTime=" << linearTotalTime << std::endl;
#if 0
printf("knnTotalTime=%zd linearTotalTime=%zd totalHits=%d totalCnts=%d "
"R@%zd=%f R@1=%f cost=%f\n",
knnTotalTime, linearTotalTime, totalHits, totalCnts, topk, recall,
topk1Recall, cost);
#endif
EXPECT_GT(recall, 0.80f);
EXPECT_GT(topk1Recall, 0.80f);
// EXPECT_GT(cost, 2.0f);
}
} // namespace core
} // namespace zvec
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_hnsw core_knn_flat
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/hnsw
)
endforeach()
@@ -0,0 +1,517 @@
#include <future>
#include <string>
#include <vector>
#include <ailego/utility/math_helper.h>
#include <ailego/utility/memory_helper.h>
#include <algorithm/hnsw/hnsw_params.h>
#include <gtest/gtest.h>
#include <zvec/core/framework/index_framework.h>
#include <zvec/core/framework/index_streamer.h>
#include "tests/test_util.h"
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
constexpr size_t static dim = 16;
class HnswStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
void hybrid_scale(std::vector<float> &dense_value,
std::vector<float> &sparse_value, float alpha_scale);
static std::string dir_;
static std::shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string HnswStreamerTest::dir_("hnsw_streamer_buffer_test_dir/");
std::shared_ptr<IndexMeta> HnswStreamerTest::index_meta_ptr_;
void HnswStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (std::nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
index_meta_ptr_->set_metric("SquaredEuclidean", 0, Params());
zvec::test_util::RemoveTestPath(dir_);
}
void HnswStreamerTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
TEST_F(HnswStreamerTest, TestHnswSearch) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
params.set(PARAM_HNSW_STREAMER_GET_VECTOR_ENABLE, true);
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/HnswSearch", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/HnswSearch", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
read_streamer->close();
read_streamer.reset();
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
}
TEST_F(HnswStreamerTest, TestHnswSearchBuffer) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
params.set(PARAM_HNSW_STREAMER_GET_VECTOR_ENABLE, true);
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/TestHnswSearchBuffer", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/TestHnswSearchBuffer", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
read_streamer->close();
read_streamer.reset();
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
}
TEST_F(HnswStreamerTest, TestHnswSearchBufferMMap) {
MemoryLimitPool::get_instance().init(2 * 1024UL * 1024UL * 1024UL);
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
params.set(PARAM_HNSW_STREAMER_GET_VECTOR_ENABLE, true);
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/TestHnswSearchBufferMMap", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/TestHnswSearchBufferMMap", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
ElapsedTime elapsed_time;
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
read_streamer->close();
read_streamer.reset();
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
}
TEST_F(HnswStreamerTest, TestHnswSearchMMap) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_TRUE(write_streamer != nullptr);
Params params;
params.set(PARAM_HNSW_STREAMER_GET_VECTOR_ENABLE, true);
ASSERT_EQ(0, write_streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "Test/HnswSearchMMap", true));
ASSERT_EQ(0, write_streamer->open(storage));
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
size_t cnt = 10000UL;
IndexQueryMeta qmeta(IndexMeta::DT_FP32, dim);
for (size_t i = 0; i < cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
write_streamer->add_impl(i, vec.data(), qmeta, ctx);
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
storage->close();
ElapsedTime elapsed_time;
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/HnswSearchMMap", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
size_t topk = 3;
auto provider = read_streamer->create_provider();
for (size_t i = 0; i < cnt; i += 1) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(topk, result1.size());
IndexStorage::MemoryBlock block;
ASSERT_EQ(0, provider->get_vector(result1[0].key(), block));
const float *data = (float *)block.data();
for (size_t j = 0; j < dim; ++j) {
ASSERT_FLOAT_EQ(data[j], i);
}
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < dim; ++j) {
vec[j] = i + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, read_streamer->search_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
ctx->set_topk(100U);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 10.1f;
}
ASSERT_EQ(0, read_streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result = ctx->result();
ASSERT_EQ(100U, result.size());
ASSERT_EQ(10, result[0].key());
ASSERT_EQ(11, result[1].key());
ASSERT_EQ(5, result[10].key());
ASSERT_EQ(0, result[20].key());
ASSERT_EQ(30, result[30].key());
ASSERT_EQ(35, result[35].key());
ASSERT_EQ(99, result[99].key());
read_streamer->close();
read_streamer.reset();
cout << "Elapsed time: " << elapsed_time.milli_seconds() << " ms" << endl;
}
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
include(${PROJECT_ROOT_DIR}/cmake/option.cmake)
if(APPLE)
set(APPLE_FRAMEWORK_LIBS
-framework CoreFoundation
-framework CoreGraphics
-framework CoreData
-framework CoreText
-framework Security
-framework Foundation
-Wl,-U,_MallocExtension_ReleaseFreeMemory
-Wl,-U,_ProfilerStart
-Wl,-U,_ProfilerStop
-Wl,-U,_RegisterThriftProtocol
)
endif()
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_hnsw_rabitq core_knn_flat core_knn_cluster
${CMAKE_THREAD_LIBS_INIT}
${CMAKE_DL_LIBS}
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/hnsw_rabitq
LDFLAGS ${APPLE_FRAMEWORK_LIBS}
)
cc_test_suite(hnsw_rabitq ${CC_TARGET})
endforeach()
@@ -0,0 +1,660 @@
// 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 "hnsw_rabitq_streamer.h"
#include <memory>
#include <string>
#include <gtest/gtest.h>
#include "zvec/ailego/container/params.h"
#include "zvec/ailego/utility/file_helper.h"
#include "zvec/core/framework/index_holder.h"
#include "zvec/core/framework/index_streamer.h"
#include "hnsw_rabitq_streamer.h"
#include "rabitq_converter.h"
#include "rabitq_reformer.h"
using namespace std;
using namespace zvec::ailego;
namespace zvec {
namespace core {
constexpr size_t static dim = 128;
class HnswRabitqStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
static std::string dir_;
static shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string HnswRabitqStreamerTest::dir_("hnswRabitqStreamerTest");
shared_ptr<IndexMeta> HnswRabitqStreamerTest::index_meta_ptr_;
void HnswRabitqStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, dim));
index_meta_ptr_->set_metric("SquaredEuclidean", 0, ailego::Params());
}
void HnswRabitqStreamerTest::TearDown(void) {
ailego::FileHelper::RemovePath(dir_.c_str());
}
TEST_F(HnswRabitqStreamerTest, TestBuildAndSearch) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i * dim + j) / 1000.0f;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/Test/AddVector", true));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
for (auto it = holder->create_iterator(); it->is_valid(); it->next()) {
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
ASSERT_EQ(0,
streamer->add_impl(it->key(), it->data(), query_meta, context));
}
streamer->flush(0UL);
// Perform search verification
NumericalVector<float> query_vec(dim);
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = static_cast<float>(j) / 1000.0f;
}
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
context->set_topk(10);
ASSERT_EQ(0, streamer->search_impl(query_vec.data(), query_meta, 1, context));
const auto &result = context->result(0);
ASSERT_GT(result.size(), 0UL);
ASSERT_LE(result.size(), 10UL);
// reopen and load reformer from storage
ASSERT_EQ(0, streamer->close());
IndexStreamer::Pointer new_streamer =
std::make_shared<HnswRabitqStreamer>(holder);
ASSERT_EQ(0, new_streamer->init(*index_meta_ptr_, params));
ASSERT_EQ(0, new_streamer->open(storage));
}
TEST_F(HnswRabitqStreamerTest, TestLinearSearch) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestLinearSearch", true));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
for (auto it = holder->create_iterator(); it->is_valid(); it->next()) {
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
ASSERT_EQ(0,
streamer->add_impl(it->key(), it->data(), query_meta, context));
}
// Test linear search with exact match
size_t topk = 3;
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> query_vec(dim);
for (size_t i = 0; i < doc_cnt; i += 100) {
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = static_cast<float>(i);
}
context->set_topk(1U);
ASSERT_EQ(0,
streamer->search_bf_impl(query_vec.data(), query_meta, context));
auto &result1 = context->result();
ASSERT_EQ(1UL, result1.size());
ASSERT_EQ(i, result1[0].key());
// Test with slight offset
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = static_cast<float>(i) + 0.1f;
}
context->set_topk(topk);
ASSERT_EQ(0,
streamer->search_bf_impl(query_vec.data(), query_meta, context));
auto &result2 = context->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
}
}
TEST_F(HnswRabitqStreamerTest, TestKnnSearch) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 2000UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 10U);
params.set("proxima.hnsw_rabitq.streamer.efconstruction", 100U);
params.set("proxima.hnsw_rabitq.streamer.ef", 50U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestKnnSearch", true));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
for (auto it = holder->create_iterator(); it->is_valid(); it->next()) {
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
ASSERT_EQ(0,
streamer->add_impl(it->key(), it->data(), query_meta, context));
}
// Compare KNN search with brute force search
auto linear_ctx = streamer->create_context();
auto knn_ctx = streamer->create_context();
size_t topk = 50;
linear_ctx->set_topk(topk);
knn_ctx->set_topk(topk);
int total_hits = 0;
int total_cnts = 0;
int topk1_hits = 0;
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> query_vec(dim);
for (size_t i = 0; i < doc_cnt; i += 100) {
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = static_cast<float>(i) + 0.1f;
}
ASSERT_EQ(0,
streamer->search_impl(query_vec.data(), query_meta, 1, knn_ctx));
ASSERT_EQ(
0, streamer->search_bf_impl(query_vec.data(), query_meta, linear_ctx));
auto &knn_result = knn_ctx->result(0);
ASSERT_EQ(topk, knn_result.size());
topk1_hits += (i == knn_result[0].key());
auto &linear_result = linear_ctx->result();
ASSERT_EQ(topk, linear_result.size());
ASSERT_EQ(i, linear_result[0].key());
for (size_t k = 0; k < topk; ++k) {
total_cnts++;
for (size_t j = 0; j < topk; ++j) {
if (linear_result[j].key() == knn_result[k].key()) {
total_hits++;
break;
}
}
}
}
float recall = total_hits * 1.0f / total_cnts;
float topk1_recall = topk1_hits * 100.0f / static_cast<float>(doc_cnt);
EXPECT_GT(recall, 0.60f);
// actual: no guarantee
// TODO(jiliang.ljl): check if ok?
EXPECT_GT(topk1_recall, 0.00f);
}
TEST_F(HnswRabitqStreamerTest, TestRandomData) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 1500UL;
// Add random vectors
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 32U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 20U);
params.set("proxima.hnsw_rabitq.streamer.efconstruction", 200U);
params.set("proxima.hnsw_rabitq.streamer.ef", 100U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestRandomData", true));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
for (auto it = holder->create_iterator(); it->is_valid(); it->next()) {
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
ASSERT_EQ(0,
streamer->add_impl(it->key(), it->data(), query_meta, context));
}
// Test with random queries
auto linear_ctx = streamer->create_context();
auto knn_ctx = streamer->create_context();
size_t topk = 50;
linear_ctx->set_topk(topk);
knn_ctx->set_topk(topk);
int total_hits = 0;
int total_cnts = 0;
int topk1_hits = 0;
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> query_vec(dim);
size_t query_cnt = 200;
for (size_t i = 0; i < query_cnt; i++) {
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
ASSERT_EQ(
0, streamer->search_bf_impl(query_vec.data(), query_meta, linear_ctx));
ASSERT_EQ(0,
streamer->search_impl(query_vec.data(), query_meta, 1, knn_ctx));
auto &knn_result = knn_ctx->result(0);
ASSERT_EQ(topk, knn_result.size());
auto &linear_result = linear_ctx->result();
ASSERT_EQ(topk, linear_result.size());
topk1_hits += (linear_result[0].key() == knn_result[0].key());
for (size_t k = 0; k < topk; ++k) {
total_cnts++;
for (size_t j = 0; j < topk; ++j) {
if (linear_result[j].key() == knn_result[k].key()) {
total_hits++;
break;
}
}
}
}
float recall = total_hits * 1.0f / total_cnts;
float topk1_recall = topk1_hits * 1.0f / query_cnt;
EXPECT_GT(recall, 0.50f);
EXPECT_GT(topk1_recall, 0.70f);
}
TEST_F(HnswRabitqStreamerTest, TestOpenClose) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 500UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestOpenClose", true));
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
// Add first half of vectors
for (size_t i = 0; i < doc_cnt / 2; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), query_meta, context));
}
ASSERT_EQ(0, streamer->flush(0UL));
ASSERT_EQ(0, streamer->close());
// Reopen and add second half
IndexStreamer::Pointer streamer2 =
std::make_shared<HnswRabitqStreamer>(holder);
ASSERT_EQ(0, streamer2->init(*index_meta_ptr_, params));
ASSERT_EQ(0, streamer2->open(storage));
auto context2 = streamer2->create_context();
for (size_t i = doc_cnt / 2; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer2->add_impl(i, vec.data(), query_meta, context2));
}
ASSERT_EQ(0, streamer2->flush(0UL));
// Verify search works after reopen
NumericalVector<float> query_vec(dim);
for (size_t j = 0; j < dim; ++j) {
query_vec[j] = 10.0f;
}
context2->set_topk(5);
ASSERT_EQ(0,
streamer2->search_impl(query_vec.data(), query_meta, 1, context2));
const auto &result = context2->result(0);
ASSERT_EQ(5UL, result.size());
ASSERT_EQ(10UL, result[0].key());
}
TEST_F(HnswRabitqStreamerTest, TestCreateIterator) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 300UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_TRUE(holder->emplace(i, vec));
}
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestCreateIterator", true));
ASSERT_EQ(0, streamer->open(storage));
auto context = streamer->create_context();
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, dim);
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), query_meta, context));
}
streamer->flush(0UL);
// Test iterator
auto provider = streamer->create_provider();
auto iter = provider->create_iterator();
ASSERT_TRUE(!!iter);
size_t count = 0;
while (iter->is_valid()) {
ASSERT_EQ(count, iter->key());
// const float *data = (const float *)iter->data();
// for (size_t j = 0; j < dim; ++j) {
// ASSERT_EQ(static_cast<float>(count), data[j]);
// }
iter->next();
count++;
}
ASSERT_EQ(doc_cnt, count);
// Test get_vector
// for (size_t i = 0; i < doc_cnt; i++) {
// const float *data = (const float *)provider->get_vector(i);
// ASSERT_NE(data, nullptr);
// for (size_t j = 0; j < dim; ++j) {
// ASSERT_EQ(static_cast<float>(i), data[j]);
// }
// }
}
TEST_F(HnswRabitqStreamerTest, TestDimensions) {
std::vector<size_t> dimensions = {1, 2, 4, 8, 16, 32, 33,
63, 64, 128, 256, 512, 1024, 2047,
2048, 2049, 4095, 4096, 4097, 8192, 16384};
size_t doc_cnt = 100;
for (size_t test_dim : dimensions) {
std::cout << "Testing dimension: " << test_dim << std::endl;
IndexMeta index_meta(IndexMeta::DataType::DT_FP32, test_dim);
index_meta.set_metric("SquaredEuclidean", 0, ailego::Params());
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(
test_dim);
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", test_dim);
int ret = streamer->init(index_meta, params);
// dimension <= 63 or >= 4096: init() should return -31
if (test_dim <= 63 || test_dim >= 4096) {
ASSERT_EQ(-31, ret) << "expected init to fail with -31, dim=" << test_dim;
std::cout << "Dimension " << test_dim
<< " correctly rejected with ret=" << ret << std::endl;
continue;
}
// Valid dimensions: verify full streaming build succeeds
ASSERT_EQ(0, ret) << "init failed, dim=" << test_dim;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(test_dim);
for (size_t j = 0; j < test_dim; ++j) {
vec[j] = static_cast<float>(i * test_dim + j) / 1000.0f;
}
ASSERT_TRUE(holder->emplace(i, std::move(vec))) << "dim=" << test_dim;
}
RabitqConverter converter;
converter.init(index_meta, ailego::Params());
ASSERT_EQ(0, converter.train(holder))
<< "converter train failed, dim=" << test_dim;
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(0, converter.to_reformer(&index_reformer)) << "dim=" << test_dim;
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
// Recreate streamer with reformer
streamer = std::make_shared<HnswRabitqStreamer>(holder, reformer);
ASSERT_EQ(0, streamer->init(index_meta, params))
<< "init with reformer failed, dim=" << test_dim;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
std::string storage_path =
dir_ + "/TestDimensions_" + std::to_string(test_dim);
ASSERT_EQ(0, storage->open(storage_path, true))
<< "storage open failed, dim=" << test_dim;
ASSERT_EQ(0, streamer->open(storage))
<< "streamer open failed, dim=" << test_dim;
auto context = streamer->create_context();
IndexQueryMeta query_meta(IndexMeta::DataType::DT_FP32, test_dim);
for (auto it = holder->create_iterator(); it->is_valid(); it->next()) {
ASSERT_EQ(0,
streamer->add_impl(it->key(), it->data(), query_meta, context))
<< "add failed, dim=" << test_dim << ", key=" << it->key();
}
ASSERT_EQ(0, streamer->flush(0UL)) << "flush failed, dim=" << test_dim;
std::cout << "Dimension " << test_dim << " passed" << std::endl;
}
}
TEST_F(HnswRabitqStreamerTest, TestExBitsMismatch) {
auto holder =
make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 500UL;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i * dim + j) / 1000.0f;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
// Train converter with default total_bits (7, ex_bits=6)
RabitqConverter converter;
converter.init(*index_meta_ptr_, ailego::Params());
ASSERT_EQ(converter.train(holder), 0);
std::shared_ptr<IndexReformer> index_reformer;
ASSERT_EQ(converter.to_reformer(&index_reformer), 0);
auto reformer = std::dynamic_pointer_cast<RabitqReformer>(index_reformer);
// Create streamer with total_bits=2 (ex_bits=1), mismatching the reformer
IndexStreamer::Pointer streamer =
std::make_shared<HnswRabitqStreamer>(holder, reformer);
ailego::Params params;
params.set("proxima.hnsw_rabitq.streamer.max_neighbor_count", 16U);
params.set("proxima.hnsw_rabitq.streamer.upper_neighbor_count", 8U);
params.set("proxima.hnsw_rabitq.streamer.scaling_factor", 5U);
params.set("proxima.hnsw_rabitq.general.dimension", dim);
params.set(PARAM_RABITQ_TOTAL_BITS, 2U);
ASSERT_EQ(0, streamer->init(*index_meta_ptr_, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "/TestExBitsMismatch", true));
// open() should detect ex_bits mismatch and return IndexError_Mismatch
ASSERT_EQ(IndexError_Mismatch, streamer->open(storage));
}
} // namespace core
} // namespace zvec
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_hnsw_sparse
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/hnsw_sparse
)
endforeach()
@@ -0,0 +1,469 @@
// 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 "hnsw_sparse_builder.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <future>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include "tests/test_util.h"
#include "zvec/core/framework/index_framework.h"
#include "hnsw_sparse_params.h"
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
using namespace std;
using namespace testing;
using namespace zvec::ailego;
namespace zvec {
namespace core {
class HnswSparseBuilderTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
static std::string _dir;
static shared_ptr<IndexMeta> _index_meta_ptr;
};
std::string HnswSparseBuilderTest::_dir("HnswSparseBuilderTest/");
shared_ptr<IndexMeta> HnswSparseBuilderTest::_index_meta_ptr;
void HnswSparseBuilderTest::SetUp(void) {
_index_meta_ptr.reset(new (nothrow) IndexMeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32));
_index_meta_ptr->set_metric("InnerProductSparse", 0, ailego::Params());
}
void HnswSparseBuilderTest::TearDown(void) {
zvec::test_util::RemoveTestPath(_dir);
}
TEST_F(HnswSparseBuilderTest, TestGeneral) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<OnePassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_BUILDER_THREAD_COUNT, 1);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(0, builder->build(holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestGeneral";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
auto holder2 =
make_shared<MultiPassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
size_t doc_cnt2 = 2000UL;
for (size_t i = 0; i < doc_cnt2; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder2->emplace(i, vec));
}
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder2));
ASSERT_EQ(0, builder->build(holder2));
auto dumper2 = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper2, nullptr);
ASSERT_EQ(0, dumper2->create(path));
ASSERT_EQ(0, builder->dump(dumper2));
ASSERT_EQ(0, dumper2->close());
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt2, stats.built_count());
ASSERT_EQ(doc_cnt2, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
}
TEST_F(HnswSparseBuilderTest, TestMemquota) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<OnePassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
size_t doc_cnt = 1000UL;
uint32_t sparse_count = 32;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
ailego::Params params;
params.set("proxima.hnsw.sparse_builder.memory_quota", 100000UL);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder));
ASSERT_EQ(IndexError_NoMemory, builder->build(holder));
}
TEST_F(HnswSparseBuilderTest, TestIndexThreads) {
IndexBuilder::Pointer builder1 =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder1, nullptr);
IndexBuilder::Pointer builder2 =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder2, nullptr);
auto holder =
make_shared<MultiPassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
size_t doc_cnt = 1000UL;
uint32_t sparse_count = 32;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
ailego::Params params;
std::srand(ailego::Realtime::MilliSeconds());
auto threads =
std::make_shared<SingleQueueIndexThreads>(std::rand() % 4, false);
ASSERT_EQ(0, builder1->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder2->init(*_index_meta_ptr, params));
auto build_index1 = [&]() {
ASSERT_EQ(0, builder1->train(threads, holder));
ASSERT_EQ(0, builder1->build(threads, holder));
};
auto build_index2 = [&]() {
ASSERT_EQ(0, builder2->train(threads, holder));
ASSERT_EQ(0, builder2->build(threads, holder));
};
auto t1 = std::async(std::launch::async, build_index1);
auto t2 = std::async(std::launch::async, build_index2);
t1.wait();
t2.wait();
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestIndexThreads";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder1->dump(dumper));
ASSERT_EQ(0, dumper->close());
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder2->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats1 = builder1->stats();
ASSERT_EQ(doc_cnt, stats1.built_count());
auto &stats2 = builder2->stats();
ASSERT_EQ(doc_cnt, stats2.built_count());
}
TEST_F(HnswSparseBuilderTest, TestHalfFloatConverter) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder, nullptr);
auto holder =
make_shared<OnePassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
for (size_t i = 0; i < doc_cnt; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder->emplace(i, vec));
}
ailego::Params converter_params;
auto converter = IndexFactory::CreateConverter("HalfFloatSparseConverter");
converter->init(*_index_meta_ptr, converter_params);
IndexMeta index_meta = converter->meta();
converter->transform(holder);
auto converted_holder = converter->sparse_result();
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_BUILDER_THREAD_COUNT, 1);
ASSERT_EQ(0, builder->init(index_meta, converter_params));
ASSERT_EQ(0, builder->train(converted_holder));
ASSERT_EQ(0, builder->build(converted_holder));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestHalFloatConverter";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
// cleanup and rebuild
ASSERT_EQ(0, builder->cleanup());
auto holder2 =
make_shared<MultiPassIndexSparseHolder<IndexMeta::DataType::DT_FP32>>();
size_t doc_cnt2 = 2000UL;
for (size_t i = 0; i < doc_cnt2; i++) {
SparseVector<float> vec;
NumericalVector<uint32_t> sparse_indices(sparse_count);
NumericalVector<float> sparse_values(sparse_count);
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices[j] = 20 * j;
sparse_values[j] = i;
}
vec.add_sparses(sparse_indices, sparse_values);
ASSERT_TRUE(holder2->emplace(i, vec));
}
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->train(holder2));
ASSERT_EQ(0, builder->build(holder2));
auto dumper2 = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper2, nullptr);
ASSERT_EQ(0, dumper2->create(path));
ASSERT_EQ(0, builder->dump(dumper2));
ASSERT_EQ(0, dumper2->close());
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt2, stats.built_count());
ASSERT_EQ(doc_cnt2, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
}
TEST_F(HnswSparseBuilderTest, TestIndptr) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder, nullptr);
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
std::vector<uint64_t> keys;
keys.reserve(doc_cnt);
std::vector<uint64_t> sparse_indptr;
sparse_indptr.reserve(doc_cnt + 1);
std::vector<uint32_t> sparse_indices;
sparse_indices.reserve(doc_cnt * sparse_count);
std::vector<float> sparse_values;
sparse_values.reserve(doc_cnt * sparse_count);
size_t sparse_count_total = 0;
sparse_indptr.push_back(0);
for (size_t i = 0; i < doc_cnt; i++) {
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices.push_back(20 * j);
sparse_values.push_back(i);
}
keys.push_back(i);
sparse_count_total += sparse_count;
sparse_indptr.push_back(sparse_count_total);
}
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_BUILDER_THREAD_COUNT, 1);
ASSERT_EQ(0, builder->init(*_index_meta_ptr, params));
ASSERT_EQ(0, builder->build(doc_cnt, keys.data(), sparse_indptr.data(),
sparse_indices.data(), sparse_values.data()));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestIndptr";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
}
TEST_F(HnswSparseBuilderTest, TestIndptrFp16) {
IndexBuilder::Pointer builder =
IndexFactory::CreateBuilder("HnswSparseBuilder");
ASSERT_NE(builder, nullptr);
uint32_t sparse_count = 4;
size_t doc_cnt = 1000UL;
std::vector<uint64_t> keys;
keys.reserve(doc_cnt);
std::vector<uint64_t> sparse_indptr;
sparse_indptr.reserve(doc_cnt + 1);
std::vector<uint32_t> sparse_indices;
sparse_indices.reserve(doc_cnt * sparse_count);
std::vector<float> sparse_values;
sparse_values.reserve(doc_cnt * sparse_count);
size_t sparse_count_total = 0;
sparse_indptr.push_back(0);
for (size_t i = 0; i < doc_cnt; i++) {
for (size_t j = 0; j < sparse_count; ++j) {
sparse_indices.push_back(20 * j);
sparse_values.push_back(i);
}
keys.push_back(i);
sparse_count_total += sparse_count;
sparse_indptr.push_back(sparse_count_total);
}
IndexMeta meta(IndexMeta::MetaType::MT_SPARSE, IndexMeta::DataType::DT_FP16);
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_BUILDER_THREAD_COUNT, 1);
ASSERT_EQ(0, builder->init(meta, params));
IndexQueryMeta qmeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
ASSERT_EQ(0, builder->build(qmeta, doc_cnt, keys.data(), sparse_indptr.data(),
sparse_indices.data(), sparse_values.data()));
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
string path = _dir + "TestIndptrFp16";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats = builder->stats();
ASSERT_EQ(0UL, stats.trained_count());
ASSERT_EQ(doc_cnt, stats.built_count());
ASSERT_EQ(doc_cnt, stats.dumped_count());
ASSERT_EQ(0UL, stats.discarded_count());
ASSERT_EQ(0UL, stats.trained_costtime());
ASSERT_GT(stats.built_costtime(), 0UL);
// ASSERT_GT(stats.dumped_costtime(), 0UL);
}
} // namespace core
} // namespace zvec
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,360 @@
// 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 <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <future>
#include <iostream>
#include <memory>
#include <ailego/math/norm_matrix.h>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include "tests/test_util.h"
#include "hnsw_sparse_streamer.h"
using namespace std;
using namespace testing;
using namespace zvec::ailego;
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
namespace zvec {
namespace core {
class HnswSparseStreamerTest : public testing::Test {
protected:
void SetUp(void);
void TearDown(void);
void generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm);
static std::string dir_;
static shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string HnswSparseStreamerTest::dir_(
"hnsw_sparse_streamer_buffer_test_dir/");
shared_ptr<IndexMeta> HnswSparseStreamerTest::index_meta_ptr_;
void HnswSparseStreamerTest::generate_sparse_data(
size_t cnt, uint32_t sparse_dim_count,
std::vector<NumericalVector<uint32_t>> &sparse_indices_list,
std::vector<NumericalVector<float>> &sparse_vec_list, bool norm) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
for (size_t i = 0; i < cnt; ++i) {
// prepare sparse
NumericalVector<uint32_t> sparse_indices(sparse_dim_count);
NumericalVector<float> sparse_vec(sparse_dim_count);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_indices[j] = j * 20;
sparse_vec[j] = dist(gen);
}
float norm;
ailego::Norm2Matrix<float, 1>::Compute(sparse_vec.data(), sparse_dim_count,
&norm);
for (size_t j = 0; j < sparse_dim_count; ++j) {
sparse_vec[j] = sparse_vec[j] / norm;
}
sparse_indices_list.push_back(sparse_indices);
sparse_vec_list.push_back(sparse_vec);
}
}
void HnswSparseStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (nothrow) IndexMeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32));
index_meta_ptr_->set_metric("InnerProductSparse", 0, ailego::Params());
zvec::test_util::RemoveTestPath(dir_);
}
void HnswSparseStreamerTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
TEST_F(HnswSparseStreamerTest, TestGeneral) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswSparseStreamer");
ASSERT_TRUE(write_streamer != nullptr);
size_t sparse_dim_count = 32;
IndexMeta index_meta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
index_meta.set_metric("InnerProductSparse", 0, ailego::Params());
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_STREAMER_MAX_NEIGHBOR_COUNT, 20);
params.set(PARAM_HNSW_SPARSE_STREAMER_SCALING_FACTOR, 16);
params.set(PARAM_HNSW_SPARSE_STREAMER_EFCONSTRUCTION, 10);
params.set(PARAM_HNSW_SPARSE_STREAMER_EF, 5);
params.set(PARAM_HNSW_SPARSE_STREAMER_BRUTE_FORCE_THRESHOLD, 1000U);
ailego::Params stg_params;
auto write_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, write_storage->init(stg_params));
ASSERT_EQ(0, write_storage->open(dir_ + "Test/HnswSparseSearch", true));
ASSERT_EQ(0, write_streamer->init(index_meta, params));
ASSERT_EQ(0, write_streamer->open(write_storage));
size_t cnt = 20000U;
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
IndexQueryMeta qmeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, write_streamer->add_impl(
i, sparse_dim_count, sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
write_storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswSparseStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/HnswSparseSearch", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
auto linearCtx = read_streamer->create_context();
ASSERT_TRUE(!!linearCtx);
auto knnCtx = read_streamer->create_context();
ASSERT_TRUE(!!knnCtx);
// streamer->print_debug_info();
size_t topk = 200;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
for (size_t i = 0; i < cnt; i += 100) {
const auto &sparse_indices = sparse_indices_list[i];
const auto &sparse_vec = sparse_vec_list[i];
auto t1 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(
0, read_streamer->search_impl(sparse_dim_count, sparse_indices.data(),
sparse_vec.data(), qmeta, knnCtx));
auto t2 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(0, read_streamer->search_bf_impl(
sparse_dim_count, sparse_indices.data(), sparse_vec.data(),
qmeta, linearCtx));
auto t3 = ailego::Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
// std::cout << "i: " << i << std::endl;
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * 1.0f / totalCnts;
float topk1Recall = topk1Hits * 100.0f / cnt;
float cost = linearTotalTime * 1.0f / knnTotalTime;
#if 0
printf("knnTotalTime=%zd linearTotalTime=%zd totalHits=%d totalCnts=%d "
"R@%zd=%f R@1=%f cost=%f\n",
knnTotalTime, linearTotalTime, totalHits, totalCnts, topk, recall,
topk1Recall, cost);
#endif
EXPECT_GT(recall, 0.80f);
EXPECT_GT(topk1Recall, 0.80f);
// EXPECT_GT(cost, 2.0f);
}
TEST_F(HnswSparseStreamerTest, TestHnswSearchMMap) {
IndexStreamer::Pointer write_streamer =
IndexFactory::CreateStreamer("HnswSparseStreamer");
ASSERT_TRUE(write_streamer != nullptr);
size_t sparse_dim_count = 32;
IndexMeta index_meta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
index_meta.set_metric("InnerProductSparse", 0, ailego::Params());
ailego::Params params;
params.set(PARAM_HNSW_SPARSE_STREAMER_MAX_NEIGHBOR_COUNT, 20);
params.set(PARAM_HNSW_SPARSE_STREAMER_SCALING_FACTOR, 16);
params.set(PARAM_HNSW_SPARSE_STREAMER_EFCONSTRUCTION, 10);
params.set(PARAM_HNSW_SPARSE_STREAMER_EF, 5);
params.set(PARAM_HNSW_SPARSE_STREAMER_BRUTE_FORCE_THRESHOLD, 1000U);
ailego::Params stg_params;
auto write_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, write_storage->init(stg_params));
ASSERT_EQ(0, write_storage->open(dir_ + "Test/HnswSparseSearch", true));
ASSERT_EQ(0, write_streamer->init(index_meta, params));
ASSERT_EQ(0, write_streamer->open(write_storage));
size_t cnt = 20000U;
auto ctx = write_streamer->create_context();
ASSERT_TRUE(!!ctx);
std::vector<NumericalVector<uint32_t>> sparse_indices_list;
std::vector<NumericalVector<float>> sparse_vec_list;
generate_sparse_data(cnt, sparse_dim_count, sparse_indices_list,
sparse_vec_list, true);
IndexQueryMeta qmeta(IndexMeta::MetaType::MT_SPARSE,
IndexMeta::DataType::DT_FP32);
for (size_t i = 0; i < cnt; i++) {
ASSERT_EQ(0, write_streamer->add_impl(
i, sparse_dim_count, sparse_indices_list[i].data(),
sparse_vec_list[i].data(), qmeta, ctx));
}
write_streamer->flush(0UL);
write_streamer->close();
write_streamer.reset();
write_storage->close();
IndexStreamer::Pointer read_streamer =
IndexFactory::CreateStreamer("HnswSparseStreamer");
ASSERT_EQ(0, read_streamer->init(*index_meta_ptr_, params));
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, read_storage);
ASSERT_EQ(0, read_storage->init(stg_params));
ASSERT_EQ(0, read_storage->open(dir_ + "Test/HnswSparseSearch", false));
ASSERT_EQ(0, read_streamer->open(read_storage));
auto linearCtx = read_streamer->create_context();
ASSERT_TRUE(!!linearCtx);
auto knnCtx = read_streamer->create_context();
ASSERT_TRUE(!!knnCtx);
// streamer->print_debug_info();
size_t topk = 200;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
uint64_t knnTotalTime = 0;
uint64_t linearTotalTime = 0;
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
for (size_t i = 0; i < cnt; i += 100) {
const auto &sparse_indices = sparse_indices_list[i];
const auto &sparse_vec = sparse_vec_list[i];
auto t1 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(
0, read_streamer->search_impl(sparse_dim_count, sparse_indices.data(),
sparse_vec.data(), qmeta, knnCtx));
auto t2 = ailego::Realtime::MicroSeconds();
ASSERT_EQ(0, read_streamer->search_bf_impl(
sparse_dim_count, sparse_indices.data(), sparse_vec.data(),
qmeta, linearCtx));
auto t3 = ailego::Realtime::MicroSeconds();
knnTotalTime += t2 - t1;
linearTotalTime += t3 - t2;
// std::cout << "i: " << i << std::endl;
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * 1.0f / totalCnts;
float topk1Recall = topk1Hits * 100.0f / cnt;
float cost = linearTotalTime * 1.0f / knnTotalTime;
#if 0
printf("knnTotalTime=%zd linearTotalTime=%zd totalHits=%d totalCnts=%d "
"R@%zd=%f R@1=%f cost=%f\n",
knnTotalTime, linearTotalTime, totalHits, totalCnts, topk, recall,
topk1Recall, cost);
#endif
EXPECT_GT(recall, 0.80f);
EXPECT_GT(topk1Recall, 0.80f);
// EXPECT_GT(cost, 2.0f);
}
} // namespace core
} // namespace zvec
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_cluster core_knn_flat core_knn_ivf
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/ivf
)
endforeach()
@@ -0,0 +1,528 @@
// 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 "ivf_builder.h"
#include <future>
#include <iostream>
#include <vector>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
using namespace zvec::core;
using namespace zvec::ailego;
using namespace std;
class IVFBuilderTest : public testing::Test {
protected:
void SetUp() override;
void TearDown() override;
void prepare_index_holder(uint32_t base_key, uint32_t num);
IndexMeta index_meta_;
Params params_;
uint32_t dimension_;
IndexHolder::Pointer holder_;
IndexThreads::Pointer threads_{};
};
void IVFBuilderTest::SetUp() {
dimension_ = 8U;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
index_meta_.set_metric("SquaredEuclidean", 0, Params());
params_.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "8");
params_.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster");
std::mt19937 gen((std::random_device())());
bool v = std::uniform_int_distribution<size_t>(0, 1)(gen);
if (v) {
threads_ = std::make_shared<SingleQueueIndexThreads>();
}
}
void IVFBuilderTest::TearDown() {}
void IVFBuilderTest::prepare_index_holder(uint32_t base_key, uint32_t num) {
MultiPassIndexHolder<IndexMeta::DataType::DT_FP32> *holder =
new MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>(dimension_);
uint32_t key = base_key;
for (size_t i = 0; i < num; ++i) {
NumericalVector<float> vec(dimension_);
for (size_t j = 0; j < dimension_; ++j) {
vec[j] = 1.0f * i;
}
holder->emplace(key + i, vec);
}
holder_.reset(holder);
}
TEST_F(IVFBuilderTest, TestInitSuccess) {
IVFBuilder builder;
int ret = builder.init(index_meta_, params_);
EXPECT_EQ(0, ret);
}
TEST_F(IVFBuilderTest, TestInitFailedWithInvalidMetric) {
IVFBuilder builder;
index_meta_.set_metric("invalid", 0, Params());
int ret = builder.init(index_meta_, params_);
EXPECT_EQ(IndexError_NoExist, ret);
}
TEST_F(IVFBuilderTest, TestInitFailedWithInvalidCentroidsNum) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
ret = builder.train(threads_, holder_);
EXPECT_EQ(IndexError_InvalidArgument, ret);
}
TEST_F(IVFBuilderTest, TestTrainWithHolder1Level) {
IVFBuilder builder;
int ret = builder.init(index_meta_, params_);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_GT(centroid_index->centroids_count(), 0u);
}
TEST_F(IVFBuilderTest, TestTrainWithHolder2Level) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_EQ(centroid_index->centroids_count(), 8);
}
TEST_F(IVFBuilderTest, TestTrainWithTrainer2Level) {
IndexTrainer::Pointer trainer =
IndexFactory::CreateTrainer("StratifiedClusterTrainer");
ASSERT_TRUE(!!trainer);
prepare_index_holder(0, 1000);
Params params;
params.set("zvec.stratified.trainer.cluster_count", "4*2");
ASSERT_EQ(0, trainer->init(index_meta_, params));
ASSERT_EQ(0, trainer->train(threads_, holder_));
IVFBuilder builder;
int ret = builder.init(index_meta_, params_);
EXPECT_EQ(0, ret);
ret = builder.train(trainer);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_EQ(centroid_index->centroids_count(), 8);
}
TEST_F(IVFBuilderTest, TestTrainWithTrainer1Level) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
IndexTrainer::Pointer trainer =
IndexFactory::CreateTrainer("StratifiedClusterTrainer");
ASSERT_TRUE(!!trainer);
prepare_index_holder(0, 1000);
Params params1;
params1.set("zvec.stratified.trainer.cluster_count", "4");
ASSERT_EQ(0, trainer->init(index_meta_, params1));
ASSERT_EQ(0, trainer->train(threads_, holder_));
ret = builder.train(trainer);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_EQ(centroid_index->centroids_count(), 4);
}
TEST_F(IVFBuilderTest, TestBuildWith2Level) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
}
TEST_F(IVFBuilderTest, TestBuildWith1Level) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
}
TEST_F(IVFBuilderTest, TestDump) {
IVFBuilder builder;
int ret = builder.init(index_meta_, params_);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
ret = dumper->create("path");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
EXPECT_EQ((size_t)1000, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
}
#if 0
TEST_F(IVFBuilderTest, TestBuildWithNoEnoughMemory)
{
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
dimension_ = 256;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(IndexError_IndexFull, ret);
}
#endif
TEST_F(IVFBuilderTest, TestBuildWithEnoughMemory) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
dimension_ = 256;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
ret = dumper->create("path");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
EXPECT_EQ((size_t)1000, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
}
#if 0
TEST_F(IVFBuilderTest, TestBuildWithRowMajorAndNoEnoughMemory)
{
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
dimension_ = 256;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
index_meta_.set_major_order(IndexMeta::MajorOrder::MO_ROW);
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(IndexError_IndexFull, ret);
}
#endif
TEST_F(IVFBuilderTest, TestBuildWithRowMajorAndMemory) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
dimension_ = 256;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
index_meta_.set_major_order(IndexMeta::MajorOrder::MO_ROW);
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
ret = dumper->create("path");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
EXPECT_EQ((size_t)1000, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
}
TEST_F(IVFBuilderTest, TestBuildWithEmptyCentroid) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "2*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
dimension_ = 256;
index_meta_.set_meta(IndexMeta::DataType::DT_FP32, dimension_);
index_meta_.set_major_order(IndexMeta::MajorOrder::MO_ROW);
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
size_t doc_cnt = 10;
MultiPassIndexHolder<IndexMeta::DataType::DT_FP32> *holder =
new MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>(dimension_);
for (size_t i = 0; i < doc_cnt; ++i) {
NumericalVector<float> vec(dimension_);
for (size_t j = 0; j < dimension_; ++j) {
vec[j] = 1.0f;
}
holder->emplace(i, vec);
}
holder_.reset(holder);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
ret = dumper->create("path");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)10, builder.stats().built_count());
EXPECT_EQ((size_t)10, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
}
TEST_F(IVFBuilderTest, TestTrainClusterParams) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "2*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster");
prepare_index_holder(0, 1000);
EXPECT_EQ(0, builder.init(index_meta_, params));
EXPECT_EQ(0, builder.train(threads_, holder_));
EXPECT_EQ(0, builder.build(threads_, holder_));
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
EXPECT_EQ(0, dumper->create("test.index"));
EXPECT_EQ(0, builder.dump(dumper));
}
TEST_F(IVFBuilderTest, TestBuildWithConverterClass) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster");
params.set(PARAM_IVF_BUILDER_CONVERTER_CLASS, "HalfFloatConverter");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_GT(centroid_index->centroids_count(), 0u);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("MemoryDumper");
ret = dumper->create("path");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
EXPECT_EQ((size_t)1000, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
}
TEST_F(IVFBuilderTest, TestBuildWithConverterClassMultiLevel) {
IVFBuilder builder;
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "4*2");
params.set(PARAM_IVF_BUILDER_CLUSTER_CLASS, "KmeansCluster*KmeansCluster");
params.set(PARAM_IVF_BUILDER_CONVERTER_CLASS, "HalfFloatConverter");
int ret = builder.init(index_meta_, params);
EXPECT_EQ(0, ret);
prepare_index_holder(0, 1000);
ret = builder.train(threads_, holder_);
EXPECT_EQ(0, ret);
ret = builder.build(threads_, holder_);
EXPECT_EQ(0, ret);
auto centroid_index = builder.centroid_index();
EXPECT_EQ(centroid_index->centroids_count(), 8);
IndexDumper::Pointer dumper = IndexFactory::CreateDumper("FileDumper");
ret = dumper->create("./ivf_converter_test.index");
EXPECT_EQ(0, ret);
ret = builder.dump(dumper);
EXPECT_EQ((size_t)1000, builder.stats().built_count());
EXPECT_EQ((size_t)1000, builder.stats().dumped_count());
EXPECT_EQ((size_t)0, builder.stats().discarded_count());
EXPECT_EQ(0, dumper->close());
File::RemovePath("./ivf_converter_test.index");
}
TEST_F(IVFBuilderTest, TestIndexThreads) {
IndexBuilder::Pointer builder1 = IndexFactory::CreateBuilder("IVFBuilder");
ASSERT_NE(builder1, nullptr);
IndexBuilder::Pointer builder2 = IndexFactory::CreateBuilder("IVFBuilder");
ASSERT_NE(builder2, nullptr);
size_t dim = 128UL;
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim);
std::srand(Realtime::MilliSeconds());
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(dim);
size_t doc_cnt = 1000;
for (size_t i = 0; i < doc_cnt; i++) {
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = i;
}
ASSERT_TRUE(holder->emplace(i, vec));
}
Params params;
params.set(PARAM_IVF_BUILDER_CENTROID_COUNT, "2*2");
ASSERT_EQ(0, builder1->init(meta, params));
ASSERT_EQ(0, builder2->init(meta, params));
auto threads =
std::make_shared<SingleQueueIndexThreads>(std::rand() % 4, false);
auto build_index1 = [&]() {
ASSERT_EQ(0, builder1->train(threads, holder));
ASSERT_EQ(0, builder1->build(threads, holder));
};
auto build_index2 = [&]() {
ASSERT_EQ(0, builder2->train(threads, holder));
ASSERT_EQ(0, builder2->build(threads, holder));
};
auto t1 = std::async(std::launch::async, build_index1);
auto t2 = std::async(std::launch::async, build_index2);
t1.wait();
t2.wait();
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_NE(dumper, nullptr);
std::string path = "./hc_index";
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder1->dump(dumper));
ASSERT_EQ(0, dumper->close());
ASSERT_EQ(0, dumper->create(path));
ASSERT_EQ(0, builder2->dump(dumper));
ASSERT_EQ(0, dumper->close());
auto &stats1 = builder1->stats();
ASSERT_EQ(doc_cnt, stats1.built_count());
auto &stats2 = builder2->stats();
ASSERT_EQ(doc_cnt, stats2.built_count());
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_metric core_quantizer core_knn_vamana core_knn_hnsw core_knn_flat
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm/vamana
)
endforeach()
@@ -0,0 +1,892 @@
// 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 "vamana_streamer.h"
#include <sys/stat.h>
#include <sys/types.h>
#ifndef _MSC_VER
#include <fcntl.h>
#include <unistd.h>
#endif
#include <future>
#include <iostream>
#include <memory>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include "tests/test_util.h"
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
using namespace std;
using namespace testing;
using namespace zvec::ailego;
namespace zvec {
namespace core {
constexpr size_t kDim = 16;
class VamanaStreamerTest : public testing::Test {
protected:
void SetUp(void) override;
void TearDown(void) override;
IndexStreamer::Pointer CreateVamanaStreamer(
const ailego::Params &extra_params = ailego::Params());
static std::string dir_;
static shared_ptr<IndexMeta> index_meta_ptr_;
};
std::string VamanaStreamerTest::dir_("vamana_streamer_test_dir/");
shared_ptr<IndexMeta> VamanaStreamerTest::index_meta_ptr_;
void VamanaStreamerTest::SetUp(void) {
index_meta_ptr_.reset(new (nothrow)
IndexMeta(IndexMeta::DataType::DT_FP32, kDim));
index_meta_ptr_->set_metric("SquaredEuclidean", 0, ailego::Params());
zvec::test_util::RemoveTestPath(dir_);
}
void VamanaStreamerTest::TearDown(void) {
zvec::test_util::RemoveTestPath(dir_);
}
IndexStreamer::Pointer VamanaStreamerTest::CreateVamanaStreamer(
const ailego::Params &extra_params) {
auto streamer = IndexFactory::CreateStreamer("VamanaStreamer");
if (!streamer) return nullptr;
ailego::Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 32U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 100U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
params.set(PARAM_VAMANA_STREAMER_EF, 64U);
params.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 500U);
params.merge(extra_params);
if (streamer->init(*index_meta_ptr_, params) != 0) {
return nullptr;
}
return streamer;
}
TEST_F(VamanaStreamerTest, TestAddVector) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestAddVector", true));
ASSERT_EQ(0, streamer->open(storage));
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
for (size_t i = 0; i < 1000UL; i++) {
NumericalVector<float> vec(kDim);
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
streamer->flush(0UL);
streamer.reset();
}
TEST_F(VamanaStreamerTest, TestLinearSearch) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestLinearSearch.index", true));
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 5000UL;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
NumericalVector<float> vec(kDim);
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
size_t topk = 3;
for (size_t i = 0; i < cnt; i += 1) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ctx->set_topk(1U);
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result1 = ctx->result();
ASSERT_EQ(1UL, result1.size());
ASSERT_EQ(i, result1[0].key());
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i) + 0.1f;
}
ctx->set_topk(topk);
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, ctx));
auto &result2 = ctx->result();
ASSERT_EQ(topk, result2.size());
ASSERT_EQ(i, result2[0].key());
ASSERT_EQ(i == cnt - 1 ? i - 1 : i + 1, result2[1].key());
ASSERT_EQ(i == 0 ? 2 : (i == cnt - 1 ? i - 2 : i - 1), result2[2].key());
}
}
TEST_F(VamanaStreamerTest, TestKnnSearch) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
ailego::Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestKnnSearch.index", true));
ASSERT_EQ(0, streamer->open(storage));
NumericalVector<float> vec(kDim);
size_t cnt = 5000U;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
auto linearCtx = streamer->create_context();
auto knnCtx = streamer->create_context();
size_t topk = 100;
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
int totalHits = 0;
int totalCnts = 0;
int topk1Hits = 0;
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i) + 0.1f;
}
ASSERT_EQ(0, streamer->search_impl(vec.data(), qmeta, knnCtx));
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
topk1Hits += i == knnResult[0].key();
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * 1.0f / totalCnts;
float topk1Recall = topk1Hits * 1.0f / cnt;
EXPECT_GT(recall, 0.90f);
EXPECT_GT(topk1Recall, 0.95f);
}
TEST_F(VamanaStreamerTest, TestOpenClose) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
constexpr size_t dim_large = 128;
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim_large);
meta.set_metric("SquaredEuclidean", 0, ailego::Params());
ailego::Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 32U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 100U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
streamer = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer);
ASSERT_EQ(0, streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestOpenClose.index", true));
ASSERT_EQ(0, streamer->open(storage));
size_t testCnt = 200;
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim_large);
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
for (size_t i = 0; i < testCnt; i++) {
std::vector<float> vec(dim_large);
for (size_t d = 0; d < dim_large; ++d) {
vec[d] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
ASSERT_EQ(0, streamer->flush(0UL));
ASSERT_EQ(0, streamer->close());
// Re-open and verify data
ASSERT_EQ(0, streamer->open(storage));
auto provider = streamer->create_provider();
auto iter = provider->create_iterator();
ASSERT_TRUE(!!iter);
size_t total = 0;
while (iter->is_valid()) {
float *data = (float *)iter->data();
for (size_t d = 0; d < dim_large; ++d) {
ASSERT_FLOAT_EQ(static_cast<float>(iter->key()), data[d]);
}
total++;
iter->next();
}
ASSERT_EQ(testCnt, total);
}
TEST_F(VamanaStreamerTest, TestKnnMultiThread) {
constexpr size_t dim = 32;
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim);
meta.set_metric("SquaredEuclidean", 0, ailego::Params());
ailego::Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 64U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 500U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
params.set(PARAM_VAMANA_STREAMER_EF, 200U);
params.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 1000U);
params.set(PARAM_VAMANA_STREAMER_MAX_INDEX_SIZE, 30U * 1024U * 1024U);
params.set(PARAM_VAMANA_STREAMER_GET_VECTOR_ENABLE, true);
auto streamer = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer);
ASSERT_EQ(0, streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestKnnMultiThread", true));
ASSERT_EQ(0, streamer->open(storage));
auto addVector = [&streamer, dim](int baseKey, size_t addCnt) {
NumericalVector<float> vec(dim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
size_t succAdd = 0;
auto ctx = streamer->create_context();
for (size_t i = 0; i < addCnt; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i + baseKey);
}
succAdd += !streamer->add_impl(baseKey + i, vec.data(), qmeta, ctx);
}
streamer->flush(0UL);
return succAdd;
};
auto t1 = std::async(std::launch::async, addVector, 0, 1000);
auto t2 = std::async(std::launch::async, addVector, 1000, 1000);
auto t3 = std::async(std::launch::async, addVector, 2000, 1000);
ASSERT_EQ(1000U, t1.get());
ASSERT_EQ(1000U, t2.get());
ASSERT_EQ(1000U, t3.get());
streamer->close();
// Verify data
ASSERT_EQ(0, streamer->open(storage));
auto provider = streamer->create_provider();
auto iter = provider->create_iterator();
ASSERT_TRUE(!!iter);
size_t total = 0;
uint64_t minKey = 10000;
uint64_t maxKey = 0;
while (iter->is_valid()) {
float *data = (float *)iter->data();
for (size_t d = 0; d < dim; ++d) {
ASSERT_FLOAT_EQ(static_cast<float>(iter->key()), data[d]);
}
total++;
minKey = std::min(minKey, iter->key());
maxKey = std::max(maxKey, iter->key());
iter->next();
}
ASSERT_EQ(3000, total);
ASSERT_EQ(0, minKey);
ASSERT_EQ(2999, maxKey);
// Multi-thread search
size_t topk = 100;
size_t cnt = 3000;
auto knnSearch = [&]() {
NumericalVector<float> vec(dim);
auto linearCtx = streamer->create_context();
auto knnCtx = streamer->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
size_t totalCnts = 0;
size_t totalHits = 0;
for (size_t i = 0; i < cnt; i += 1) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i) + 0.1f;
}
ASSERT_EQ(0, streamer->search_impl(vec.data(), qmeta, knnCtx));
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
ASSERT_TRUE((totalHits * 1.0f / totalCnts) > 0.80f);
};
auto s1 = std::async(std::launch::async, knnSearch);
auto s2 = std::async(std::launch::async, knnSearch);
auto s3 = std::async(std::launch::async, knnSearch);
s1.wait();
s2.wait();
s3.wait();
}
TEST_F(VamanaStreamerTest, TestContiguousMemory) {
ailego::Params extra;
extra.set(PARAM_VAMANA_STREAMER_USE_CONTIGUOUS_MEMORY, true);
extra.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 2000U);
auto streamer = CreateVamanaStreamer(extra);
ASSERT_NE(nullptr, streamer);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestContiguous.index", true));
// First build with default mmap mode
{
auto builder_streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, builder_streamer);
ASSERT_EQ(0, builder_streamer->open(storage));
auto ctx = builder_streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
NumericalVector<float> vec(kDim);
size_t cnt = 3000UL;
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, builder_streamer->add_impl(i, vec.data(), qmeta, ctx));
}
ASSERT_EQ(0, builder_streamer->flush(0UL));
ASSERT_EQ(0, builder_streamer->close());
}
// Re-open with contiguous memory mode for search
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 3000UL;
size_t topk = 50;
NumericalVector<float> vec(kDim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
auto linearCtx = streamer->create_context();
auto knnCtx = streamer->create_context();
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
int totalHits = 0;
int totalCnts = 0;
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i) + 0.1f;
}
ASSERT_EQ(0, streamer->search_impl(vec.data(), qmeta, knnCtx));
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
float recall = totalHits * 1.0f / totalCnts;
EXPECT_GT(recall, 0.90f);
}
TEST_F(VamanaStreamerTest, TestContiguousMultiThreadSearch) {
constexpr size_t dim = 32;
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim);
meta.set_metric("SquaredEuclidean", 0, ailego::Params());
// Build with mmap mode
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestContiguousMT", true));
{
ailego::Params build_params;
build_params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 64U);
build_params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 128U);
build_params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
build_params.set(PARAM_VAMANA_STREAMER_EF, 64U);
build_params.set(PARAM_VAMANA_STREAMER_MAX_INDEX_SIZE, 30U * 1024U * 1024U);
build_params.set(PARAM_VAMANA_STREAMER_GET_VECTOR_ENABLE, true);
auto builder = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, builder);
ASSERT_EQ(0, builder->init(meta, build_params));
ASSERT_EQ(0, builder->open(storage));
auto ctx = builder->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t i = 0; i < 3000; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, builder->add_impl(i, vec.data(), qmeta, ctx));
}
ASSERT_EQ(0, builder->flush(0UL));
ASSERT_EQ(0, builder->close());
}
// Re-open with contiguous memory
ailego::Params search_params;
search_params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 64U);
search_params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 128U);
search_params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
search_params.set(PARAM_VAMANA_STREAMER_EF, 64U);
search_params.set(PARAM_VAMANA_STREAMER_MAX_INDEX_SIZE, 30U * 1024U * 1024U);
search_params.set(PARAM_VAMANA_STREAMER_GET_VECTOR_ENABLE, true);
search_params.set(PARAM_VAMANA_STREAMER_USE_CONTIGUOUS_MEMORY, true);
auto searcher = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, searcher);
ASSERT_EQ(0, searcher->init(meta, search_params));
ASSERT_EQ(0, searcher->open(storage));
size_t topk = 50;
size_t cnt = 3000;
auto knnSearch = [&]() {
NumericalVector<float> vec(dim);
auto linearCtx = searcher->create_context();
auto knnCtx = searcher->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
linearCtx->set_topk(topk);
knnCtx->set_topk(topk);
size_t totalCnts = 0;
size_t totalHits = 0;
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i) + 0.1f;
}
ASSERT_EQ(0, searcher->search_impl(vec.data(), qmeta, knnCtx));
ASSERT_EQ(0, searcher->search_bf_impl(vec.data(), qmeta, linearCtx));
auto &knnResult = knnCtx->result();
ASSERT_EQ(topk, knnResult.size());
auto &linearResult = linearCtx->result();
ASSERT_EQ(topk, linearResult.size());
ASSERT_EQ(i, linearResult[0].key());
for (size_t k = 0; k < topk; ++k) {
totalCnts++;
for (size_t j = 0; j < topk; ++j) {
if (linearResult[j].key() == knnResult[k].key()) {
totalHits++;
break;
}
}
}
}
ASSERT_TRUE((totalHits * 1.0f / totalCnts) > 0.80f);
};
auto s1 = std::async(std::launch::async, knnSearch);
auto s2 = std::async(std::launch::async, knnSearch);
auto s3 = std::async(std::launch::async, knnSearch);
s1.wait();
s2.wait();
s3.wait();
}
TEST_F(VamanaStreamerTest, TestProvider) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestProvider", true));
ASSERT_EQ(0, streamer->open(storage));
size_t cnt = 500;
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
NumericalVector<float> vec(kDim);
for (size_t i = 0; i < cnt; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
ASSERT_EQ(0, streamer->flush(0UL));
auto provider = streamer->create_provider();
ASSERT_NE(nullptr, provider);
auto iter = provider->create_iterator();
ASSERT_TRUE(!!iter);
size_t total = 0;
while (iter->is_valid()) {
ASSERT_NE(nullptr, iter->data());
float *data = (float *)iter->data();
for (size_t d = 0; d < kDim; ++d) {
ASSERT_FLOAT_EQ(static_cast<float>(iter->key()), data[d]);
}
total++;
iter->next();
}
ASSERT_EQ(cnt, total);
}
TEST_F(VamanaStreamerTest, TestAddAndSearch) {
auto streamer = CreateVamanaStreamer();
ASSERT_NE(nullptr, streamer);
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestAddAndSearch.index", true));
ASSERT_EQ(0, streamer->open(storage));
NumericalVector<float> vec(kDim);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kDim);
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
// Add and search interleaved
for (size_t batch = 0; batch < 5; batch++) {
size_t base = batch * 200;
for (size_t i = 0; i < 200; i++) {
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(base + i);
}
ASSERT_EQ(0, streamer->add_impl(base + i, vec.data(), qmeta, ctx));
}
// Search for recently added vectors
size_t current_cnt = (batch + 1) * 200;
size_t topk = std::min(current_cnt, (size_t)10);
auto searchCtx = streamer->create_context();
searchCtx->set_topk(topk);
for (size_t j = 0; j < kDim; ++j) {
vec[j] = static_cast<float>(base);
}
ASSERT_EQ(0, streamer->search_bf_impl(vec.data(), qmeta, searchCtx));
auto &result = searchCtx->result();
ASSERT_EQ(topk, result.size());
ASSERT_EQ(base, result[0].key());
}
}
TEST_F(VamanaStreamerTest, TestKnnConcurrentAddAndSearch) {
constexpr size_t dim = 32;
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim);
meta.set_metric("SquaredEuclidean", 0, ailego::Params());
ailego::Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 64U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 128U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
params.set(PARAM_VAMANA_STREAMER_EF, 64U);
params.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 500U);
params.set(PARAM_VAMANA_STREAMER_MAX_INDEX_SIZE, 30U * 1024U * 1024U);
params.set(PARAM_VAMANA_STREAMER_GET_VECTOR_ENABLE, true);
auto streamer = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer);
ASSERT_EQ(0, streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestConcurrentAddSearch", true));
ASSERT_EQ(0, streamer->open(storage));
// First add some base data
{
auto ctx = streamer->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t i = 0; i < 2000; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
ASSERT_EQ(0, streamer->add_impl(i, vec.data(), qmeta, ctx));
}
}
std::atomic<bool> stop_search{false};
// Concurrent add
auto addFuture = std::async(std::launch::async, [&]() {
auto ctx = streamer->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t i = 2000; i < 3000; i++) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i);
}
streamer->add_impl(i, vec.data(), qmeta, ctx);
}
stop_search.store(true);
});
// Concurrent search
auto searchFuture = std::async(std::launch::async, [&]() {
auto ctx = streamer->create_context();
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
ctx->set_topk(10);
while (!stop_search.load()) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = 100.1f;
}
int ret = streamer->search_impl(vec.data(), qmeta, ctx);
ASSERT_EQ(0, ret);
auto &result = ctx->result();
ASSERT_GT(result.size(), 0UL);
}
});
addFuture.wait();
searchFuture.wait();
}
// Test concurrent build (parallel add_impl) which was crashing due to
// unprotected node_chunks_ / node_chunk_bases_ access during chunk allocation.
TEST_F(VamanaStreamerTest, TestConcurrentBuild) {
constexpr size_t dim = kDim;
constexpr size_t total_vectors = 5000;
constexpr size_t thread_count = 4;
ailego::Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 32U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 100U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
params.set(PARAM_VAMANA_STREAMER_EF, 64U);
params.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 500U);
params.set(PARAM_VAMANA_STREAMER_MAX_INDEX_SIZE, 50U * 1024U * 1024U);
IndexMeta meta(IndexMeta::DataType::DT_FP32, dim);
meta.set_metric("SquaredEuclidean", 0, ailego::Params());
auto streamer = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer);
ASSERT_EQ(0, streamer->init(meta, params));
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ailego::Params stg_params;
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestConcurrentBuild", true));
ASSERT_EQ(0, streamer->open(storage));
// Parallel insertion from multiple threads (mimics local_builder behavior)
std::atomic<int> error_count{0};
std::vector<std::future<void>> futures;
for (size_t t = 0; t < thread_count; ++t) {
futures.push_back(std::async(std::launch::async, [&, t]() {
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t i = t; i < total_vectors; i += thread_count) {
for (size_t j = 0; j < dim; ++j) {
vec[j] = static_cast<float>(i) + static_cast<float>(j) * 0.01f;
}
int ret = streamer->add_impl(i, vec.data(), qmeta, ctx);
if (ret != 0) {
error_count.fetch_add(1);
return;
}
}
}));
}
for (auto &f : futures) {
f.wait();
}
ASSERT_EQ(0, error_count.load());
// Verify search still works correctly after concurrent build
auto search_ctx = streamer->create_context();
ASSERT_TRUE(!!search_ctx);
search_ctx->set_topk(1);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, dim);
NumericalVector<float> vec(dim);
for (size_t j = 0; j < dim; ++j) {
vec[j] = 0.0f;
}
ASSERT_EQ(0, streamer->search_impl(vec.data(), qmeta, search_ctx));
auto &result = search_ctx->result();
ASSERT_GT(result.size(), 0UL);
}
// Test Vamana + INT8 quantization + rotation end-to-end
TEST_F(VamanaStreamerTest, TestInt8WithRotate) {
constexpr size_t kTestDim = 128;
constexpr size_t kCnt = 2000U;
constexpr size_t kTopk = 10;
IndexStreamer::Pointer streamer =
IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer);
Params params;
params.set(PARAM_VAMANA_STREAMER_MAX_DEGREE, 32U);
params.set(PARAM_VAMANA_STREAMER_SEARCH_LIST_SIZE, 100U);
params.set(PARAM_VAMANA_STREAMER_ALPHA, 1.2f);
params.set(PARAM_VAMANA_STREAMER_EF, 64U);
params.set(PARAM_VAMANA_STREAMER_BRUTE_FORCE_THRESHOLD, 500U);
IndexMeta index_meta_raw(IndexMeta::DataType::DT_FP32, kTestDim);
index_meta_raw.set_metric("SquaredEuclidean", 0, Params());
// Create INT8 converter with rotation enabled
Params converter_params;
converter_params.set("integer_streaming.converter.enable_rotate", true);
auto converter = IndexFactory::CreateConverter("Int8StreamingConverter");
ASSERT_NE(nullptr, converter);
ASSERT_EQ(0, converter->init(index_meta_raw, converter_params));
IndexMeta index_meta = converter->meta();
auto reformer = IndexFactory::CreateReformer(index_meta.reformer_name());
ASSERT_NE(nullptr, reformer);
ASSERT_EQ(0, reformer->init(index_meta.reformer_params()));
Params stg_params;
auto storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage);
ASSERT_EQ(0, storage->init(stg_params));
ASSERT_EQ(0, storage->open(dir_ + "TestInt8WithRotate.index", true));
ASSERT_EQ(0, streamer->init(index_meta, params));
ASSERT_EQ(0, streamer->open(storage));
// Add 2000 vectors
auto ctx = streamer->create_context();
ASSERT_TRUE(!!ctx);
IndexQueryMeta qmeta(IndexMeta::DataType::DT_FP32, kTestDim);
std::mt19937 gen(42);
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
for (size_t i = 0; i < kCnt; i++) {
NumericalVector<float> vec(kTestDim);
for (size_t j = 0; j < kTestDim; ++j) vec[j] = dist(gen);
std::string new_vec;
IndexQueryMeta new_meta;
ASSERT_EQ(0, reformer->convert(vec.data(), qmeta, &new_vec, &new_meta));
ASSERT_EQ(0, streamer->add_impl(i, new_vec.data(), new_meta, ctx));
}
streamer->flush(0UL);
streamer.reset();
storage.reset();
// Reopen: reformer should auto-detect rotator from storage
auto storage2 = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_NE(nullptr, storage2);
ASSERT_EQ(0, storage2->init(stg_params));
ASSERT_EQ(0, storage2->open(dir_ + "TestInt8WithRotate.index", false));
auto streamer2 = IndexFactory::CreateStreamer("VamanaStreamer");
ASSERT_NE(nullptr, streamer2);
ASSERT_EQ(0, streamer2->init(index_meta, params));
ASSERT_EQ(0, streamer2->open(storage2));
auto reformer2 = IndexFactory::CreateReformer(index_meta.reformer_name());
ASSERT_NE(nullptr, reformer2);
ASSERT_EQ(0, reformer2->init(index_meta.reformer_params()));
ASSERT_EQ(0, reformer2->load(storage2));
// Search: verify knn results are non-empty
auto knnCtx = streamer2->create_context();
knnCtx->set_topk(kTopk);
auto linearCtx = streamer2->create_context();
linearCtx->set_topk(kTopk);
NumericalVector<float> query(kTestDim);
for (size_t j = 0; j < kTestDim; ++j) query[j] = dist(gen);
std::string new_query;
IndexQueryMeta new_qmeta;
ASSERT_EQ(0,
reformer2->transform(query.data(), qmeta, &new_query, &new_qmeta));
ASSERT_EQ(0, streamer2->search_impl(new_query.data(), new_qmeta, knnCtx));
ASSERT_EQ(0,
streamer2->search_bf_impl(new_query.data(), new_qmeta, linearCtx));
EXPECT_EQ(kTopk, knnCtx->result().size());
EXPECT_EQ(kTopk, linearCtx->result().size());
}
} // namespace core
} // namespace zvec
#if defined(__GNUC__) || defined(__GNUG__)
#pragma GCC diagnostic pop
#endif
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework
SRCS ${CC_SRCS}
INCS ../../src
)
endforeach()
+15
View File
@@ -0,0 +1,15 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_metric core_interface core_knn_flat core_utility core_quantizer sparsehash core_knn_hnsw core_mix_reducer
core_knn_flat_sparse core_knn_hnsw_sparse core_knn_ivf core_knn_hnsw_rabitq core_knn_vamana
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core/algorithm
)
endforeach()
+539
View File
@@ -0,0 +1,539 @@
// 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 <numeric>
#include <set>
#include <string>
#include <vector>
#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/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"
using namespace zvec::core_interface;
namespace {
constexpr uint32_t kDimension = 4;
constexpr uint32_t kNumDocs = 12;
constexpr uint32_t kNumGroups = 3;
constexpr uint32_t kGroupTopk = 2;
constexpr uint32_t kSearchTopk = 100;
struct GroupByCase {
std::string name;
BaseIndexParam::Pointer index_param;
BaseIndexQueryParam::Pointer query_param;
bool is_sparse = false;
uint32_t dimension = kDimension;
bool with_refiner = false;
};
std::shared_ptr<std::vector<uint64_t>> AllPks() {
auto pks = std::make_shared<std::vector<uint64_t>>();
pks->reserve(kNumDocs);
for (uint32_t i = 0; i < kNumDocs; ++i) {
pks->push_back(i);
}
return pks;
}
void AttachGroupBy(const BaseIndexQueryParam::Pointer &query_param) {
query_param->group_by_param = std::make_shared<GroupByParam>();
query_param->group_by_param->group_count = kNumGroups;
query_param->group_by_param->group_topk = kGroupTopk;
query_param->group_by_param->group_by = [](uint64_t key) {
return std::to_string(key % kNumGroups);
};
}
BaseIndexParam::Pointer DenseFlatParam(uint32_t dimension = kDimension) {
return FlatIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(dimension)
.WithIsSparse(false)
.Build();
}
BaseIndexParam::Pointer SparseFlatParam() {
return FlatIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithIsSparse(true)
.Build();
}
BaseIndexParam::Pointer DenseHnswParam(uint32_t dimension = kDimension) {
return HNSWIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(dimension)
.WithIsSparse(false)
.WithEFConstruction(100)
.Build();
}
BaseIndexParam::Pointer SparseHnswParam() {
return HNSWIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithIsSparse(true)
.WithEFConstruction(100)
.Build();
}
BaseIndexQueryParam::Pointer FlatQuery(bool fetch_vector = false) {
return FlatQueryParamBuilder()
.with_topk(kSearchTopk)
.with_fetch_vector(fetch_vector)
.build();
}
BaseIndexQueryParam::Pointer FlatQuery(bool fetch_vector, bool is_linear,
bool with_bf_pks) {
auto builder = FlatQueryParamBuilder()
.with_topk(kSearchTopk)
.with_fetch_vector(fetch_vector)
.with_is_linear(is_linear);
if (with_bf_pks) {
builder.with_bf_pks(AllPks());
}
return builder.build();
}
BaseIndexQueryParam::Pointer HnswQuery(bool fetch_vector = false,
bool is_linear = false,
bool with_bf_pks = false) {
auto builder = HNSWQueryParamBuilder()
.with_topk(kSearchTopk)
.with_ef_search(kSearchTopk)
.with_fetch_vector(fetch_vector)
.with_is_linear(is_linear);
if (with_bf_pks) {
builder.with_bf_pks(AllPks());
}
return builder.build();
}
#if RABITQ_SUPPORTED
BaseIndexParam::Pointer DenseHnswRabitqParam(uint32_t dimension) {
using namespace zvec::ailego;
using namespace zvec::core;
constexpr size_t kTrainCount = 500;
auto holder =
std::make_shared<MultiPassIndexProvider<IndexMeta::DataType::DT_FP32>>(
dimension);
for (size_t i = 0; i < kTrainCount; ++i) {
NumericalVector<float> vec(dimension, static_cast<float>(i));
EXPECT_TRUE(holder->emplace(i, vec));
}
auto index_meta =
std::make_shared<IndexMeta>(IndexMeta::DataType::DT_FP32, dimension);
index_meta->set_metric("InnerProduct", 0, Params());
RabitqConverter converter;
EXPECT_EQ(0, converter.init(*index_meta, Params()));
EXPECT_EQ(0, converter.train(holder));
std::shared_ptr<IndexReformer> reformer;
EXPECT_EQ(0, converter.to_reformer(&reformer));
return HNSWRabitqIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(dimension)
.WithIsSparse(false)
.WithEFConstruction(100)
.WithProvider(holder)
.WithReformer(reformer)
.Build();
}
BaseIndexQueryParam::Pointer HnswRabitqQuery(bool fetch_vector = false,
bool is_linear = false,
bool with_bf_pks = false) {
auto builder = HNSWRabitqQueryParamBuilder()
.with_topk(kSearchTopk)
.with_ef_search(kSearchTopk)
.with_fetch_vector(fetch_vector)
.with_is_linear(is_linear);
if (with_bf_pks) {
builder.with_bf_pks(AllPks());
}
return builder.build();
}
#endif
#if DISKANN_SUPPORTED
BaseIndexParam::Pointer DenseDiskAnnParam(uint32_t dimension = kDimension) {
return DiskAnnIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(dimension)
.WithIsSparse(false)
.WithMaxDegree(32)
.WithListSize(kSearchTopk)
.WithPqChunkNum(0)
.Build();
}
BaseIndexQueryParam::Pointer DiskAnnQuery(bool fetch_vector = false,
bool is_linear = false,
bool with_bf_pks = false) {
auto query = std::make_shared<DiskAnnQueryParam>();
query->topk = kSearchTopk;
query->list_size = kSearchTopk;
query->fetch_vector = fetch_vector;
query->is_linear = is_linear;
if (with_bf_pks) {
query->bf_pks = AllPks();
}
return query;
}
#endif
class GroupByInterfaceTest : public ::testing::Test {
protected:
void RunOk(const GroupByCase &test_case) {
Run(test_case, /*expect_error=*/false);
}
void RunRejected(const GroupByCase &test_case) {
Run(test_case, /*expect_error=*/true);
}
private:
struct QueryHolder {
std::vector<float> values;
std::vector<uint32_t> indices;
VectorData data;
};
void Run(const GroupByCase &test_case, bool expect_error) {
const std::string index_name = "test_groupby_" + test_case.name;
const std::string source_index_name = index_name + "_source";
zvec::test_util::RemoveTestFiles(index_name + "*");
zvec::test_util::RemoveTestFiles(source_index_name + "*");
auto source = IndexFactory::CreateAndInitIndex(*FlatSourceParam(test_case));
ASSERT_NE(nullptr, source) << test_case.name;
ASSERT_EQ(0, source->Open(source_index_name,
{StorageOptions::StorageType::kMMAP, true}))
<< test_case.name;
for (uint32_t i = 0; i < kNumDocs; ++i) {
AddDoc(source, i, test_case);
}
ASSERT_EQ(0, source->Train()) << test_case.name;
auto index = IndexFactory::CreateAndInitIndex(*test_case.index_param);
ASSERT_NE(nullptr, index) << test_case.name;
ASSERT_EQ(
0, index->Open(index_name, {StorageOptions::StorageType::kMMAP, true}))
<< test_case.name;
ASSERT_EQ(0, index->Merge({source}, IndexFilter())) << test_case.name;
auto query_param = test_case.query_param->Clone();
AttachGroupBy(query_param);
if (test_case.with_refiner) {
query_param->refiner_param = std::make_shared<RefinerParam>();
query_param->refiner_param->scale_factor_ = 1.0f;
query_param->refiner_param->reference_index = source;
}
auto query = MakeQuery(test_case);
SearchResult result;
const int ret = index->Search(query.data, query_param, &result);
if (expect_error) {
ASSERT_NE(0, ret) << test_case.name;
} else {
ASSERT_EQ(0, ret) << test_case.name;
AssertGroupedResult(result, query_param, test_case);
}
ASSERT_EQ(0, index->Close()) << test_case.name;
ASSERT_EQ(0, source->Close()) << test_case.name;
zvec::test_util::RemoveTestFiles(index_name + "*");
zvec::test_util::RemoveTestFiles(source_index_name + "*");
}
BaseIndexParam::Pointer FlatSourceParam(const GroupByCase &test_case) {
if (test_case.is_sparse) {
return SparseFlatParam();
}
return DenseFlatParam(test_case.dimension);
}
void AddDoc(const Index::Pointer &index, uint32_t key,
const GroupByCase &test_case) {
std::vector<float> values(test_case.dimension, static_cast<float>(key));
if (test_case.is_sparse) {
std::vector<uint32_t> indices(test_case.dimension);
std::iota(indices.begin(), indices.end(), 0u);
VectorData data{
SparseVector{test_case.dimension, indices.data(), values.data()}};
ASSERT_EQ(0, index->Add(data, key)) << key;
return;
}
VectorData data{DenseVector{values.data()}};
ASSERT_EQ(0, index->Add(data, key)) << key;
}
QueryHolder MakeQuery(const GroupByCase &test_case) {
QueryHolder holder;
holder.values.assign(test_case.dimension, 1.0f);
if (test_case.is_sparse) {
holder.indices.resize(test_case.dimension);
std::iota(holder.indices.begin(), holder.indices.end(), 0u);
holder.data = VectorData{SparseVector{
test_case.dimension, holder.indices.data(), holder.values.data()}};
} else {
holder.data = VectorData{DenseVector{holder.values.data()}};
}
return holder;
}
void AssertGroupedResult(const SearchResult &result,
const BaseIndexQueryParam::Pointer &query_param,
const GroupByCase &test_case) {
ASSERT_TRUE(result.doc_list_.empty());
ASSERT_EQ(kNumGroups, result.group_doc_list_.size());
std::set<std::string> group_ids;
for (const auto &group : result.group_doc_list_) {
group_ids.insert(group.group_id());
ASSERT_LE(group.docs().size(), kGroupTopk);
ASSERT_GE(group.docs().size(), 1u);
const uint32_t expected_mod = std::stoul(group.group_id());
for (const auto &doc : group.docs()) {
ASSERT_EQ(expected_mod, doc.key() % kNumGroups);
}
for (size_t i = 1; i < group.docs().size(); ++i) {
ASSERT_GE(group.docs()[i - 1].score(), group.docs()[i].score());
}
}
for (uint32_t group = 0; group < kNumGroups; ++group) {
ASSERT_TRUE(group_ids.count(std::to_string(group)) > 0);
}
if (!query_param->fetch_vector) {
return;
}
if (test_case.is_sparse) {
AssertSparseVectorsFetched(result, test_case.dimension);
} else {
AssertDenseVectorsFetched(result, test_case.dimension, test_case.name);
}
}
void AssertDenseVectorsFetched(const SearchResult &result, uint32_t dimension,
const std::string &case_name = "") {
const bool has_reverted = !result.group_reverted_vector_list_.empty();
if (has_reverted) {
ASSERT_EQ(result.group_doc_list_.size(),
result.group_reverted_vector_list_.size());
}
for (size_t group_idx = 0; group_idx < result.group_doc_list_.size();
++group_idx) {
const auto &group = result.group_doc_list_[group_idx];
const std::vector<std::string> *group_vectors = nullptr;
if (has_reverted) {
group_vectors = &result.group_reverted_vector_list_[group_idx];
ASSERT_EQ(group.docs().size(), group_vectors->size());
}
for (size_t doc_idx = 0; doc_idx < group.docs().size(); ++doc_idx) {
const auto &doc = group.docs()[doc_idx];
const float expected = static_cast<float>(doc.key());
const float *vector = nullptr;
if (has_reverted) {
vector =
reinterpret_cast<const float *>((*group_vectors)[doc_idx].data());
} else if (doc.vector() != nullptr) {
vector = reinterpret_cast<const float *>(doc.vector());
} else {
// DiskAnn stores fetched vectors in vector_string_ rather than
// the raw pointer field.
ASSERT_FALSE(doc.vector_string().empty())
<< case_name << " key=" << doc.key();
vector = reinterpret_cast<const float *>(doc.vector_string().data());
}
for (uint32_t i = 0; i < dimension; ++i) {
ASSERT_FLOAT_EQ(expected, vector[i])
<< case_name << " key=" << doc.key() << " i=" << i;
}
}
}
}
void AssertSparseVectorsFetched(const SearchResult &result,
uint32_t dimension) {
const bool has_reverted =
!result.group_reverted_sparse_values_list_.empty();
if (has_reverted) {
ASSERT_EQ(result.group_doc_list_.size(),
result.group_reverted_sparse_values_list_.size());
}
for (size_t group_idx = 0; group_idx < result.group_doc_list_.size();
++group_idx) {
const auto &group = result.group_doc_list_[group_idx];
const std::vector<std::string> *group_sparse_values = nullptr;
if (has_reverted) {
group_sparse_values =
&result.group_reverted_sparse_values_list_[group_idx];
ASSERT_EQ(group.docs().size(), group_sparse_values->size());
}
for (size_t doc_idx = 0; doc_idx < group.docs().size(); ++doc_idx) {
const auto &doc = group.docs()[doc_idx];
const auto &sparse = doc.sparse_doc();
ASSERT_EQ(dimension, sparse.sparse_count());
const auto *indices =
reinterpret_cast<const uint32_t *>(sparse.sparse_indices().data());
const float *values = nullptr;
if (has_reverted) {
values = reinterpret_cast<const float *>(
(*group_sparse_values)[doc_idx].data());
} else {
values =
reinterpret_cast<const float *>(sparse.sparse_values().data());
}
const float expected = static_cast<float>(doc.key());
for (uint32_t i = 0; i < dimension; ++i) {
ASSERT_EQ(i, indices[i]);
ASSERT_FLOAT_EQ(expected, values[i]);
}
}
}
}
};
} // namespace
TEST_F(GroupByInterfaceTest, Dense) {
std::vector<GroupByCase> cases{
{"dense_flat_graph", DenseFlatParam(), FlatQuery()},
{"dense_flat_linear", DenseFlatParam(),
FlatQuery(/*fetch_vector=*/false, /*is_linear=*/true,
/*with_bf_pks=*/false)},
{"dense_flat_bf_pks", DenseFlatParam(),
FlatQuery(/*fetch_vector=*/false, /*is_linear=*/false,
/*with_bf_pks=*/true)},
{"dense_flat_fetch_vector", DenseFlatParam(),
FlatQuery(/*fetch_vector=*/true, /*is_linear=*/false,
/*with_bf_pks=*/false)},
{"dense_hnsw_graph", DenseHnswParam(), HnswQuery()},
{"dense_hnsw_linear", DenseHnswParam(),
HnswQuery(/*fetch_vector=*/false, /*is_linear=*/true)},
{"dense_hnsw_bf_pks", DenseHnswParam(),
HnswQuery(/*fetch_vector=*/false, /*is_linear=*/false,
/*with_bf_pks=*/true)},
{"dense_hnsw_fetch_vector", DenseHnswParam(),
HnswQuery(/*fetch_vector=*/true)},
#if RABITQ_SUPPORTED
{"dense_hnsw_rabitq_graph", DenseHnswRabitqParam(64), HnswRabitqQuery(),
/*is_sparse=*/false, /*dimension=*/64},
{"dense_hnsw_rabitq_linear", DenseHnswRabitqParam(64),
HnswRabitqQuery(/*fetch_vector=*/false, /*is_linear=*/true),
/*is_sparse=*/false, /*dimension=*/64},
{"dense_hnsw_rabitq_bf_pks", DenseHnswRabitqParam(64),
HnswRabitqQuery(/*fetch_vector=*/false, /*is_linear=*/false,
/*with_bf_pks=*/true),
/*is_sparse=*/false, /*dimension=*/64},
// Note: fetch_vector is not supported for RabitQ because the entity
// stores quantized binary data (not original float vectors), and
// RabitqReformer does not implement revert().
#endif
};
for (const auto &test_case : cases) {
RunOk(test_case);
}
}
TEST_F(GroupByInterfaceTest, Sparse) {
std::vector<GroupByCase> cases{
{"sparse_flat_graph", SparseFlatParam(), FlatQuery(),
/*is_sparse=*/true},
{"sparse_hnsw_graph", SparseHnswParam(), HnswQuery(),
/*is_sparse=*/true},
{"sparse_hnsw_linear", SparseHnswParam(),
HnswQuery(/*fetch_vector=*/false, /*is_linear=*/true),
/*is_sparse=*/true},
{"sparse_hnsw_bf_pks", SparseHnswParam(),
HnswQuery(/*fetch_vector=*/false, /*is_linear=*/false,
/*with_bf_pks=*/true),
/*is_sparse=*/true},
{"sparse_hnsw_fetch_vector", SparseHnswParam(),
HnswQuery(/*fetch_vector=*/true), /*is_sparse=*/true},
};
for (const auto &test_case : cases) {
RunOk(test_case);
}
}
TEST_F(GroupByInterfaceTest, UnsupportedIndexTypes) {
std::vector<GroupByCase> cases{
{"unsupported_vamana",
VamanaIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(kDimension)
.WithIsSparse(false)
.WithMaxDegree(32)
.WithSearchListSize(100)
.WithAlpha(1.2f)
.Build(),
VamanaQueryParamBuilder()
.with_topk(kSearchTopk)
.with_ef_search(kSearchTopk)
.build()},
{"unsupported_ivf",
IVFIndexParamBuilder()
.WithMetricType(MetricType::kInnerProduct)
.WithDataType(DataType::DT_FP32)
.WithDimension(kDimension)
.WithIsSparse(false)
.WithNList(4)
.Build(),
IVFQueryParamBuilder().with_topk(kSearchTopk).build()},
{"unsupported_refiner", DenseHnswParam(), HnswQuery(),
/*is_sparse=*/false,
/*dimension=*/kDimension,
/*with_refiner=*/true},
#if DISKANN_SUPPORTED
{"unsupported_diskann_graph", DenseDiskAnnParam(), DiskAnnQuery()},
{"unsupported_diskann_linear", DenseDiskAnnParam(),
DiskAnnQuery(/*fetch_vector=*/false, /*is_linear=*/true)},
{"unsupported_diskann_bf_pks", DenseDiskAnnParam(),
DiskAnnQuery(/*fetch_vector=*/false, /*is_linear=*/false,
/*with_bf_pks=*/true)},
{"unsupported_diskann_fetch_vector", DenseDiskAnnParam(),
DiskAnnQuery(/*fetch_vector=*/true)},
#endif
};
for (const auto &test_case : cases) {
RunRejected(test_case);
}
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_metric core_quantizer
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core/
)
endforeach()
+606
View File
@@ -0,0 +1,606 @@
// 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 <iostream>
#include <ailego/math/norm_matrix.h>
#include <gtest/gtest.h>
#include <zvec/ailego/utility/float_helper.h>
#include "zvec/core/framework/index_factory.h"
using namespace zvec;
using namespace zvec::core;
using namespace zvec::ailego;
static void Norm2(std::vector<Float16> &vec, std::string *out) {
float norm = 0.0f;
out->resize(vec.size() * sizeof(Float16) + sizeof(float));
Norm2Matrix<Float16, 1>::Compute(vec.data(), vec.size(), &norm);
Float16 *buf = reinterpret_cast<Float16 *>(&(*out)[0]);
for (uint32_t i = 0; i < vec.size(); ++i) {
buf[i] = vec[i] / norm;
}
float *norm_buf =
reinterpret_cast<float *>(&(*out)[vec.size() * sizeof(Float16)]);
memcpy(norm_buf, &norm, sizeof(float));
}
static void Norm2(std::vector<float> &vec, std::string *out) {
float norm = 0.0f;
out->resize((vec.size() + 1) * sizeof(float));
Norm2Matrix<float, 1>::Compute(vec.data(), vec.size(), &norm);
float *buf = reinterpret_cast<float *>(&(*out)[0]);
for (uint32_t i = 0; i < vec.size(); ++i) {
buf[i] = vec[i] / norm;
}
buf[vec.size()] = norm;
}
static size_t ExtraDimension(IndexMeta::DataType type) {
// The extra quantized params storage size to save for each vector
if (type == IndexMeta::DT_FP32) return 1;
if (type == IndexMeta::DT_FP16) return 2;
return 0;
}
TEST(CosineMeasure_General_Test, General) {
auto measure = IndexFactory::CreateMetric("Cosine");
EXPECT_TRUE(measure);
IndexMeta meta;
meta.set_meta(IndexMeta::DT_INT16, 64);
ASSERT_NE(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_FP16, 64);
ASSERT_EQ(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_FP32, 64);
ASSERT_EQ(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_INT8, 64);
ASSERT_NE(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_BINARY32, 64);
ASSERT_NE(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_BINARY64, 64);
ASSERT_NE(0, measure->init(meta, Params()));
meta.set_meta(IndexMeta::DT_INT4, 64);
ASSERT_NE(0, measure->init(meta, Params()));
IndexMeta meta2;
meta2.set_meta(IndexMeta::DT_BINARY32, 64);
EXPECT_FALSE(measure->is_matched(meta2));
EXPECT_TRUE(
measure->is_matched(meta, IndexQueryMeta(IndexMeta::DT_FP32, 64)));
EXPECT_FALSE(
measure->is_matched(meta, IndexQueryMeta(IndexMeta::DT_FP32, 63)));
EXPECT_FALSE(measure->distance_matrix(0, 0));
EXPECT_FALSE(measure->distance_matrix(3, 5));
EXPECT_FALSE(measure->distance_matrix(31, 65));
EXPECT_TRUE(measure->distance_matrix(1, 1));
EXPECT_FALSE(measure->distance_matrix(2, 1));
EXPECT_FALSE(measure->distance_matrix(2, 2));
EXPECT_FALSE(measure->distance_matrix(4, 1));
EXPECT_FALSE(measure->distance_matrix(4, 2));
EXPECT_FALSE(measure->distance_matrix(4, 4));
EXPECT_FALSE(measure->distance_matrix(8, 1));
EXPECT_FALSE(measure->distance_matrix(8, 2));
EXPECT_FALSE(measure->distance_matrix(8, 4));
EXPECT_FALSE(measure->distance_matrix(8, 8));
EXPECT_FALSE(measure->distance_matrix(16, 1));
EXPECT_FALSE(measure->distance_matrix(16, 2));
EXPECT_FALSE(measure->distance_matrix(16, 4));
EXPECT_FALSE(measure->distance_matrix(16, 8));
EXPECT_FALSE(measure->distance_matrix(16, 16));
EXPECT_FALSE(measure->distance_matrix(32, 1));
EXPECT_FALSE(measure->distance_matrix(32, 2));
EXPECT_FALSE(measure->distance_matrix(32, 4));
EXPECT_FALSE(measure->distance_matrix(32, 8));
EXPECT_FALSE(measure->distance_matrix(32, 16));
EXPECT_FALSE(measure->distance_matrix(32, 32));
EXPECT_FALSE(measure->support_normalize());
float result = 1.0f;
measure->normalize(&result);
EXPECT_FLOAT_EQ(1.0f, result);
}
TEST(CosineMeasure_General_Test, TestDistanceFp32) {
{
constexpr uint32_t dimension = 2;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP32, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<float> a = {0.2f, 0.9f};
std::vector<float> b = {0.3f, 0.5f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
distance(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.05131668f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.05131668f));
}
{
constexpr uint32_t dimension = 3;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP32, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<float> a = {0.2f, 0.9f, 0.6f};
std::vector<float> b = {0.3f, 0.5f, 0.7f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
distance(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.07199293f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.07199293f));
}
{
constexpr uint32_t dimension = 11;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP32, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<float> a = {1.0f, 2.0f, 3.0f, 0.2f, 0.3f, 0.1f,
5.2f, 2.1f, 7.1f, 6.8f, 1.2f};
std::vector<float> b = {2.0f, 4.0f, 6.0f, 0.6f, 0.7f, 0.9f,
1.0f, 2.3f, 3.4f, 4.5f, 6.4f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
distance(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.2803060f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP32), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.00001f, std::abs(result - 0.2803060f));
}
}
TEST(CosineMeasure_General_Test, TestDistanceFp16) {
{
constexpr uint32_t dimension = 2;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP16, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<Float16> a = {0.2f, 0.9f};
std::vector<Float16> b = {0.3f, 0.5f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
distance(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.05131668f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.05131668f));
}
{
constexpr uint32_t dimension = 3;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP16, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<Float16> a = {0.2f, 0.9f, 0.6f};
std::vector<Float16> b = {0.3f, 0.5f, 0.7f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
distance(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.07199293f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.07199293f));
}
{
constexpr uint32_t dimension = 11;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP16, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto distance = measure->distance();
ASSERT_NE(distance, nullptr);
auto dist_matrix = measure->distance_matrix(1, 1);
ASSERT_NE(dist_matrix, nullptr);
std::vector<Float16> a = {1.0f, 2.0f, 3.0f, 0.2f, 0.3f, 0.1f,
5.2f, 2.1f, 7.1f, 6.8f, 1.2f};
std::vector<Float16> b = {2.0f, 4.0f, 6.0f, 0.6f, 0.7f, 0.9f,
1.0f, 2.3f, 3.4f, 4.5f, 6.4f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float result = 0.0f;
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.2803060f));
dist_matrix(a_out.data(), b_out.data(),
dimension + ExtraDimension(IndexMeta::DT_FP16), &result);
if (measure->support_normalize()) {
measure->normalize(&result);
}
EXPECT_GE(0.001f, std::abs(result - 0.2803060f));
}
}
TEST(CosineMeasure_General_Test, TestDistanceBatchFp16Simple) {
{
constexpr uint32_t dimension = 2;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP16, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto dist_batch = measure->batch_distance();
ASSERT_NE(dist_batch, nullptr);
std::vector<Float16> a = {0.2f, 0.9f};
std::vector<Float16> b = {0.3f, 0.5f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float results[2] = {0.0f, 0.0f};
const void *vecs[2];
vecs[0] = a_out.data();
vecs[1] = b_out.data();
dist_batch(vecs, b_out.data(), 2,
dimension + ExtraDimension(IndexMeta::DT_FP16), results);
if (measure->support_normalize()) {
measure->normalize(&results[0]);
measure->normalize(&results[1]);
}
EXPECT_GE(0.001f, std::abs(results[0] - 0.05131668f));
EXPECT_GE(0.001f, std::abs(results[1] - 0.0f));
}
}
TEST(CosineMeasure_General_Test, TestDistanceBatchFp32Simple) {
{
constexpr uint32_t dimension = 2;
IndexMeta meta;
meta.set_meta(IndexMeta::DT_FP32, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto dist_batch = measure->batch_distance();
ASSERT_NE(dist_batch, nullptr);
std::vector<float> a = {0.2f, 0.9f};
std::vector<float> b = {0.3f, 0.5f};
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float results[2] = {0.0f, 0.0f};
const void *vecs[2];
vecs[0] = a_out.data();
vecs[1] = b_out.data();
dist_batch(vecs, b_out.data(), 2,
dimension + ExtraDimension(IndexMeta::DT_FP32), results);
if (measure->support_normalize()) {
measure->normalize(&results[0]);
measure->normalize(&results[1]);
}
EXPECT_GE(0.00001f, std::abs(results[0] - 0.05131668f));
EXPECT_GE(0.00001f, std::abs(results[1] - 0.0f));
}
}
template <typename T>
void calculate_distance(std::vector<T> &a, std::vector<T> &b, size_t dimension,
IndexMeta::DataType data_type, size_t batch_size,
float expected_distance, float epsilon = 0.00001f) {
IndexMeta meta;
meta.set_meta(data_type, dimension);
auto measure = IndexFactory::CreateMetric("Cosine");
ASSERT_TRUE(measure);
Params params;
ASSERT_EQ(0, measure->init(meta, params));
ASSERT_EQ(false, measure->support_train());
auto dist_batch = measure->batch_distance();
ASSERT_NE(dist_batch, nullptr);
std::string a_out;
std::string b_out;
Norm2(a, &a_out);
Norm2(b, &b_out);
float results[2] = {0.0f, 0.0f};
const void *vecs[2];
vecs[0] = a_out.data();
vecs[1] = b_out.data();
dist_batch(vecs, b_out.data(), batch_size,
dimension + ExtraDimension(data_type), results);
if (measure->support_normalize()) {
measure->normalize(&results[0]);
measure->normalize(&results[1]);
}
EXPECT_GE(epsilon, std::abs(results[0] - expected_distance));
EXPECT_GE(epsilon, std::abs(results[1] - 0.0f));
}
TEST(CosineMeasure_General_Test, TestDistanceBatch) {
{
constexpr uint32_t dimension = 2;
{
std::vector<float> a = {0.2f, 0.9f};
std::vector<float> b = {0.3f, 0.5f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 1, 0.05131668f,
0.00001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 2, 0.05131668f,
0.00001f);
}
{
std::vector<Float16> a = {0.2f, 0.9f};
std::vector<Float16> b = {0.3f, 0.5f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 1, 0.05131668f,
0.001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 2, 0.05131668f,
0.001f);
}
}
{
constexpr uint32_t dimension = 3;
{
std::vector<float> a = {0.2f, 0.9f, 0.6f};
std::vector<float> b = {0.3f, 0.5f, 0.7f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 1, 0.07199293f,
0.00001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 2, 0.07199293f,
0.00001f);
}
{
std::vector<Float16> a = {0.2f, 0.9f, 0.6f};
std::vector<Float16> b = {0.3f, 0.5f, 0.7f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 1, 0.07199293f,
0.001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 2, 0.07199293f,
0.001f);
}
}
{
constexpr uint32_t dimension = 11;
{
std::vector<float> a = {1.0f, 2.0f, 3.0f, 0.2f, 0.3f, 0.1f,
5.2f, 2.1f, 7.1f, 6.8f, 1.2f};
std::vector<float> b = {2.0f, 4.0f, 6.0f, 0.6f, 0.7f, 0.9f,
1.0f, 2.3f, 3.4f, 4.5f, 6.4f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 1, 0.2803060f,
0.00001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP32, 2, 0.2803060f,
0.00001f);
}
{
std::vector<Float16> a = {1.0f, 2.0f, 3.0f, 0.2f, 0.3f, 0.1f,
5.2f, 2.1f, 7.1f, 6.8f, 1.2f};
std::vector<Float16> b = {2.0f, 4.0f, 6.0f, 0.6f, 0.7f, 0.9f,
1.0f, 2.3f, 3.4f, 4.5f, 6.4f};
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 1, 0.2803060f,
0.001f);
calculate_distance(a, b, dimension, IndexMeta::DT_FP16, 2, 0.2803060f,
0.001f);
}
}
}
+134
View File
@@ -0,0 +1,134 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
using namespace zvec;
using namespace zvec::core;
TEST(SquaredEuclideanMetric, General) {
auto metric = IndexFactory::CreateMetric("SquaredEuclidean");
EXPECT_TRUE(metric);
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_INT16, 64);
ASSERT_NE(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP16, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP32, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT4, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT8, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
IndexMeta meta2;
meta2.set_meta(IndexMeta::DataType::DT_BINARY32, 64);
EXPECT_TRUE(metric->is_matched(meta));
EXPECT_FALSE(metric->is_matched(meta2));
EXPECT_TRUE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 64)));
EXPECT_FALSE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 63)));
EXPECT_FALSE(metric->distance_matrix(0, 0));
EXPECT_FALSE(metric->distance_matrix(3, 5));
EXPECT_FALSE(metric->distance_matrix(31, 65));
EXPECT_TRUE(metric->distance_matrix(1, 1));
EXPECT_TRUE(metric->distance_matrix(2, 1));
EXPECT_TRUE(metric->distance_matrix(2, 2));
EXPECT_TRUE(metric->distance_matrix(4, 1));
EXPECT_TRUE(metric->distance_matrix(4, 2));
EXPECT_TRUE(metric->distance_matrix(4, 4));
EXPECT_TRUE(metric->distance_matrix(8, 1));
EXPECT_TRUE(metric->distance_matrix(8, 2));
EXPECT_TRUE(metric->distance_matrix(8, 4));
EXPECT_TRUE(metric->distance_matrix(8, 8));
EXPECT_FALSE(metric->distance_matrix(8, 32));
EXPECT_FALSE(metric->distance_matrix(8, 9));
EXPECT_TRUE(metric->distance_matrix(16, 1));
EXPECT_TRUE(metric->distance_matrix(16, 2));
EXPECT_TRUE(metric->distance_matrix(16, 4));
EXPECT_TRUE(metric->distance_matrix(16, 8));
EXPECT_TRUE(metric->distance_matrix(16, 16));
EXPECT_FALSE(metric->distance_matrix(16, 17));
EXPECT_TRUE(metric->distance_matrix(32, 1));
EXPECT_TRUE(metric->distance_matrix(32, 2));
EXPECT_TRUE(metric->distance_matrix(32, 4));
EXPECT_TRUE(metric->distance_matrix(32, 8));
EXPECT_TRUE(metric->distance_matrix(32, 16));
EXPECT_TRUE(metric->distance_matrix(32, 32));
EXPECT_FALSE(metric->support_normalize());
float result = 1.0f;
metric->normalize(&result);
EXPECT_FLOAT_EQ(1.0f, result);
}
TEST(EuclideanMetric, General) {
auto metric = IndexFactory::CreateMetric("Euclidean");
EXPECT_TRUE(metric);
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_INT16, 64);
ASSERT_NE(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP16, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP32, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT4, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT8, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
IndexMeta meta2;
meta2.set_meta(IndexMeta::DataType::DT_BINARY32, 64);
EXPECT_TRUE(metric->is_matched(meta));
EXPECT_FALSE(metric->is_matched(meta2));
EXPECT_TRUE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 64)));
EXPECT_FALSE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 63)));
EXPECT_FALSE(metric->distance_matrix(0, 0));
EXPECT_FALSE(metric->distance_matrix(3, 5));
EXPECT_FALSE(metric->distance_matrix(31, 65));
EXPECT_TRUE(metric->distance_matrix(1, 1));
EXPECT_TRUE(metric->distance_matrix(2, 1));
EXPECT_TRUE(metric->distance_matrix(2, 2));
EXPECT_TRUE(metric->distance_matrix(4, 1));
EXPECT_TRUE(metric->distance_matrix(4, 2));
EXPECT_TRUE(metric->distance_matrix(4, 4));
EXPECT_TRUE(metric->distance_matrix(8, 1));
EXPECT_TRUE(metric->distance_matrix(8, 2));
EXPECT_TRUE(metric->distance_matrix(8, 4));
EXPECT_TRUE(metric->distance_matrix(8, 8));
EXPECT_TRUE(metric->distance_matrix(16, 1));
EXPECT_TRUE(metric->distance_matrix(16, 2));
EXPECT_TRUE(metric->distance_matrix(16, 4));
EXPECT_TRUE(metric->distance_matrix(16, 8));
EXPECT_TRUE(metric->distance_matrix(16, 16));
EXPECT_TRUE(metric->distance_matrix(32, 1));
EXPECT_TRUE(metric->distance_matrix(32, 2));
EXPECT_TRUE(metric->distance_matrix(32, 4));
EXPECT_TRUE(metric->distance_matrix(32, 8));
EXPECT_TRUE(metric->distance_matrix(32, 16));
EXPECT_TRUE(metric->distance_matrix(32, 32));
EXPECT_FALSE(metric->support_normalize());
float result = 1.0f;
metric->normalize(&result);
EXPECT_FLOAT_EQ(1.0f, result);
}
@@ -0,0 +1,77 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
using namespace zvec;
using namespace zvec::core;
TEST(InnerProductMetric, General) {
auto metric = IndexFactory::CreateMetric("InnerProduct");
ASSERT_TRUE(metric);
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_BINARY32, 64);
ASSERT_NE(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_BINARY64, 64);
ASSERT_NE(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP16, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_FP32, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT4, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
meta.set_meta(IndexMeta::DataType::DT_INT8, 64);
ASSERT_EQ(0, metric->init(meta, ailego::Params()));
IndexMeta meta2;
meta2.set_meta(IndexMeta::DataType::DT_BINARY32, 64);
EXPECT_TRUE(metric->is_matched(meta));
EXPECT_FALSE(metric->is_matched(meta2));
EXPECT_TRUE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 64)));
EXPECT_FALSE(metric->is_matched(
meta, IndexQueryMeta(IndexMeta::DataType::DT_INT8, 63)));
EXPECT_FALSE(metric->distance_matrix(0, 0));
EXPECT_FALSE(metric->distance_matrix(3, 5));
EXPECT_FALSE(metric->distance_matrix(31, 65));
EXPECT_TRUE(metric->distance_matrix(1, 1));
EXPECT_TRUE(metric->distance_matrix(2, 1));
EXPECT_TRUE(metric->distance_matrix(2, 2));
EXPECT_TRUE(metric->distance_matrix(4, 1));
EXPECT_TRUE(metric->distance_matrix(4, 2));
EXPECT_TRUE(metric->distance_matrix(4, 4));
EXPECT_TRUE(metric->distance_matrix(8, 1));
EXPECT_TRUE(metric->distance_matrix(8, 2));
EXPECT_TRUE(metric->distance_matrix(8, 4));
EXPECT_TRUE(metric->distance_matrix(8, 8));
EXPECT_TRUE(metric->distance_matrix(16, 1));
EXPECT_TRUE(metric->distance_matrix(16, 2));
EXPECT_TRUE(metric->distance_matrix(16, 4));
EXPECT_TRUE(metric->distance_matrix(16, 8));
EXPECT_TRUE(metric->distance_matrix(16, 16));
EXPECT_TRUE(metric->distance_matrix(32, 1));
EXPECT_TRUE(metric->distance_matrix(32, 2));
EXPECT_TRUE(metric->distance_matrix(32, 4));
EXPECT_TRUE(metric->distance_matrix(32, 8));
EXPECT_TRUE(metric->distance_matrix(32, 16));
EXPECT_TRUE(metric->distance_matrix(32, 32));
EXPECT_TRUE(metric->support_normalize());
float result = 1.0f;
metric->normalize(&result);
EXPECT_FLOAT_EQ(-1.0f, result);
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET}
STRICT
LIBS zvec_ailego core_framework core_utility core_quantizer
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core/
)
endforeach()
@@ -0,0 +1,119 @@
// 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 <iostream>
#include <random>
// #include <zvec/ailego/container/vector.h>
// #include <zvec/ailego/container/params.h>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_holder.h"
using namespace zvec::core;
TEST(HalfFloatReformer, General) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0, 1.0);
const size_t COUNT = 1000;
const size_t DIMENSION = 128;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter = IndexFactory::CreateConverter("HalfFloatConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto reformer = IndexFactory::CreateReformer("HalfFloatReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
EXPECT_EQ(COUNT, holder->count());
EXPECT_EQ(IndexMeta::DataType::DT_FP32, holder->data_type());
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
auto holder2 = converter->result();
EXPECT_EQ(COUNT, holder2->count());
EXPECT_EQ(IndexMeta::DataType::DT_FP16, holder2->data_type());
EXPECT_EQ(holder->dimension(), holder2->dimension());
EXPECT_EQ(holder->element_size(), holder2->element_size() * 2);
auto iter = holder->create_iterator();
auto iter2 = holder2->create_iterator();
std::string buffer;
for (; iter->is_valid(); iter->next(), iter2->next()) {
EXPECT_TRUE(iter2->is_valid());
EXPECT_TRUE(iter->data());
EXPECT_TRUE(iter2->data());
const float *f32 = (const float *)iter->data();
const zvec::ailego::Float16 *f16 =
(const zvec::ailego::Float16 *)iter2->data();
printf("%f %f\n", f32[0], (float)f16[0]);
std::string buffer2(
std::string((const char *)iter2->data(), holder2->element_size()));
IndexQueryMeta qmeta;
EXPECT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_FP16, qmeta.data_type());
EXPECT_EQ(holder->dimension(), qmeta.dimension());
EXPECT_EQ(buffer, buffer2);
EXPECT_EQ(0, reformer->transform(iter->data(),
IndexQueryMeta(holder->data_type(),
holder->dimension() / 4),
4, &buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_FP16, qmeta.data_type());
EXPECT_EQ(holder->dimension() / 4, qmeta.dimension());
EXPECT_EQ(buffer, buffer2);
// Test reformer convert
buffer.clear();
EXPECT_EQ(0, reformer->convert(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_FP16, qmeta.data_type());
EXPECT_EQ(holder->dimension(), qmeta.dimension());
EXPECT_EQ(buffer, buffer2);
buffer.clear();
EXPECT_EQ(0, reformer->convert(iter->data(),
IndexQueryMeta(holder->data_type(),
holder->dimension() / 4),
4, &buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_FP16, qmeta.data_type());
EXPECT_EQ(holder->dimension() / 4, qmeta.dimension());
EXPECT_EQ(buffer, buffer2);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,539 @@
// 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 <cmath>
#include <iostream>
#include <limits>
#include <random>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <zvec/ailego/container/vector.h>
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_holder.h"
using namespace zvec::core;
// ---------------------------------------------------------------------------
// UniformInt8 Converter + Reformer: General (MultiPassHolder, uniform dist)
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, General) {
std::mt19937 gen(42);
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
const size_t COUNT = 5000;
const size_t DIMENSION = 64;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
EXPECT_EQ(COUNT, holder->count());
EXPECT_EQ(IndexMeta::DataType::DT_FP32, holder->data_type());
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
auto &stats = converter->stats();
EXPECT_EQ(COUNT, stats.trained_count());
EXPECT_EQ(COUNT, stats.transformed_count());
auto holder2 = converter->result();
ASSERT_TRUE(holder2);
EXPECT_EQ(COUNT, holder2->count());
EXPECT_EQ(IndexMeta::DataType::DT_INT8, holder2->data_type());
EXPECT_EQ(DIMENSION, holder2->dimension());
// INT8: 1 byte per dim; FP32: 4 bytes per dim
EXPECT_EQ(holder->element_size(), holder2->element_size() * 4);
// Verify quantized values are in [0, 127]
auto iter_check = holder2->create_iterator();
for (; iter_check->is_valid(); iter_check->next()) {
const int8_t *quantized =
reinterpret_cast<const int8_t *>(iter_check->data());
for (size_t d = 0; d < DIMENSION; ++d) {
EXPECT_GE(quantized[d], 0) << "dim=" << d;
EXPECT_LE(quantized[d], 127) << "dim=" << d;
}
}
// Create reformer from converter's trained params
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(converter->meta().reformer_params()));
// Verify transform() produces the same int8 as the converter
auto iter = holder->create_iterator();
auto iter2 = holder2->create_iterator();
std::string buffer;
for (; iter->is_valid(); iter->next(), iter2->next()) {
ASSERT_TRUE(iter2->is_valid());
ASSERT_TRUE(iter->data());
ASSERT_TRUE(iter2->data());
std::string expected(reinterpret_cast<const char *>(iter2->data()),
holder2->element_size());
IndexQueryMeta qmeta;
EXPECT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(DIMENSION, qmeta.dimension());
EXPECT_EQ(expected, buffer);
// Batch transform (count=4, dimension/4 per sub-vector)
EXPECT_EQ(0, reformer->transform(iter->data(),
IndexQueryMeta(holder->data_type(),
holder->dimension() / 4),
4, &buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(DIMENSION / 4, qmeta.dimension());
EXPECT_EQ(expected, buffer);
// convert() should produce the same result
buffer.clear();
EXPECT_EQ(0, reformer->convert(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(DIMENSION, qmeta.dimension());
EXPECT_EQ(expected, buffer);
// Batch convert
buffer.clear();
EXPECT_EQ(0, reformer->convert(iter->data(),
IndexQueryMeta(holder->data_type(),
holder->dimension() / 4),
4, &buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(DIMENSION / 4, qmeta.dimension());
EXPECT_EQ(expected, buffer);
}
}
// ---------------------------------------------------------------------------
// OnePassHolder: verify converter works with single-pass holders
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, OnePassHolder) {
std::mt19937 gen(123);
std::normal_distribution<float> dist(5.0f, 2.0f);
const size_t COUNT = 5000;
const size_t DIMENSION = 128;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<OnePassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
auto holder_mirror =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
holder_mirror->emplace(i + 1, vec);
}
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
auto holder2 = converter->result();
ASSERT_TRUE(holder2);
EXPECT_EQ(COUNT, holder2->count());
EXPECT_EQ(IndexMeta::DataType::DT_INT8, holder2->data_type());
EXPECT_EQ(DIMENSION, holder2->dimension());
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(converter->meta().reformer_params()));
auto iter = holder_mirror->create_iterator();
auto iter2 = holder2->create_iterator();
std::string buffer;
for (; iter->is_valid(); iter->next(), iter2->next()) {
ASSERT_TRUE(iter2->is_valid());
std::string expected(reinterpret_cast<const char *>(iter2->data()),
holder2->element_size());
IndexQueryMeta qmeta;
EXPECT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(expected, buffer);
}
}
// ---------------------------------------------------------------------------
// TrainedParams: verify scale/bias are persisted correctly after train
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, TrainedParams) {
std::mt19937 gen(99);
std::uniform_real_distribution<float> dist(-3.0f, 7.0f);
const size_t COUNT = 5000;
const size_t DIMENSION = 32;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
EXPECT_EQ(COUNT, converter->stats().trained_count());
// Verify reformer params contain scale and bias
auto reformer_params = converter->meta().reformer_params();
float scale = 0.0f, bias = 0.0f;
EXPECT_TRUE(reformer_params.get("uniform_int8.reformer.scale", &scale));
EXPECT_TRUE(reformer_params.get("uniform_int8.reformer.bias", &bias));
EXPECT_GT(scale, 0.0f);
EXPECT_TRUE(std::isfinite(scale));
EXPECT_TRUE(std::isfinite(bias));
// Verify converter params also contain scale/bias (for persistence)
auto conv_params = converter->meta().converter_params();
float conv_scale = 0.0f, conv_bias = 0.0f;
EXPECT_TRUE(conv_params.get("uniform_int8.reformer.scale", &conv_scale));
EXPECT_TRUE(conv_params.get("uniform_int8.reformer.bias", &conv_bias));
EXPECT_FLOAT_EQ(scale, conv_scale);
EXPECT_FLOAT_EQ(bias, conv_bias);
// Verify meta reflects the correct reformer and metric
EXPECT_EQ("UniformInt8StreamingReformer", converter->meta().reformer_name());
EXPECT_EQ("UniformInt8", converter->meta().metric_name());
}
// ---------------------------------------------------------------------------
// Revert: verify int8 → float dequantization round-trip quality
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, Revert) {
std::mt19937 gen(77);
std::uniform_real_distribution<float> dist(0.0f, 10.0f);
const size_t COUNT = 100;
const size_t DIMENSION = 16;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(converter->meta().reformer_params()));
// Verify round-trip: float → int8 → float
auto iter = holder->create_iterator();
std::string quantized_buf, reverted_buf;
for (; iter->is_valid(); iter->next()) {
const float *original = reinterpret_cast<const float *>(iter->data());
IndexQueryMeta qmeta;
ASSERT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&quantized_buf, &qmeta));
ASSERT_EQ(0, reformer->revert(quantized_buf.data(), qmeta, &reverted_buf));
const float *reverted =
reinterpret_cast<const float *>(reverted_buf.data());
// Quantization error should be bounded by step_size / 2
// step_size ≈ range / 127
float range = 10.0f; // approximate
float max_error = range / 127.0f;
for (size_t d = 0; d < DIMENSION; ++d) {
EXPECT_NEAR(original[d], reverted[d], max_error * 1.5f)
<< "dim=" << d << " original=" << original[d]
<< " reverted=" << reverted[d];
}
}
}
// ---------------------------------------------------------------------------
// Normalize: verify score rescaling from int8 L2 to float L2
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, Normalize) {
const size_t COUNT = 1000;
const size_t DIMENSION = 32;
std::mt19937 gen(55);
std::uniform_real_distribution<float> dist(0.0f, 5.0f);
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
auto reformer_params = converter->meta().reformer_params();
float scale = 0.0f;
ASSERT_TRUE(reformer_params.get("uniform_int8.reformer.scale", &scale));
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(reformer_params));
// Create mock results and verify normalize rescales by 1/scale^2
IndexDocumentList results;
float int8_score = 100.0f;
IndexDocument doc;
*doc.mutable_score() = int8_score;
results.push_back(doc);
// normalize is independent of query, pass nullptr
ASSERT_EQ(
0, reformer->normalize(
nullptr, IndexQueryMeta(IndexMeta::DataType::DT_FP32, DIMENSION),
results));
float expected_score = int8_score / (scale * scale);
EXPECT_NEAR(results[0].score(), expected_score, expected_score * 1e-5f);
}
// ---------------------------------------------------------------------------
// InitConverterWithTrainedParams: simulate the search-only path where
// scale/bias come from persisted converter params (no re-train needed)
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, InitConverterWithTrainedParams) {
std::mt19937 gen(42);
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
const size_t COUNT = 5000;
const size_t DIMENSION = 12;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
// First pass: train to get params
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = dist(gen);
}
holder->emplace(i + 1, vec);
}
ASSERT_EQ(0, converter->train(holder));
auto reformer_params = converter->meta().reformer_params();
auto converter_params = converter->meta().converter_params();
// Second pass: create a new converter with trained params (skip train)
auto converter2 =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter2);
ASSERT_EQ(0, converter2->init(meta, converter_params));
ASSERT_EQ(0, converter2->transform(holder));
auto &stats = converter2->stats();
EXPECT_EQ(0u, stats.trained_count());
EXPECT_EQ(COUNT, stats.transformed_count());
auto holder2 = converter2->result();
ASSERT_TRUE(holder2);
EXPECT_EQ(COUNT, holder2->count());
EXPECT_EQ(IndexMeta::DataType::DT_INT8, holder2->data_type());
EXPECT_EQ(DIMENSION, holder2->dimension());
// Verify reformer with persisted params produces same results
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(reformer_params));
auto iter = holder->create_iterator();
auto iter2 = holder2->create_iterator();
std::string buffer;
for (; iter->is_valid(); iter->next(), iter2->next()) {
ASSERT_TRUE(iter2->is_valid());
std::string expected(reinterpret_cast<const char *>(iter2->data()),
holder2->element_size());
IndexQueryMeta qmeta;
EXPECT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(IndexMeta::DataType::DT_INT8, qmeta.data_type());
EXPECT_EQ(DIMENSION, qmeta.dimension());
EXPECT_EQ(expected, buffer);
// convert() path
buffer.clear();
EXPECT_EQ(0, reformer->convert(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&buffer, &qmeta));
EXPECT_EQ(expected, buffer);
}
}
// ---------------------------------------------------------------------------
// LosslessIntegerFastPath: when all training values are integers within
// [0, 127], scale should be 1.0 for exact mapping
// ---------------------------------------------------------------------------
TEST(UniformInt8Reformer, LosslessIntegerFastPath) {
const size_t COUNT = 100;
const size_t DIMENSION = 8;
IndexMeta meta;
meta.set_meta(IndexMeta::DataType::DT_FP32, DIMENSION);
auto converter =
IndexFactory::CreateConverter("UniformInt8StreamingConverter");
ASSERT_TRUE(converter);
ASSERT_EQ(0u, converter->init(meta, zvec::ailego::Params()));
auto holder =
std::make_shared<MultiPassIndexHolder<IndexMeta::DataType::DT_FP32>>(
DIMENSION);
// Fill with integer values in [0, 50]
std::mt19937 gen(10);
std::uniform_int_distribution<int> idist(0, 50);
for (size_t i = 0; i < COUNT; ++i) {
zvec::ailego::NumericalVector<float> vec(DIMENSION);
for (size_t j = 0; j < DIMENSION; ++j) {
vec[j] = static_cast<float>(idist(gen));
}
holder->emplace(i + 1, vec);
}
ASSERT_EQ(0u, IndexConverter::TrainAndTransform(converter, holder));
// scale should be 1.0 for lossless integer path
auto reformer_params = converter->meta().reformer_params();
float scale = 0.0f;
ASSERT_TRUE(reformer_params.get("uniform_int8.reformer.scale", &scale));
EXPECT_FLOAT_EQ(1.0f, scale);
// Verify exact round-trip for integer values
auto reformer = IndexFactory::CreateReformer("UniformInt8StreamingReformer");
ASSERT_TRUE(reformer);
ASSERT_EQ(0u, reformer->init(reformer_params));
auto iter = holder->create_iterator();
std::string quantized_buf, reverted_buf;
for (; iter->is_valid(); iter->next()) {
const float *original = reinterpret_cast<const float *>(iter->data());
IndexQueryMeta qmeta;
ASSERT_EQ(0, reformer->transform(
iter->data(),
IndexQueryMeta(holder->data_type(), holder->dimension()),
&quantized_buf, &qmeta));
// Verify quantized values match original integers
const int8_t *quantized =
reinterpret_cast<const int8_t *>(quantized_buf.data());
for (size_t d = 0; d < DIMENSION; ++d) {
EXPECT_EQ(static_cast<int8_t>(original[d] - 0 /* global_min offset */),
quantized[d])
<< "dim=" << d;
}
// Revert should give exact values back
ASSERT_EQ(0, reformer->revert(quantized_buf.data(), qmeta, &reverted_buf));
const float *reverted =
reinterpret_cast<const float *>(reverted_buf.data());
for (size_t d = 0; d < DIMENSION; ++d) {
EXPECT_FLOAT_EQ(original[d], reverted[d]) << "dim=" << d;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake)
include(${PROJECT_ROOT_DIR}/cmake/option.cmake)
file(GLOB_RECURSE ALL_TEST_SRCS *_test.cc)
foreach(CC_SRCS ${ALL_TEST_SRCS})
get_filename_component(CC_TARGET ${CC_SRCS} NAME_WE)
cc_gtest(
NAME ${CC_TARGET} STRICT
LIBS zvec_ailego core_framework core_utility
SRCS ${CC_SRCS}
INCS . ${PROJECT_ROOT_DIR}/src/core/
)
cc_test_suite(zvec_ailego ${CC_TARGET})
endforeach()
@@ -0,0 +1,70 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include <zvec/core/framework/index_factory.h>
#include <zvec/core/framework/index_helper.h>
using namespace zvec;
using namespace zvec::core;
TEST(BufferStorage, General) {
std::string file_path = "buffer_storage_test_file";
ailego::File::Delete(file_path);
auto write_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_TRUE(write_storage);
std::cout << file_path << std::endl;
EXPECT_NE(0, write_storage->open(file_path, false));
ailego::Params params;
EXPECT_EQ(0, write_storage->init(params));
std::cout << file_path << std::endl;
EXPECT_EQ(0, write_storage->open(file_path, true));
IndexMeta meta;
meta.set_trainer("trainer", 111, ailego::Params());
meta.set_searcher("searcher", 222, ailego::Params());
meta.set_builder("builder", 333, ailego::Params());
EXPECT_EQ(0, IndexHelper::SerializeToStorage(meta, write_storage.get()));
EXPECT_EQ(0, write_storage->append("AAAA", 1234));
EXPECT_EQ(0, write_storage->append("BBBB", 1234));
auto aaaa = write_storage->get("AAAA");
ASSERT_TRUE(aaaa);
auto aaaa1 = aaaa->clone();
ASSERT_TRUE(aaaa1);
std::string hello = "Hello world!!!";
EXPECT_EQ(hello.size(), aaaa1->write(0, hello.data(), hello.size()));
EXPECT_EQ(0, write_storage->close());
// Reopen it
auto read_storage = IndexFactory::CreateStorage("BufferStorage");
EXPECT_EQ(0, read_storage->open(file_path, false));
IndexMeta meta2;
EXPECT_EQ(0, IndexHelper::DeserializeFromStorage(read_storage.get(), &meta2));
EXPECT_EQ("trainer", meta2.trainer_name());
EXPECT_EQ("searcher", meta2.searcher_name());
EXPECT_EQ("builder", meta2.builder_name());
auto aaaa2 = read_storage->get("AAAA");
ASSERT_TRUE(aaaa2);
const void *data;
EXPECT_EQ(hello.size(), aaaa2->read(0, &data, hello.size()));
auto aaaa3 = aaaa2->clone();
ASSERT_TRUE(aaaa3);
EXPECT_EQ(hello.size(), aaaa3->read(0, &data, hello.size()));
EXPECT_EQ(hello, std::string((const char *)data, hello.size()));
}
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_helper.h"
#include "zvec/core/framework/index_segment_storage.h"
using namespace zvec;
using namespace zvec::core;
TEST(FileDumper, General) {
std::string file_path = "file_dumper_test_file";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_TRUE(dumper);
IndexMeta meta1;
meta1.set_trainer("index_trainer", 0, ailego::Params());
ASSERT_EQ(0, dumper->create(file_path));
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper.get()));
for (size_t i = 0; i < 10; ++i) {
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper->append(std::to_string(i), hello.size(), 0, 0));
}
ASSERT_EQ(0, dumper->close());
auto container = IndexFactory::CreateStorage("MMapFileReadStorage");
ASSERT_TRUE(container);
ailego::Params params;
params.set("proxima.mmap_file.container.memory_locking", true);
params.set("proxima.mmap_file.container.memory_warmup", true);
params.set("proxima.mmap_file.container.checksum_validation", true);
ASSERT_EQ(0, container->init(params));
IndexMeta meta2;
EXPECT_EQ("", meta2.trainer_name());
ASSERT_EQ(0, container->open(file_path, false));
EXPECT_EQ(0, IndexHelper::DeserializeFromStorage(container.get(), &meta2));
EXPECT_EQ("index_trainer", meta2.trainer_name());
for (size_t i = 0; i < 10; ++i) {
auto seg = container->get(std::to_string(i));
const void *data = nullptr;
EXPECT_EQ(seg->data_size(), seg->read(0, &data, seg->data_size()));
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello, std::string((const char *)data, seg->data_size()));
}
}
TEST(IndexSegmentDumper, General) {
std::string file_path = "index_segment_dumper_test_file";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_TRUE(dumper);
ASSERT_EQ(0, dumper->create(file_path));
{
IndexDumper::Pointer dumper2 =
std::make_shared<IndexSegmentDumper>(dumper, "AAAAA");
IndexMeta meta1;
meta1.set_trainer("index_trainer", 0, ailego::Params());
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper2.get()));
for (size_t i = 0; i < 10; ++i) {
std::string hello = "A: Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper2->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper2->append(std::to_string(i), hello.size(), 0, 0));
}
}
{
IndexDumper::Pointer dumper2 =
std::make_shared<IndexSegmentDumper>(dumper, "BBBBB");
IndexMeta meta1;
meta1.set_builder("index_builder", 0, ailego::Params());
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper2.get()));
for (size_t i = 100; i < 110; ++i) {
std::string hello = "B: Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper2->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper2->append(std::to_string(i), hello.size(), 0, 0));
}
}
{
IndexDumper::Pointer dumper2 =
std::make_shared<IndexSegmentDumper>(dumper, "CCCCC");
IndexMeta meta1;
meta1.set_converter("index_converter", 0, ailego::Params());
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper2.get()));
for (size_t i = 1000; i < 1010; ++i) {
std::string hello = "C: Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper2->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper2->append(std::to_string(i), hello.size(), 0, 0));
}
}
ASSERT_EQ(0, dumper->close());
///// Read data with container
auto container = IndexFactory::CreateStorage("MMapFileReadStorage");
ASSERT_TRUE(container);
ailego::Params params;
params.set("proxima.mmap_file.container.memory_locking", true);
params.set("proxima.mmap_file.container.memory_warmup", true);
params.set("proxima.mmap_file.container.checksum_validation", true);
ASSERT_EQ(0, container->init(params));
ASSERT_EQ(0, container->open(file_path, false));
}
+62
View File
@@ -0,0 +1,62 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_helper.h"
using namespace zvec;
using namespace zvec::core;
TEST(MemoryDumper, General) {
std::string file_path = "memory_dumper_test_file";
auto dumper = IndexFactory::CreateDumper("MemoryDumper");
ASSERT_TRUE(dumper);
IndexMeta meta1;
meta1.set_trainer("index_trainer", 0, ailego::Params());
ASSERT_EQ(0, dumper->create(file_path));
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper.get()));
for (size_t i = 0; i < 10; ++i) {
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper->append(std::to_string(i), hello.size(), 0, 0));
}
ASSERT_EQ(0, dumper->close());
auto container = IndexFactory::CreateStorage("MemoryReadStorage");
ASSERT_TRUE(container);
ailego::Params params;
params.set("memory.container.checksum_validation", true);
ASSERT_EQ(0, container->init(params));
IndexMeta meta2;
EXPECT_EQ("", meta2.trainer_name());
ASSERT_EQ(0, container->open(file_path, false));
EXPECT_EQ(0, IndexHelper::DeserializeFromStorage(container.get(), &meta2));
EXPECT_EQ("index_trainer", meta2.trainer_name());
for (size_t i = 0; i < 10; ++i) {
auto seg = container->get(std::to_string(i));
const void *data = nullptr;
EXPECT_EQ(seg->data_size(), seg->read(0, &data, seg->data_size()));
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello, std::string((const char *)data, seg->data_size()));
}
}
@@ -0,0 +1,109 @@
// 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 <iostream>
#include <gtest/gtest.h>
#include "zvec/core/framework/index_factory.h"
#include "zvec/core/framework/index_helper.h"
using namespace zvec;
using namespace zvec::core;
static int GenRandInt(int m, int n) {
static std::mt19937 gen((std::random_device())());
return std::uniform_int_distribution<int>(m, n)(gen);
}
static void AddRandomPadding(const std::string &in, const std::string &out,
size_t header_padding_size,
size_t footer_padding_size) {
ailego::File out_file;
out_file.create(out, 0);
for (size_t i = 0; i < header_padding_size; ++i) {
uint8_t r = GenRandInt(0, 255);
out_file.write(&r, 1);
}
ailego::File in_file;
ASSERT_TRUE(in_file.open(in, true));
std::string buf(in_file.size(), '\0');
ASSERT_EQ(buf.size(), in_file.read(&buf[0], buf.size()));
out_file.write(buf.data(), buf.size());
for (size_t i = 0; i < footer_padding_size; ++i) {
uint8_t r = GenRandInt(0, 255);
out_file.write(&r, 1);
}
}
TEST(MMapFileReadStorage, General) {
std::string file_path = "mmap_file_container_test_file";
std::string file_path_padding = "mmap_file_container_test_file_padding";
auto dumper = IndexFactory::CreateDumper("FileDumper");
ASSERT_TRUE(dumper);
IndexMeta meta1;
meta1.set_trainer("index_trainer", 0, ailego::Params());
ASSERT_EQ(0, dumper->create(file_path));
EXPECT_EQ(0, IndexHelper::SerializeToDumper(meta1, dumper.get()));
for (size_t i = 0; i < 21; ++i) {
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello.size(), dumper->write(hello.data(), hello.size()));
EXPECT_EQ(0, dumper->append(std::to_string(i), hello.size(), 0, 0));
}
ASSERT_EQ(0, dumper->close());
size_t header_paddings = GenRandInt(0, 1024);
size_t footer_paddings = GenRandInt(0, 1024);
AddRandomPadding(file_path, file_path_padding, header_paddings,
footer_paddings);
ailego::File file;
file.open(file_path_padding, true);
int64_t header_offset =
GenRandInt(0, 1) ? header_paddings : header_paddings - file.size();
int64_t footer_offset =
(GenRandInt(0, 1) ? file.size() : 0) - footer_paddings;
auto container = IndexFactory::CreateStorage("MMapFileReadStorage");
ASSERT_TRUE(container);
ailego::Params params;
params.set("proxima.mmap_file.container.memory_locking", true);
params.set("proxima.mmap_file.container.memory_warmup", true);
params.set("proxima.mmap_file.container.checksum_validation", true);
params.set("proxima.mmap_file.container.header_offset", header_offset);
params.set("proxima.mmap_file.container.footer_offset", footer_offset);
ASSERT_EQ(0, container->init(params));
IndexMeta meta2;
EXPECT_EQ(0u, container->get_all().size());
EXPECT_EQ("", meta2.trainer_name());
EXPECT_EQ("", meta2.searcher_name());
ASSERT_EQ(0, container->open(file_path_padding, false));
EXPECT_EQ(0, IndexHelper::DeserializeFromStorage(container.get(), &meta2));
EXPECT_EQ(23u, container->get_all().size());
EXPECT_EQ("index_trainer", meta2.trainer_name());
EXPECT_EQ("", meta2.searcher_name());
for (size_t i = 0; i < 21; ++i) {
auto seg = container->get(std::to_string(i));
auto seg1 = seg->clone();
const void *data = nullptr;
EXPECT_EQ(seg1->data_size(), seg1->read(0, &data, seg1->data_size()));
std::string hello = "Hello world!!! #" + std::to_string(i);
EXPECT_EQ(hello, std::string((const char *)data, seg1->data_size()));
}
container->cleanup();
}
@@ -0,0 +1,99 @@
// 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 <fstream>
#include <iostream>
#include <gtest/gtest.h>
#include <zvec/core/framework/index_factory.h>
#include <zvec/core/framework/index_helper.h>
using namespace zvec;
using namespace zvec::core;
TEST(MMapFileStorage, TestHugePage) {
std::string file_path = "/mnt/huge/mmap_file_storage_test_file";
// std::string file_path = "mmap_file_storage_test_file";
ailego::File::Delete(file_path);
auto write_storage = IndexFactory::CreateStorage("MMapFileStorage");
ASSERT_TRUE(write_storage);
ailego::Params params;
params.set("proxima.mmap_file.storage.huge_page", true);
EXPECT_EQ(0, write_storage->init(params));
EXPECT_EQ(0, write_storage->open(file_path, true));
IndexMeta meta;
meta.set_trainer("trainer", 111, ailego::Params());
meta.set_searcher("searcher", 222, ailego::Params());
meta.set_builder("builder", 333, ailego::Params());
EXPECT_EQ(0, IndexHelper::SerializeToStorage(meta, write_storage.get()));
EXPECT_EQ(0, write_storage->append("AAAA", 1234));
EXPECT_EQ(0, write_storage->append("BBBB", 1234));
auto aaaa = write_storage->get("AAAA");
ASSERT_TRUE(aaaa);
auto aaaa1 = aaaa->clone();
ASSERT_TRUE(aaaa1);
std::string hello = "Hello world!!!";
EXPECT_EQ(hello.size(), aaaa1->write(0, hello.data(), hello.size()));
auto hasHugePageInUse = [&]() {
std::ifstream smaps("/proc/self/smaps");
if (!smaps.is_open()) {
std::cerr << "Cannot open /proc/self/smaps\n";
return false;
}
std::string line;
while (std::getline(smaps, line)) {
// 查找 KernelPageSize 行
if (line.find("KernelPageSize:") != std::string::npos) {
// 提取页大小(单位 kB
size_t pos = line.find_first_of("0123456789");
if (pos != std::string::npos) {
uint64_t pageSizeKB = std::stoull(line.substr(pos));
// std::cerr << pageSizeKB << std::endl;
if (pageSizeKB > 4) { // 普通页是 4kB,大于即为 HugePage
std::cout << "Found HugePage region with KernelPageSize: "
<< pageSizeKB << " kB\n";
return true;
}
}
}
}
return false;
};
if (!hasHugePageInUse()) {
EXPECT_EQ(0, 1);
}
EXPECT_EQ(0, write_storage->close());
// Reopen it
auto read_storage = IndexFactory::CreateStorage("MMapFileStorage");
EXPECT_EQ(0, write_storage->init(params));
EXPECT_EQ(0, read_storage->open(file_path, false));
IndexMeta meta2;
EXPECT_EQ(0, IndexHelper::DeserializeFromStorage(read_storage.get(), &meta2));
EXPECT_EQ("trainer", meta2.trainer_name());
EXPECT_EQ("searcher", meta2.searcher_name());
EXPECT_EQ("builder", meta2.builder_name());
auto aaaa2 = read_storage->get("AAAA");
ASSERT_TRUE(aaaa2);
const void *data;
EXPECT_EQ(hello.size(), aaaa2->read(0, &data, hello.size()));
auto aaaa3 = aaaa2->clone();
ASSERT_TRUE(aaaa3);
EXPECT_EQ(hello.size(), aaaa3->read(0, &data, hello.size()));
EXPECT_EQ(hello, std::string((const char *)data, hello.size()));
}