blob: 8b7f56221cf8cfd6681c0f5da8a576d8b140bb73 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 "storage/index/inverted/inverted_index_writer.h"
#include <CLucene.h>
#include <CLucene/config/repl_wchar.h>
#include <CLucene/index/IndexReader.h>
#include <gen_cpp/olap_file.pb.h>
#include <gtest/gtest-message.h>
#include <gtest/gtest-test-part.h>
#include <gtest/gtest.h>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "core/block/block.h"
#include "core/data_type/data_type_factory.hpp"
#include "core/data_type/data_type_number.h"
#include "core/field.h"
#include "io/fs/local_file_system.h"
#include "runtime/runtime_state.h"
#include "storage/index/index_file_reader.h"
#include "storage/index/index_file_writer.h"
#include "storage/index/inverted/inverted_index_desc.h"
#include "storage/index/inverted/inverted_index_fs_directory.h"
#include "storage/index/inverted/inverted_index_reader.h"
#include "storage/iterator/olap_data_convertor.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/types.h"
#include "util/faststring.h"
#include "util/slice.h"
using namespace lucene::index;
using doris::segment_v2::IndexFileWriter;
namespace doris::segment_v2 {
// Define InvertedIndexDirectoryMap
using InvertedIndexDirectoryMap =
std::map<std::pair<int64_t, std::string>, std::shared_ptr<lucene::store::Directory>>;
class InvertedIndexWriterTest : public testing::Test {
using ExpectedDocMap = std::map<std::string, std::vector<int>>;
public:
const std::string kTestDir = "./ut_dir/inverted_index_writer_test";
void check_terms_stats(std::string index_prefix, ExpectedDocMap* expected,
std::vector<int> expected_null_bitmap = {},
InvertedIndexStorageFormatPB format = InvertedIndexStorageFormatPB::V1,
const TabletIndex* index_meta = nullptr) {
std::string file_str;
if (format == InvertedIndexStorageFormatPB::V1) {
file_str = InvertedIndexDescriptor::get_index_file_path_v1(index_prefix,
index_meta->index_id(), "");
} else if (format == InvertedIndexStorageFormatPB::V2) {
file_str = InvertedIndexDescriptor::get_index_file_path_v2(index_prefix);
}
std::unique_ptr<IndexFileReader> reader = std::make_unique<IndexFileReader>(
io::global_local_filesystem(), index_prefix, format);
auto st = reader->init();
EXPECT_EQ(st, Status::OK());
auto result = reader->open(index_meta);
EXPECT_TRUE(result.has_value()) << "Failed to open compound reader" << result.error();
auto compound_reader = std::move(result.value());
try {
CLuceneError err;
CL_NS(store)::IndexInput* index_input = nullptr;
auto ok = DorisFSDirectory::FSIndexInput::open(
io::global_local_filesystem(), file_str.c_str(), index_input, err, 4096);
if (!ok) {
throw err;
}
std::shared_ptr<roaring::Roaring> null_bitmap = std::make_shared<roaring::Roaring>();
const char* null_bitmap_file_name =
InvertedIndexDescriptor::get_temporary_null_bitmap_file_name();
if (compound_reader->fileExists(null_bitmap_file_name)) {
std::unique_ptr<lucene::store::IndexInput> null_bitmap_in;
assert(compound_reader->openInput(null_bitmap_file_name, null_bitmap_in, err,
4096));
size_t null_bitmap_size = null_bitmap_in->length();
doris::faststring buf;
buf.resize(null_bitmap_size);
null_bitmap_in->readBytes(reinterpret_cast<uint8_t*>(buf.data()), null_bitmap_size);
*null_bitmap = roaring::Roaring::read(reinterpret_cast<char*>(buf.data()), false);
EXPECT_TRUE(expected_null_bitmap.size() == null_bitmap->cardinality());
for (int i : expected_null_bitmap) {
EXPECT_TRUE(null_bitmap->contains(i));
}
}
index_input->close();
_CLLDELETE(index_input);
} catch (const CLuceneError& e) {
EXPECT_TRUE(false) << "CLuceneError: " << e.what();
}
std::cout << "Term statistics for " << file_str << std::endl;
std::cout << "==================================" << std::endl;
lucene::store::Directory* dir = compound_reader.get();
lucene::index::IndexReader* r = lucene::index::IndexReader::open(dir);
printf("Max Docs: %d\n", r->maxDoc());
printf("Num Docs: %d\n", r->numDocs());
TermEnum* te = r->terms();
int32_t nterms;
for (nterms = 0; te->next(); nterms++) {
/* empty */
std::string token =
lucene_wcstoutf8string(te->term(false)->text(), te->term(false)->textLength());
printf("Term: %s ", token.c_str());
if (expected) {
auto it = expected->find(token);
if (it != expected->end()) {
TermDocs* td = r->termDocs(te->term(false));
std::vector<int> actual_docs;
while (td->next()) {
actual_docs.push_back(td->doc());
}
td->close();
_CLLDELETE(td);
EXPECT_EQ(actual_docs, it->second) << "Term: " << token;
}
}
printf("Freq: %d\n", te->docFreq());
}
printf("Term count: %d\n\n", nterms);
te->close();
_CLLDELETE(te);
r->close();
_CLLDELETE(r);
}
void check_bkd_index(std::string index_prefix, const TabletIndex* index_meta,
const std::vector<int32_t>& values, const std::vector<int>& doc_ids) {
OlapReaderStatistics stats;
RuntimeState runtime_state;
TQueryOptions query_options;
query_options.enable_inverted_index_searcher_cache = false;
runtime_state.set_query_options(query_options);
// Create a BkdIndexReader to verify the index
auto reader = std::make_shared<IndexFileReader>(io::global_local_filesystem(), index_prefix,
InvertedIndexStorageFormatPB::V2);
auto st = reader->init();
EXPECT_EQ(st, Status::OK());
auto result = reader->open(index_meta);
EXPECT_TRUE(result.has_value()) << "Failed to open compound reader" << result.error();
auto bkd_reader = doris::segment_v2::BkdIndexReader::create_shared(index_meta, reader);
EXPECT_NE(bkd_reader, nullptr);
// Test EQUAL query for each value
for (size_t i = 0; i < values.size(); i++) {
std::shared_ptr<roaring::Roaring> bitmap = std::make_shared<roaring::Roaring>();
auto context = std::make_shared<segment_v2::IndexQueryContext>();
context->stats = &stats;
context->runtime_state = &runtime_state;
Field qp = Field::create_field<TYPE_INT>(values[i]);
auto status = bkd_reader->query(context, "c1", qp,
doris::segment_v2::InvertedIndexQueryType::EQUAL_QUERY,
bitmap);
EXPECT_TRUE(status.ok()) << status;
// Verify the result contains the expected document ID
EXPECT_TRUE(bitmap->contains(doc_ids[i]))
<< "Value " << values[i] << " should match document " << doc_ids[i];
// For duplicate values, verify all matching documents are found
for (size_t j = 0; j < values.size(); j++) {
if (values[j] == values[i]) {
EXPECT_TRUE(bitmap->contains(doc_ids[j]))
<< "Value " << values[i] << " should match document " << doc_ids[j];
}
}
}
// Test range queries
// Test LESS_THAN query
std::shared_ptr<roaring::Roaring> less_than_bitmap = std::make_shared<roaring::Roaring>();
int32_t test_value = 200;
auto context = std::make_shared<segment_v2::IndexQueryContext>();
context->stats = &stats;
context->runtime_state = &runtime_state;
Field test_qp = Field::create_field<TYPE_INT>(test_value);
auto status = bkd_reader->query(context, "c1", test_qp,
doris::segment_v2::InvertedIndexQueryType::LESS_THAN_QUERY,
less_than_bitmap);
EXPECT_TRUE(status.ok()) << status;
// Verify documents with values less than test_value are in the result
for (size_t i = 0; i < values.size(); i++) {
if (values[i] < test_value) {
EXPECT_TRUE(less_than_bitmap->contains(doc_ids[i]))
<< "Value " << values[i] << " should be less than " << test_value;
} else {
EXPECT_FALSE(less_than_bitmap->contains(doc_ids[i]))
<< "Value " << values[i] << " should not be less than " << test_value;
}
}
// Test GREATER_THAN query
std::shared_ptr<roaring::Roaring> greater_than_bitmap =
std::make_shared<roaring::Roaring>();
status = bkd_reader->query(context, "c1", test_qp,
doris::segment_v2::InvertedIndexQueryType::GREATER_THAN_QUERY,
greater_than_bitmap);
EXPECT_TRUE(status.ok()) << status;
// Verify documents with values greater than test_value are in the result
for (size_t i = 0; i < values.size(); i++) {
if (values[i] > test_value) {
EXPECT_TRUE(greater_than_bitmap->contains(doc_ids[i]))
<< "Value " << values[i] << " should be greater than " << test_value;
} else {
EXPECT_FALSE(greater_than_bitmap->contains(doc_ids[i]))
<< "Value " << values[i] << " should not be greater than " << test_value;
}
}
}
void SetUp() override {
auto st = io::global_local_filesystem()->delete_directory(kTestDir);
ASSERT_TRUE(st.ok()) << st;
st = io::global_local_filesystem()->create_directory(kTestDir);
ASSERT_TRUE(st.ok()) << st;
std::vector<StorePath> paths;
paths.emplace_back(kTestDir, 1024);
auto tmp_file_dirs = std::make_unique<segment_v2::TmpFileDirs>(paths);
st = tmp_file_dirs->init();
if (!st.ok()) {
std::cout << "init tmp file dirs error:" << st.to_string() << std::endl;
return;
}
ExecEnv::GetInstance()->set_tmp_file_dir(std::move(tmp_file_dirs));
// use memory limit
int64_t inverted_index_cache_limit = 1024 * 1024 * 1024;
_inverted_index_searcher_cache = std::unique_ptr<segment_v2::InvertedIndexSearcherCache>(
InvertedIndexSearcherCache::create_global_instance(inverted_index_cache_limit, 1));
_inverted_index_query_cache = std::unique_ptr<segment_v2::InvertedIndexQueryCache>(
InvertedIndexQueryCache::create_global_cache(inverted_index_cache_limit, 1));
ExecEnv::GetInstance()->set_inverted_index_searcher_cache(
_inverted_index_searcher_cache.get());
ExecEnv::GetInstance()->_inverted_index_query_cache = _inverted_index_query_cache.get();
}
void TearDown() override {
ASSERT_TRUE(io::global_local_filesystem()->delete_directory(_absolute_dir).ok());
}
TabletSchemaSPtr create_schema(KeysType keys_type = DUP_KEYS) {
TabletSchemaSPtr tablet_schema = std::make_shared<TabletSchema>();
TabletSchemaPB tablet_schema_pb;
tablet_schema_pb.set_keys_type(keys_type);
tablet_schema_pb.set_num_short_key_columns(1);
tablet_schema_pb.set_num_rows_per_row_block(1024);
tablet_schema_pb.set_compress_kind(COMPRESS_NONE);
tablet_schema_pb.set_next_column_unique_id(4);
tablet_schema->init_from_pb(tablet_schema_pb);
// Add key column (INT)
TabletColumn column_1;
column_1.set_name("c1");
column_1.set_type(FieldType::OLAP_FIELD_TYPE_INT);
column_1.set_length(4);
column_1.set_index_length(4);
column_1.set_is_key(true);
column_1.set_is_nullable(true);
tablet_schema->append_column(column_1);
// Add value column (VARCHAR)
TabletColumn column_2;
column_2.set_name("c2");
column_2.set_type(FieldType::OLAP_FIELD_TYPE_VARCHAR);
column_2.set_length(65535);
column_2.set_is_key(false);
column_2.set_is_nullable(false);
tablet_schema->append_column(column_2);
return tablet_schema;
}
std::string local_segment_path(std::string base, std::string_view rowset_id, int64_t seg_id) {
return fmt::format("{}/{}_{}.dat", base, rowset_id, seg_id);
}
// Check if .nrm file exists in the inverted index
// Norms files store normalization factors for scoring, typically created when field is tokenized
bool check_norms_file_exists(const std::string& index_prefix, const TabletIndex* index_meta) {
try {
std::unique_ptr<IndexFileReader> reader = std::make_unique<IndexFileReader>(
io::global_local_filesystem(), index_prefix, InvertedIndexStorageFormatPB::V2);
auto st = reader->init();
EXPECT_TRUE(st.ok());
auto result = reader->open(index_meta);
EXPECT_TRUE(result.has_value());
auto compound_reader = std::move(result.value());
CLuceneError err;
CL_NS(store)::IndexInput* index_input = nullptr;
std::string file_str = InvertedIndexDescriptor::get_index_file_path_v2(index_prefix);
auto ok = DorisFSDirectory::FSIndexInput::open(
io::global_local_filesystem(), file_str.c_str(), index_input, err, 4096);
EXPECT_TRUE(ok);
// Try to open the index reader to list all files
lucene::store::Directory* dir = compound_reader.get();
lucene::index::IndexReader* r = lucene::index::IndexReader::open(dir);
// Get the list of files in the directory
std::vector<std::string> files;
dir->list(&files);
bool norms_found = false;
for (const auto& file_name : files) {
// .nrm files are the norms data files in Lucene
// They have pattern: _N.nrm where N is a number (field number)
if (file_name.find(".nrm") != std::string::npos) {
norms_found = true;
}
}
r->close();
_CLLDELETE(r);
index_input->close();
_CLLDELETE(index_input);
return norms_found;
} catch (const CLuceneError& e) {
std::cout << "Error checking norms file: " << e.what() << std::endl;
return false;
} catch (const std::exception& e) {
std::cout << "Exception checking norms file: " << e.what() << std::endl;
return false;
}
}
// Helper method to create an inverted index with tokenization enabled
void create_tokenized_index(std::string_view rowset_id, int seg_id, bool enable_analyzer) {
auto tablet_schema = create_schema();
// Create index meta with tokenization setting
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
// Add parser type property to control tokenization
// should_analyzer returns true if:
// 1. analyzer or normalizer property is not empty, OR
// 2. parser type is not UNKNOWN and not NONE
auto* properties = index_meta_pb->mutable_properties();
if (enable_analyzer) {
// Enable tokenization by setting parser to standard
// This will make should_analyzer() return true
(*properties)["parser"] = "standard";
}
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, rowset_id, seg_id))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, std::string {rowset_id}, seg_id,
InvertedIndexStorageFormatPB::V2, std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status = IndexColumnWriter::create(field, &column_writer, index_file_writer.get(),
&idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add some string values
std::vector<Slice> values = {Slice("hello world"), Slice("testing value"),
Slice("sample data")};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and close
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
void test_string_write(std::string_view rowset_id, int seg_id) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, rowset_id, seg_id))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, std::string {rowset_id}, seg_id,
InvertedIndexStorageFormatPB::V2, std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status = IndexColumnWriter::create(field, &column_writer, index_file_writer.get(),
&idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add string values
std::vector<Slice> values = {Slice("apple"), Slice("banana"), Slice("cherry"),
Slice("apple"), // Duplicate to test frequency
Slice("date")};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and close
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
// Verify the terms stats
ExpectedDocMap expected {
{"apple", {0, 3}}, {"banana", {1}}, {"cherry", {2}}, {"date", {4}}};
check_terms_stats(index_path_prefix, &expected, {}, InvertedIndexStorageFormatPB::V2,
&idx_meta);
}
void test_nulls_write(std::string_view rowset_id, int seg_id) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, rowset_id, seg_id))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, std::string {rowset_id}, seg_id,
InvertedIndexStorageFormatPB::V2, std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status = IndexColumnWriter::create(field, &column_writer, index_file_writer.get(),
&idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add null values
status = column_writer->add_nulls(3);
EXPECT_TRUE(status.ok()) << status;
// Add some regular values
std::vector<Slice> values = {Slice("apple"), Slice("banana")};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Add more nulls
status = column_writer->add_nulls(2);
EXPECT_TRUE(status.ok()) << status;
// Finish and close
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
// Verify the terms stats
ExpectedDocMap expected {{"apple", {3}}, {"banana", {4}}};
// Null values should be at positions 0, 1, 2, 5, 6
std::vector<int> expected_nulls = {0, 1, 2, 5, 6};
check_terms_stats(index_path_prefix, &expected, expected_nulls,
InvertedIndexStorageFormatPB::V2, &idx_meta);
}
void test_numeric_write(std::string_view rowset_id, int seg_id) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(0); // c1 column id
// Set index properties for BKD index
auto* properties = index_meta_pb->mutable_properties();
(*properties)["type"] = "bkd";
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, rowset_id, seg_id))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, std::string {rowset_id}, seg_id,
InvertedIndexStorageFormatPB::V2, std::move(file_writer));
// Get field for column c1
const TabletColumn& column = tablet_schema->column(0);
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status = IndexColumnWriter::create(field, &column_writer, index_file_writer.get(),
&idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add integer values
std::vector<int32_t> values = {42, 100, 42, 200, 300};
status = column_writer->add_values("c1", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and close
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
// For BKD index, we need to verify using BkdIndexReader instead of check_terms_stats
// Create a vector of document IDs corresponding to the values
std::vector<int> doc_ids = {0, 1, 2, 3, 4};
// Verify the BKD index using the appropriate method
check_bkd_index(index_path_prefix, &idx_meta, values, doc_ids);
}
void test_unicode_string_write(std::string_view rowset_id, int seg_id,
bool enable_correct_term_write) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, rowset_id, seg_id))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, std::string {rowset_id}, seg_id,
InvertedIndexStorageFormatPB::V2, std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Save original config value
bool original_config_value = config::enable_inverted_index_correct_term_write;
// Set the config value for this test
config::enable_inverted_index_correct_term_write = enable_correct_term_write;
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status = IndexColumnWriter::create(field, &column_writer, index_file_writer.get(),
&idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add string values with Unicode characters above 0xFFFF
// U+10000 (𐀀) LINEAR B SYLLABLE B008 A
// U+1F600 (😀) GRINNING FACE
// U+1F914 (🤔) THINKING FACE
std::vector<Slice> values = {
Slice("regular"),
Slice("unicode_𐀀"), // Contains U+10000
Slice("emoji_😀"), // Contains U+1F600
Slice("thinking_🤔"), // Contains U+1F914
Slice("regular") // Duplicate to test frequency
};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and close
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
// Restore original config value
config::enable_inverted_index_correct_term_write = original_config_value;
// Verify the terms stats
ExpectedDocMap expected {
{"regular", {0, 4}}, {"unicode_𐀀", {1}}, {"emoji_😀", {2}}, {"thinking_🤔", {3}}};
check_terms_stats(index_path_prefix, &expected, {}, InvertedIndexStorageFormatPB::V2,
&idx_meta);
// Now query the index to verify the results
OlapReaderStatistics stats;
RuntimeState runtime_state;
TQueryOptions query_options;
query_options.enable_inverted_index_searcher_cache = false;
runtime_state.set_query_options(query_options);
// Create a reader to verify the index
auto reader = std::make_shared<IndexFileReader>(
io::global_local_filesystem(), index_path_prefix, InvertedIndexStorageFormatPB::V2);
status = reader->init();
EXPECT_EQ(status, Status::OK());
auto result = reader->open(&idx_meta);
EXPECT_TRUE(result.has_value()) << "Failed to open compound reader" << result.error();
// Create an inverted index reader
auto inverted_reader = StringTypeInvertedIndexReader::create_shared(&idx_meta, reader);
EXPECT_NE(inverted_reader, nullptr);
// Create IO context for query
io::IOContext io_ctx;
std::string field_name = std::to_string(field->unique_id());
// Test querying for each value
for (size_t i = 0; i < values.size(); i++) {
std::shared_ptr<roaring::Roaring> bitmap = std::make_shared<roaring::Roaring>();
StringRef str_ref(values[i].data, values[i].size);
auto context = std::make_shared<segment_v2::IndexQueryContext>();
context->io_ctx = &io_ctx;
context->stats = &stats;
context->runtime_state = &runtime_state;
Field qp = Field::create_field<TYPE_VARCHAR>(std::string(str_ref.data, str_ref.size));
auto query_status = inverted_reader->query(context, field_name, qp,
InvertedIndexQueryType::EQUAL_QUERY, bitmap);
EXPECT_TRUE(query_status.ok()) << query_status;
// For regular strings, both should work the same
if (i == 0 || i == 4) {
EXPECT_TRUE(bitmap->contains(0)) << "Value 'regular' should match document 0";
EXPECT_TRUE(bitmap->contains(4)) << "Value 'regular' should match document 4";
}
// For Unicode strings, the behavior might differ based on enable_correct_term_write
else {
// Check if the document is found
if (enable_correct_term_write) {
EXPECT_TRUE(bitmap->contains(i))
<< "Value '" << values[i].to_string() << "' should match document " << i
<< " with enable_correct_term_write=" << enable_correct_term_write;
} else {
EXPECT_FALSE(bitmap->contains(i))
<< "Value '" << values[i].to_string() << "' should not match document "
<< i << " with enable_correct_term_write=" << enable_correct_term_write;
}
}
}
}
private:
std::string _current_dir;
std::string _absolute_dir;
std::unique_ptr<InvertedIndexSearcherCache> _inverted_index_searcher_cache;
std::unique_ptr<InvertedIndexQueryCache> _inverted_index_query_cache;
};
// Test case for writing string values
TEST_F(InvertedIndexWriterTest, StringWrite) {
test_string_write("test_rowset_1", 0);
}
// Test case for nulls writing
TEST_F(InvertedIndexWriterTest, NullsWrite) {
test_nulls_write("test_rowset_2", 0);
}
// Test case for numeric values
TEST_F(InvertedIndexWriterTest, NumericWrite) {
test_numeric_write("test_rowset_3", 0);
}
// Test case for Unicode string values with enable_correct_term_write=true
TEST_F(InvertedIndexWriterTest, UnicodeStringWriteEnabled) {
test_unicode_string_write("test_rowset_4", 0, true);
}
// Test case for Unicode string values with enable_correct_term_write=false
TEST_F(InvertedIndexWriterTest, UnicodeStringWriteDisabled) {
test_unicode_string_write("test_rowset_5", 0, false);
}
// Test case to compare the difference between enabled and disabled correct term write
TEST_F(InvertedIndexWriterTest, CompareUnicodeStringWriteResults) {
// First write with enabled correct term write
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
// Create two indexes with different settings
std::string index_path_prefix_enabled {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_rowset_compare", 1))};
std::string index_path_enabled =
InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix_enabled);
std::string index_path_prefix_disabled {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_rowset_compare", 2))};
std::string index_path_disabled =
InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix_disabled);
// Create file writers
io::FileWriterPtr file_writer_enabled, file_writer_disabled;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path_enabled, &file_writer_enabled, &opts);
ASSERT_TRUE(sts.ok()) << sts;
sts = fs->create_file(index_path_disabled, &file_writer_disabled, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer_enabled = std::make_unique<IndexFileWriter>(
fs, index_path_prefix_enabled, "test_rowset_compare", 1,
InvertedIndexStorageFormatPB::V2, std::move(file_writer_enabled));
auto index_file_writer_disabled = std::make_unique<IndexFileWriter>(
fs, index_path_prefix_disabled, "test_rowset_compare", 2,
InvertedIndexStorageFormatPB::V2, std::move(file_writer_disabled));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Save original config value
bool original_config_value = config::enable_inverted_index_correct_term_write;
// Create column writers with different settings
std::unique_ptr<IndexColumnWriter> column_writer_enabled, column_writer_disabled;
// Set config to enabled for first writer
config::enable_inverted_index_correct_term_write = true;
auto status = IndexColumnWriter::create(field, &column_writer_enabled,
index_file_writer_enabled.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Set config to disabled for second writer
config::enable_inverted_index_correct_term_write = false;
status = IndexColumnWriter::create(field, &column_writer_disabled,
index_file_writer_disabled.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add string values with Unicode characters above 0xFFFF
// U+10000 (𐀀) LINEAR B SYLLABLE B008 A
// U+1F600 (😀) GRINNING FACE
// U+1F914 (🤔) THINKING FACE
std::vector<Slice> values = {
Slice("unicode_𐀀"), // Contains U+10000
Slice("emoji_😀"), // Contains U+1F600
Slice("thinking_🤔"), // Contains U+1F914
};
// Add values to both writers
status = column_writer_enabled->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
status = column_writer_disabled->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and close both writers
status = column_writer_enabled->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer_enabled->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer_enabled->finish_close();
EXPECT_TRUE(status.ok()) << status;
status = column_writer_disabled->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer_disabled->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer_disabled->finish_close();
EXPECT_TRUE(status.ok()) << status;
// Restore original config value
config::enable_inverted_index_correct_term_write = original_config_value;
// Now query both indexes and compare results
OlapReaderStatistics stats;
RuntimeState runtime_state;
TQueryOptions query_options;
query_options.enable_inverted_index_searcher_cache = false;
runtime_state.set_query_options(query_options);
io::IOContext io_ctx;
// Create readers for both indexes
auto reader_enabled = std::make_shared<IndexFileReader>(io::global_local_filesystem(),
index_path_prefix_enabled,
InvertedIndexStorageFormatPB::V2);
status = reader_enabled->init();
EXPECT_EQ(status, Status::OK());
auto result_enabled = reader_enabled->open(&idx_meta);
EXPECT_TRUE(result_enabled.has_value())
<< "Failed to open compound reader" << result_enabled.error();
auto reader_disabled = std::make_shared<IndexFileReader>(io::global_local_filesystem(),
index_path_prefix_disabled,
InvertedIndexStorageFormatPB::V2);
status = reader_disabled->init();
EXPECT_EQ(status, Status::OK());
auto result_disabled = reader_disabled->open(&idx_meta);
EXPECT_TRUE(result_disabled.has_value())
<< "Failed to open compound reader" << result_disabled.error();
// Create inverted index readers
auto inverted_reader_enabled =
StringTypeInvertedIndexReader::create_shared(&idx_meta, reader_enabled);
EXPECT_NE(inverted_reader_enabled, nullptr);
auto inverted_reader_disabled =
StringTypeInvertedIndexReader::create_shared(&idx_meta, reader_disabled);
EXPECT_NE(inverted_reader_disabled, nullptr);
std::string field_name = std::to_string(field->unique_id());
// Test querying for each value and compare results
for (size_t i = 0; i < values.size(); i++) {
std::shared_ptr<roaring::Roaring> bitmap_enabled = std::make_shared<roaring::Roaring>();
std::shared_ptr<roaring::Roaring> bitmap_disabled = std::make_shared<roaring::Roaring>();
auto context = std::make_shared<segment_v2::IndexQueryContext>();
context->io_ctx = &io_ctx;
context->stats = &stats;
context->runtime_state = &runtime_state;
StringRef str_ref(values[i].data, values[i].size);
Field qp = Field::create_field<TYPE_VARCHAR>(std::string(str_ref.data, str_ref.size));
auto query_status_enabled = inverted_reader_enabled->query(
context, field_name, qp, InvertedIndexQueryType::EQUAL_QUERY, bitmap_enabled);
auto query_status_disabled = inverted_reader_disabled->query(
context, field_name, qp, InvertedIndexQueryType::EQUAL_QUERY, bitmap_disabled);
EXPECT_TRUE(query_status_enabled.ok()) << query_status_enabled;
EXPECT_TRUE(query_status_disabled.ok()) << query_status_disabled;
// Check if both queries found the document
EXPECT_TRUE(bitmap_enabled->contains(i))
<< "Value '" << values[i].to_string() << "' should match document " << i
<< " with enable_correct_term_write=true";
EXPECT_FALSE(bitmap_disabled->contains(i))
<< "Value '" << values[i].to_string() << "' should match document " << i
<< " with enable_correct_term_write=false";
// Print the cardinality of both bitmaps to see if there's any difference
std::cout << "Value: " << values[i].to_string()
<< ", enabled cardinality: " << bitmap_enabled->cardinality()
<< ", disabled cardinality: " << bitmap_disabled->cardinality() << std::endl;
}
}
// Test case for error handling in inverted index file writer
TEST_F(InvertedIndexWriterTest, ErrorHandlingInFileWriter) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_error_handling", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
// Create index file writer with error conditions
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_error_handling", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Test with empty values array to trigger certain error paths
std::vector<Slice> empty_values;
status = column_writer->add_values("c2", empty_values.data(), 0);
EXPECT_TRUE(status.ok()) << status;
// Test with very large strings that might trigger ignore_above behavior
std::vector<Slice> large_values;
std::string large_string(100000, 'a'); // Very large string
large_values.push_back(Slice(large_string));
status = column_writer->add_values("c2", large_values.data(), large_values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and write
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
// Test case for array values with mixed null and non-null elements
TEST_F(InvertedIndexWriterTest, ArrayValuesWithNulls) {
// Create TabletSchema with array column (reference inverted_index_array_test.cpp)
TabletSchemaSPtr tablet_schema = std::make_shared<TabletSchema>();
TabletSchemaPB tablet_schema_pb;
tablet_schema_pb.set_keys_type(DUP_KEYS);
tablet_schema->init_from_pb(tablet_schema_pb);
TabletColumn array_column;
array_column.set_name("arr1");
array_column.set_type(FieldType::OLAP_FIELD_TYPE_ARRAY);
array_column.set_length(0);
array_column.set_index_length(0);
array_column.set_is_nullable(false);
TabletColumn child_column;
child_column.set_name("arr_sub_string");
child_column.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
child_column.set_length(INT_MAX);
array_column.add_sub_column(child_column);
tablet_schema->append_column(array_column);
// Create index meta for array
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(0); // array column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_array_nulls", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_array_nulls", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for array column
const TabletColumn* field = &(array_column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Construct arrays with mixed null and non-null elements (reference inverted_index_array_test.cpp)
// Array 1: ["apple", null, "cherry"]
// Array 2: ["banana"]
// Array 3: [null, "date"]
Array a1, a2, a3;
a1.push_back(Field::create_field<TYPE_STRING>("apple"));
a1.push_back(Field()); // null element
a1.push_back(Field::create_field<TYPE_STRING>("cherry"));
a2.push_back(Field::create_field<TYPE_STRING>("banana"));
a3.push_back(Field()); // null element
a3.push_back(Field::create_field<TYPE_STRING>("date"));
// Construct array type: DataTypeArray(DataTypeNullable(DataTypeString))
DataTypePtr inner_string_type =
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>());
DataTypePtr array_type = std::make_shared<DataTypeArray>(inner_string_type);
MutableColumnPtr col = array_type->create_column();
col->insert(Field::create_field<TYPE_ARRAY>(a1));
col->insert(Field::create_field<TYPE_ARRAY>(a2));
col->insert(Field::create_field<TYPE_ARRAY>(a3));
ColumnPtr column_array = std::move(col);
ColumnWithTypeAndName type_and_name(column_array, array_type, "arr1");
// Put the array column into the Block
Block block;
block.insert(type_and_name);
// Use OlapBlockDataConvertor to convert (reference inverted_index_array_test.cpp)
OlapBlockDataConvertor convertor(tablet_schema.get(), {0});
convertor.set_source_content(&block, 0, block.rows());
auto [st, accessor] = convertor.convert_column_data(0);
EXPECT_EQ(st, Status::OK());
// The conversion result is an array of 4 pointers:
// [0]: Total number of elements (elem_cnt)
// [1]: Offsets array pointer
// [2]: Nested item data pointer
// [3]: Nested nullmap pointer
const auto* data_ptr = reinterpret_cast<const uint64_t*>(accessor->get_data());
const auto* offsets_ptr = reinterpret_cast<const uint8_t*>(data_ptr[1]);
const void* item_data = reinterpret_cast<const void*>(data_ptr[2]);
const auto* item_nullmap = reinterpret_cast<const uint8_t*>(data_ptr[3]);
// Get the length of the subfield
auto field_size = field_type_size(field->get_sub_column(0).type());
// Call the inverted index writing interface
status = column_writer->add_array_values(field_size, item_data, item_nullmap, offsets_ptr,
block.rows());
EXPECT_TRUE(status.ok()) << status;
// Add array nulls
const auto* null_map = accessor->get_nullmap();
status = column_writer->add_array_nulls(null_map, block.rows());
EXPECT_TRUE(status.ok()) << status;
// Finish and write
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
// Test case for numeric array values with error conditions
TEST_F(InvertedIndexWriterTest, NumericArrayWithErrorConditions) {
// Create TabletSchema with numeric array column (reference inverted_index_array_test.cpp)
TabletSchemaSPtr tablet_schema = std::make_shared<TabletSchema>();
TabletSchemaPB tablet_schema_pb;
tablet_schema_pb.set_keys_type(DUP_KEYS);
tablet_schema->init_from_pb(tablet_schema_pb);
TabletColumn array_column;
array_column.set_name("arr_num");
array_column.set_type(FieldType::OLAP_FIELD_TYPE_ARRAY);
array_column.set_length(0);
array_column.set_index_length(0);
array_column.set_is_nullable(false);
TabletColumn child_column;
child_column.set_name("arr_sub_int");
child_column.set_type(FieldType::OLAP_FIELD_TYPE_INT);
child_column.set_length(4);
array_column.add_sub_column(child_column);
tablet_schema->append_column(array_column);
// Create index meta for numeric BKD
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(0); // array column id
// Set index properties for BKD index
auto* properties = index_meta_pb->mutable_properties();
(*properties)["type"] = "bkd";
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_numeric_array_error", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_numeric_array_error", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for array column
const TabletColumn* field = &(array_column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Construct numeric arrays (reference inverted_index_array_test.cpp)
// Array 1: [42, 100]
// Array 2: [200, 300, 400]
DataTypePtr inner_int_type = std::make_shared<DataTypeInt32>();
DataTypePtr array_type = std::make_shared<DataTypeArray>(inner_int_type);
MutableColumnPtr col = array_type->create_column();
// Array 1: [42, 100]
{
Array arr;
arr.push_back(Field::create_field<TYPE_INT>(42));
arr.push_back(Field::create_field<TYPE_INT>(100));
col->insert(Field::create_field<TYPE_ARRAY>(arr));
}
// Array 2: [200, 300, 400]
{
Array arr;
arr.push_back(Field::create_field<TYPE_INT>(200));
arr.push_back(Field::create_field<TYPE_INT>(300));
arr.push_back(Field::create_field<TYPE_INT>(400));
col->insert(Field::create_field<TYPE_ARRAY>(arr));
}
ColumnPtr column_array = std::move(col);
ColumnWithTypeAndName type_and_name(column_array, array_type, "arr_num");
// Put the array column into the Block
Block block;
block.insert(type_and_name);
// Use OlapBlockDataConvertor to convert (reference inverted_index_array_test.cpp)
OlapBlockDataConvertor convertor(tablet_schema.get(), {0});
convertor.set_source_content(&block, 0, block.rows());
auto [st, accessor] = convertor.convert_column_data(0);
EXPECT_EQ(st, Status::OK());
// The conversion result is an array of 4 pointers:
// [0]: Total number of elements (elem_cnt)
// [1]: Offsets array pointer
// [2]: Nested item data pointer
// [3]: Nested nullmap pointer
const auto* data_ptr = reinterpret_cast<const uint64_t*>(accessor->get_data());
const auto* offsets_ptr = reinterpret_cast<const uint8_t*>(data_ptr[1]);
const void* item_data = reinterpret_cast<const void*>(data_ptr[2]);
const auto* item_nullmap = reinterpret_cast<const uint8_t*>(data_ptr[3]);
// Get the length of the subfield
auto field_size = field_type_size(field->get_sub_column(0).type());
// Call the inverted index writing interface
status = column_writer->add_array_values(field_size, item_data, item_nullmap, offsets_ptr,
block.rows());
EXPECT_TRUE(status.ok()) << status;
// Add array nulls
const auto* null_map = accessor->get_nullmap();
status = column_writer->add_array_nulls(null_map, block.rows());
EXPECT_TRUE(status.ok()) << status;
// Test with zero count to trigger specific branch
status = column_writer->add_array_values(field_size, item_data, nullptr, offsets_ptr, 0);
EXPECT_TRUE(status.ok()) << status;
// Finish and write
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
// Test case for copy file error handling
TEST_F(InvertedIndexWriterTest, CopyFileErrorHandling) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_copy_error", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_copy_error", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add some values to create index files
std::vector<Slice> values = {Slice("test1"), Slice("test2"), Slice("test3")};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Finish and write
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
// Test case for BKD writer error conditions
TEST_F(InvertedIndexWriterTest, BKDWriterErrorConditions) {
auto tablet_schema = create_schema();
// Create index meta for BKD
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(0); // c1 column id
// Set index properties for BKD index
auto* properties = index_meta_pb->mutable_properties();
(*properties)["type"] = "bkd";
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_bkd_error", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_bkd_error", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for column c1
const TabletColumn& column = tablet_schema->column(0);
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add some numeric values with edge cases
std::vector<int32_t> values = {std::numeric_limits<int32_t>::min(), 0,
std::numeric_limits<int32_t>::max()};
status = column_writer->add_values("c1", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Add some nulls to test null handling in BKD
status = column_writer->add_nulls(5);
EXPECT_TRUE(status.ok()) << status;
// Finish and write
status = column_writer->finish();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->begin_close();
EXPECT_TRUE(status.ok()) << status;
status = index_file_writer->finish_close();
EXPECT_TRUE(status.ok()) << status;
}
// Test case for file creation and output error handling
TEST_F(InvertedIndexWriterTest, FileCreationAndOutputErrorHandling) {
auto tablet_schema = create_schema();
// Create index meta
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta;
idx_meta.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix {InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_file_error", 0))};
std::string index_path = InvertedIndexDescriptor::get_index_file_path_v2(index_path_prefix);
io::FileWriterPtr file_writer;
io::FileWriterOptions opts;
auto fs = io::global_local_filesystem();
Status sts = fs->create_file(index_path, &file_writer, &opts);
ASSERT_TRUE(sts.ok()) << sts;
auto index_file_writer = std::make_unique<IndexFileWriter>(
fs, index_path_prefix, "test_file_error", 0, InvertedIndexStorageFormatPB::V2,
std::move(file_writer));
// Get field for column c2
const TabletColumn& column = tablet_schema->column(1); // c2 is the second column
ASSERT_NE(&column, nullptr);
const TabletColumn* field = &(column);
ASSERT_NE(field, nullptr);
// Create column writer
std::unique_ptr<IndexColumnWriter> column_writer;
auto status =
IndexColumnWriter::create(field, &column_writer, index_file_writer.get(), &idx_meta);
EXPECT_TRUE(status.ok()) << status;
// Add some values to ensure files are created
std::vector<Slice> values = {Slice("test1"), Slice("test2")};
status = column_writer->add_values("c2", values.data(), values.size());
EXPECT_TRUE(status.ok()) << status;
// Force close on error to test error handling paths
column_writer->close_on_error();
// Try to finish after close_on_error (should handle gracefully)
status = column_writer->finish();
// The finish might succeed or fail depending on implementation,
// but it should not crash
}
// Test case to verify .nrm file creation behavior with different tokenization settings
// This test verifies the change in inverted_index_writer.cpp lines 165-171
// where .nrm file is only created when field requires tokenization (_should_analyzer == true)
TEST_F(InvertedIndexWriterTest, NormsFileCreationWithTokenization) {
// Test case 1: Create index with tokenization enabled (parser = "standard")
// This should make _should_analyzer = true, and setOmitNorms(false) will be called
// which should create .nrm file
create_tokenized_index("test_with_analyzer", 0, true);
auto tablet_schema = create_schema();
auto index_meta_pb = std::make_unique<TabletIndexPB>();
index_meta_pb->set_index_type(IndexType::INVERTED);
index_meta_pb->set_index_id(1);
index_meta_pb->set_index_name("test");
index_meta_pb->clear_col_unique_id();
index_meta_pb->add_col_unique_id(1); // c2 column id
// Match the parser setting from create_tokenized_index(true)
auto* properties = index_meta_pb->mutable_properties();
(*properties)["parser"] = "standard";
TabletIndex idx_meta_with_analyzer;
idx_meta_with_analyzer.init_from_pb(*index_meta_pb.get());
std::string index_path_prefix_with_analyzer {
InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_with_analyzer", 0))};
// Check if .nrm file exists for tokenized index
bool norms_exists_tokenized =
check_norms_file_exists(index_path_prefix_with_analyzer, &idx_meta_with_analyzer);
// When _should_analyzer == true, setOmitNorms(false) is called, so .nrm should be created
EXPECT_TRUE(norms_exists_tokenized)
<< "Expected .nrm file to exist when tokenization is enabled (parser=standard) "
<< "because setOmitNorms(false) should be called";
// Test case 2: Create index with tokenization disabled (parser = "none")
// This should make _should_analyzer = false, and setOmitNorms(false) will NOT be called
// which means .nrm file should NOT be created (or setOmitNorms defaults to true)
create_tokenized_index("test_without_analyzer", 1, false);
auto index_meta_pb2 = std::make_unique<TabletIndexPB>();
index_meta_pb2->set_index_type(IndexType::INVERTED);
index_meta_pb2->set_index_id(1);
index_meta_pb2->set_index_name("test");
index_meta_pb2->clear_col_unique_id();
index_meta_pb2->add_col_unique_id(1); // c2 column id
TabletIndex idx_meta_without_analyzer;
idx_meta_without_analyzer.init_from_pb(*index_meta_pb2.get());
std::string index_path_prefix_without_analyzer {
InvertedIndexDescriptor::get_index_file_path_prefix(
local_segment_path(kTestDir, "test_without_analyzer", 1))};
// Check if .nrm file exists for untokenized index
bool norms_exists_untokenized =
check_norms_file_exists(index_path_prefix_without_analyzer, &idx_meta_without_analyzer);
// When _should_analyzer == false, setOmitNorms(false) is NOT called
// This validates the fix: .nrm file should not be created for untokenized fields
EXPECT_FALSE(norms_exists_untokenized)
<< "Expected .nrm file to NOT exist when tokenization is disabled (parser=none) "
<< "because setOmitNorms(false) is not called. This validates the fix in "
<< "inverted_index_writer.cpp where .nrm file creation depends on _should_analyzer.";
}
} // namespace doris::segment_v2