zvec/src/db/index/common/schema.cc

697 lines
23 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 <regex>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <zvec/db/index_params.h>
#include <zvec/db/schema.h>
#include <zvec/db/status.h>
#include <zvec/db/type.h>
#include <zvec/plugin/diskann_plugin.h>
#include "ailego/internal/cpu_features.h"
#include "db/common/constants.h"
#include "db/common/typedef.h"
#include "db/common/utils.h"
#include "db/index/column/fts_column/fts_types.h"
#include "db/index/column/fts_column/tokenizer/tokenizer_factory.h"
#include "db/index/common/type_helper.h"
namespace zvec {
#if defined(RABITQ_COMPILED_AVX512)
constexpr const int kRabitqCompiledAvx512 = RABITQ_COMPILED_AVX512;
#else
constexpr const int kRabitqCompiledAvx512 = 0;
#endif
std::unordered_map<DataType, std::set<QuantizeType>> quantize_type_map = {
{DataType::VECTOR_FP32,
{QuantizeType::FP16, QuantizeType::INT4, QuantizeType::INT8,
QuantizeType::RABITQ}},
// {DataType::VECTOR_FP64, {QuantizeType::FP16}},
{DataType::SPARSE_VECTOR_FP32, {QuantizeType::FP16}},
};
std::unordered_set<DataType> support_dense_vector_type = {
DataType::VECTOR_FP32,
DataType::VECTOR_FP16,
DataType::VECTOR_INT8,
};
std::unordered_set<DataType> support_sparse_vector_type = {
DataType::SPARSE_VECTOR_FP32,
DataType::SPARSE_VECTOR_FP16,
};
std::unordered_set<IndexType> support_dense_vector_index = {
IndexType::FLAT, IndexType::HNSW, IndexType::HNSW_RABITQ,
IndexType::IVF, IndexType::DISKANN, IndexType::VAMANA};
std::unordered_set<IndexType> support_sparse_vector_index = {IndexType::FLAT,
IndexType::HNSW};
static Status validate_fts_index_params(const FieldSchema &field) {
auto params = std::dynamic_pointer_cast<FtsIndexParams>(field.index_params());
if (!params) {
return Status::InvalidArgument(
"schema validate failed: FTS index requires FtsIndexParams, but field[",
field.name(), "] has incompatible index params");
}
fts::FtsIndexParams internal_params;
internal_params.tokenizer_name = params->tokenizer_name();
internal_params.filters = params->filters();
internal_params.extra_params = params->extra_params();
auto pipeline = fts::TokenizerFactory::create(internal_params);
if (!pipeline) {
return Status::InvalidArgument(
"schema validate failed: invalid FTS index params for field[",
field.name(), "]");
}
return Status::OK();
}
Status FieldSchema::validate() const {
if (data_type_ == DataType::UNDEFINED) {
return Status::InvalidArgument("schema validate failed: field[", name_,
"]'s data_type is not defined");
}
if (name_.empty()) {
return Status::InvalidArgument("schema validate failed: field[", name_,
"]'s name is empty");
}
if (!std::regex_match(name_, FIELD_NAME_REGEX)) {
return Status::InvalidArgument(
"schema validate failed: field[", name_,
"]'s name cannot pass the regex verification");
}
if (is_vector_field()) {
auto is_sparse = is_sparse_vector();
if (!is_sparse && (dimension_ == 0 || dimension() > kMaxDenseDimSize)) {
return Status::InvalidArgument("schema validate failed: field[", name_,
"]'s dimension must be in (0,20000]");
}
if (!is_sparse) {
if (support_dense_vector_type.find(data_type_) ==
support_dense_vector_type.end()) {
return Status::InvalidArgument(
"schema validate failed: dense_vector's data type only "
"support FP32, "
"but field[",
name_, "]'s data type is ", DataTypeCodeBook::AsString(data_type_));
}
} else {
if (support_sparse_vector_type.find(data_type_) ==
support_sparse_vector_type.end()) {
return Status::InvalidArgument(
"schema validate failed: sparse_vector's data type only "
"support FP32, "
"but field[",
name_, "]'s data type is ", DataTypeCodeBook::AsString(data_type_));
}
}
if (index_params_) {
auto vector_index_params =
std::dynamic_pointer_cast<VectorIndexParams>(index_params_);
if (is_sparse) {
if (support_sparse_vector_index.find(index_params_->type()) ==
support_sparse_vector_index.end()) {
return Status::InvalidArgument(
"schema validate failed: sparse_vector's index_params only "
"support FLAT|HNSW index, "
"but field[",
name_, "]'s index_type is ",
IndexTypeCodeBook::AsString(index_params_->type()));
}
if (vector_index_params->metric_type() != MetricType::IP) {
return Status::InvalidArgument(
"schema validate failed: sparse_vector's index_params only "
"support IP metric, but "
"field[",
name_, "]'s metric is ",
MetricTypeCodeBook::AsString(vector_index_params->metric_type()));
}
} else {
if (support_dense_vector_index.find(index_params_->type()) ==
support_dense_vector_index.end()) {
return Status::InvalidArgument(
"schema validate failed: dense_vector's index_params only "
"support FLAT|HNSW|IVF index, but field[",
name_, "]'s index_type is ",
IndexTypeCodeBook::AsString(index_params_->type()));
}
}
if (index_params_->type() == IndexType::HNSW_RABITQ) {
if (dimension_ < kMinRabitqDimSize || dimension_ > kMaxRabitqDimSize) {
return Status::InvalidArgument(
"schema validate failed: HNSW_RABITQ index only support "
"dimension in [",
kMinRabitqDimSize, ", ", kMaxRabitqDimSize, "]");
}
if (data_type_ != DataType::VECTOR_FP32) {
return Status::InvalidArgument(
"schema validate failed: HNSW_RABITQ index only support FP32 "
"data types");
}
auto metric_type = vector_index_params->metric_type();
if (metric_type != MetricType::L2 && metric_type != MetricType::IP &&
metric_type != MetricType::COSINE) {
return Status::InvalidArgument(
"schema validate failed: HNSW_RABITQ index only support "
"L2/IP/COSINE metric");
}
#if !RABITQ_SUPPORTED
return Status::NotSupported(
"RabitQ is not supported on this platform (Linux x86_64 only)");
#endif
auto &flags = zvec::ailego::internal::CpuFeatures::static_flags_;
if (!flags.AVX2 && !flags.AVX512F) {
return Status::NotSupported(
"RabitQ requires AVX2/AVX512F to be supported");
}
if constexpr (kRabitqCompiledAvx512) {
if (!flags.AVX512F) {
return Status::NotSupported(
"RabitQ compiled with AVX512F while runtime does not support");
}
}
}
if (index_params_->type() == IndexType::DISKANN) {
// Probe the DiskAnn runtime eagerly at creation time so unsupported
// platforms (non Linux x86_64), missing libaio, or a missing plugin
// .so fail fast with a clear message instead of surfacing later during
// optimize(). This reuses the same gate DiskAnnIndex applies on first
// use (zvec::LoadDiskAnnPlugin, wrapped by EnsureDiskAnnRuntimeReady).
// All validate() call sites are creation-time only, so triggering the
// plugin load here is safe (and idempotent/cached).
const int rc = ::zvec::LoadDiskAnnPlugin();
switch (rc) {
case kDiskAnnPluginOk:
break;
case kDiskAnnPluginUnsupportedPlatform:
return Status::NotSupported(
"DiskAnn is not supported on this platform (Linux x86_64 "
"only)");
case kDiskAnnPluginLibAioMissing:
return Status::NotSupported(
"DiskAnn requires libaio at runtime, but it was not found on "
"this host. Install it (e.g. 'apt-get install libaio1', or "
"'libaio1t64' on Ubuntu 24.04+) and retry.");
default:
return Status::NotSupported(
"DiskAnn runtime could not be initialized on this host");
}
}
if (vector_index_params->quantize_type() != QuantizeType::UNDEFINED) {
auto iter = quantize_type_map.find(data_type_);
if (iter == quantize_type_map.end()) {
return Status::InvalidArgument(
"schema validate failed: ",
is_sparse ? "sparse_vector" : "dense_vector",
"'s index_params of ", DataTypeCodeBook::AsString(data_type_),
" do not support quantize, but field[", name_,
"]'s quantize_type is ",
QuantizeTypeCodeBook::AsString(
vector_index_params->quantize_type()));
} else {
if (iter->second.find(vector_index_params->quantize_type()) ==
iter->second.end()) {
return Status::InvalidArgument(
"schema validate failed: ",
is_sparse ? "sparse_vector" : "dense_vector",
"'s index_params of ", DataTypeCodeBook::AsString(data_type_),
" support ", QuantizeTypeCodeBook::AsString(iter->second),
" quantize, but field[", name_, "]'s quantize_type is ",
QuantizeTypeCodeBook::AsString(
vector_index_params->quantize_type()));
}
}
}
if (index_params_->type() == IndexType::IVF &&
vector_index_params->metric_type() == MetricType::IP) {
if (data_type_ != DataType::VECTOR_FP16 &&
data_type_ != DataType::VECTOR_FP32) {
return Status::InvalidArgument(
"schema validate failed: IVF index only support FP32/FP16 data "
"types according to the IP metric");
}
}
if (vector_index_params->metric_type() == MetricType::COSINE) {
if (data_type_ != DataType::VECTOR_FP16 &&
data_type_ != DataType::VECTOR_FP32) {
return Status::InvalidArgument(
"schema validate failed: cosine metric only supports FP32/FP16 "
"data types, but field[",
name_, "]'s data type is ",
DataTypeCodeBook::AsString(data_type_));
}
}
}
} else {
if (index_params_) {
if (index_params_->is_vector_index_type()) {
return Status::InvalidArgument(
"schema validate failed: scalar field[", name_,
"] does not support vector index params, but got index_type ",
IndexTypeCodeBook::AsString(index_params_->type()));
}
if (index_params_->type() == IndexType::FTS &&
data_type_ != DataType::STRING) {
return Status::InvalidArgument(
"schema validate failed: FTS index only supports STRING data type, "
"but field[",
name_, "]'s data_type is ", DataTypeCodeBook::AsString(data_type_));
}
if (index_params_->type() == IndexType::FTS) {
auto s = validate_fts_index_params(*this);
CHECK_RETURN_STATUS(s);
}
}
}
return Status::OK();
}
std::string FieldSchema::to_string() const {
std::ostringstream oss;
oss << "FieldSchema{"
<< "name:'" << name_ << "'"
<< ",data_type:" << DataTypeCodeBook::AsString(data_type_)
<< ",nullable:" << (nullable_ ? "true" : "false")
<< ",dimension:" << dimension_;
if (index_params_) {
oss << ",index_params:" << index_params_->to_string();
} else {
oss << ",index_params:null";
}
oss << "}";
return oss.str();
}
std::string FieldSchema::to_string_formatted(int indent_level) const {
std::ostringstream oss;
if (is_vector_field()) {
oss << indent(indent_level) << "FieldSchema[vector]{\n";
} else {
oss << indent(indent_level) << "FieldSchema[scalar]{\n";
}
oss << indent(indent_level + 1) << "name: '" << name_ << "',\n"
<< indent(indent_level + 1)
<< "data_type: " << DataTypeCodeBook::AsString(data_type_) << ",\n";
if (is_vector_field()) {
if (is_dense_vector()) {
oss << indent(indent_level + 1) << "dimension: " << dimension_ << ",\n";
}
} else {
oss << indent(indent_level + 1)
<< "nullable: " << (nullable_ ? "true" : "false") << ",\n";
}
if (index_params_) {
oss << indent(indent_level + 1)
<< "index_params: " << index_params_->to_string() << "\n";
} else {
oss << indent(indent_level + 1) << "index_params: null\n";
}
oss << indent(indent_level) << "}";
return oss.str();
}
Status CollectionSchema::validate() const {
if (name_.empty()) {
return Status::InvalidArgument("schema validate failed: name is empty");
}
if (!std::regex_match(name_, COLLECTION_NAME_REGEX)) {
return Status::InvalidArgument(
"schema validate failed: collection[", name_,
"]'s name cannot pass the regex verification");
}
if (forward_fields().size() > kMaxScalarFieldSize) {
return Status::InvalidArgument(
"schema validate failed: collection[", name_,
"]'s field size must <= ", kMaxScalarFieldSize);
}
if (max_doc_count_per_segment_ < MAX_DOC_COUNT_PER_SEGMENT_MIN_THRESHOLD) {
return Status::InvalidArgument(
"schema validate failed: max_doc_count_per_segment must >= ",
MAX_DOC_COUNT_PER_SEGMENT_MIN_THRESHOLD);
}
if (fields_.empty()) {
return Status::InvalidArgument("schema validate failed: collection[", name_,
"] has no fields");
}
auto v_fields = vector_fields();
if (v_fields.size() > kMaxVectorFieldSize) {
return Status::InvalidArgument(
"schema validate failed: collection[", name_,
"]'s vector field size must <= ", kMaxVectorFieldSize);
}
for (auto &field : fields_) {
auto s = field->validate();
CHECK_RETURN_STATUS(s);
}
return Status::OK();
}
std::string CollectionSchema::to_string() const {
std::ostringstream oss;
oss << "CollectionSchema{"
<< "name:'" << name_ << "'"
<< ",max_doc_count_per_segment:" << max_doc_count_per_segment_
<< ",fields:[";
for (size_t i = 0; i < fields_.size(); ++i) {
if (i > 0) oss << ",";
oss << fields_[i]->to_string();
}
oss << "]}";
return oss.str();
}
std::string CollectionSchema::to_string_formatted(int indent_level) const {
std::ostringstream oss;
oss << indent(indent_level) << "CollectionSchema{\n"
<< indent(indent_level + 1) << "name: '" << name_ << "',\n"
<< indent(indent_level + 1)
<< "max_doc_count_per_segment: " << max_doc_count_per_segment_ << ",\n"
<< indent(indent_level + 1) << "fields: [\n";
for (size_t i = 0; i < fields_.size(); ++i) {
oss << fields_[i]->to_string_formatted(indent_level + 2);
if (i < fields_.size() - 1) {
oss << ",";
}
oss << "\n";
}
oss << indent(indent_level + 1) << "]\n" << indent(indent_level) << "}";
return oss.str();
}
Status CollectionSchema::add_field(FieldSchema::Ptr column_schema) {
// Check if field already exists
if (has_field(column_schema->name())) {
return Status::AlreadyExists("field[", column_schema->name(),
"] already exists in schema");
}
// Add field to list and map
if (column_schema->is_vector_field()) {
if (column_schema->index_params() == nullptr) {
column_schema->set_index_params(DefaultVectorIndexParams);
}
}
fields_.push_back(column_schema);
fields_map_[column_schema->name()] = column_schema;
return Status::OK();
}
Status CollectionSchema::alter_field(
const std::string &column_name,
const FieldSchema::Ptr &new_column_options) {
// Check if field exists
if (!has_field(column_name)) {
return Status::NotFound("field[", column_name, "] not found in schema");
}
std::string new_column_name = new_column_options->name();
// If renaming to an existing field name (and it's not the same field)
if (new_column_name != column_name && has_field(new_column_name)) {
return Status::AlreadyExists("field[", new_column_name,
"] already exists in schema");
}
// Update map: remove old entry if name changed, add new entry
if (new_column_name != column_name) {
fields_map_.erase(column_name);
}
fields_map_[new_column_name] = new_column_options;
// Update list
for (auto &field : fields_) {
if (field->name() == column_name) {
field = new_column_options;
break;
}
}
return Status::OK();
}
Status CollectionSchema::drop_field(const std::string &column_name) {
// Check if field exists
if (!has_field(column_name)) {
return Status::NotFound("field[", column_name, "] not found in schema");
}
// Remove from map
fields_map_.erase(column_name);
// Remove from list
fields_.erase(std::remove_if(fields_.begin(), fields_.end(),
[&column_name](const FieldSchema::Ptr &field) {
return field->name() == column_name;
}),
fields_.end());
return Status::OK();
}
bool CollectionSchema::has_field(const std::string &column) const {
return fields_map_.find(column) != fields_map_.end();
}
const FieldSchema *CollectionSchema::get_field(
const std::string &column) const {
auto it = fields_map_.find(column);
if (it != fields_map_.end()) {
return it->second.get();
}
return nullptr;
}
FieldSchema *CollectionSchema::get_field(const std::string &column) {
auto it = fields_map_.find(column);
if (it != fields_map_.end()) {
return it->second.get();
}
return nullptr;
}
const FieldSchema *CollectionSchema::get_forward_field(
const std::string &column) const {
// Forward fields are typically non-vector fields
auto field = get_field(column);
if (field && !field->is_vector_field()) {
return field;
}
return nullptr;
}
FieldSchema *CollectionSchema::get_forward_field(const std::string &column) {
// Forward fields are typically non-vector fields
auto field = get_field(column);
if (field && !field->is_vector_field()) {
return field;
}
return nullptr;
}
const FieldSchema *CollectionSchema::get_vector_field(
const std::string &column) const {
// Vector fields are fields with vector data types
auto field = get_field(column);
if (field && field->is_vector_field()) {
return field;
}
return nullptr;
}
FieldSchema *CollectionSchema::get_vector_field(const std::string &column) {
// Vector fields are fields with vector data types
auto field = get_field(column);
if (field && field->is_vector_field()) {
return field;
}
return nullptr;
}
FieldSchemaPtrList CollectionSchema::fields() const {
return fields_;
}
FieldSchemaPtrList CollectionSchema::forward_fields() const {
FieldSchemaPtrList forward_fields;
for (const auto &field : fields_) {
if (!field->is_vector_field()) {
forward_fields.push_back(field);
}
}
return forward_fields;
}
FieldSchemaPtrList CollectionSchema::forward_fields_with_index() const {
FieldSchemaPtrList forward_fields;
for (const auto &field : fields_) {
if (!field->is_vector_field() && field->index_params() != nullptr) {
forward_fields.push_back(field);
}
}
return forward_fields;
}
std::vector<std::string> CollectionSchema::forward_field_names() const {
std::vector<std::string> names;
for (const auto &field : fields_) {
if (!field->is_vector_field()) {
names.push_back(field->name());
}
}
return names;
}
std::vector<std::string> CollectionSchema::forward_field_names_with_index()
const {
std::vector<std::string> names;
for (const auto &field : fields_) {
if (!field->is_vector_field() && field->index_params() != nullptr) {
names.push_back(field->name());
}
}
return names;
}
std::vector<std::string> CollectionSchema::all_field_names() const {
std::vector<std::string> names;
for (const auto &field : fields_) {
names.push_back(field->name());
}
return names;
}
FieldSchemaPtrList CollectionSchema::vector_fields() const {
FieldSchemaPtrList vector_fields;
for (const auto &field : fields_) {
if (field->is_vector_field()) {
vector_fields.push_back(field);
}
}
return vector_fields;
}
FieldSchemaPtrList CollectionSchema::invert_fields() const {
FieldSchemaPtrList invert;
for (const auto &field : fields_) {
if (field->index_type() == IndexType::INVERT) {
invert.push_back(field);
}
}
return invert;
}
bool CollectionSchema::has_fts_field() const {
for (const auto &field : fields_) {
if (field->index_type() == IndexType::FTS) {
return true;
}
}
return false;
}
FieldSchemaPtrList CollectionSchema::fts_fields() const {
FieldSchemaPtrList fts;
for (const auto &field : fields_) {
if (field->index_type() == IndexType::FTS) {
fts.push_back(field);
}
}
return fts;
}
uint64_t CollectionSchema::max_doc_count_per_segment() const {
return max_doc_count_per_segment_;
}
void CollectionSchema::set_max_doc_count_per_segment(
uint64_t max_doc_count_per_segment) {
max_doc_count_per_segment_ = max_doc_count_per_segment;
}
Status CollectionSchema::add_index(const std::string &column,
const IndexParams::Ptr &index_params) {
// Get field and set index params
auto field = get_field(column);
if (field) {
field->set_index_params(index_params);
} else {
return Status::NotFound("field[", column, "] not found in schema");
}
return Status::OK();
}
Status CollectionSchema::drop_index(const std::string &column) {
// Get field and clear index params
auto field = get_field(column);
if (field) {
if (field->is_vector_field()) {
field->set_index_params(DefaultVectorIndexParams);
} else {
field->set_index_params(nullptr);
}
} else {
return Status::NotFound("field[", column, "] not found in schema");
}
return Status::OK();
}
bool CollectionSchema::has_index(const std::string &column) const {
auto field = get_field(column);
if (field) {
if (field->is_vector_field()) {
if (field->index_params() == nullptr) {
return false;
} else {
return *field->index_params() != DefaultVectorIndexParams;
}
}
return field->index_params() != nullptr;
}
return false;
}
} // namespace zvec