From a973f7e3f22bc28b3751861120404d2bcc683f25 Mon Sep 17 00:00:00 2001 From: Qinren Zhou Date: Wed, 10 Jun 2026 20:56:23 +0800 Subject: [PATCH 1/3] update --- src/binding/async_workers.cc | 16 +- src/binding/async_workers.h | 7 +- src/binding/collection.cc | 76 +++++- src/binding/collection.h | 4 + src/binding/params.cc | 432 +++++++++++++++++++++++------------ src/binding/params.h | 11 +- src/index.js | 36 --- 7 files changed, 387 insertions(+), 195 deletions(-) diff --git a/src/binding/async_workers.cc b/src/binding/async_workers.cc index 7ad65e8..7e47f4b 100644 --- a/src/binding/async_workers.cc +++ b/src/binding/async_workers.cc @@ -39,8 +39,22 @@ QueryWorker::QueryWorker(Napi::Env env, zvec::Collection::Ptr collection, query_(std::move(query)), deferred_(deferred) {} +QueryWorker::QueryWorker(Napi::Env env, zvec::Collection::Ptr collection, + zvec::CollectionSchema::Ptr schema, + zvec::MultiQuery query, + Napi::Promise::Deferred deferred) + : Napi::AsyncWorker(env), + collection_(collection), + schema_(schema), + query_(std::move(query)), + deferred_(deferred) {} + void QueryWorker::Execute() { - auto res = collection_->Query(query_); + auto res = std::visit( + [this](const auto &query) -> zvec::Result { + return collection_->Query(query); + }, + query_); if (res) { results_ = std::move(res.value()); } else { diff --git a/src/binding/async_workers.h b/src/binding/async_workers.h index 316579d..1968d6c 100644 --- a/src/binding/async_workers.h +++ b/src/binding/async_workers.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -33,6 +34,10 @@ class QueryWorker : public Napi::AsyncWorker { zvec::CollectionSchema::Ptr schema, zvec::SearchQuery query, Napi::Promise::Deferred deferred); + QueryWorker(Napi::Env env, zvec::Collection::Ptr collection, + zvec::CollectionSchema::Ptr schema, zvec::MultiQuery query, + Napi::Promise::Deferred deferred); + void Execute() override; void OnOK() override; void OnError(const Napi::Error &error) override; @@ -40,7 +45,7 @@ class QueryWorker : public Napi::AsyncWorker { private: zvec::Collection::Ptr collection_; zvec::CollectionSchema::Ptr schema_; - zvec::SearchQuery query_; + std::variant query_; Napi::Promise::Deferred deferred_; zvec::Status status_; zvec::DocPtrList results_; diff --git a/src/binding/collection.cc b/src/binding/collection.cc index 1284f12..c6c6dda 100644 --- a/src/binding/collection.cc +++ b/src/binding/collection.cc @@ -156,8 +156,10 @@ Napi::Object Collection::Init(Napi::Env env, Napi::Object exports, InstanceMethod("deleteSync", &Collection::Delete), InstanceMethod("deleteByFilterSync", &Collection::DeleteByFilter), InstanceMethod("deleteByFilter", &Collection::DeleteByFilterAsync), - InstanceMethod("_internalQuery", &Collection::Query), - InstanceMethod("_internalQueryAsync", &Collection::QueryAsync), + InstanceMethod("querySync", &Collection::Query), + InstanceMethod("query", &Collection::QueryAsync), + InstanceMethod("multiQuerySync", &Collection::MultiQuery), + InstanceMethod("multiQuery", &Collection::MultiQueryAsync), InstanceMethod("fetchSync", &Collection::Fetch), InstanceMethod("optimizeSync", &Collection::Optimize), InstanceMethod("optimize", &Collection::OptimizeAsync), @@ -566,8 +568,8 @@ Napi::Value Collection::Query(const Napi::CallbackInfo &info) { if (ThrowIfClosed(env)) return env.Undefined(); if (info.Length() != 1) { ThrowIfNotOk(env, zvec::Status::InvalidArgument( - "Collection.query(): Expected exactly 1 argument. " - "Argument must be a Query object")); + "Collection.querySync(): Expected exactly 1 " + "argument. Argument must be a Query object")); return env.Undefined(); } @@ -618,6 +620,64 @@ Napi::Value Collection::QueryAsync(const Napi::CallbackInfo &info) { } +Napi::Value Collection::MultiQuery(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + if (ThrowIfClosed(env)) return env.Undefined(); + if (info.Length() != 1) { + ThrowIfNotOk( + env, zvec::Status::InvalidArgument( + "Collection.multiQuerySync(): Expected exactly 1 argument. " + "Argument must be a MultiQuery object")); + return env.Undefined(); + } + + if (auto parsed_query = ParseMultiQuery(info[0], get_wrapped_schema()); + parsed_query) { + auto res = collection_->Query(parsed_query.value()); + if (res) { + Napi::Array array = Napi::Array::New(env); + const zvec::DocPtrList &doc_list = res.value(); + for (size_t i = 0; i < doc_list.size(); i++) { + array.Set(i, CreateDoc(env, get_wrapped_schema(), doc_list[i])); + } + return array; + } else { + ThrowIfNotOk(env, res.error()); + return env.Undefined(); + } + } else { + ThrowIfNotOk(env, parsed_query.error()); + return env.Undefined(); + } +} + + +Napi::Value Collection::MultiQueryAsync(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + if (ThrowIfClosed(env)) return env.Undefined(); + auto deferred = Napi::Promise::Deferred::New(env); + if (info.Length() != 1) { + RejectIfNotOk(env, + zvec::Status::InvalidArgument( + "Collection.multiQuery(): Expected exactly 1 argument. " + "Argument must be a MultiQuery object"), + deferred); + return deferred.Promise(); + } + + if (auto parsed_query = ParseMultiQuery(info[0], get_wrapped_schema()); + parsed_query) { + auto *worker = new QueryWorker(env, collection_, get_wrapped_schema(), + std::move(parsed_query.value()), deferred); + worker->Queue(); + return deferred.Promise(); + } else { + RejectIfNotOk(env, parsed_query.error(), deferred); + return deferred.Promise(); + } +} + + Napi::Value Collection::Fetch(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); if (ThrowIfClosed(env)) return env.Undefined(); @@ -646,8 +706,8 @@ Napi::Value Collection::Fetch(const Napi::CallbackInfo &info) { pks.emplace_back(pkArray.Get(i).As().Utf8Value()); } else { ThrowIfNotOk(env, zvec::Status::InvalidArgument( - "Collection.fetch(): Expected a string or array " - "of strings for 'ids'")); + "Collection.fetch(): Expected a string or " + "array of strings for 'ids'")); return false; } } @@ -676,8 +736,8 @@ Napi::Value Collection::Fetch(const Napi::CallbackInfo &info) { if (obj.Has("outputFields") && !obj.Get("outputFields").IsUndefined()) { if (!obj.Get("outputFields").IsArray()) { ThrowIfNotOk(env, zvec::Status::InvalidArgument( - "Collection.fetch(): argument 'outputFields' must " - "be an array of strings")); + "Collection.fetch(): argument 'outputFields' " + "must be an array of strings")); return env.Undefined(); } std::vector fields{}; diff --git a/src/binding/collection.h b/src/binding/collection.h index e317e52..0049fb4 100644 --- a/src/binding/collection.h +++ b/src/binding/collection.h @@ -57,6 +57,10 @@ class Collection : public Napi::ObjectWrap { Napi::Value QueryAsync(const Napi::CallbackInfo &info); + Napi::Value MultiQuery(const Napi::CallbackInfo &info); + + Napi::Value MultiQueryAsync(const Napi::CallbackInfo &info); + Napi::Value Fetch(const Napi::CallbackInfo &info); Napi::Value Optimize(const Napi::CallbackInfo &info); diff --git a/src/binding/params.cc b/src/binding/params.cc index 1ff105c..472debd 100644 --- a/src/binding/params.cc +++ b/src/binding/params.cc @@ -1,4 +1,5 @@ #include "params.h" +#include #include "types.h" @@ -497,140 +498,11 @@ Napi::Object CreateFtsIndexParams(Napi::Env env, } -zvec::Result ParseSearchQuery( - const Napi::Value &value, zvec::CollectionSchema::Ptr schema) { - if (!value.IsObject()) { - return tl::make_unexpected( - zvec::Status::InvalidArgument("Expected an object for Query")); - } - - zvec::SearchQuery query{}; - query.topk_ = 10; - auto obj = value.As(); - zvec::FieldSchema *field_schema{nullptr}; - - if (obj.Has("fieldName")) { - if (obj.Get("fieldName").IsString()) { - query.target_.field_name_ = - obj.Get("fieldName").As().Utf8Value(); - field_schema = schema->get_field(query.target_.field_name_); - if (!field_schema) { - return tl::make_unexpected( - zvec::Status::InvalidArgument("'", query.target_.field_name_, - "' not found in collection schema")); - } - } else { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "Expected a string for 'fieldName' in Query")); - } - } - - if (obj.Has("topk")) { - if (obj.Get("topk").IsNumber()) { - query.topk_ = obj.Get("topk").As().Int32Value(); - } else { - return tl::make_unexpected( - zvec::Status::InvalidArgument("Expected a number for 'topk'")); - } - } - - const bool has_vector = obj.Has("vector") && - !obj.Get("vector").IsUndefined() && - !obj.Get("vector").IsNull(); - const bool has_fts = obj.Has("fts") && !obj.Get("fts").IsUndefined() && - !obj.Get("fts").IsNull(); - - if (has_vector && has_fts) { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "Cannot combine 'vector' and 'fts' in a single Query")); - } - - if (has_vector) { - if (!field_schema) { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "A vector name must be provided when performing vector queries")); - } - auto vector = obj.Get("vector"); - if (field_schema->is_dense_vector()) { - if (auto s = ParseVectorToString(vector, field_schema, &query); !s.ok()) { - return tl::make_unexpected(s); - } - } else if (field_schema->is_sparse_vector()) { - if (auto s = ParseVectorToMap(vector, field_schema, &query); !s.ok()) { - return tl::make_unexpected(s); - } - } else { - return tl::make_unexpected( - zvec::Status::InvalidArgument("Field '", query.target_.field_name_, - "' is not a vector field and cannot be " - "queried using vector operations.")); - } - } else if (has_fts) { - if (!field_schema) { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "A field name must be provided when performing FTS queries")); - } - auto parsed_fts = ParseFtsClause(obj.Get("fts")); - if (parsed_fts) { - query.target_.clause_ = std::move(parsed_fts.value()); - } else { - return tl::make_unexpected(parsed_fts.error()); - } - } else if (field_schema) { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "A vector or FTS clause must be provided when 'fieldName' is set")); - } - - if (obj.Has("params")) { - auto parsed_params = ParseQueryParams(obj.Get("params")); - if (parsed_params) { - query.target_.query_params_ = parsed_params.value(); - } else { - return tl::make_unexpected(parsed_params.error()); - } - } - if (obj.Has("filter")) { - if (obj.Get("filter").IsString()) { - query.filter_ = obj.Get("filter").As().Utf8Value(); - } else { - return tl::make_unexpected( - zvec::Status::InvalidArgument("Expected a string for 'filter'")); - } - } - if (obj.Has("includeVector")) { - if (obj.Get("includeVector").IsBoolean()) { - query.include_vector_ = obj.Get("includeVector").As(); - } else { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "Expected a boolean for 'includeVector'")); - } - } - if (obj.Has("outputFields")) { - if (obj.Get("outputFields").IsArray()) { - std::vector output_fields{}; - Napi::Array array = obj.Get("outputFields").As(); - for (uint32_t i = 0; i < array.Length(); i++) { - if (array.Get(i).IsString()) { - output_fields.push_back(array.Get(i).As().Utf8Value()); - } else { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "Expected an array of strings for 'outputFields'")); - } - } - query.output_fields_ = output_fields; - } else { - return tl::make_unexpected(zvec::Status::InvalidArgument( - "Expected an array of strings for 'outputFields'")); - } - } - - return query; -} - +namespace { zvec::Status ParseVectorToString(const Napi::Value &value, zvec::FieldSchema *schema, - zvec::SearchQuery *query) { + zvec::QueryTarget *target) { if (value.IsTypedArray()) { auto array = value.As(); auto ta_type = array.TypedArrayType(); @@ -650,7 +522,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, std::memcpy(buf.data() + (i * sizeof(uint16_t)), &val, sizeof(uint16_t)); } - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } case zvec::DataType::VECTOR_FP32: { @@ -663,7 +535,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, std::string buf; buf.resize(ta_length * sizeof(float)); std::memcpy(buf.data(), float32Array.Data(), ta_length * sizeof(float)); - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } case zvec::DataType::VECTOR_INT8: { @@ -675,7 +547,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, std::string buf; buf.resize(ta_length * sizeof(int8_t)); std::memcpy(buf.data(), int8Array.Data(), ta_length * sizeof(int8_t)); - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } default: { @@ -702,7 +574,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, "' must be numbers"); } } - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } case zvec::DataType::VECTOR_FP32: { @@ -719,7 +591,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, "' must be numbers"); } } - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } case zvec::DataType::VECTOR_INT8: { @@ -737,7 +609,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, "' must be numbers"); } } - query->target_.set_vector(std::move(buf)); + target->set_vector(std::move(buf)); break; } default: { @@ -756,7 +628,7 @@ zvec::Status ParseVectorToString(const Napi::Value &value, zvec::Status ParseVectorToMap(const Napi::Value &value, zvec::FieldSchema *schema, - zvec::SearchQuery *query) { + zvec::QueryTarget *target) { if (!value.IsObject()) { return zvec::Status::InvalidArgument("Expected sparse vector[", schema->name(), "] to be an object"); @@ -797,7 +669,7 @@ zvec::Status ParseVectorToMap(const Napi::Value &value, indices_ptr[i] = index; values_ptr[i] = vectorValue.As().FloatValue(); } - query->target_.set_sparse_vector(std::move(indices), std::move(values)); + target->set_sparse_vector(std::move(indices), std::move(values)); break; } case zvec::DataType::SPARSE_VECTOR_FP16: { @@ -831,7 +703,7 @@ zvec::Status ParseVectorToMap(const Napi::Value &value, zvec::ailego::Float16 val = vectorValue.As().FloatValue(); values_ptr[i] = val; } - query->target_.set_sparse_vector(std::move(indices), std::move(values)); + target->set_sparse_vector(std::move(indices), std::move(values)); break; } default: { @@ -848,6 +720,7 @@ zvec::Result ParseFtsClause(const Napi::Value &value) { return tl::make_unexpected( zvec::Status::InvalidArgument("Expected an object for 'fts'")); } + auto obj = value.As(); zvec::FtsClause fts_clause{}; if (obj.Has("queryString")) { @@ -870,6 +743,285 @@ zvec::Result ParseFtsClause(const Napi::Value &value) { } +zvec::Result ParseQueryTarget( + const Napi::Object &obj, zvec::CollectionSchema::Ptr schema) { + zvec::QueryTarget target{}; + zvec::FieldSchema *field_schema{nullptr}; + + if (obj.Has("fieldName")) { + if (obj.Get("fieldName").IsString()) { + target.field_name_ = obj.Get("fieldName").As().Utf8Value(); + field_schema = schema->get_field(target.field_name_); + } else { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected a string for 'fieldName' in Query")); + } + } + + const bool has_vector = obj.Has("vector") && + !obj.Get("vector").IsUndefined() && + !obj.Get("vector").IsNull(); + const bool has_fts = obj.Has("fts") && !obj.Get("fts").IsUndefined() && + !obj.Get("fts").IsNull(); + + if (has_vector && has_fts) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Cannot combine 'vector' and 'fts' in a single Query")); + } + + if (has_vector) { + if (!field_schema) { + if (target.field_name_.empty()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Missing required 'fieldName' when performing vector queries")); + } + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Field '", target.field_name_, "' not found in collection schema")); + } + auto vector = obj.Get("vector"); + if (field_schema->is_dense_vector()) { + if (auto s = ParseVectorToString(vector, field_schema, &target); + !s.ok()) { + return tl::make_unexpected(s); + } + } else if (field_schema->is_sparse_vector()) { + if (auto s = ParseVectorToMap(vector, field_schema, &target); !s.ok()) { + return tl::make_unexpected(s); + } + } else { + return tl::make_unexpected( + zvec::Status::InvalidArgument("Field '", target.field_name_, + "' is not a vector field and cannot be " + "queried using vector operations.")); + } + } else if (has_fts) { + auto parsed_fts = ParseFtsClause(obj.Get("fts")); + if (parsed_fts) { + target.clause_ = std::move(parsed_fts.value()); + } else { + return tl::make_unexpected(parsed_fts.error()); + } + } + + if (obj.Has("params")) { + auto parsed_params = ParseQueryParams(obj.Get("params")); + if (parsed_params) { + target.query_params_ = parsed_params.value(); + } else { + return tl::make_unexpected(parsed_params.error()); + } + } + + return target; +} + + +zvec::Status ParseCommonQueryOptions( + const Napi::Object &obj, int *topk, std::string *filter, + bool *include_vector, + std::optional> *output_fields) { + if (obj.Has("topk")) { + if (obj.Get("topk").IsNumber()) { + *topk = obj.Get("topk").As().Int32Value(); + } else { + return zvec::Status::InvalidArgument("Expected a number for 'topk'"); + } + } + if (obj.Has("filter")) { + if (obj.Get("filter").IsString()) { + *filter = obj.Get("filter").As().Utf8Value(); + } else { + return zvec::Status::InvalidArgument("Expected a string for 'filter'"); + } + } + if (obj.Has("includeVector")) { + if (obj.Get("includeVector").IsBoolean()) { + *include_vector = obj.Get("includeVector").As().Value(); + } else { + return zvec::Status::InvalidArgument( + "Expected a boolean for 'includeVector'"); + } + } + if (obj.Has("outputFields")) { + if (!obj.Get("outputFields").IsArray()) { + return zvec::Status::InvalidArgument( + "Expected an array of strings for 'outputFields'"); + } + std::vector parsed_output_fields{}; + auto array = obj.Get("outputFields").As(); + for (uint32_t i = 0; i < array.Length(); i++) { + if (!array.Get(i).IsString()) { + return zvec::Status::InvalidArgument( + "Expected an array of strings for 'outputFields'"); + } + parsed_output_fields.push_back( + array.Get(i).As().Utf8Value()); + } + *output_fields = std::move(parsed_output_fields); + } + return zvec::Status::OK(); +} + + +zvec::Result ParseRerankParams( + const Napi::Value &value) { + if (!value.IsObject()) { + return tl::make_unexpected( + zvec::Status::InvalidArgument("Expected an object for 'rerank'")); + } + + auto obj = value.As(); + if (!obj.Has("type") || !obj.Get("type").IsString()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected a string for 'type' in 'rerank'")); + } + std::string type = obj.Get("type").As().Utf8Value(); + + if (type == "rrf") { + int rank_constant{60}; + if (obj.Has("rankConstant")) { + if (!obj.Get("rankConstant").IsNumber()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected a number for 'rankConstant' in 'rerank'")); + } + rank_constant = obj.Get("rankConstant").As().Int32Value(); + } + return zvec::reranker::RrfParams{rank_constant}; + } + + if (type == "weighted") { + if (!obj.Has("weights") || !obj.Get("weights").IsArray()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected an array of numbers for 'weights' in weighted rerank")); + } + + std::vector weights{}; + auto weights_array = obj.Get("weights").As(); + for (uint32_t i = 0; i < weights_array.Length(); i++) { + if (!weights_array.Get(i).IsNumber()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected an array of numbers for 'weights' in weighted rerank")); + } + weights.push_back(weights_array.Get(i).As().DoubleValue()); + } + return zvec::reranker::WeightedParams{std::move(weights)}; + } + + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Unsupported rerank type '", type, "'. Expected 'rrf' or 'weighted'")); +} + +} // namespace + + +zvec::Result ParseSearchQuery( + const Napi::Value &value, zvec::CollectionSchema::Ptr schema) { + if (!value.IsObject()) { + return tl::make_unexpected( + zvec::Status::InvalidArgument("Expected an object for Query")); + } + + zvec::SearchQuery query{}; + query.topk_ = 10; + auto obj = value.As(); + + if (auto s = ParseCommonQueryOptions(obj, &query.topk_, &query.filter_, + &query.include_vector_, + &query.output_fields_); + !s.ok()) { + return tl::make_unexpected(s); + } + + auto parsed_target = ParseQueryTarget(obj, schema); + if (parsed_target) { + query.target_ = std::move(parsed_target.value()); + } else { + return tl::make_unexpected(parsed_target.error()); + } + + return query; +} + + +zvec::Result ParseMultiQuery( + const Napi::Value &value, zvec::CollectionSchema::Ptr schema) { + if (!value.IsObject()) { + return tl::make_unexpected( + zvec::Status::InvalidArgument("Expected an object for MultiQuery")); + } + + zvec::MultiQuery query{}; + query.topk = 10; + auto obj = value.As(); + + if (auto s = + ParseCommonQueryOptions(obj, &query.topk, &query.filter, + &query.include_vector, &query.output_fields); + !s.ok()) { + return tl::make_unexpected(s); + } + + if (!obj.Has("queries") || !obj.Get("queries").IsArray()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected an array of sub-queries for 'queries'")); + } + + const int default_num_candidates = std::max(query.topk, 10); + const auto parse_sub_query = + [&](const Napi::Value &sub_query_value, + uint32_t index) -> zvec::Result { + if (!sub_query_value.IsObject()) { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected an object for sub-query at index ", index, + " in 'queries' array")); + } + auto sub_query_obj = sub_query_value.As(); + auto parsed_target = ParseQueryTarget(sub_query_obj, schema); + if (!parsed_target) { + return tl::make_unexpected(parsed_target.error()); + } + zvec::SubQuery sub_query{}; + sub_query.target_ = std::move(parsed_target.value()); + sub_query.num_candidates_ = default_num_candidates; + if (sub_query_obj.Has("numCandidates")) { + if (sub_query_obj.Get("numCandidates").IsNumber()) { + sub_query.num_candidates_ = + sub_query_obj.Get("numCandidates").As().Int32Value(); + } else { + return tl::make_unexpected(zvec::Status::InvalidArgument( + "Expected a number for 'numCandidates' in sub-query at index ", + index, " in 'queries' array")); + } + } + return sub_query; + }; + + auto queries = obj.Get("queries").As(); + query.queries.reserve(queries.Length()); + for (uint32_t i = 0; i < queries.Length(); i++) { + auto parsed_sub_query = parse_sub_query(queries.Get(i), i); + if (parsed_sub_query) { + query.queries.push_back(std::move(parsed_sub_query.value())); + } else { + return tl::make_unexpected(parsed_sub_query.error()); + } + } + + if (obj.Has("rerank")) { + auto rerank = ParseRerankParams(obj.Get("rerank")); + if (rerank) { + query.rerank = std::move(rerank.value()); + } else { + return tl::make_unexpected(rerank.error()); + } + } else { + query.rerank = zvec::reranker::RrfParams{60}; + } + + return query; +} + + zvec::Result ParseQueryParams( const Napi::Value &value) { if (!value.IsObject()) { diff --git a/src/binding/params.h b/src/binding/params.h index 338f3ea..af0375a 100644 --- a/src/binding/params.h +++ b/src/binding/params.h @@ -62,15 +62,8 @@ Napi::Object CreateFtsIndexParams(Napi::Env env, zvec::IndexParams::Ptr params); zvec::Result ParseSearchQuery( const Napi::Value &value, zvec::CollectionSchema::Ptr schema); -zvec::Status ParseVectorToString(const Napi::Value &value, - zvec::FieldSchema *schema, - zvec::SearchQuery *query); - -zvec::Status ParseVectorToMap(const Napi::Value &value, - zvec::FieldSchema *schema, - zvec::SearchQuery *query); - -zvec::Result ParseFtsClause(const Napi::Value &value); +zvec::Result ParseMultiQuery( + const Napi::Value &value, zvec::CollectionSchema::Ptr schema); zvec::Result ParseQueryParams(const Napi::Value &value); diff --git a/src/index.js b/src/index.js index ec9d54a..e2f21a4 100644 --- a/src/index.js +++ b/src/index.js @@ -22,42 +22,6 @@ try { } -function validateQueryArg(methodName, queryObj, argCount) { - if (argCount !== 1) { - const err = new Error(`Collection.${methodName}(): Expected exactly 1 argument. Argument must be an Query object`); - err.name = "InvalidArgumentError"; - err.code = "ZVEC_INVALID_ARGUMENT"; - throw err; - } - if (queryObj === null || typeof queryObj !== 'object') { - const err = new Error(`Collection.${methodName}(): Expected exactly 1 argument. Argument must be an Query object`); - err.name = "InvalidArgumentError"; - err.code = "ZVEC_INVALID_ARGUMENT"; - throw err; - } - if ('vectors' in queryObj) { - const err = new Error("MultiQuery functionality has not been implemented yet."); - err.name = "NotSupportedError"; - err.code = "ZVEC_NOT_SUPPORTED"; - throw err; - } -} - -binding.Collection.prototype.querySync = function (queryObj) { - validateQueryArg('querySync', queryObj, arguments.length); - return this._internalQuery(queryObj); -}; - -binding.Collection.prototype.query = function (queryObj) { - try { - validateQueryArg('query', queryObj, arguments.length); - } catch (err) { - return Promise.reject(err); - } - return this._internalQueryAsync(queryObj); -}; - - function isZVecError(error) { return ( typeof error === "object" && From e03f851369f54f77cf4b7b126de817e8745aa93d Mon Sep 17 00:00:00 2001 From: Qinren Zhou Date: Thu, 11 Jun 2026 11:31:14 +0800 Subject: [PATCH 2/3] update --- src/index.d.ts | 102 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index b4212f1..64f4686 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -660,7 +660,7 @@ export interface ZVecQuery { vector?: ZVecVector; /** - * Full-text search clause. Cannot be combined with `vector` in the same query. + * Full-text search clause. * @default undefined */ fts?: ZVecFtsQuery; @@ -696,6 +696,106 @@ export interface ZVecQuery { } +interface ZVecSubQuery { + /** + * The name of the field to search against. + */ + fieldName: string; + + /** + * The query vector. Can be a standard JavaScript array, a typed array, or a sparse vector object map. + */ + vector?: ZVecVector; + + /** + * Full-text search clause. + */ + fts?: ZVecFtsQuery; + + /** + * Number of candidates to retrieve for this sub-query before reranking. + * @default max(topk, 10) + */ + numCandidates?: number; + + /** + * Query-time parameters to fine-tune search behavior. + * @default undefined + */ + params?: + | ZVecHnswQueryParams + | ZVecHnswRabitqQueryParams + | ZVecIVFQueryParams + | ZVecDiskAnnQueryParams + | ZVecFtsQueryParams; +} + + +/** + * Multi-query object used to combine multiple vector and/or full-text searches. + * + * Each sub-query contributes candidates, then `rerank` combines them into a + * single result list. + * + * @group Query Parameters + */ +export interface ZVecMultiQuery { + /** + * Sub-queries to execute and merge. At least two are required. + */ + queries: ZVecSubQuery[]; + + /** + * Number of final documents to return. + * @default 10 + */ + topk?: number; + + /** + * Boolean expression to pre-filter candidates for every sub-query. + * @default undefined + */ + filter?: string; + + /** + * Whether to include vector data in results. + * @default false + */ + includeVector?: boolean; + + /** + * Scalar fields to include. If undefined, all fields are returned. + * @default undefined + */ + outputFields?: string[]; + + /** + * Strategy for merging sub-query candidates. If omitted, reciprocal rank + * fusion with rank constant 60 is used. + */ + rerank?: + | { + /** Reciprocal rank fusion. */ + type: 'rrf'; + + /** + * RRF rank constant. + * @default 60 + */ + rankConstant?: number; + } + | { + /** Weighted score fusion. */ + type: 'weighted'; + + /** + * Per-sub-query weights. The length must match `queries.length`. + */ + weights: number[]; + }; +} + + /** * Configuration options for opening or creating a collection. * From a3f7d7a6e29ff9d367eaf9434738e56a49abc92a Mon Sep 17 00:00:00 2001 From: Qinren Zhou Date: Thu, 11 Jun 2026 13:36:19 +0800 Subject: [PATCH 3/3] update --- src/zvec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zvec b/src/zvec index da39a33..815a678 160000 --- a/src/zvec +++ b/src/zvec @@ -1 +1 @@ -Subproject commit da39a33feb3d70a2ffc5ea7e59a1319954e18757 +Subproject commit 815a6783ff78b25926087af7a1b778582d78f848