806 lines
31 KiB
C++
806 lines
31 KiB
C++
// Copyright 2025-present the zvec project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "db/index/segment/segment_helper.h"
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <thread>
|
|
#include <variant>
|
|
#include <arrow/array/array_binary.h>
|
|
#include <arrow/io/file.h>
|
|
#include <arrow/ipc/reader.h>
|
|
#include <arrow/pretty_print.h>
|
|
#include <arrow/result.h>
|
|
#include <arrow/table.h>
|
|
#include <gtest/gtest.h>
|
|
#include "db/common/constants.h"
|
|
#include "db/common/file_helper.h"
|
|
#include "db/index/column/vector_column/vector_column_indexer.h"
|
|
#include "db/index/column/vector_column/vector_column_params.h"
|
|
#include "db/index/column/vector_column/vector_index_results.h"
|
|
#include "db/index/common/delete_store.h"
|
|
#include "db/index/common/id_map.h"
|
|
#include "db/index/common/meta.h"
|
|
#include "db/index/common/version_manager.h"
|
|
#include "db/index/segment/segment.h"
|
|
#include "utils/utils.h"
|
|
#include "zvec/db/options.h"
|
|
#include "zvec/db/query_params.h"
|
|
#include "zvec/db/schema.h"
|
|
|
|
using namespace zvec;
|
|
|
|
class SegmentHelperTest : public testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
ailego::LoggerBroker::SetLevel(ailego::Logger::LEVEL_INFO);
|
|
|
|
FileHelper::RemoveDirectory(col_path);
|
|
FileHelper::CreateDirectory(col_path);
|
|
|
|
std::string idmap_path =
|
|
FileHelper::MakeFilePath(col_path, FileID::ID_FILE, 0);
|
|
id_map = IDMap::CreateAndOpen(col_name, idmap_path, true, false);
|
|
if (id_map == nullptr) {
|
|
throw std::runtime_error("Failed to create id map");
|
|
}
|
|
|
|
std::string delete_store_path =
|
|
FileHelper::MakeFilePath(col_path, FileID::DELETE_FILE, 0);
|
|
delete_store = std::make_shared<DeleteStore>(col_name);
|
|
}
|
|
|
|
void TearDown() override {
|
|
id_map.reset();
|
|
delete_store.reset();
|
|
|
|
// FileHelper::RemoveDirectory(col_path);
|
|
}
|
|
|
|
public:
|
|
std::string GetColPath() {
|
|
return col_path;
|
|
}
|
|
|
|
protected:
|
|
VersionManager::Ptr CreateVersionManager(const CollectionSchema &schema) {
|
|
Version version;
|
|
version.set_schema(schema);
|
|
auto vm = VersionManager::Create(col_path, version);
|
|
if (!vm.has_value()) {
|
|
throw std::runtime_error("Failed to create version manager");
|
|
}
|
|
return vm.value();
|
|
}
|
|
|
|
SegmentOptions WriteOptions() const {
|
|
return SegmentOptions{false, true, DEFAULT_MAX_BUFFER_SIZE};
|
|
}
|
|
|
|
struct CompactResult {
|
|
CompactTask compact_task;
|
|
Segment::Ptr output_segment; // null when filter dropped every doc
|
|
};
|
|
|
|
// Execute a CompactTask end-to-end: build it, run it, move the tmp segment
|
|
// dir into place, and reopen the output segment in read-only mode.
|
|
CompactResult RunCompactAndOpen(CollectionSchema::Ptr schema,
|
|
std::vector<Segment::Ptr> segments,
|
|
SegmentID output_segment_id,
|
|
IndexFilter::Ptr filter,
|
|
const VersionManager::Ptr &version_manager,
|
|
int concurrency = 1) {
|
|
const bool forward_use_parquet = false;
|
|
CompactTask task(col_path, schema, std::move(segments), output_segment_id,
|
|
std::move(filter), forward_use_parquet, concurrency);
|
|
auto segment_task = SegmentTask::CreateCompactTask(task);
|
|
EXPECT_NE(segment_task, nullptr);
|
|
if (segment_task == nullptr) return {task, nullptr};
|
|
|
|
auto status = SegmentHelper::Execute(segment_task);
|
|
EXPECT_TRUE(status.ok()) << status.message();
|
|
|
|
auto executed = std::get<CompactTask>(segment_task->task_info());
|
|
if (executed.output_segment_meta_ == nullptr) {
|
|
return {executed, nullptr};
|
|
}
|
|
|
|
auto tmp_path =
|
|
FileHelper::MakeTempSegmentPath(col_path, output_segment_id);
|
|
auto dst_path = FileHelper::MakeSegmentPath(col_path, output_segment_id);
|
|
EXPECT_TRUE(FileHelper::MoveDirectory(tmp_path, dst_path));
|
|
|
|
SegmentOptions read_options{true, !forward_use_parquet,
|
|
DEFAULT_MAX_BUFFER_SIZE};
|
|
version_manager->set_enable_mmap(!forward_use_parquet);
|
|
auto seg_ret =
|
|
Segment::Open(col_path, *schema, *executed.output_segment_meta_, id_map,
|
|
delete_store, version_manager, read_options);
|
|
EXPECT_TRUE(seg_ret.has_value());
|
|
if (!seg_ret.has_value()) return {executed, nullptr};
|
|
return {executed, std::move(seg_ret.value())};
|
|
}
|
|
|
|
std::string col_name = "test_segment_helper";
|
|
std::string col_path = "./test_collection";
|
|
IDMap::Ptr id_map;
|
|
DeleteStore::Ptr delete_store;
|
|
};
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_General) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 1000);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 1000, id_map, delete_store, version_manager,
|
|
write_options, 1000, 1000);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
|
|
SegmentID output_segment_id = 2;
|
|
auto [compact_task, seg3] = RunCompactAndOpen(
|
|
schema, {seg1, seg2}, output_segment_id, nullptr, version_manager);
|
|
|
|
ASSERT_NE(seg3, nullptr);
|
|
ASSERT_EQ(compact_task.output_segment_meta_->id(), output_segment_id);
|
|
ASSERT_FALSE(
|
|
compact_task.output_segment_meta_->writing_forward_block().has_value());
|
|
ASSERT_EQ(seg3->id(), output_segment_id);
|
|
ASSERT_EQ(seg3->doc_count(), seg1->doc_count() + seg2->doc_count());
|
|
|
|
for (uint64_t i = 0; i < seg3->doc_count(); i++) {
|
|
auto doc = seg3->Fetch(i);
|
|
ASSERT_NE(doc, nullptr);
|
|
auto expect_doc = test::TestHelper::CreateDoc(i, *schema);
|
|
ASSERT_EQ(*doc, expect_doc);
|
|
}
|
|
|
|
ASSERT_TRUE(seg1->destroy().ok());
|
|
ASSERT_TRUE(seg2->destroy().ok());
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_ScalarIndex) {
|
|
auto schema = test::TestHelper::CreateSchemaWithScalarIndex(false);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 1000);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 1000, id_map, delete_store, version_manager,
|
|
write_options, 1000, 1000);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
|
|
SegmentID output_segment_id = 2;
|
|
auto [compact_task, seg3] = RunCompactAndOpen(
|
|
schema, {seg1, seg2}, output_segment_id, nullptr, version_manager);
|
|
|
|
ASSERT_NE(seg3, nullptr);
|
|
ASSERT_EQ(compact_task.output_segment_meta_->id(), output_segment_id);
|
|
ASSERT_FALSE(
|
|
compact_task.output_segment_meta_->writing_forward_block().has_value());
|
|
ASSERT_EQ(seg3->id(), output_segment_id);
|
|
ASSERT_EQ(seg3->doc_count(), seg1->doc_count() + seg2->doc_count());
|
|
|
|
for (uint64_t i = 0; i < seg3->doc_count(); i++) {
|
|
auto doc = seg3->Fetch(i);
|
|
ASSERT_NE(doc, nullptr);
|
|
auto expect_doc = test::TestHelper::CreateDoc(i, *schema);
|
|
ASSERT_EQ(*doc, expect_doc);
|
|
}
|
|
|
|
ASSERT_TRUE(seg1->destroy().ok());
|
|
ASSERT_TRUE(seg2->destroy().ok());
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_VectorIndex) {
|
|
auto schema = test::TestHelper::CreateSchemaWithVectorIndex();
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 1000);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 1000, id_map, delete_store, version_manager,
|
|
write_options, 1000, 1000);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
|
|
SegmentID output_segment_id = 2;
|
|
auto [compact_task, seg3] = RunCompactAndOpen(
|
|
schema, {seg1, seg2}, output_segment_id, nullptr, version_manager);
|
|
|
|
ASSERT_NE(seg3, nullptr);
|
|
ASSERT_EQ(compact_task.output_segment_meta_->id(), output_segment_id);
|
|
ASSERT_FALSE(
|
|
compact_task.output_segment_meta_->writing_forward_block().has_value());
|
|
ASSERT_EQ(seg3->id(), output_segment_id);
|
|
ASSERT_EQ(seg3->doc_count(), seg1->doc_count() + seg2->doc_count());
|
|
|
|
for (uint64_t i = 0; i < seg3->doc_count(); i++) {
|
|
auto doc = seg3->Fetch(i);
|
|
ASSERT_NE(doc, nullptr);
|
|
auto expect_doc = test::TestHelper::CreateDoc(i, *schema);
|
|
ASSERT_EQ(*doc, expect_doc);
|
|
}
|
|
|
|
ASSERT_TRUE(seg1->destroy().ok());
|
|
ASSERT_TRUE(seg2->destroy().ok());
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_MultipleSegments) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
std::vector<Segment::Ptr> input_segs;
|
|
const int seg_count = 10;
|
|
const int doc_count_per_seg = 100;
|
|
for (int i = 0; i < seg_count; i++) {
|
|
auto seg = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, i, i * doc_count_per_seg, id_map, delete_store,
|
|
version_manager, write_options, i * doc_count_per_seg,
|
|
doc_count_per_seg);
|
|
ASSERT_TRUE(seg != nullptr);
|
|
ASSERT_TRUE(seg->flush().ok());
|
|
input_segs.push_back(seg);
|
|
}
|
|
|
|
SegmentID output_segment_id = seg_count;
|
|
auto [compact_task, seg3] = RunCompactAndOpen(
|
|
schema, input_segs, output_segment_id, nullptr, version_manager);
|
|
|
|
ASSERT_NE(seg3, nullptr);
|
|
ASSERT_EQ(compact_task.output_segment_meta_->id(), output_segment_id);
|
|
ASSERT_FALSE(
|
|
compact_task.output_segment_meta_->writing_forward_block().has_value());
|
|
ASSERT_EQ(seg3->id(), output_segment_id);
|
|
ASSERT_EQ(seg3->doc_count(), seg_count * doc_count_per_seg);
|
|
|
|
for (uint64_t i = 0; i < seg3->doc_count(); i++) {
|
|
auto doc = seg3->Fetch(i);
|
|
ASSERT_NE(doc, nullptr);
|
|
auto expect_doc = test::TestHelper::CreateDoc(i, *schema);
|
|
ASSERT_EQ(*doc, expect_doc);
|
|
}
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_Filter) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 1000);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
|
|
auto filter = std::make_shared<EasyIndexFilter>(
|
|
[](uint64_t id) -> bool { return id < 10; });
|
|
|
|
SegmentID output_segment_id = 1;
|
|
auto [compact_task, seg2] = RunCompactAndOpen(
|
|
schema, {seg1}, output_segment_id, filter, version_manager);
|
|
|
|
ASSERT_NE(seg2, nullptr);
|
|
ASSERT_EQ(compact_task.output_segment_meta_->id(), output_segment_id);
|
|
ASSERT_FALSE(
|
|
compact_task.output_segment_meta_->writing_forward_block().has_value());
|
|
ASSERT_EQ(seg2->id(), output_segment_id);
|
|
ASSERT_EQ(seg2->doc_count(), seg1->doc_count() - 10);
|
|
|
|
ASSERT_TRUE(seg1->destroy().ok());
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_FilterAll) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 1000);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
|
|
auto filter = std::make_shared<EasyIndexFilter>(
|
|
[](uint64_t /*id*/) -> bool { return true; });
|
|
|
|
SegmentID output_segment_id = 1;
|
|
auto [compact_task, output_segment] = RunCompactAndOpen(
|
|
schema, {seg1}, output_segment_id, filter, version_manager);
|
|
|
|
ASSERT_EQ(compact_task.output_segment_meta_, nullptr);
|
|
ASSERT_EQ(output_segment, nullptr);
|
|
ASSERT_FALSE(FileHelper::DirectoryExists(
|
|
FileHelper::MakeTempSegmentPath(col_path, output_segment_id)));
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CreateVectorIndexTask_AllFields) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
|
|
Version version;
|
|
version.set_schema(*schema);
|
|
auto version_manager_tmp = VersionManager::Create(col_path, version);
|
|
if (!version_manager_tmp.has_value()) {
|
|
throw std::runtime_error("Failed to create version manager");
|
|
}
|
|
|
|
auto version_manager = version_manager_tmp.value();
|
|
|
|
// Create a segment
|
|
auto segment = test::TestHelper::CreateSegmentWithDoc(
|
|
GetColPath(), *schema, 0, 0, id_map, delete_store, version_manager,
|
|
SegmentOptions{false, true, DEFAULT_MAX_BUFFER_SIZE}, 0, 1000);
|
|
ASSERT_TRUE(segment != nullptr);
|
|
ASSERT_TRUE(segment->dump().ok());
|
|
|
|
// Create index params
|
|
auto index_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::L2, // metric_type
|
|
16, // m
|
|
100 // ef_construction
|
|
);
|
|
|
|
// Create create index task
|
|
CreateVectorIndexTask task(
|
|
segment,
|
|
"", // column_to_build_vector_index (empty means all vector columns)
|
|
index_params,
|
|
1 // concurrency
|
|
);
|
|
|
|
// Create segment task
|
|
auto segment_task = SegmentTask::CreateCreateVectorIndexTask(task);
|
|
|
|
// Verify task creation
|
|
ASSERT_TRUE(segment_task != nullptr);
|
|
|
|
// Execute the task
|
|
Status status = SegmentHelper::Execute(segment_task);
|
|
std::cout << "status: " << status.message() << std::endl;
|
|
EXPECT_TRUE(status.ok());
|
|
|
|
// Verify output segment meta
|
|
auto index_task = std::get<CreateVectorIndexTask>(segment_task->task_info());
|
|
auto output_segment_meta = index_task.output_segment_meta_;
|
|
std::cout << "output_segment_meta: "
|
|
<< output_segment_meta->to_string_formatted() << std::endl;
|
|
ASSERT_EQ(output_segment_meta->id(), 0);
|
|
ASSERT_FALSE(output_segment_meta->writing_forward_block().has_value());
|
|
|
|
auto segment_meta = std::make_shared<SegmentMeta>(*segment->meta());
|
|
segment_meta->remove_writing_forward_block();
|
|
// create all vector index will not change segment meta
|
|
ASSERT_EQ(*output_segment_meta, *segment_meta);
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CreateVectorIndexTask_SingleField) {
|
|
auto schema = test::TestHelper::CreateNormalSchema(false, col_name);
|
|
|
|
Version version;
|
|
version.set_schema(*schema);
|
|
auto version_manager_tmp = VersionManager::Create(col_path, version);
|
|
if (!version_manager_tmp.has_value()) {
|
|
throw std::runtime_error("Failed to create version manager");
|
|
}
|
|
|
|
auto version_manager = version_manager_tmp.value();
|
|
|
|
// Create a segment
|
|
auto segment = test::TestHelper::CreateSegmentWithDoc(
|
|
GetColPath(), *schema, 0, 0, id_map, delete_store, version_manager,
|
|
SegmentOptions{false, true, DEFAULT_MAX_BUFFER_SIZE}, 0, 1000);
|
|
ASSERT_TRUE(segment != nullptr);
|
|
ASSERT_TRUE(segment->dump().ok());
|
|
|
|
// Create index params
|
|
auto index_params =
|
|
std::make_shared<HnswIndexParams>(MetricType::IP, // metric_type
|
|
16, // m
|
|
100 // ef_construction
|
|
);
|
|
|
|
// Create create index task
|
|
CreateVectorIndexTask task(segment,
|
|
"dense_fp32", // column_to_build_vector_index
|
|
// (empty means all vector columns)
|
|
index_params,
|
|
1 // concurrency
|
|
);
|
|
|
|
// Create segment task
|
|
auto segment_task = SegmentTask::CreateCreateVectorIndexTask(task);
|
|
|
|
// Verify task creation
|
|
ASSERT_TRUE(segment_task != nullptr);
|
|
|
|
// Execute the task
|
|
Status status = SegmentHelper::Execute(segment_task);
|
|
std::cout << "status: " << status.message() << std::endl;
|
|
EXPECT_TRUE(status.ok());
|
|
|
|
// Verify output segment meta
|
|
auto index_task = std::get<CreateVectorIndexTask>(segment_task->task_info());
|
|
auto output_segment_meta = index_task.output_segment_meta_;
|
|
std::cout << "output_segment_meta: "
|
|
<< output_segment_meta->to_string_formatted() << std::endl;
|
|
ASSERT_EQ(output_segment_meta->id(), 0);
|
|
ASSERT_FALSE(output_segment_meta->writing_forward_block().has_value());
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_VectorIndexThreeSegmentsRegression) {
|
|
auto schema = test::TestHelper::CreateSchemaWithVectorIndex();
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 300);
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 300, id_map, delete_store, version_manager,
|
|
write_options, 300, 300);
|
|
auto seg3 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 2, 600, id_map, delete_store, version_manager,
|
|
write_options, 600, 300);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg3 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
ASSERT_TRUE(seg3->flush().ok());
|
|
|
|
auto [compact_task, output_segment] = RunCompactAndOpen(
|
|
schema, {seg1, seg2, seg3}, 3, nullptr, version_manager);
|
|
|
|
ASSERT_NE(output_segment, nullptr);
|
|
ASSERT_EQ(output_segment->doc_count(), 900);
|
|
ASSERT_NE(output_segment->Fetch(0), nullptr);
|
|
ASSERT_NE(output_segment->Fetch(899), nullptr);
|
|
}
|
|
|
|
TEST_F(SegmentHelperTest,
|
|
CompactTask_QuantizedVectorIndexThreeSegmentsRegression) {
|
|
auto schema = test::TestHelper::CreateSchemaWithVectorIndex(
|
|
false, col_name,
|
|
std::make_shared<HnswIndexParams>(MetricType::IP, 16, 20,
|
|
QuantizeType::FP16));
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 300);
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 300, id_map, delete_store, version_manager,
|
|
write_options, 300, 300);
|
|
auto seg3 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 2, 600, id_map, delete_store, version_manager,
|
|
write_options, 600, 300);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg3 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
ASSERT_TRUE(seg3->flush().ok());
|
|
ASSERT_GT(seg1->get_quant_vector_indexer("dense_fp32").size(), 0u);
|
|
ASSERT_GT(seg2->get_quant_vector_indexer("dense_fp32").size(), 0u);
|
|
ASSERT_GT(seg3->get_quant_vector_indexer("dense_fp32").size(), 0u);
|
|
|
|
auto [compact_task, output_segment] = RunCompactAndOpen(
|
|
schema, {seg1, seg2, seg3}, 3, nullptr, version_manager);
|
|
|
|
ASSERT_NE(output_segment, nullptr);
|
|
ASSERT_EQ(output_segment->doc_count(), 900);
|
|
ASSERT_NE(output_segment->Fetch(0), nullptr);
|
|
ASSERT_NE(output_segment->Fetch(899), nullptr);
|
|
ASSERT_GT(output_segment->get_vector_indexer("dense_fp32").size(), 0u);
|
|
ASSERT_GT(output_segment->get_quant_vector_indexer("dense_fp32").size(), 0u);
|
|
}
|
|
|
|
struct SegmentCompactReuseParam {
|
|
IndexParams::Ptr vector_index_params;
|
|
IndexType expected_output_type;
|
|
};
|
|
|
|
class SegmentCompactReuseTest
|
|
: public SegmentHelperTest,
|
|
public testing::WithParamInterface<SegmentCompactReuseParam> {
|
|
protected:
|
|
// Returns the indexer's underlying VectorIndexParams::type(), defaulting to
|
|
// FLAT if the params can't be downcast (matches the freshly-inserted state).
|
|
static IndexType IndexerType(const VectorColumnIndexer::Ptr &indexer) {
|
|
auto params = std::dynamic_pointer_cast<VectorIndexParams>(
|
|
indexer->field_schema().index_params());
|
|
return params ? params->type() : IndexType::FLAT;
|
|
}
|
|
|
|
static QuantizeType QuantizeTypeOf(const IndexParams::Ptr ¶ms) {
|
|
auto vp = std::dynamic_pointer_cast<VectorIndexParams>(params);
|
|
return vp ? vp->quantize_type() : QuantizeType::UNDEFINED;
|
|
}
|
|
|
|
// Run CreateVectorIndexTask on `segment` for `column` with `index_params`,
|
|
// then reload the segment so its in-memory indexer reflects the new index
|
|
// (matching collection.cc's post-optimize reload path).
|
|
void OptimizeSegmentToVectorIndex(const Segment::Ptr &segment,
|
|
const CollectionSchema &schema,
|
|
const std::string &column,
|
|
const IndexParams::Ptr &index_params) {
|
|
CreateVectorIndexTask task(segment, column, index_params, 1);
|
|
auto segment_task = SegmentTask::CreateCreateVectorIndexTask(task);
|
|
ASSERT_NE(segment_task, nullptr);
|
|
ASSERT_TRUE(SegmentHelper::Execute(segment_task).ok());
|
|
auto executed = std::get<CreateVectorIndexTask>(segment_task->task_info());
|
|
ASSERT_NE(executed.output_segment_meta_, nullptr);
|
|
ASSERT_TRUE(
|
|
segment
|
|
->reload_vector_index(schema, executed.output_segment_meta_,
|
|
executed.output_vector_indexers_,
|
|
executed.output_quant_vector_indexers_)
|
|
.ok());
|
|
}
|
|
|
|
struct ScoredDoc {
|
|
uint64_t doc_id;
|
|
float score;
|
|
};
|
|
|
|
// CreateDoc seeds VECTOR_FP32 with a constant vector of value (doc_id+0.1f).
|
|
static std::vector<float> MakeFp32QueryVector(uint64_t doc_id_value,
|
|
uint32_t dim) {
|
|
return std::vector<float>(dim, static_cast<float>(doc_id_value) + 0.1f);
|
|
}
|
|
|
|
static std::vector<ScoredDoc> RunSearch(
|
|
const VectorColumnIndexer::Ptr &indexer, const std::vector<float> &qvec,
|
|
uint32_t topk, const zvec::QueryParams::Ptr &query_params) {
|
|
vector_column_params::QueryParams qp;
|
|
qp.topk = topk;
|
|
qp.filter = nullptr;
|
|
qp.fetch_vector = false;
|
|
qp.query_params = query_params;
|
|
vector_column_params::VectorData data{
|
|
vector_column_params::DenseVector{qvec.data()}};
|
|
auto results = indexer->Search(data, qp);
|
|
EXPECT_TRUE(results.has_value());
|
|
if (!results.has_value()) return {};
|
|
auto vec_res = dynamic_cast<VectorIndexResults *>(results.value().get());
|
|
EXPECT_NE(vec_res, nullptr);
|
|
if (vec_res == nullptr) return {};
|
|
std::vector<ScoredDoc> out;
|
|
for (auto it = vec_res->create_iterator(); it->valid(); it->next()) {
|
|
out.push_back({it->doc_id(), it->score()});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// All test instantiations use IP — higher score is better.
|
|
static std::set<uint64_t> MergeTopKIds(
|
|
std::vector<std::vector<ScoredDoc>> per_seg, uint32_t topk) {
|
|
std::vector<ScoredDoc> all;
|
|
for (auto &v : per_seg)
|
|
for (auto &d : v) all.push_back(d);
|
|
std::sort(all.begin(), all.end(),
|
|
[](const ScoredDoc &a, const ScoredDoc &b) {
|
|
return a.score > b.score;
|
|
});
|
|
std::set<uint64_t> ids;
|
|
for (size_t i = 0; i < all.size() && ids.size() < topk; ++i) {
|
|
ids.insert(all[i].doc_id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
static zvec::QueryParams::Ptr MakeIsLinearQueryParam(IndexType type) {
|
|
switch (type) {
|
|
case IndexType::HNSW: {
|
|
auto p = std::make_shared<zvec::HnswQueryParams>();
|
|
p->set_is_linear(true);
|
|
return p;
|
|
}
|
|
case IndexType::IVF: {
|
|
auto p = std::make_shared<zvec::IVFQueryParams>();
|
|
p->set_is_linear(true);
|
|
return p;
|
|
}
|
|
case IndexType::HNSW_RABITQ: {
|
|
auto p = std::make_shared<zvec::HnswRabitqQueryParams>();
|
|
p->set_is_linear(true);
|
|
return p;
|
|
}
|
|
case IndexType::FLAT:
|
|
default:
|
|
return std::make_shared<zvec::FlatQueryParams>();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Mimic the normal insertion lifecycle: small segments accumulate vectors
|
|
// in flat storage (no vector index built yet), then compaction merges them
|
|
// into a single segment whose vector column is built per schema.
|
|
TEST_P(SegmentCompactReuseTest, OptimizedSegmentsReuseFirstIndexer) {
|
|
const auto ¶m = GetParam();
|
|
auto schema = test::TestHelper::CreateSchemaWithVectorIndex(
|
|
false, col_name, param.vector_index_params);
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
constexpr int kSegCount = 3;
|
|
constexpr int kDocsPerSeg = 300;
|
|
constexpr uint32_t kTopK = 10;
|
|
constexpr uint32_t kDim = 128;
|
|
std::vector<Segment::Ptr> segs;
|
|
for (int i = 0; i < kSegCount; i++) {
|
|
auto seg = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, i, i * kDocsPerSeg, id_map, delete_store,
|
|
version_manager, write_options, i * kDocsPerSeg, kDocsPerSeg);
|
|
ASSERT_NE(seg, nullptr);
|
|
ASSERT_TRUE(seg->flush().ok());
|
|
segs.push_back(seg);
|
|
}
|
|
|
|
// Capture groundtruth via FlatQuery on each source segment while every
|
|
// segment is still backed by a flat indexer (before seg[0] is optimized).
|
|
const std::vector<uint64_t> query_doc_values{0, kDocsPerSeg,
|
|
kSegCount * kDocsPerSeg - 1};
|
|
std::vector<std::set<uint64_t>> groundtruth;
|
|
groundtruth.reserve(query_doc_values.size());
|
|
auto flat_qp = std::make_shared<zvec::FlatQueryParams>();
|
|
for (uint64_t qv : query_doc_values) {
|
|
auto qvec = MakeFp32QueryVector(qv, kDim);
|
|
std::vector<std::vector<ScoredDoc>> per_seg;
|
|
per_seg.reserve(segs.size());
|
|
// Per-segment indexers use block-local doc ids (0..kDocsPerSeg-1).
|
|
// The compacted output indexer reindexes them sequentially across
|
|
// segments, so add segment offset to align id spaces before merging.
|
|
for (size_t s = 0; s < segs.size(); ++s) {
|
|
auto in_indexers = segs[s]->get_vector_indexer("dense_fp32");
|
|
ASSERT_FALSE(in_indexers.empty());
|
|
ASSERT_EQ(IndexerType(in_indexers.front()), IndexType::FLAT);
|
|
auto local = RunSearch(in_indexers.front(), qvec, kTopK, flat_qp);
|
|
const uint64_t offset = static_cast<uint64_t>(s) * kDocsPerSeg;
|
|
for (auto &d : local) d.doc_id += offset;
|
|
per_seg.push_back(std::move(local));
|
|
}
|
|
auto gt = MergeTopKIds(std::move(per_seg), kTopK);
|
|
ASSERT_EQ(gt.size(), kTopK);
|
|
groundtruth.push_back(std::move(gt));
|
|
}
|
|
|
|
// Optimize seg[0]'s vector fields to the parametric index type, mimicking
|
|
// the lifecycle the compact path exercises.
|
|
for (const auto &vf : schema->vector_fields()) {
|
|
OptimizeSegmentToVectorIndex(segs[0], *schema, vf->name(),
|
|
vf->index_params());
|
|
}
|
|
|
|
// For quantized index types (e.g. HNSW_RABITQ) the built index lives in
|
|
// get_quant_vector_indexer; get_vector_indexer keeps the raw FLAT
|
|
// indexer. See CompactTask_QuantizedVectorIndexThreeSegmentsRegression.
|
|
const bool quantized =
|
|
QuantizeTypeOf(param.vector_index_params) != QuantizeType::UNDEFINED;
|
|
for (int i = 0; i < kSegCount; i++) {
|
|
auto in_indexers = quantized
|
|
? segs[i]->get_quant_vector_indexer("dense_fp32")
|
|
: segs[i]->get_vector_indexer("dense_fp32");
|
|
ASSERT_FALSE(in_indexers.empty());
|
|
ASSERT_EQ(IndexerType(in_indexers.front()),
|
|
i == 0 ? param.expected_output_type : IndexType::FLAT);
|
|
}
|
|
|
|
auto [compact_task, output_segment] =
|
|
RunCompactAndOpen(schema, segs, kSegCount, nullptr, version_manager);
|
|
|
|
ASSERT_NE(output_segment, nullptr);
|
|
ASSERT_EQ(output_segment->doc_count(), kSegCount * kDocsPerSeg);
|
|
ASSERT_NE(output_segment->Fetch(0), nullptr);
|
|
ASSERT_NE(output_segment->Fetch(kSegCount * kDocsPerSeg - 1), nullptr);
|
|
|
|
auto out_indexers =
|
|
quantized ? output_segment->get_quant_vector_indexer("dense_fp32")
|
|
: output_segment->get_vector_indexer("dense_fp32");
|
|
ASSERT_FALSE(out_indexers.empty());
|
|
EXPECT_EQ(IndexerType(out_indexers.front()), param.expected_output_type);
|
|
|
|
// is_linear queries on the merged indexer must reproduce the pre-compact
|
|
// groundtruth. Quantized indexers are allowed a small recall hit.
|
|
auto linear_qp = MakeIsLinearQueryParam(param.expected_output_type);
|
|
const double kMinRecall = quantized ? 0.8 : 1.0;
|
|
for (size_t qi = 0; qi < query_doc_values.size(); ++qi) {
|
|
auto qvec = MakeFp32QueryVector(query_doc_values[qi], kDim);
|
|
auto hits = RunSearch(out_indexers.front(), qvec, kTopK, linear_qp);
|
|
ASSERT_EQ(hits.size(), kTopK);
|
|
size_t intersect = 0;
|
|
for (const auto &h : hits) {
|
|
if (groundtruth[qi].count(h.doc_id)) intersect++;
|
|
}
|
|
double recall = static_cast<double>(intersect) / kTopK;
|
|
EXPECT_GE(recall, kMinRecall)
|
|
<< "query[" << qi << "] (value=" << query_doc_values[qi]
|
|
<< ") recall=" << recall;
|
|
}
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(Hnsw, SegmentCompactReuseTest,
|
|
testing::Values(SegmentCompactReuseParam{
|
|
std::make_shared<HnswIndexParams>(MetricType::IP,
|
|
16, 200),
|
|
IndexType::HNSW}));
|
|
|
|
// CreateNormalSchema() only puts the test's vector_index_params on dense_fp32.
|
|
// The other 4 vector fields are hardcoded — dense_fp16/dense_int8/sparse_fp16
|
|
// are always FlatIndexParams, and sparse_fp32 gets the
|
|
// cloned params only if supports_sparse is true (utils.cc:117-124), which
|
|
// excludes IVF and HNSW_RABITQ — so for IVF it also falls back to FLAT.
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
Ivf, SegmentCompactReuseTest,
|
|
testing::Values(SegmentCompactReuseParam{
|
|
std::make_shared<IVFIndexParams>(MetricType::IP, 10, 4, false,
|
|
QuantizeType::UNDEFINED),
|
|
IndexType::IVF}));
|
|
|
|
#if RABITQ_SUPPORTED
|
|
INSTANTIATE_TEST_SUITE_P(HnswRabitq, SegmentCompactReuseTest,
|
|
testing::Values(SegmentCompactReuseParam{
|
|
std::make_shared<HnswRabitqIndexParams>(
|
|
MetricType::IP, 7, 256, 16, 200, 0),
|
|
IndexType::HNSW_RABITQ}));
|
|
#endif
|
|
|
|
TEST_F(SegmentHelperTest, CompactTask_FilterMultiSegmentsRegression) {
|
|
auto schema = test::TestHelper::CreateSchemaWithVectorIndex();
|
|
auto version_manager = CreateVersionManager(*schema);
|
|
auto write_options = WriteOptions();
|
|
|
|
auto seg1 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 0, 0, id_map, delete_store, version_manager,
|
|
write_options, 0, 400);
|
|
auto seg2 = test::TestHelper::CreateSegmentWithDoc(
|
|
col_path, *schema, 1, 400, id_map, delete_store, version_manager,
|
|
write_options, 400, 400);
|
|
ASSERT_TRUE(seg1 != nullptr);
|
|
ASSERT_TRUE(seg2 != nullptr);
|
|
ASSERT_TRUE(seg1->flush().ok());
|
|
ASSERT_TRUE(seg2->flush().ok());
|
|
|
|
auto filter = std::make_shared<EasyIndexFilter>(
|
|
[](uint64_t id) -> bool { return id < 100 || (id >= 400 && id < 450); });
|
|
|
|
auto [compact_task, output_segment] =
|
|
RunCompactAndOpen(schema, {seg1, seg2}, 2, filter, version_manager);
|
|
|
|
ASSERT_NE(output_segment, nullptr);
|
|
ASSERT_EQ(output_segment->doc_count(), 650);
|
|
}
|