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
+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