Files
tasq/node_modules/hnswlib-node/src/addon.cc
T
2026-04-09 19:01:53 +08:00

1399 lines
51 KiB
C++

/**
* hnswlib-node provides Node.js bindings for Hnswlib.
*
* Copyright (c) 2022-2024 Atsushi Tatsuma
*
* 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 <cmath>
#include <fstream>
#include <memory>
#include <new>
#include <numeric>
#include <string>
#include <vector>
#include <napi.h>
#include "hnswlib/hnswlib.h"
namespace {
void normalizePoint(std::vector<float>& vec) {
const size_t dim = vec.size();
const float norm = std::sqrt(std::fabs(std::inner_product(vec.begin(), vec.end(), vec.begin(), 0.0f)));
if (norm > 0.0f) {
for (size_t i = 0; i < dim; i++) vec[i] /= norm;
}
}
} // namespace
class L2Space : public Napi::ObjectWrap<L2Space> {
public:
uint32_t dim_;
std::unique_ptr<hnswlib::L2Space> l2space_;
L2Space(const Napi::CallbackInfo& info) : Napi::ObjectWrap<L2Space>(info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return;
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return;
}
dim_ = info[0].As<Napi::Number>().Uint32Value();
l2space_ = std::unique_ptr<hnswlib::L2Space>(new hnswlib::L2Space(static_cast<size_t>(dim_)));
}
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// clang-format off
Napi::Function func = DefineClass(env, "L2Space", {
InstanceMethod("distance", &L2Space::distance),
InstanceMethod("getNumDimensions", &L2Space::getNumDimensions)
});
// clang-format on
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("L2Space", func);
return exports;
}
private:
Napi::Value distance(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 2) {
Napi::Error::New(env, "Expected 2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsArray()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Array arr_a = info[0].As<Napi::Array>();
Napi::Array arr_b = info[1].As<Napi::Array>();
if (arr_a.Length() != dim_) {
Napi::Error::New(env, "Invalid the first array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr_a.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
if (arr_b.Length() != dim_) {
Napi::Error::New(env, "Invalid the second array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr_b.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec_a(dim_, 0.0);
std::vector<float> vec_b(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val_a = arr_a[i];
Napi::Value val_b = arr_b[i];
vec_a[i] = val_a.As<Napi::Number>().FloatValue();
vec_b[i] = val_b.As<Napi::Number>().FloatValue();
}
hnswlib::DISTFUNC<float> df = l2space_->get_dist_func();
const float d = df(vec_a.data(), vec_b.data(), l2space_->get_dist_func_param());
return Napi::Number::New(info.Env(), d);
}
Napi::Value getNumDimensions(const Napi::CallbackInfo& info) { return Napi::Number::New(info.Env(), dim_); }
};
class InnerProductSpace : public Napi::ObjectWrap<InnerProductSpace> {
public:
uint32_t dim_;
std::unique_ptr<hnswlib::InnerProductSpace> ipspace_;
InnerProductSpace(const Napi::CallbackInfo& info) : Napi::ObjectWrap<InnerProductSpace>(info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return;
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return;
}
dim_ = info[0].As<Napi::Number>().Uint32Value();
ipspace_ = std::unique_ptr<hnswlib::InnerProductSpace>(new hnswlib::InnerProductSpace(static_cast<size_t>(dim_)));
}
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// clang-format off
Napi::Function func = DefineClass(env, "InnerProductSpace", {
InstanceMethod("distance", &InnerProductSpace::distance),
InstanceMethod("getNumDimensions", &InnerProductSpace::getNumDimensions)
});
// clang-format on
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("InnerProductSpace", func);
return exports;
}
private:
Napi::Value distance(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 2) {
Napi::Error::New(env, "Expected 2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsArray()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Array arr_a = info[0].As<Napi::Array>();
Napi::Array arr_b = info[1].As<Napi::Array>();
if (arr_a.Length() != dim_) {
Napi::Error::New(env, "Invalid the first array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr_a.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
if (arr_b.Length() != dim_) {
Napi::Error::New(env, "Invalid the second array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr_b.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec_a(dim_, 0.0);
std::vector<float> vec_b(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val_a = arr_a[i];
Napi::Value val_b = arr_b[i];
vec_a[i] = val_a.As<Napi::Number>().FloatValue();
vec_b[i] = val_b.As<Napi::Number>().FloatValue();
}
hnswlib::DISTFUNC<float> df = ipspace_->get_dist_func();
const float d = df(vec_a.data(), vec_b.data(), ipspace_->get_dist_func_param());
return Napi::Number::New(info.Env(), d);
}
Napi::Value getNumDimensions(const Napi::CallbackInfo& info) { return Napi::Number::New(info.Env(), dim_); }
};
class CustomFilterFunctor : public hnswlib::BaseFilterFunctor {
public:
CustomFilterFunctor(const Napi::Env& env, const Napi::Function& callback) : env_(env), callback_(callback) {}
bool operator()(hnswlib::labeltype id) { return callback_.Call({Napi::Number::New(env_, id)}).ToBoolean().Value(); }
private:
Napi::Env env_;
Napi::Function callback_;
};
class LoadBruteforceSearchIndexWorker : public Napi::AsyncWorker {
public:
LoadBruteforceSearchIndexWorker(const std::string& filename, hnswlib::BruteforceSearch<float>** index,
hnswlib::SpaceInterface<float>** space, Napi::Promise::Deferred deferred,
Napi::Function& callback)
: Napi::AsyncWorker(callback), deferred(deferred), filename_(filename), result_(false), index_(index), space_(space) {}
~LoadBruteforceSearchIndexWorker() {}
void Execute() {
try {
std::ifstream ifs(filename_);
if (ifs.is_open()) {
ifs.close();
} else {
throw std::runtime_error("failed to open file: " + filename_);
}
if (*index_) delete *index_;
*index_ = new hnswlib::BruteforceSearch<float>(*space_, filename_);
result_ = true;
} catch (const std::exception& e) {
result_ = false;
SetError("Hnswlib Error: " + std::string(e.what()));
}
}
void OnOK() {
Napi::HandleScope scope(Env());
Napi::Boolean result = Napi::Boolean::New(Env(), result_);
deferred.Resolve(result);
}
void OnError(const Napi::Error& e) { deferred.Reject(e.Value()); }
Napi::Promise::Deferred deferred;
private:
std::string filename_;
bool result_;
hnswlib::BruteforceSearch<float>** index_;
hnswlib::SpaceInterface<float>** space_;
};
class SaveBruteforceSearchIndexWorker : public Napi::AsyncWorker {
public:
SaveBruteforceSearchIndexWorker(const std::string& filename, hnswlib::BruteforceSearch<float>** index,
Napi::Promise::Deferred deferred, Napi::Function& callback)
: Napi::AsyncWorker(callback), deferred(deferred), filename_(filename), result_(false), index_(index) {}
~SaveBruteforceSearchIndexWorker() {}
void Execute() {
try {
if (*index_ == nullptr) throw std::runtime_error("search index is not constructed.");
(*index_)->saveIndex(filename_);
result_ = true;
} catch (const std::exception& e) {
result_ = false;
SetError("Hnswlib Error: " + std::string(e.what()));
}
}
void OnOK() {
Napi::HandleScope scope(Env());
Napi::Boolean result = Napi::Boolean::New(Env(), result_);
deferred.Resolve(result);
}
void OnError(const Napi::Error& e) { deferred.Reject(e.Value()); }
Napi::Promise::Deferred deferred;
private:
std::string filename_;
bool result_;
hnswlib::BruteforceSearch<float>** index_;
};
class BruteforceSearch : public Napi::ObjectWrap<BruteforceSearch> {
public:
uint32_t dim_;
hnswlib::BruteforceSearch<float>* index_;
hnswlib::SpaceInterface<float>* space_;
bool normalize_;
BruteforceSearch(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<BruteforceSearch>(info), index_(nullptr), space_(nullptr), normalize_(false) {
Napi::Env env = info.Env();
if (info.Length() != 2) {
Napi::Error::New(env, "Expected 2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return;
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return;
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return;
}
const std::string space_name = info[0].As<Napi::String>().ToString();
if (space_name != "l2" && space_name != "ip" && space_name != "cosine") {
Napi::Error::New(env, "Wrong space name, expected \"l2\", \"ip\", or \"cosine\".").ThrowAsJavaScriptException();
return;
}
dim_ = info[1].As<Napi::Number>().Uint32Value();
try {
if (space_name == "l2") {
space_ = new hnswlib::L2Space(static_cast<size_t>(dim_));
} else if (space_name == "ip") {
space_ = new hnswlib::InnerProductSpace(static_cast<size_t>(dim_));
} else {
space_ = new hnswlib::InnerProductSpace(static_cast<size_t>(dim_));
normalize_ = true;
}
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return;
}
}
~BruteforceSearch() {
if (space_) delete space_;
if (index_) delete index_;
}
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// clang-format off
Napi::Function func = DefineClass(env, "BruteforceSearch", {
InstanceMethod("initIndex", &BruteforceSearch::initIndex),
InstanceMethod("readIndex", &BruteforceSearch::readIndex),
InstanceMethod("readIndexSync", &BruteforceSearch::readIndexSync),
InstanceMethod("writeIndex", &BruteforceSearch::writeIndex),
InstanceMethod("writeIndexSync", &BruteforceSearch::writeIndexSync),
InstanceMethod("addPoint", &BruteforceSearch::addPoint),
InstanceMethod("removePoint", &BruteforceSearch::removePoint),
InstanceMethod("searchKnn", &BruteforceSearch::searchKnn),
InstanceMethod("getMaxElements", &BruteforceSearch::getMaxElements),
InstanceMethod("getCurrentCount", &BruteforceSearch::getCurrentCount),
InstanceMethod("getNumDimensions", &BruteforceSearch::getNumDimensions)
});
// clang-format on
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData<Napi::FunctionReference>(constructor);
exports.Set("BruteforceSearch", func);
return exports;
}
private:
Napi::Value initIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t max_elements = info[0].As<Napi::Number>().Uint32Value();
if (index_) delete index_;
try {
index_ = new hnswlib::BruteforceSearch<float>(space_, static_cast<size_t>(max_elements));
} catch (const std::bad_alloc& err) {
index_ = nullptr;
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value readIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
Napi::Function callback = Napi::Function::New(env, [](const Napi::CallbackInfo& info) { return info.Env().Undefined(); });
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
LoadBruteforceSearchIndexWorker* worker =
new LoadBruteforceSearchIndexWorker(filename, &index_, &space_, deferred, callback);
worker->Queue();
return worker->deferred.Promise();
}
Napi::Value readIndexSync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
try {
std::ifstream ifs(filename);
if (ifs.is_open()) {
ifs.close();
} else {
throw std::runtime_error("failed to open file: " + filename);
}
if (index_) delete index_;
index_ = new hnswlib::BruteforceSearch<float>(space_, filename);
} catch (const std::exception& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value writeIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
Napi::Function callback = Napi::Function::New(env, [](const Napi::CallbackInfo& info) { return info.Env().Undefined(); });
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
SaveBruteforceSearchIndexWorker* worker = new SaveBruteforceSearchIndexWorker(filename, &index_, deferred, callback);
worker->Queue();
return worker->deferred.Promise();
}
Napi::Value writeIndexSync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
if (index_ == nullptr) {
Napi::Error::New(env, "Search index is not constructed.").ThrowAsJavaScriptException();
return env.Null();
}
index_->saveIndex(filename);
return env.Null();
}
Napi::Value addPoint(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 2) {
Napi::Error::New(env, "Expected 2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Array arr = info[0].As<Napi::Array>();
if (arr.Length() != dim_) {
Napi::Error::New(env, "Invalid the given array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val = arr[i];
vec[i] = val.As<Napi::Number>().FloatValue();
}
if (normalize_) normalizePoint(vec);
const uint32_t idx = info[1].As<Napi::Number>().Uint32Value();
try {
index_->addPoint(reinterpret_cast<void*>(vec.data()), static_cast<hnswlib::labeltype>(idx));
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value removePoint(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t idx = info[0].As<Napi::Number>().Uint32Value();
index_->removePoint(static_cast<hnswlib::labeltype>(idx));
return env.Null();
}
Napi::Value searchKnn(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::Error::New(env, "Expected 2-3 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[2].IsUndefined() && !info[2].IsFunction()) {
Napi::TypeError::New(env, "Invalid the third argument type, must be a function.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
CustomFilterFunctor* filterFn = nullptr;
if (info[2].IsFunction()) {
try {
filterFn = new CustomFilterFunctor(env, info[2].As<Napi::Function>());
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return env.Null();
}
}
Napi::Array arr = info[0].As<Napi::Array>();
if (arr.Length() != dim_) {
Napi::Error::New(env, "Invalid the given array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t k = info[1].As<Napi::Number>().Uint32Value();
if (k > index_->maxelements_) {
Napi::Error::New(env, "Invalid the number of k-nearest neighbors (cannot be given a value greater than `maxElements`: " +
std::to_string(index_->maxelements_) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
if (k <= 0) {
Napi::Error::New(env, "Invalid the number of k-nearest neighbors (must be a positive number).")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val = arr[i];
vec[i] = val.As<Napi::Number>().FloatValue();
}
if (normalize_) normalizePoint(vec);
std::priority_queue<std::pair<float, size_t>> knn =
index_->searchKnn(reinterpret_cast<void*>(vec.data()), static_cast<size_t>(k), filterFn);
const size_t n_results = knn.size();
Napi::Array arr_distances = Napi::Array::New(env, n_results);
Napi::Array arr_neighbors = Napi::Array::New(env, n_results);
for (int32_t i = static_cast<int32_t>(n_results) - 1; i >= 0; i--) {
const std::pair<float, size_t>& nn = knn.top();
arr_distances[i] = Napi::Number::New(env, nn.first);
arr_neighbors[i] = Napi::Number::New(env, nn.second);
knn.pop();
}
if (filterFn) delete filterFn;
Napi::Object results = Napi::Object::New(env);
results.Set("distances", arr_distances);
results.Set("neighbors", arr_neighbors);
return results;
}
Napi::Value getMaxElements(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) return Napi::Number::New(env, 0);
return Napi::Number::New(env, index_->maxelements_);
}
Napi::Value getCurrentCount(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) return Napi::Number::New(env, 0);
return Napi::Number::New(env, index_->cur_element_count);
}
Napi::Value getNumDimensions(const Napi::CallbackInfo& info) { return Napi::Number::New(info.Env(), dim_); }
};
class LoadHierarchicalNSWIndexWorker : public Napi::AsyncWorker {
public:
LoadHierarchicalNSWIndexWorker(const std::string& filename, const bool allow_replace_deleted,
hnswlib::HierarchicalNSW<float>** index, hnswlib::SpaceInterface<float>** space,
Napi::Promise::Deferred deferred, Napi::Function& callback)
: Napi::AsyncWorker(callback), deferred(deferred), filename_(filename), allow_replace_deleted_(allow_replace_deleted),
result_(false), index_(index), space_(space) {}
~LoadHierarchicalNSWIndexWorker() {}
void Execute() {
try {
std::ifstream ifs(filename_);
if (ifs.is_open()) {
ifs.close();
} else {
throw std::runtime_error("failed to open file: " + filename_);
}
if (*index_) delete *index_;
*index_ = new hnswlib::HierarchicalNSW<float>(*space_, filename_, false, 0, allow_replace_deleted_);
result_ = true;
} catch (const std::exception& e) {
result_ = false;
SetError("Hnswlib Error: " + std::string(e.what()));
}
}
void OnOK() {
Napi::HandleScope scope(Env());
Napi::Boolean result = Napi::Boolean::New(Env(), result_);
deferred.Resolve(result);
}
void OnError(const Napi::Error& e) { deferred.Reject(e.Value()); }
Napi::Promise::Deferred deferred;
private:
std::string filename_;
bool allow_replace_deleted_;
bool result_;
hnswlib::HierarchicalNSW<float>** index_;
hnswlib::SpaceInterface<float>** space_;
};
class SaveHierarchicalNSWIndexWorker : public Napi::AsyncWorker {
public:
SaveHierarchicalNSWIndexWorker(const std::string& filename, hnswlib::HierarchicalNSW<float>** index,
Napi::Promise::Deferred deferred, Napi::Function& callback)
: Napi::AsyncWorker(callback), deferred(deferred), filename_(filename), result_(false), index_(index) {}
~SaveHierarchicalNSWIndexWorker() {}
void Execute() {
try {
if (*index_ == nullptr) throw std::runtime_error("search index is not constructed.");
(*index_)->saveIndex(filename_);
result_ = true;
} catch (const std::exception& e) {
result_ = false;
SetError("Hnswlib Error: " + std::string(e.what()));
}
}
void OnOK() {
Napi::HandleScope scope(Env());
Napi::Boolean result = Napi::Boolean::New(Env(), result_);
deferred.Resolve(result);
}
void OnError(const Napi::Error& e) { deferred.Reject(e.Value()); }
Napi::Promise::Deferred deferred;
private:
std::string filename_;
bool result_;
hnswlib::HierarchicalNSW<float>** index_;
};
class HierarchicalNSW : public Napi::ObjectWrap<HierarchicalNSW> {
public:
uint32_t dim_;
hnswlib::HierarchicalNSW<float>* index_;
hnswlib::SpaceInterface<float>* space_;
bool normalize_;
HierarchicalNSW(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<HierarchicalNSW>(info), index_(nullptr), space_(nullptr), normalize_(false) {
Napi::Env env = info.Env();
if (info.Length() != 2) {
Napi::Error::New(env, "Expected 2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return;
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return;
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return;
}
const std::string space_name = info[0].As<Napi::String>().ToString();
if (space_name != "l2" && space_name != "ip" && space_name != "cosine") {
Napi::Error::New(env, "Wrong space name, expected \"l2\", \"ip\", or \"cosine\".").ThrowAsJavaScriptException();
return;
}
dim_ = info[1].As<Napi::Number>().Uint32Value();
try {
if (space_name == "l2") {
space_ = new hnswlib::L2Space(static_cast<size_t>(dim_));
} else if (space_name == "ip") {
space_ = new hnswlib::InnerProductSpace(static_cast<size_t>(dim_));
} else {
space_ = new hnswlib::InnerProductSpace(static_cast<size_t>(dim_));
normalize_ = true;
}
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return;
}
}
~HierarchicalNSW() {
if (space_) delete space_;
if (index_) delete index_;
}
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// clang-format off
Napi::Function func = DefineClass(env, "HierarchicalNSW", {
InstanceMethod("initIndex", &HierarchicalNSW::initIndex),
InstanceMethod("readIndex", &HierarchicalNSW::readIndex),
InstanceMethod("readIndexSync", &HierarchicalNSW::readIndexSync),
InstanceMethod("writeIndex", &HierarchicalNSW::writeIndex),
InstanceMethod("writeIndexSync", &HierarchicalNSW::writeIndexSync),
InstanceMethod("resizeIndex", &HierarchicalNSW::resizeIndex),
InstanceMethod("addPoint", &HierarchicalNSW::addPoint),
InstanceMethod("markDelete", &HierarchicalNSW::markDelete),
InstanceMethod("unmarkDelete", &HierarchicalNSW::unmarkDelete),
InstanceMethod("searchKnn", &HierarchicalNSW::searchKnn),
InstanceMethod("getIdsList", &HierarchicalNSW::getIdsList),
InstanceMethod("getPoint", &HierarchicalNSW::getPoint),
InstanceMethod("getMaxElements", &HierarchicalNSW::getMaxElements),
InstanceMethod("getCurrentCount", &HierarchicalNSW::getCurrentCount),
InstanceMethod("getNumDimensions", &HierarchicalNSW::getNumDimensions),
InstanceMethod("getEf", &HierarchicalNSW::getEf),
InstanceMethod("setEf", &HierarchicalNSW::setEf)
});
// clang-format on
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData<Napi::FunctionReference>(constructor);
exports.Set("HierarchicalNSW", func);
return exports;
}
private:
Napi::Value initIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1) {
Napi::Error::New(env, "Expected 1-5 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
const bool given_named_args = info.Length() == 1 && info[0].IsObject();
const Napi::Object named_args = given_named_args ? info[0].As<Napi::Object>() : Napi::Object::New(env);
if (given_named_args) {
if (!named_args.Has("maxElements")) {
Napi::TypeError::New(env, "Missing named argument `maxElements`, must be specified.").ThrowAsJavaScriptException();
return env.Null();
}
if (!named_args.Get("maxElements").IsNumber()) {
Napi::TypeError::New(env, "Invalid the named argument type `maxElements`, must be a number.")
.ThrowAsJavaScriptException();
return env.Null();
}
if (named_args.Has("m") && !named_args.Get("m").IsNumber()) {
Napi::TypeError::New(env, "Invalid the named argument type `m`, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (named_args.Has("efConstruction") && !named_args.Get("efConstruction").IsNumber()) {
Napi::TypeError::New(env, "Invalid the named argument type `efConstruction`, must be a number.")
.ThrowAsJavaScriptException();
return env.Null();
}
if (named_args.Has("randomSeed") && !named_args.Get("randomSeed").IsNumber()) {
Napi::TypeError::New(env, "Invalid the named argument type `randomSeed`, must be a number.")
.ThrowAsJavaScriptException();
return env.Null();
}
if (named_args.Has("allowReplaceDeleted") && !named_args.Get("allowReplaceDeleted").IsBoolean()) {
Napi::TypeError::New(env, "Invalid the named argument type `allowRepalceDeleted`, must be a boolean.")
.ThrowAsJavaScriptException();
return env.Null();
}
} else {
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsUndefined() && !info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the sencond argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[2].IsUndefined() && !info[2].IsNumber()) {
Napi::TypeError::New(env, "Invalid the third argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[3].IsUndefined() && !info[3].IsNumber()) {
Napi::TypeError::New(env, "Invalid the fourth argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[4].IsUndefined() && !info[4].IsBoolean()) {
Napi::TypeError::New(env, "Invalid the fifth argument type, must be a boolean.").ThrowAsJavaScriptException();
return env.Null();
}
}
const uint32_t max_elements_ = given_named_args ? named_args.Get("maxElements").As<Napi::Number>().Uint32Value()
: info[0].As<Napi::Number>().Uint32Value();
const uint32_t m_ = given_named_args ? (!named_args.Has("m") ? 16 : named_args.Get("m").As<Napi::Number>().Uint32Value())
: (info[1].IsUndefined() ? 16 : info[1].As<Napi::Number>().Uint32Value());
const uint32_t ef_construction_ =
given_named_args
? (!named_args.Has("efConstruction") ? 200 : named_args.Get("efConstruction").As<Napi::Number>().Uint32Value())
: (info[2].IsUndefined() ? 200 : info[2].As<Napi::Number>().Uint32Value());
const uint32_t random_seed_ =
given_named_args ? (!named_args.Has("randomSeed") ? 100 : named_args.Get("randomSeed").As<Napi::Number>().Uint32Value())
: (info[3].IsUndefined() ? 100 : info[3].As<Napi::Number>().Uint32Value());
const bool allow_replace_deleted_ =
given_named_args
? (!named_args.Has("allowReplaceDeleted") ? false : named_args.Get("allowReplaceDeleted").As<Napi::Boolean>().Value())
: (info[4].IsUndefined() ? false : info[4].As<Napi::Boolean>().Value());
if (index_) delete index_;
try {
index_ = new hnswlib::HierarchicalNSW<float>(space_, static_cast<size_t>(max_elements_), static_cast<size_t>(m_),
static_cast<size_t>(ef_construction_), static_cast<size_t>(random_seed_),
allow_replace_deleted_);
} catch (const std::bad_alloc& err) {
index_ = nullptr;
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value readIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1) {
Napi::Error::New(env, "Expected 1-2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsUndefined() && !info[1].IsBoolean()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a boolean.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
const bool allow_replace_deleted = info[1].IsUndefined() ? false : info[1].As<Napi::Boolean>().Value();
Napi::Function callback = Napi::Function::New(env, [](const Napi::CallbackInfo& info) { return info.Env().Undefined(); });
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
LoadHierarchicalNSWIndexWorker* worker =
new LoadHierarchicalNSWIndexWorker(filename, allow_replace_deleted, &index_, &space_, deferred, callback);
worker->Queue();
return worker->deferred.Promise();
}
Napi::Value readIndexSync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1) {
Napi::Error::New(env, "Expected 1-2 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsUndefined() && !info[1].IsBoolean()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a boolean.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
const bool replace_deleted = info[1].IsUndefined() ? false : info[1].As<Napi::Boolean>().Value();
if (index_) delete index_;
try {
std::ifstream ifs(filename);
if (ifs.is_open()) {
ifs.close();
} else {
throw std::runtime_error("failed to open file: " + filename);
}
index_ = new hnswlib::HierarchicalNSW<float>(space_, filename, false, 0, replace_deleted);
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(err.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value writeIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
Napi::Function callback = Napi::Function::New(env, [](const Napi::CallbackInfo& info) { return info.Env().Undefined(); });
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
SaveHierarchicalNSWIndexWorker* worker = new SaveHierarchicalNSWIndexWorker(filename, &index_, deferred, callback);
worker->Queue();
return worker->deferred.Promise();
}
Napi::Value writeIndexSync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a string.").ThrowAsJavaScriptException();
return env.Null();
}
const std::string filename = info[0].As<Napi::String>().ToString();
if (index_ == nullptr) {
Napi::Error::New(env, "Search index is not constructed.").ThrowAsJavaScriptException();
return env.Null();
}
index_->saveIndex(filename);
return env.Null();
}
Napi::Value resizeIndex(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t new_max_elements = info[0].As<Napi::Number>().Uint32Value();
try {
index_->resizeIndex(static_cast<size_t>(new_max_elements));
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value addPoint(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::Error::New(env, "Expected 2-3 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[2].IsUndefined() && !info[2].IsBoolean()) {
Napi::TypeError::New(env, "Invalid the third argument type, must be a boolean.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Array arr = info[0].As<Napi::Array>();
if (arr.Length() != dim_) {
Napi::Error::New(env, "Invalid the given array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val = arr[i];
vec[i] = val.As<Napi::Number>().FloatValue();
}
if (normalize_) normalizePoint(vec);
const uint32_t idx = info[1].As<Napi::Number>().Uint32Value();
const bool replace_deleted = info[2].IsUndefined() ? false : info[2].As<Napi::Boolean>().Value();
try {
index_->addPoint(reinterpret_cast<void*>(vec.data()), static_cast<hnswlib::labeltype>(idx), replace_deleted);
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value markDelete(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t idx = info[0].As<Napi::Number>().Uint32Value();
try {
index_->markDelete(static_cast<hnswlib::labeltype>(idx));
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value unmarkDelete(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t idx = info[0].As<Napi::Number>().Uint32Value();
try {
index_->unmarkDelete(static_cast<hnswlib::labeltype>(idx));
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return env.Null();
}
return env.Null();
}
Napi::Value searchKnn(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::Error::New(env, "Expected 2-3 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsArray()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be an Array.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[1].IsNumber()) {
Napi::TypeError::New(env, "Invalid the second argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[2].IsUndefined() && !info[2].IsFunction()) {
Napi::TypeError::New(env, "Invalid the third argument type, must be a function.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Array arr = info[0].As<Napi::Array>();
if (arr.Length() != dim_) {
Napi::Error::New(env, "Invalid the given array length (expected " + std::to_string(dim_) + ", but got " +
std::to_string(arr.Length()) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
CustomFilterFunctor* filterFn = nullptr;
if (info[2].IsFunction()) {
try {
filterFn = new CustomFilterFunctor(env, info[2].As<Napi::Function>());
} catch (const std::bad_alloc& err) {
Napi::Error::New(env, err.what()).ThrowAsJavaScriptException();
return env.Null();
}
}
const uint32_t k = info[1].As<Napi::Number>().Uint32Value();
if (k > index_->max_elements_) {
Napi::Error::New(env, "Invalid the number of k-nearest neighbors (cannot be given a value greater than `maxElements`: " +
std::to_string(index_->max_elements_) + ").")
.ThrowAsJavaScriptException();
return env.Null();
}
std::vector<float> vec(dim_, 0.0);
for (uint32_t i = 0; i < dim_; i++) {
Napi::Value val = arr[i];
vec[i] = val.As<Napi::Number>().FloatValue();
}
if (normalize_) normalizePoint(vec);
std::priority_queue<std::pair<float, size_t>> knn =
index_->searchKnn(reinterpret_cast<void*>(vec.data()), static_cast<size_t>(k), filterFn);
const size_t n_results = knn.size();
Napi::Array arr_distances = Napi::Array::New(env, n_results);
Napi::Array arr_neighbors = Napi::Array::New(env, n_results);
for (int32_t i = static_cast<int32_t>(n_results) - 1; i >= 0; i--) {
const std::pair<float, size_t>& nn = knn.top();
arr_distances[i] = Napi::Number::New(env, nn.first);
arr_neighbors[i] = Napi::Number::New(env, nn.second);
knn.pop();
}
if (filterFn) delete filterFn;
Napi::Object results = Napi::Object::New(env);
results.Set("distances", arr_distances);
results.Set("neighbors", arr_neighbors);
return results;
}
Napi::Value getIdsList(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) return Napi::Array::New(env, 0);
uint32_t n_elements = index_->label_lookup_.size();
Napi::Array ids = Napi::Array::New(env, n_elements);
uint32_t counter = 0;
for (auto kv : index_->label_lookup_) {
ids[counter] = Napi::Number::New(env, kv.first);
counter++;
}
return ids;
}
Napi::Value getPoint(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) return Napi::Array::New(env, 0);
try {
const uint32_t label = info[0].As<Napi::Number>().Uint32Value();
std::vector<float> vec = index_->getDataByLabel<float>(static_cast<size_t>(label));
Napi::Array point = Napi::Array::New(env, vec.size());
for (size_t i = 0; i < vec.size(); i++) point[i] = Napi::Number::New(env, vec[i]);
return point;
} catch (const std::runtime_error& e) {
Napi::Error::New(env, "Hnswlib Error: " + std::string(e.what())).ThrowAsJavaScriptException();
return Napi::Array::New(env, 0);
}
}
Napi::Value getMaxElements(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) return Napi::Number::New(env, 0);
return Napi::Number::New(env, index_->max_elements_);
}
Napi::Value getCurrentCount(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) return Napi::Number::New(env, 0);
return Napi::Number::New(env, index_->cur_element_count);
}
Napi::Value getNumDimensions(const Napi::CallbackInfo& info) { return Napi::Number::New(info.Env(), dim_); }
Napi::Value getEf(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
return Napi::Number::New(env, index_->ef_);
}
Napi::Value setEf(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() != 1) {
Napi::Error::New(env, "Expected 1 arguments, but got " + std::to_string(info.Length()) + ".")
.ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber()) {
Napi::TypeError::New(env, "Invalid the first argument type, must be a number.").ThrowAsJavaScriptException();
return env.Null();
}
if (index_ == nullptr) {
Napi::Error::New(env, "Search index has not been initialized, call `initIndex` in advance.").ThrowAsJavaScriptException();
return env.Null();
}
const uint32_t ef = info[0].As<Napi::Number>().Uint32Value();
index_->setEf(static_cast<size_t>(ef));
return env.Null();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
L2Space::Init(env, exports);
InnerProductSpace::Init(env, exports);
BruteforceSearch::Init(env, exports);
HierarchicalNSW::Init(env, exports);
return exports;
}
NODE_API_MODULE(addon, Init)