2149 lines
66 KiB
C++
2149 lines
66 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 <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <shared_mutex>
|
|
#include <string>
|
|
#include <variant>
|
|
#include <vector>
|
|
#include <ailego/io/file_lock.h>
|
|
#include <zvec/ailego/io/file.h>
|
|
#include <zvec/ailego/logger/logger.h>
|
|
#include <zvec/ailego/pattern/expected.hpp>
|
|
#include <zvec/ailego/utility/file_helper.h>
|
|
#include <zvec/db/collection.h>
|
|
#include <zvec/db/doc.h>
|
|
#include <zvec/db/options.h>
|
|
#include <zvec/db/reranker.h>
|
|
#include <zvec/db/schema.h>
|
|
#include <zvec/db/status.h>
|
|
#include "db/common/constants.h"
|
|
#include "db/common/file_helper.h"
|
|
#include "db/common/global_resource.h"
|
|
#include "db/common/profiler.h"
|
|
#include "db/common/typedef.h"
|
|
#include "db/index/common/delete_store.h"
|
|
#include "db/index/common/id_map.h"
|
|
#include "db/index/common/index_filter.h"
|
|
#include "db/index/common/type_helper.h"
|
|
#include "db/index/common/version_manager.h"
|
|
#include "db/index/segment/segment.h"
|
|
#include "db/index/segment/segment_helper.h"
|
|
#include "db/index/segment/segment_manager.h"
|
|
#include "db/sqlengine/sqlengine.h"
|
|
#include "zvec/core/interface/index.h"
|
|
|
|
namespace zvec {
|
|
|
|
enum class WriteMode : uint8_t {
|
|
UNDEFINED = 0,
|
|
INSERT,
|
|
UPDATE,
|
|
UPSERT,
|
|
};
|
|
|
|
|
|
Collection::~Collection() = default;
|
|
|
|
class CollectionImpl : public Collection {
|
|
friend class Collection;
|
|
|
|
public:
|
|
explicit CollectionImpl(const std::string &path,
|
|
const CollectionSchema &schema);
|
|
|
|
explicit CollectionImpl(const std::string &path);
|
|
|
|
~CollectionImpl() override;
|
|
|
|
private:
|
|
Status Open(const CollectionOptions &options);
|
|
|
|
Status Close();
|
|
|
|
public:
|
|
Status Destroy() override;
|
|
|
|
Status Flush() override;
|
|
|
|
Result<std::string> Path() const override;
|
|
|
|
Result<CollectionStats> Stats() const override;
|
|
|
|
Result<CollectionSchema> Schema() const override;
|
|
|
|
Result<CollectionOptions> Options() const override;
|
|
|
|
public:
|
|
Status CreateIndex(const std::string &column_name,
|
|
const IndexParams::Ptr &index_params,
|
|
const CreateIndexOptions &options) override;
|
|
|
|
Status DropIndex(const std::string &column_name) override;
|
|
|
|
Status Optimize(const OptimizeOptions &options) override;
|
|
|
|
Status AddColumn(const FieldSchema::Ptr &column_schema,
|
|
const std::string &expression,
|
|
const AddColumnOptions &options) override;
|
|
|
|
Status DropColumn(const std::string &column_name) override;
|
|
|
|
Status AlterColumn(
|
|
const std::string &column_name, const std::string &rename,
|
|
const FieldSchema::Ptr &new_column_schema = nullptr,
|
|
const AlterColumnOptions &options = AlterColumnOptions()) override;
|
|
|
|
Result<WriteResults> Insert(std::vector<Doc> &docs) override;
|
|
|
|
Result<WriteResults> Upsert(std::vector<Doc> &docs) override;
|
|
|
|
Result<WriteResults> Update(std::vector<Doc> &docs) override;
|
|
|
|
Result<WriteResults> Delete(const std::vector<std::string> &pks) override;
|
|
|
|
Status DeleteByFilter(const std::string &filter) override;
|
|
|
|
Result<DocPtrList> Query(const SearchQuery &query) const override;
|
|
|
|
Result<DocPtrList> Query(const MultiQuery &query) const override;
|
|
|
|
Result<GroupResults> GroupByQuery(
|
|
const GroupByVectorQuery &query) const override;
|
|
|
|
Result<DocPtrMap> Fetch(const std::vector<std::string> &pks,
|
|
const std::optional<std::vector<std::string>>
|
|
&output_fields = std::nullopt,
|
|
bool include_vector = true) const override;
|
|
|
|
Result<std::string> DebugGetHnswStorageMode(
|
|
const std::string &column_name) const override;
|
|
|
|
private:
|
|
void prepare_schema();
|
|
|
|
Status close_unsafe();
|
|
|
|
Status flush_unsafe();
|
|
|
|
Status create();
|
|
|
|
Status recovery();
|
|
|
|
Status create_idmap_and_delete_store();
|
|
|
|
Status recover_idmap_and_delete_store();
|
|
|
|
Status acquire_file_lock(bool create = false);
|
|
|
|
Status init_version_manager();
|
|
|
|
Status init_writing_segment();
|
|
|
|
bool need_switch_to_new_segment() const;
|
|
|
|
Status switch_to_new_segment_for_writing(
|
|
const CollectionSchema::Ptr &schema = nullptr);
|
|
|
|
Result<WriteResults> write_impl(std::vector<Doc> &docs, WriteMode mode);
|
|
|
|
std::vector<Segment::Ptr> get_all_segments() const;
|
|
|
|
std::vector<Segment::Ptr> get_all_persist_segments() const;
|
|
|
|
Segment::Ptr local_segment_by_doc_id(
|
|
uint64_t doc_id, const std::vector<Segment::Ptr> &segments) const;
|
|
|
|
SegmentID allocate_segment_id() {
|
|
return segment_id_allocator_.fetch_add(1);
|
|
}
|
|
|
|
SegmentID allocate_segment_id_for_tmp_segment() {
|
|
return tmp_segment_id_allocator_.fetch_add(1);
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> build_compact_task(
|
|
const CollectionSchema::Ptr &schema,
|
|
const std::vector<Segment::Ptr> &segments, int concurrency,
|
|
const IndexFilter::Ptr filter);
|
|
|
|
Status execute_compact_task(std::vector<SegmentTask::Ptr> &tasks) const;
|
|
|
|
std::vector<SegmentTask::Ptr> build_create_vector_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params, int concurrency);
|
|
|
|
std::vector<SegmentTask::Ptr> build_create_scalar_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params, int concurrency);
|
|
|
|
std::vector<SegmentTask::Ptr> build_drop_vector_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column);
|
|
|
|
std::vector<SegmentTask::Ptr> build_drop_scalar_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column);
|
|
|
|
std::vector<SegmentTask::Ptr> build_create_fts_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params);
|
|
|
|
std::vector<SegmentTask::Ptr> build_drop_fts_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column);
|
|
|
|
Status execute_tasks(std::vector<SegmentTask::Ptr> &tasks) const;
|
|
|
|
private:
|
|
Status handle_upsert(Doc &doc);
|
|
|
|
Status handle_update(Doc &doc);
|
|
|
|
Status handle_insert(Doc &doc);
|
|
|
|
Status internal_fetch_by_doc(const Doc &doc, Doc::Ptr *doc_out);
|
|
|
|
private:
|
|
// Helper functions for add/alter/drop column
|
|
Status validate(const std::string &column, const FieldSchema::Ptr &schema,
|
|
const std::string &expression, const std::string &rename,
|
|
ColumnOp op);
|
|
|
|
private:
|
|
std::string path_;
|
|
|
|
bool destroyed_{false};
|
|
|
|
CollectionSchema::Ptr schema_;
|
|
|
|
CollectionOptions options_;
|
|
|
|
mutable std::shared_mutex schema_handle_mtx_;
|
|
mutable std::shared_mutex write_mtx_;
|
|
|
|
std::atomic<SegmentID> segment_id_allocator_;
|
|
std::atomic<SegmentID> tmp_segment_id_allocator_;
|
|
|
|
// writing segment
|
|
Segment::Ptr writing_segment_;
|
|
// non-writing segments, sort by doc_id range
|
|
SegmentManager::Ptr segment_manager_;
|
|
|
|
// latest version: std::vector<SegmentMeta>
|
|
VersionManager::Ptr version_manager_;
|
|
|
|
// file lock
|
|
ailego::File lock_file_;
|
|
|
|
IDMap::Ptr id_map_;
|
|
DeleteStore::Ptr delete_store_;
|
|
|
|
sqlengine::SQLEngine::Ptr sql_engine_;
|
|
};
|
|
|
|
Result<Collection::Ptr> Collection::CreateAndOpen(
|
|
const std::string &path, const CollectionSchema &schema,
|
|
const CollectionOptions &options) {
|
|
auto collection = std::make_shared<CollectionImpl>(path, schema);
|
|
|
|
auto s = collection->Open(options);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
|
|
return collection;
|
|
}
|
|
|
|
Result<Collection::Ptr> Collection::Open(const std::string &path,
|
|
const CollectionOptions &options) {
|
|
auto collection = std::make_shared<CollectionImpl>(path);
|
|
|
|
auto s = collection->Open(options);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
|
|
return collection;
|
|
}
|
|
|
|
CollectionImpl::CollectionImpl(const std::string &path,
|
|
const CollectionSchema &schema)
|
|
: path_(path), schema_(std::make_shared<CollectionSchema>(schema)) {
|
|
prepare_schema();
|
|
}
|
|
|
|
void CollectionImpl::prepare_schema() {
|
|
// set default index params for vector fields
|
|
for (auto &field : schema_->fields()) {
|
|
if (field->is_vector_field()) {
|
|
if (field->index_params() == nullptr) {
|
|
field->set_index_params(DefaultVectorIndexParams);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CollectionImpl::CollectionImpl(const std::string &path) : path_(path) {}
|
|
|
|
CollectionImpl::~CollectionImpl() {
|
|
if (!destroyed_) {
|
|
Close();
|
|
}
|
|
}
|
|
|
|
Status CollectionImpl::Open(const CollectionOptions &options) {
|
|
options_ = options;
|
|
|
|
if (schema_ != nullptr && options_.read_only_) {
|
|
return Status::InvalidArgument(
|
|
"Unable to create collection with read-only mode.");
|
|
}
|
|
|
|
Status s;
|
|
if (schema_ == nullptr) {
|
|
// recovery from disk
|
|
s = recovery();
|
|
} else {
|
|
// create new collection with existing schema
|
|
s = create();
|
|
}
|
|
|
|
auto profiler = std::make_shared<Profiler>();
|
|
sql_engine_ = sqlengine::SQLEngine::create(profiler);
|
|
|
|
return s;
|
|
}
|
|
|
|
Status CollectionImpl::Close() {
|
|
// only called in deconstructor
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
return close_unsafe();
|
|
}
|
|
|
|
Status CollectionImpl::close_unsafe() {
|
|
Status result = Status::OK();
|
|
|
|
// flush
|
|
if (!options_.read_only_) {
|
|
auto s = flush_unsafe();
|
|
if (!s.ok()) {
|
|
result = s;
|
|
}
|
|
}
|
|
|
|
// always release resources regardless of flush outcome
|
|
writing_segment_.reset();
|
|
segment_manager_.reset();
|
|
version_manager_.reset();
|
|
id_map_.reset();
|
|
delete_store_.reset();
|
|
|
|
lock_file_.close();
|
|
|
|
return result;
|
|
}
|
|
|
|
Status CollectionImpl::Destroy() {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
auto s = close_unsafe();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
ailego::FileHelper::RemoveDirectory(path_.c_str());
|
|
|
|
destroyed_ = true;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::Flush() {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
return flush_unsafe();
|
|
}
|
|
|
|
Status CollectionImpl::flush_unsafe() {
|
|
if (!writing_segment_) {
|
|
return Status::InternalError(
|
|
"flush writing segment failed because writing segment is nullptr");
|
|
}
|
|
return writing_segment_->flush();
|
|
}
|
|
|
|
Result<std::string> CollectionImpl::Path() const {
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
return path_;
|
|
}
|
|
|
|
Result<CollectionStats> CollectionImpl::Stats() const {
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
auto segments = get_all_segments();
|
|
|
|
CollectionStats stats;
|
|
auto vector_fields = schema_->vector_fields();
|
|
if (segments.empty()) {
|
|
stats.doc_count = 0;
|
|
for (auto &field : vector_fields) {
|
|
stats.index_completeness[field->name()] =
|
|
1; // if no doc, completeness is 1
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
for (auto &segment : segments) {
|
|
stats.doc_count += segment->doc_count(delete_store_->make_filter());
|
|
}
|
|
|
|
for (auto &field : vector_fields) {
|
|
if (stats.doc_count == 0) {
|
|
stats.index_completeness[field->name()] = 1;
|
|
continue;
|
|
}
|
|
|
|
uint32_t indexed_doc_count{0};
|
|
for (auto &segment : segments) {
|
|
if (segment->meta()->vector_indexed(field->name())) {
|
|
indexed_doc_count += segment->doc_count(delete_store_->make_filter());
|
|
}
|
|
}
|
|
stats.index_completeness[field->name()] =
|
|
indexed_doc_count * 1.0 / stats.doc_count;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
Result<CollectionSchema> CollectionImpl::Schema() const {
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
return *schema_;
|
|
}
|
|
|
|
Result<CollectionOptions> CollectionImpl::Options() const {
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
return options_;
|
|
}
|
|
|
|
Status CollectionImpl::CreateIndex(const std::string &column_name,
|
|
const IndexParams::Ptr &index_params,
|
|
const CreateIndexOptions &options) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
if (index_params == nullptr) {
|
|
return Status::InvalidArgument("CreateIndex: index_params is null");
|
|
}
|
|
|
|
auto new_schema = std::make_shared<CollectionSchema>(*schema_);
|
|
auto s = new_schema->add_index(column_name, index_params);
|
|
CHECK_RETURN_STATUS(s);
|
|
s = new_schema->validate();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
auto field = schema_->get_field(column_name);
|
|
if (field->index_params() != nullptr &&
|
|
*field->index_params() == *index_params) {
|
|
// equal index params
|
|
return Status::OK();
|
|
}
|
|
|
|
// Reject creating a non-vector index when the column already has a different
|
|
// non-vector index type (e.g. adding FTS when INVERT exists, or vice versa).
|
|
if (!field->is_vector_field() && field->index_params() != nullptr &&
|
|
field->index_params()->type() != index_params->type()) {
|
|
return Status::NotSupported(
|
|
"CreateIndex: column[", column_name, "] already has index type [",
|
|
IndexTypeCodeBook::AsString(field->index_params()->type()),
|
|
"], cannot create index type [",
|
|
IndexTypeCodeBook::AsString(index_params->type()),
|
|
"] on the same column");
|
|
}
|
|
|
|
// forbidden writing until index is ready
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
if (writing_segment_->doc_count() > 0) {
|
|
s = writing_segment_->dump();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
s = segment_manager_->add_segment(writing_segment_);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
auto seg_options =
|
|
SegmentOptions{false, options_.enable_mmap_, options_.max_buffer_size_};
|
|
auto new_segment = Segment::CreateAndOpen(
|
|
path_, *new_schema, allocate_segment_id(),
|
|
writing_segment_->meta()->max_doc_id() + 1, id_map_, delete_store_,
|
|
version_manager_, seg_options);
|
|
if (!new_segment) {
|
|
return new_segment.error();
|
|
}
|
|
|
|
s = new_version.add_persisted_segment_meta(writing_segment_->meta());
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
writing_segment_ = new_segment.value();
|
|
new_version.set_next_segment_id(segment_id_allocator_.load());
|
|
|
|
} else {
|
|
// TODO: allocate new segment id and clear current writing segment at last
|
|
// recreate writing segment
|
|
s = writing_segment_->destroy();
|
|
CHECK_RETURN_STATUS(s);
|
|
auto id = writing_segment_->id();
|
|
auto min_doc_id = writing_segment_->meta()->min_doc_id();
|
|
writing_segment_.reset();
|
|
SegmentOptions seg_options;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
seg_options.read_only_ = options_.read_only_;
|
|
auto writing_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, id, min_doc_id, id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
writing_segment_ = writing_segment.value();
|
|
}
|
|
new_version.reset_writing_segment_meta(writing_segment_->meta());
|
|
|
|
// get_all_segment will return writing segment if it has docs
|
|
auto persist_segments = get_all_persist_segments();
|
|
|
|
bool is_vector_field = field->is_vector_field();
|
|
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
if (is_vector_field) {
|
|
tasks = build_create_vector_index_task(persist_segments, column_name,
|
|
index_params, options.concurrency_);
|
|
} else if (index_params->type() == IndexType::INVERT) {
|
|
tasks = build_create_scalar_index_task(persist_segments, column_name,
|
|
index_params, options.concurrency_);
|
|
} else if (index_params->type() == IndexType::FTS) {
|
|
tasks = build_create_fts_index_task(persist_segments, column_name,
|
|
index_params);
|
|
} else {
|
|
return Status::NotSupported(
|
|
"CreateIndex: index type [",
|
|
IndexTypeCodeBook::AsString(index_params->type()),
|
|
"] is not supported");
|
|
}
|
|
|
|
if (tasks.empty()) {
|
|
new_version.set_schema(*new_schema);
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
schema_ = new_schema;
|
|
return Status::OK();
|
|
}
|
|
|
|
s = execute_tasks(tasks);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
new_version.set_schema(*new_schema);
|
|
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<CreateVectorIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateVectorIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
create_index_task.output_segment_meta_);
|
|
} else if (std::holds_alternative<CreateScalarIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateScalarIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
create_index_task.output_segment_meta_);
|
|
} else if (std::holds_alternative<CreateFtsIndexTask>(task_info)) {
|
|
auto fts_task = std::get<CreateFtsIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
fts_task.output_segment_meta_);
|
|
}
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
// 2. update version
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// 3. persist version
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// 4. remove old segments or block
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<CreateVectorIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateVectorIndexTask>(task_info);
|
|
s = create_index_task.input_segment_->reload_vector_index(
|
|
*new_schema, create_index_task.output_segment_meta_,
|
|
create_index_task.output_vector_indexers_,
|
|
create_index_task.output_quant_vector_indexers_);
|
|
} else if (std::holds_alternative<CreateScalarIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateScalarIndexTask>(task_info);
|
|
s = create_index_task.input_segment_->reload_scalar_index(
|
|
*new_schema, create_index_task.output_segment_meta_,
|
|
create_index_task.output_scalar_indexer_);
|
|
} else if (std::holds_alternative<CreateFtsIndexTask>(task_info)) {
|
|
auto fts_task = std::get<CreateFtsIndexTask>(task_info);
|
|
s = fts_task.input_segment_->reload_fts_index(
|
|
*new_schema, fts_task.output_segment_meta_,
|
|
fts_task.output_fts_indexer_);
|
|
}
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
schema_ = new_schema;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_create_vector_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params, int concurrency) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
if (!segment->vector_index_ready(column, index_params)) {
|
|
tasks.push_back(SegmentTask::CreateCreateVectorIndexTask(
|
|
CreateVectorIndexTask{segment, column, index_params, concurrency}));
|
|
}
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_create_scalar_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params, int concurrency) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
tasks.push_back(SegmentTask::CreateCreateScalarIndexTask(
|
|
CreateScalarIndexTask{segment, {column}, index_params, concurrency}));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_create_fts_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column,
|
|
const IndexParams::Ptr &index_params) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
tasks.push_back(SegmentTask::CreateCreateFtsIndexTask(
|
|
CreateFtsIndexTask{segment, column, index_params}));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_drop_fts_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
tasks.push_back(
|
|
SegmentTask::CreateDropFtsIndexTask(DropFtsIndexTask{segment, column}));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
Status CollectionImpl::execute_tasks(
|
|
std::vector<SegmentTask::Ptr> &tasks) const {
|
|
Status s;
|
|
for (auto &task : tasks) {
|
|
s = SegmentHelper::Execute(task);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::DropIndex(const std::string &column_name) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
auto new_schema = std::make_shared<CollectionSchema>(*schema_);
|
|
auto s = new_schema->drop_index(column_name);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
auto field = schema_->get_field(column_name);
|
|
if (field->index_params() == nullptr) {
|
|
return Status::OK(); // return ok if not indexed
|
|
}
|
|
|
|
if (field->is_vector_field() &&
|
|
*field->index_params() == DefaultVectorIndexParams) {
|
|
return Status::OK();
|
|
}
|
|
|
|
// forbidden writing until index is ready
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
if (writing_segment_->doc_count() > 0) {
|
|
s = writing_segment_->dump();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
s = segment_manager_->add_segment(writing_segment_);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
auto new_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, allocate_segment_id(),
|
|
writing_segment_->meta()->max_doc_id() + 1,
|
|
id_map_, delete_store_, version_manager_,
|
|
SegmentOptions{false, options_.enable_mmap_,
|
|
options_.max_buffer_size_});
|
|
if (!new_segment) {
|
|
return new_segment.error();
|
|
}
|
|
|
|
s = new_version.add_persisted_segment_meta(writing_segment_->meta());
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
writing_segment_ = new_segment.value();
|
|
new_version.set_next_segment_id(segment_id_allocator_.load());
|
|
|
|
} else {
|
|
// recreate writing segment
|
|
s = writing_segment_->destroy();
|
|
CHECK_RETURN_STATUS(s);
|
|
auto id = writing_segment_->id();
|
|
auto min_doc_id = writing_segment_->meta()->min_doc_id();
|
|
writing_segment_.reset();
|
|
SegmentOptions seg_options;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
seg_options.read_only_ = options_.read_only_;
|
|
auto writing_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, id, min_doc_id, id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
|
|
writing_segment_ = writing_segment.value();
|
|
}
|
|
new_version.reset_writing_segment_meta(writing_segment_->meta());
|
|
|
|
auto persist_segments = get_all_persist_segments();
|
|
|
|
bool is_vector_field = field->is_vector_field();
|
|
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
if (is_vector_field) {
|
|
tasks = build_drop_vector_index_task(persist_segments, column_name);
|
|
} else if (field->index_params()->type() == IndexType::INVERT) {
|
|
tasks = build_drop_scalar_index_task(persist_segments, column_name);
|
|
} else if (field->index_params()->type() == IndexType::FTS) {
|
|
tasks = build_drop_fts_index_task(persist_segments, column_name);
|
|
} else {
|
|
return Status::NotSupported(
|
|
"DropIndex: index type [",
|
|
IndexTypeCodeBook::AsString(field->index_params()->type()),
|
|
"] on column[", column_name, "] is not supported");
|
|
}
|
|
|
|
if (tasks.empty()) {
|
|
new_version.set_schema(*new_schema);
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
schema_ = new_schema;
|
|
return Status::OK();
|
|
}
|
|
|
|
s = execute_tasks(tasks);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
new_version.set_schema(*new_schema);
|
|
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<DropVectorIndexTask>(task_info)) {
|
|
auto drop_index_task = std::get<DropVectorIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
drop_index_task.output_segment_meta_);
|
|
} else if (std::holds_alternative<DropScalarIndexTask>(task_info)) {
|
|
auto drop_index_task = std::get<DropScalarIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
drop_index_task.output_segment_meta_);
|
|
} else if (std::holds_alternative<DropFtsIndexTask>(task_info)) {
|
|
auto fts_task = std::get<DropFtsIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
fts_task.output_segment_meta_);
|
|
}
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// 4. remove old segments or block
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<DropVectorIndexTask>(task_info)) {
|
|
auto drop_index_task = std::get<DropVectorIndexTask>(task_info);
|
|
s = drop_index_task.input_segment_->reload_vector_index(
|
|
*new_schema, drop_index_task.output_segment_meta_,
|
|
drop_index_task.output_vector_indexers_);
|
|
} else if (std::holds_alternative<DropScalarIndexTask>(task_info)) {
|
|
auto drop_index_task = std::get<DropScalarIndexTask>(task_info);
|
|
s = drop_index_task.input_segment_->reload_scalar_index(
|
|
*new_schema, drop_index_task.output_segment_meta_,
|
|
drop_index_task.output_scalar_indexer_);
|
|
} else if (std::holds_alternative<DropFtsIndexTask>(task_info)) {
|
|
auto fts_task = std::get<DropFtsIndexTask>(task_info);
|
|
s = fts_task.input_segment_->reload_fts_index(
|
|
*new_schema, fts_task.output_segment_meta_,
|
|
fts_task.output_fts_indexer_);
|
|
}
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
schema_ = new_schema;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_drop_vector_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
tasks.emplace_back(SegmentTask::CreateDropVectorIndexTask(
|
|
DropVectorIndexTask{segment, column}));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_drop_scalar_index_task(
|
|
const std::vector<Segment::Ptr> &segments, const std::string &column) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
for (auto &segment : segments) {
|
|
tasks.emplace_back(SegmentTask::CreateDropScalarIndexTask(
|
|
DropScalarIndexTask(segment, {column})));
|
|
}
|
|
return tasks;
|
|
}
|
|
|
|
Status CollectionImpl::Optimize(const OptimizeOptions &options) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
// when optimizing, schema operations(include another optimize) are not
|
|
// allowed
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
std::vector<Segment::Ptr> persist_segments;
|
|
|
|
{
|
|
// forbidden writing for a while
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
if (writing_segment_->doc_count() != 0) {
|
|
// flush and create new segment
|
|
auto s = switch_to_new_segment_for_writing();
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
persist_segments =
|
|
get_all_persist_segments(); // will not return writing segment
|
|
// after leave this scope, writing action is allowed
|
|
}
|
|
|
|
if (persist_segments.size() == 0) {
|
|
// no need to optimize
|
|
return Status::OK();
|
|
}
|
|
|
|
// build segment compact task
|
|
auto delete_store_clone = delete_store_->clone();
|
|
auto tasks =
|
|
build_compact_task(schema_, persist_segments, options.concurrency_,
|
|
delete_store_clone->make_filter());
|
|
|
|
// execute segment compact task
|
|
auto s = execute_compact_task(tasks);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
{
|
|
// forbidden writing for updating version
|
|
// writing action may trigger updating version where confict occurs
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<CompactTask>(task_info)) {
|
|
auto compact_task = std::get<CompactTask>(task_info);
|
|
|
|
// 0. check if has output segment meta
|
|
if (compact_task.output_segment_meta_) {
|
|
// 1. rename built tmp segments
|
|
auto tmp_segment_id = compact_task.output_segment_id_;
|
|
auto tmp_segment_path =
|
|
FileHelper::MakeTempSegmentPath(path_, tmp_segment_id);
|
|
|
|
auto new_segment_id = allocate_segment_id();
|
|
auto new_segment_path =
|
|
FileHelper::MakeSegmentPath(path_, new_segment_id);
|
|
|
|
if (!FileHelper::MoveDirectory(tmp_segment_path, new_segment_path)) {
|
|
return Status::InternalError("move segment directory failed");
|
|
}
|
|
|
|
// update output_segment_meta_'s segment id
|
|
compact_task.output_segment_meta_->set_id(new_segment_id);
|
|
|
|
s = new_version.add_persisted_segment_meta(
|
|
compact_task.output_segment_meta_);
|
|
CHECK_RETURN_STATUS(s);
|
|
new_version.set_next_segment_id(segment_id_allocator_.load());
|
|
}
|
|
|
|
for (auto input_segment : compact_task.input_segments_) {
|
|
s = new_version.remove_persisted_segment_meta(input_segment->id());
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
} else if (std::holds_alternative<CreateVectorIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateVectorIndexTask>(task_info);
|
|
s = new_version.update_persisted_segment_meta(
|
|
create_index_task.output_segment_meta_);
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
}
|
|
|
|
// 2. update version
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// 3. persist version
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// 4. remove old segments or block
|
|
for (auto &task : tasks) {
|
|
auto task_info = task->task_info();
|
|
|
|
if (std::holds_alternative<CompactTask>(task_info)) {
|
|
auto compact_task = std::get<CompactTask>(task_info);
|
|
|
|
if (compact_task.output_segment_meta_) {
|
|
auto new_segment =
|
|
Segment::Open(path_, *schema_, *compact_task.output_segment_meta_,
|
|
id_map_, delete_store_, version_manager_,
|
|
SegmentOptions{true, options_.enable_mmap_});
|
|
if (!new_segment.has_value()) {
|
|
return new_segment.error();
|
|
}
|
|
s = segment_manager_->add_segment(new_segment.value());
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
for (auto input_segment : compact_task.input_segments_) {
|
|
s = segment_manager_->destroy_segment(input_segment->id());
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
} else if (std::holds_alternative<CreateVectorIndexTask>(task_info)) {
|
|
auto create_index_task = std::get<CreateVectorIndexTask>(task_info);
|
|
|
|
s = create_index_task.input_segment_->reload_vector_index(
|
|
*schema_, create_index_task.output_segment_meta_,
|
|
create_index_task.output_vector_indexers_,
|
|
create_index_task.output_quant_vector_indexers_);
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
std::vector<SegmentTask::Ptr> CollectionImpl::build_compact_task(
|
|
const CollectionSchema::Ptr &schema,
|
|
const std::vector<Segment::Ptr> &segments, int concurrency,
|
|
const IndexFilter::Ptr filter) {
|
|
std::vector<SegmentTask::Ptr> tasks;
|
|
if (segments.empty()) return tasks;
|
|
|
|
bool rebuild = false;
|
|
size_t current_doc_count = 0;
|
|
size_t current_actual_doc_count = 0;
|
|
for (auto &segment : segments) {
|
|
current_doc_count += segment->doc_count();
|
|
current_actual_doc_count += segment->doc_count(filter);
|
|
}
|
|
if (current_actual_doc_count <
|
|
current_doc_count * (1 - COMPACT_DELETE_RATIO_THRESHOLD)) {
|
|
// if delete ratio is large enough, rebuild
|
|
rebuild = true;
|
|
}
|
|
|
|
auto max_doc_count_per_segment = schema->max_doc_count_per_segment();
|
|
|
|
std::vector<Segment::Ptr> current_group;
|
|
current_doc_count = 0;
|
|
current_actual_doc_count = 0;
|
|
|
|
for (const auto &seg : segments) {
|
|
uint64_t doc_count = seg->doc_count();
|
|
uint64_t actual_doc_count = seg->doc_count(filter);
|
|
|
|
if (!current_group.empty()) {
|
|
SegmentTask::Ptr task;
|
|
bool skip_task{false};
|
|
if (rebuild) {
|
|
if (current_actual_doc_count + actual_doc_count >
|
|
max_doc_count_per_segment) {
|
|
// only create SegmentCompactTask when rebuild=true
|
|
task = SegmentTask::CreateCompactTask(
|
|
CompactTask{path_, schema, current_group,
|
|
allocate_segment_id_for_tmp_segment(), filter,
|
|
!options_.enable_mmap_, concurrency});
|
|
}
|
|
} else {
|
|
if (current_doc_count + doc_count > max_doc_count_per_segment) {
|
|
// check current_group size
|
|
if (current_group.size() == 1) {
|
|
task =
|
|
SegmentTask::CreateCreateVectorIndexTask(CreateVectorIndexTask{
|
|
current_group[0], "", nullptr, concurrency});
|
|
skip_task = current_group[0]->all_vector_index_ready();
|
|
} else {
|
|
task = SegmentTask::CreateCompactTask(
|
|
CompactTask{path_, schema, current_group,
|
|
allocate_segment_id_for_tmp_segment(), nullptr,
|
|
!options_.enable_mmap_, concurrency});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (task) {
|
|
current_group.clear();
|
|
current_doc_count = 0;
|
|
current_actual_doc_count = 0;
|
|
if (!skip_task) {
|
|
tasks.push_back(task);
|
|
}
|
|
}
|
|
}
|
|
|
|
current_group.push_back(seg);
|
|
current_doc_count += doc_count;
|
|
current_actual_doc_count += actual_doc_count;
|
|
}
|
|
|
|
if (current_group.size() > 0) {
|
|
SegmentTask::Ptr task;
|
|
if (current_group.size() == 1 && !rebuild) {
|
|
task = SegmentTask::CreateCreateVectorIndexTask(
|
|
CreateVectorIndexTask{current_group[0], "", nullptr, concurrency});
|
|
} else {
|
|
task = SegmentTask::CreateCompactTask(CompactTask{
|
|
path_, schema, current_group, allocate_segment_id_for_tmp_segment(),
|
|
rebuild ? filter : nullptr, !options_.enable_mmap_, concurrency});
|
|
}
|
|
tasks.push_back(task);
|
|
}
|
|
|
|
return tasks;
|
|
}
|
|
|
|
Status CollectionImpl::execute_compact_task(
|
|
std::vector<SegmentTask::Ptr> &tasks) const {
|
|
Status s;
|
|
for (auto &task : tasks) {
|
|
s = SegmentHelper::Execute(task);
|
|
if (!s.ok()) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::validate(const std::string &column,
|
|
const FieldSchema::Ptr &schema,
|
|
const std::string &expression,
|
|
const std::string &rename, ColumnOp op) {
|
|
auto check_data_type = [&](const FieldSchema *field) -> Status {
|
|
if (field->data_type() < DataType::INT32 ||
|
|
field->data_type() > DataType::DOUBLE) {
|
|
return Status::InvalidArgument(
|
|
"Only support basic numeric data type [int32, int64, uint32, uint64, "
|
|
"float, double]: ",
|
|
field->to_string());
|
|
}
|
|
return Status::OK();
|
|
};
|
|
|
|
switch (op) {
|
|
case ColumnOp::ADD: {
|
|
if (schema == nullptr) {
|
|
return Status::InvalidArgument("Column schema is null");
|
|
}
|
|
|
|
if (schema->name().empty()) {
|
|
return Status::InvalidArgument("Column name is empty");
|
|
}
|
|
if (schema_->has_field(schema->name())) {
|
|
return Status::InvalidArgument("column already exists: ",
|
|
schema->name());
|
|
}
|
|
|
|
auto s = schema->validate();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
s = check_data_type(schema.get());
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (expression.empty() && !schema->nullable()) {
|
|
return Status::InvalidArgument(
|
|
"Add column is not supported for non-nullable column: ",
|
|
schema->name());
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ColumnOp::ALTER: {
|
|
if (column.empty()) {
|
|
return Status::InvalidArgument("column name is empty");
|
|
}
|
|
|
|
if (!schema_->has_field(column)) {
|
|
return Status::InvalidArgument("column ", column, " not found");
|
|
}
|
|
|
|
if (!rename.empty() && schema) {
|
|
return Status::InvalidArgument(
|
|
"cannot specify both rename and new column schema");
|
|
}
|
|
|
|
auto *old_field_schema = schema_->get_field(column);
|
|
auto s = check_data_type(old_field_schema);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (!rename.empty()) {
|
|
// rename case
|
|
if (schema_->has_field(rename)) {
|
|
return Status::InvalidArgument("new column name ", rename,
|
|
" already exists");
|
|
}
|
|
} else {
|
|
// schema change case
|
|
if (!schema) {
|
|
return Status::InvalidArgument("New column schema is null");
|
|
}
|
|
|
|
s = schema->validate();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (schema->name().empty()) {
|
|
return Status::InvalidArgument("new column schema name is empty");
|
|
}
|
|
|
|
if (!schema->nullable() && old_field_schema->nullable()) {
|
|
return Status::InvalidArgument(
|
|
"new column schema is not nullable, but old column schema is "
|
|
"nullable");
|
|
}
|
|
|
|
if (*old_field_schema == *schema) {
|
|
// equal schema
|
|
return Status::OK();
|
|
}
|
|
|
|
s = check_data_type(schema.get());
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ColumnOp::DROP: {
|
|
if (!schema_->has_field(column)) {
|
|
return Status::InvalidArgument("Column not exists: ", column);
|
|
}
|
|
|
|
auto *old_field_schema = schema_->get_field(column);
|
|
auto s = check_data_type(old_field_schema);
|
|
CHECK_RETURN_STATUS(s);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::AddColumn(const FieldSchema::Ptr &column_schema,
|
|
const std::string &expression,
|
|
const AddColumnOptions &options) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
// validate
|
|
auto s = validate("", column_schema, expression, "", ColumnOp::ADD);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// forbidden writing until index is ready
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
auto new_schema = std::make_shared<CollectionSchema>(*schema_);
|
|
s = new_schema->add_field(column_schema);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (writing_segment_->doc_count() > 0) {
|
|
s = switch_to_new_segment_for_writing();
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
// add column on segment manager
|
|
s = segment_manager_->add_column(column_schema, expression,
|
|
options.concurrency_);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// reset writing segment with new schema
|
|
auto id = writing_segment_->id();
|
|
auto min_doc_id = writing_segment_->meta()->min_doc_id();
|
|
|
|
s = writing_segment_->destroy();
|
|
CHECK_RETURN_STATUS(s);
|
|
writing_segment_.reset();
|
|
|
|
SegmentOptions seg_options;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
seg_options.read_only_ = options_.read_only_;
|
|
auto writing_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, id, min_doc_id, id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
writing_segment_ = writing_segment.value();
|
|
|
|
// update new version
|
|
new_version.set_schema(*new_schema);
|
|
new_version.reset_writing_segment_meta(writing_segment_->meta());
|
|
|
|
auto new_segment_metas = segment_manager_->get_segments_meta();
|
|
for (auto meta : new_segment_metas) {
|
|
s = new_version.update_persisted_segment_meta(meta);
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
schema_ = new_schema;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::DropColumn(const std::string &column_name) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
// validate
|
|
auto s = validate(column_name, nullptr, "", "", ColumnOp::DROP);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// forbidden writing until index is ready
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
auto new_schema = std::make_shared<CollectionSchema>(*schema_);
|
|
s = new_schema->drop_field(column_name);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (writing_segment_->doc_count() > 0) {
|
|
s = switch_to_new_segment_for_writing();
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
// drop column on segment manager
|
|
s = segment_manager_->drop_column(column_name);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// reset writing segment with new schema
|
|
auto id = writing_segment_->id();
|
|
auto min_doc_id = writing_segment_->meta()->min_doc_id();
|
|
|
|
s = writing_segment_->destroy();
|
|
CHECK_RETURN_STATUS(s);
|
|
writing_segment_.reset();
|
|
|
|
SegmentOptions seg_options;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
seg_options.read_only_ = options_.read_only_;
|
|
auto writing_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, id, min_doc_id, id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
writing_segment_ = writing_segment.value();
|
|
|
|
// update new version
|
|
new_version.set_schema(*new_schema);
|
|
new_version.reset_writing_segment_meta(writing_segment_->meta());
|
|
|
|
auto new_segment_metas = segment_manager_->get_segments_meta();
|
|
for (auto meta : new_segment_metas) {
|
|
s = new_version.update_persisted_segment_meta(meta);
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
schema_ = new_schema;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::AlterColumn(const std::string &column_name,
|
|
const std::string &rename,
|
|
const FieldSchema::Ptr &new_column_schema,
|
|
const AlterColumnOptions &options) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::lock_guard lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
// validate
|
|
auto s =
|
|
validate(column_name, new_column_schema, "", rename, ColumnOp::ALTER);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// forbidden writing until index is ready
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
std::shared_ptr<FieldSchema> new_field_schema{nullptr};
|
|
if (!rename.empty()) {
|
|
new_field_schema =
|
|
std::make_shared<FieldSchema>(*schema_->get_field(column_name));
|
|
new_field_schema->set_name(rename);
|
|
} else {
|
|
new_field_schema = std::make_shared<FieldSchema>(*new_column_schema);
|
|
}
|
|
|
|
auto new_schema = std::make_shared<CollectionSchema>(*schema_);
|
|
s = new_schema->alter_field(column_name, new_field_schema);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (writing_segment_->doc_count() > 0) {
|
|
s = switch_to_new_segment_for_writing();
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
Version new_version = version_manager_->get_current_version();
|
|
|
|
// alter column on segment manager
|
|
s = segment_manager_->alter_column(column_name, new_field_schema,
|
|
options.concurrency_);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// reset writing segment with new schema
|
|
auto id = writing_segment_->id();
|
|
auto min_doc_id = writing_segment_->meta()->min_doc_id();
|
|
|
|
s = writing_segment_->destroy();
|
|
CHECK_RETURN_STATUS(s);
|
|
writing_segment_.reset();
|
|
|
|
SegmentOptions seg_options;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
seg_options.read_only_ = options_.read_only_;
|
|
auto writing_segment =
|
|
Segment::CreateAndOpen(path_, *new_schema, id, min_doc_id, id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
writing_segment_ = writing_segment.value();
|
|
|
|
// update new version
|
|
new_version.set_schema(*new_schema);
|
|
new_version.reset_writing_segment_meta(writing_segment_->meta());
|
|
|
|
auto new_segment_metas = segment_manager_->get_segments_meta();
|
|
for (auto meta : new_segment_metas) {
|
|
s = new_version.update_persisted_segment_meta(meta);
|
|
CHECK_RETURN_STATUS(s);
|
|
}
|
|
|
|
s = version_manager_->apply(new_version);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// persist manifest
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
schema_ = new_schema;
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Result<WriteResults> CollectionImpl::Insert(std::vector<Doc> &docs) {
|
|
return write_impl(docs, WriteMode::INSERT);
|
|
}
|
|
|
|
Result<WriteResults> CollectionImpl::Update(std::vector<Doc> &docs) {
|
|
return write_impl(docs, WriteMode::UPDATE);
|
|
}
|
|
|
|
Result<WriteResults> CollectionImpl::Upsert(std::vector<Doc> &docs) {
|
|
return write_impl(docs, WriteMode::UPSERT);
|
|
}
|
|
|
|
Status CollectionImpl::internal_fetch_by_doc(const Doc &doc,
|
|
Doc::Ptr *doc_out) {
|
|
auto segments = get_all_segments();
|
|
uint64_t doc_id;
|
|
bool has = id_map_->has(doc.pk(), &doc_id);
|
|
if (!has) {
|
|
return Status::NotFound("Document not found");
|
|
}
|
|
if (delete_store_->is_deleted(doc_id)) {
|
|
return Status::NotFound("Document already deleted");
|
|
}
|
|
|
|
auto segment = local_segment_by_doc_id(doc_id, segments);
|
|
if (!segment) {
|
|
LOG_WARN("doc_id: %zu segment not found", (size_t)doc_id);
|
|
return Status::InternalError("Segment not found");
|
|
}
|
|
|
|
auto old_doc = segment->Fetch(doc_id, std::nullopt, true);
|
|
if (!old_doc) {
|
|
LOG_WARN("doc_id: %zu fetch doc failed", (size_t)doc_id);
|
|
return Status::InternalError("Fetch doc failed");
|
|
}
|
|
*doc_out = old_doc;
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::handle_upsert(Doc &doc) {
|
|
return writing_segment_->Upsert(doc);
|
|
}
|
|
|
|
Status CollectionImpl::handle_update(Doc &doc) {
|
|
Doc::Ptr old_doc{nullptr};
|
|
auto s = internal_fetch_by_doc(doc, &old_doc);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
old_doc->merge(doc);
|
|
return writing_segment_->Update(*old_doc);
|
|
}
|
|
|
|
Status CollectionImpl::handle_insert(Doc &doc) {
|
|
return writing_segment_->Insert(doc);
|
|
}
|
|
|
|
Result<WriteResults> CollectionImpl::write_impl(std::vector<Doc> &docs,
|
|
WriteMode mode) {
|
|
CHECK_READONLY_RETURN_STATUS_EXPECTED();
|
|
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
for (auto &&doc : docs) {
|
|
auto s = doc.validate_and_sanitize(schema_, mode == WriteMode::UPDATE);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
}
|
|
|
|
// TODO: The granularity of the write_lock is too coarse.
|
|
std::lock_guard write_lock(write_mtx_);
|
|
|
|
WriteResults results;
|
|
// validate write batch size
|
|
if (docs.size() > kMaxWriteBatchSize) {
|
|
CHECK_RETURN_STATUS_EXPECTED(Status::InvalidArgument(
|
|
"Too many docs: ", docs.size(), " exceeds max write batch size of ",
|
|
kMaxWriteBatchSize));
|
|
}
|
|
|
|
for (auto &&doc : docs) {
|
|
if (need_switch_to_new_segment()) {
|
|
auto s = switch_to_new_segment_for_writing();
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
}
|
|
|
|
Status s;
|
|
|
|
switch (mode) {
|
|
case WriteMode::UPSERT:
|
|
s = handle_upsert(doc);
|
|
break;
|
|
case WriteMode::UPDATE:
|
|
s = handle_update(doc);
|
|
break;
|
|
case WriteMode::INSERT:
|
|
s = handle_insert(doc);
|
|
break;
|
|
default:
|
|
s = Status::InvalidArgument("Invalid write mode");
|
|
}
|
|
|
|
results.push_back(s);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
bool CollectionImpl::need_switch_to_new_segment() const {
|
|
return writing_segment_->doc_count() >= schema_->max_doc_count_per_segment();
|
|
}
|
|
|
|
Status CollectionImpl::switch_to_new_segment_for_writing(
|
|
const CollectionSchema::Ptr &schema) {
|
|
auto s = writing_segment_->dump();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
s = segment_manager_->add_segment(writing_segment_);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// when create new segment, segment meta should create a first new block
|
|
// meta
|
|
auto new_segment = Segment::CreateAndOpen(
|
|
path_, schema == nullptr ? *schema_ : *schema, allocate_segment_id(),
|
|
writing_segment_->meta()->max_doc_id() + 1, id_map_, delete_store_,
|
|
version_manager_,
|
|
SegmentOptions{false, options_.enable_mmap_, options_.max_buffer_size_});
|
|
if (!new_segment) {
|
|
return new_segment.error();
|
|
}
|
|
|
|
Version version = version_manager_->get_current_version();
|
|
auto writing_segment_meta = writing_segment_->meta();
|
|
writing_segment_meta->remove_writing_forward_block();
|
|
s = version.add_persisted_segment_meta(writing_segment_meta);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
writing_segment_ = new_segment.value();
|
|
version.reset_writing_segment_meta(writing_segment_->meta());
|
|
version.set_next_segment_id(segment_id_allocator_.load());
|
|
|
|
s = version_manager_->apply(version);
|
|
CHECK_RETURN_STATUS(s);
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Result<WriteResults> CollectionImpl::Delete(
|
|
const std::vector<std::string> &pks) {
|
|
CHECK_READONLY_RETURN_STATUS_EXPECTED();
|
|
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
// TODO: The granularity of the write_lock is too coarse.
|
|
std::lock_guard write_lock(write_mtx_);
|
|
WriteResults results;
|
|
for (auto &&pk : pks) {
|
|
Status s = writing_segment_->Delete(pk);
|
|
results.push_back(s);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
Status CollectionImpl::DeleteByFilter(const std::string &filter) {
|
|
CHECK_COLLECTION_READONLY_RETURN_STATUS;
|
|
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS(destroyed_, false);
|
|
|
|
SearchQuery query;
|
|
query.filter_ = filter;
|
|
query.topk_ = INT32_MAX;
|
|
query.output_fields_ = std::vector<std::string>{};
|
|
query.include_doc_id_ = true;
|
|
|
|
auto ret =
|
|
sql_engine_->execute(schema_, std::move(query), get_all_segments());
|
|
if (!ret.has_value()) {
|
|
return ret.error();
|
|
}
|
|
|
|
// TODO: The granularity of the write_lock is too coarse.
|
|
std::lock_guard write_lock(write_mtx_);
|
|
for (auto &doc : ret.value()) {
|
|
Status s = writing_segment_->Delete(doc->doc_id());
|
|
if (!s.ok()) {
|
|
LOG_ERROR("Delete doc_id: %zu failed", (size_t)doc->doc_id());
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Result<DocPtrList> CollectionImpl::Query(const SearchQuery &query) const {
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
// When field_name_ is set, use get_field to retrieve the schema uniformly.
|
|
// validate checks that the field type matches the query type
|
|
// (FTS query requires an FTS field, vector query requires a vector field).
|
|
const auto &field_name = query.target_.field_name_;
|
|
const FieldSchema *field_schema =
|
|
field_name.empty() ? nullptr : schema_->get_field(field_name);
|
|
bool need_sanitize = false;
|
|
auto s = query.validate(field_schema, &need_sanitize);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
|
|
auto segments = get_all_segments();
|
|
if (segments.empty()) {
|
|
return DocPtrList();
|
|
}
|
|
|
|
if (!need_sanitize) {
|
|
return sql_engine_->execute(schema_, query, segments);
|
|
}
|
|
|
|
// Sparse needs sanitization: make a mutable copy and sort indices in place.
|
|
SearchQuery sanitized_query = query;
|
|
auto ss = sanitize_sparse_vector(sanitized_query.target_, field_schema);
|
|
CHECK_RETURN_STATUS_EXPECTED(ss);
|
|
return sql_engine_->execute(schema_, std::move(sanitized_query), segments);
|
|
}
|
|
|
|
Result<DocPtrList> CollectionImpl::Query(const MultiQuery &query) const {
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
if (query.queries.size() < 2) {
|
|
return tl::make_unexpected(Status::InvalidArgument(
|
|
"Invalid query: MultiQuery requires at least 2 sub-queries, got ",
|
|
query.queries.size()));
|
|
}
|
|
|
|
if (auto s = validate_topk_and_output_fields(query.topk, query.output_fields);
|
|
!s.ok()) {
|
|
return tl::make_unexpected(s);
|
|
}
|
|
|
|
auto segments = get_all_segments();
|
|
if (segments.empty()) {
|
|
return DocPtrList();
|
|
}
|
|
|
|
// Convert each SubQuery to a SearchQuery and validate.
|
|
std::vector<SearchQuery> pending_queries;
|
|
std::vector<FieldSchema::Ptr> field_schemas;
|
|
pending_queries.reserve(query.queries.size());
|
|
field_schemas.reserve(query.queries.size());
|
|
|
|
for (const auto &sub : query.queries) {
|
|
const auto &target = sub.target_;
|
|
auto field_ptr = schema_->get_field_ptr(target.field_name_);
|
|
if (!field_ptr) {
|
|
return tl::make_unexpected(Status::InvalidArgument(
|
|
"Invalid query: field ", target.field_name_, " not found"));
|
|
}
|
|
auto *field_schema = field_ptr.get();
|
|
|
|
bool need_sanitize = false;
|
|
auto s = target.validate(field_schema, &need_sanitize);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
|
|
SearchQuery sq;
|
|
sq.target_ = target;
|
|
sq.topk_ = sub.num_candidates_;
|
|
sq.filter_ = query.filter;
|
|
sq.include_vector_ = query.include_vector;
|
|
sq.include_doc_id_ = query.include_doc_id_;
|
|
sq.output_fields_ = query.output_fields;
|
|
|
|
if (need_sanitize) {
|
|
auto ss = sanitize_sparse_vector(sq.target_, field_schema);
|
|
CHECK_RETURN_STATUS_EXPECTED(ss);
|
|
}
|
|
pending_queries.push_back(std::move(sq));
|
|
field_schemas.push_back(std::move(field_ptr));
|
|
}
|
|
|
|
auto execute_query = [&](SearchQuery &pending) -> Result<DocPtrList> {
|
|
auto engine = sqlengine::SQLEngine::create(std::make_shared<Profiler>());
|
|
return engine->execute(schema_, std::move(pending), segments);
|
|
};
|
|
|
|
std::vector<Result<DocPtrList>> results(pending_queries.size());
|
|
|
|
// Single-segment queries have no segment-level fanout; multi-segment queries
|
|
// already use the query pool per sub-query.
|
|
if (segments.size() == 1) {
|
|
auto group = GlobalResource::Instance().query_thread_pool()->make_group();
|
|
for (size_t i = 0; i < pending_queries.size(); ++i) {
|
|
group->execute(
|
|
[&, i]() { results[i] = execute_query(pending_queries[i]); });
|
|
}
|
|
group->wait_finish();
|
|
} else {
|
|
for (size_t i = 0; i < pending_queries.size(); ++i) {
|
|
results[i] = execute_query(pending_queries[i]);
|
|
}
|
|
}
|
|
|
|
std::vector<DocPtrList> query_results;
|
|
query_results.reserve(pending_queries.size());
|
|
for (size_t i = 0; i < pending_queries.size(); ++i) {
|
|
if (!results[i]) {
|
|
return tl::make_unexpected(results[i].error());
|
|
}
|
|
query_results.push_back(std::move(results[i].value()));
|
|
}
|
|
|
|
// Dispatch rerank — schema info injected via field_schemas
|
|
return reranker::rerank(query.rerank, query_results, field_schemas,
|
|
query.topk);
|
|
}
|
|
|
|
Result<GroupResults> CollectionImpl::GroupByQuery(
|
|
const GroupByVectorQuery &query) const {
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
auto segments = get_all_segments();
|
|
if (segments.empty()) {
|
|
return GroupResults();
|
|
}
|
|
|
|
// Determine vector data source (zero-copy for dense, copy+sort for sparse)
|
|
const FieldSchema *field_schema =
|
|
schema_->get_field(query.target_.field_name_);
|
|
bool need_sanitize = false;
|
|
auto s = query.target_.validate(field_schema, &need_sanitize);
|
|
CHECK_RETURN_STATUS_EXPECTED(s);
|
|
|
|
if (!need_sanitize) {
|
|
return sql_engine_->execute_group_by(schema_, query, segments);
|
|
}
|
|
|
|
// Sparse needs sanitization: make a mutable copy and sort indices in place.
|
|
GroupByVectorQuery sanitized_query = query;
|
|
auto ss = sanitize_sparse_vector(sanitized_query.target_, field_schema);
|
|
CHECK_RETURN_STATUS_EXPECTED(ss);
|
|
return sql_engine_->execute_group_by(schema_, sanitized_query, segments);
|
|
}
|
|
|
|
Result<DocPtrMap> CollectionImpl::Fetch(
|
|
const std::vector<std::string> &pks,
|
|
const std::optional<std::vector<std::string>> &output_fields,
|
|
bool include_vector) const {
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
auto segments = get_all_segments();
|
|
|
|
DocPtrMap results;
|
|
|
|
for (auto &pk : pks) {
|
|
uint64_t doc_id;
|
|
bool has = id_map_->has(pk, &doc_id);
|
|
if (!has) {
|
|
results.insert({pk, nullptr});
|
|
continue;
|
|
}
|
|
if (delete_store_->is_deleted(doc_id)) {
|
|
results.insert({pk, nullptr});
|
|
continue;
|
|
}
|
|
auto segment = local_segment_by_doc_id(doc_id, segments);
|
|
if (!segment) {
|
|
LOG_WARN("doc_id: %zu segment not found", (size_t)doc_id);
|
|
results.insert({pk, nullptr});
|
|
continue;
|
|
}
|
|
results.insert({pk, segment->Fetch(doc_id, output_fields, include_vector)});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
Result<std::string> CollectionImpl::DebugGetHnswStorageMode(
|
|
const std::string &column_name) const {
|
|
std::shared_lock lock(schema_handle_mtx_);
|
|
|
|
CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
|
|
|
|
// Try all segments (including the writing one). The first segment that has
|
|
// a fully-built HNSW index wins; if only a building segment exists we still
|
|
// surface its current storage mode so that tests can observe the entity
|
|
// type right after Open().
|
|
auto segments = get_all_segments();
|
|
|
|
for (const auto &segment : segments) {
|
|
if (!segment) {
|
|
continue;
|
|
}
|
|
auto indexers = segment->get_vector_indexer(column_name);
|
|
for (const auto &indexer : indexers) {
|
|
if (!indexer) {
|
|
continue;
|
|
}
|
|
auto index = indexer->debug_get_index();
|
|
if (!index) {
|
|
continue;
|
|
}
|
|
auto *hnsw_index = dynamic_cast<core_interface::HNSWIndex *>(index.get());
|
|
if (!hnsw_index) {
|
|
return tl::make_unexpected(Status::InvalidArgument(
|
|
"Column '", column_name,
|
|
"' does not have an HNSW index (or index is sparse)"));
|
|
}
|
|
auto mode = hnsw_index->storage_mode();
|
|
if (mode.empty()) {
|
|
// streamer not initialized yet; skip and look at other segments
|
|
continue;
|
|
}
|
|
return mode;
|
|
}
|
|
}
|
|
|
|
return tl::make_unexpected(
|
|
Status::NotFound("No HNSW index found for column '", column_name, "'"));
|
|
}
|
|
|
|
Status CollectionImpl::recovery() {
|
|
if (!FileHelper::DirectoryExists(path_.c_str())) {
|
|
return Status::InvalidArgument("collection path{", path_, "} not exist.");
|
|
}
|
|
|
|
// get lock file
|
|
auto s = acquire_file_lock(false);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// recovery version first
|
|
auto version_manager = VersionManager::Recovery(path_);
|
|
if (!version_manager.has_value()) {
|
|
return version_manager.error();
|
|
}
|
|
|
|
version_manager_ = version_manager.value();
|
|
const auto v = version_manager_->get_current_version();
|
|
schema_ = std::make_shared<CollectionSchema>(v.schema());
|
|
options_.enable_mmap_ = v.enable_mmap();
|
|
s = recover_idmap_and_delete_store();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// recover persist segments
|
|
segment_manager_ = std::make_shared<SegmentManager>();
|
|
|
|
auto segment_metas = v.persisted_segment_metas();
|
|
|
|
SegmentOptions seg_options;
|
|
seg_options.read_only_ = true;
|
|
seg_options.enable_mmap_ = options_.enable_mmap_;
|
|
for (size_t i = 0; i < segment_metas.size(); ++i) {
|
|
auto segment = Segment::Open(path_, *schema_, *segment_metas[i], id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!segment) {
|
|
return segment.error();
|
|
}
|
|
|
|
segment_manager_->add_segment(segment.value());
|
|
}
|
|
|
|
seg_options.read_only_ = options_.read_only_;
|
|
seg_options.max_buffer_size_ = options_.max_buffer_size_;
|
|
|
|
// recover writing segment
|
|
auto writing_segment =
|
|
Segment::Open(path_, *schema_, *v.writing_segment_meta(), id_map_,
|
|
delete_store_, version_manager_, seg_options);
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
|
|
writing_segment_ = writing_segment.value();
|
|
segment_id_allocator_.store(v.next_segment_id());
|
|
|
|
// recover id map & delete store
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::recover_idmap_and_delete_store() {
|
|
const auto v = version_manager_->get_current_version();
|
|
|
|
// idmap
|
|
std::string idmap_path =
|
|
FileHelper::MakeFilePath(path_, FileID::ID_FILE, v.id_map_path_suffix());
|
|
id_map_ = IDMap::CreateAndOpen(schema_->name(), idmap_path, false,
|
|
options_.read_only_);
|
|
if (!id_map_) {
|
|
return Status::InternalError("recovery idmap failed, path: ", idmap_path);
|
|
}
|
|
|
|
// delete store
|
|
std::string delete_store_path = FileHelper::MakeFilePath(
|
|
path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix());
|
|
delete_store_ =
|
|
DeleteStore::CreateAndLoad(schema_->name(), delete_store_path);
|
|
if (!delete_store_) {
|
|
return Status::InternalError("recovery delete store failed, path: ",
|
|
delete_store_path);
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::create() {
|
|
// check path
|
|
if (path_.empty()) {
|
|
return Status::InvalidArgument("path validate failed: path is empty");
|
|
}
|
|
if (!FileHelper::PathSimpleValidation(path_)) {
|
|
return Status::InvalidArgument("path validate failed: path[", path_,
|
|
"] is not a valid path");
|
|
}
|
|
if (ailego::FileHelper::IsExist(path_.c_str())) {
|
|
return Status::InvalidArgument("path validate failed: path[", path_,
|
|
"] exists, create expects a path that does "
|
|
"not exist");
|
|
}
|
|
|
|
// check schema
|
|
auto s = schema_->validate();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
if (!ailego::FileHelper::MakePath(path_.c_str())) {
|
|
return Status::InvalidArgument(
|
|
"create collection path failed: ", path_,
|
|
", error: ", ailego::FileHelper::GetLastErrorString());
|
|
}
|
|
|
|
// init lock file
|
|
s = acquire_file_lock(true);
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// init idmap & delete store
|
|
s = create_idmap_and_delete_store();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// init version manager
|
|
s = init_version_manager();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// create segment
|
|
s = init_writing_segment();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
// init version
|
|
Version version;
|
|
version.set_schema(*schema_);
|
|
version.set_enable_mmap(options_.enable_mmap_);
|
|
version.reset_writing_segment_meta(writing_segment_->meta());
|
|
version.set_id_map_path_suffix(0);
|
|
version.set_delete_snapshot_path_suffix(0);
|
|
version.set_next_segment_id(1);
|
|
|
|
version_manager_->apply(version);
|
|
s = version_manager_->flush();
|
|
CHECK_RETURN_STATUS(s);
|
|
|
|
segment_id_allocator_.store(1);
|
|
segment_manager_ = std::make_unique<SegmentManager>();
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::create_idmap_and_delete_store() {
|
|
// idmap
|
|
std::string idmap_path = FileHelper::MakeFilePath(path_, FileID::ID_FILE, 0);
|
|
id_map_ = IDMap::CreateAndOpen(schema_->name(), idmap_path, true,
|
|
options_.read_only_);
|
|
if (!id_map_) {
|
|
return Status::InternalError("create id map failed, path: ", idmap_path);
|
|
}
|
|
|
|
std::string delete_store_path =
|
|
FileHelper::MakeFilePath(path_, FileID::DELETE_FILE, 0);
|
|
delete_store_ = std::make_shared<DeleteStore>(schema_->name());
|
|
// when first create collection, delete store will flush a empty snapshot
|
|
delete_store_->flush(delete_store_path);
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::init_version_manager() {
|
|
// use empty version to init version manager
|
|
auto version_manager = VersionManager::Create(path_, Version{});
|
|
if (!version_manager.has_value()) {
|
|
return version_manager.error();
|
|
}
|
|
|
|
version_manager_ = version_manager.value();
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::init_writing_segment() {
|
|
SegmentOptions options;
|
|
options.enable_mmap_ = options_.enable_mmap_;
|
|
options.max_buffer_size_ = options_.max_buffer_size_;
|
|
options.read_only_ = options_.read_only_;
|
|
|
|
auto writing_segment = Segment::CreateAndOpen(
|
|
path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options);
|
|
|
|
if (!writing_segment) {
|
|
return writing_segment.error();
|
|
}
|
|
|
|
writing_segment_ = writing_segment.value();
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Status CollectionImpl::acquire_file_lock(bool create) {
|
|
std::string lock_file_path = ailego::FileHelper::PathJoin(path_, "LOCK");
|
|
|
|
if (create) {
|
|
if (!lock_file_.create(lock_file_path.c_str(), 0)) {
|
|
return Status::InternalError("Can't create lock file: ", lock_file_path);
|
|
}
|
|
} else {
|
|
if (!lock_file_.open(lock_file_path.c_str(), options_.read_only_)) {
|
|
return Status::InternalError("Can't open lock file: ", lock_file_path);
|
|
}
|
|
}
|
|
|
|
if (options_.read_only_) {
|
|
if (!ailego::FileLock::TryLockShared(lock_file_.native_handle())) {
|
|
return Status::InternalError("Can't lock read-only collection: ",
|
|
lock_file_path);
|
|
}
|
|
} else {
|
|
if (!ailego::FileLock::TryLock(lock_file_.native_handle())) {
|
|
return Status::InternalError("Can't lock read-write collection: ",
|
|
lock_file_path);
|
|
}
|
|
}
|
|
|
|
return Status::OK();
|
|
}
|
|
|
|
Segment::Ptr CollectionImpl::local_segment_by_doc_id(
|
|
uint64_t doc_id, const std::vector<Segment::Ptr> &segments) const {
|
|
size_t left = 0;
|
|
size_t right = segments.size();
|
|
|
|
while (left < right) {
|
|
size_t mid = left + (right - left) / 2;
|
|
uint64_t min_id = segments[mid]->meta()->min_doc_id();
|
|
uint64_t max_id = segments[mid]->meta()->max_doc_id();
|
|
|
|
if (doc_id < min_id) {
|
|
right = mid;
|
|
} else if (doc_id > max_id) {
|
|
left = mid + 1;
|
|
} else {
|
|
return segments[mid];
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<Segment::Ptr> CollectionImpl::get_all_segments() const {
|
|
std::vector<Segment::Ptr> segments = get_all_persist_segments();
|
|
if (writing_segment_->doc_count() > 0) {
|
|
segments.push_back(writing_segment_);
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
std::vector<Segment::Ptr> CollectionImpl::get_all_persist_segments() const {
|
|
return segment_manager_->get_segments();
|
|
}
|
|
|
|
} // namespace zvec
|