@samyok/annoy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@samyok/annoy",
3
+ "version": "1.0.0",
4
+ "description": "Native Node.js bindings for Spotify's Annoy — fast approximate nearest neighbor search",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "install": "node-gyp rebuild",
9
+ "build:native": "node-gyp rebuild",
10
+ "build:ts": "tsc",
11
+ "build": "npm run build:native && npm run build:ts",
12
+ "test": "npm run build:ts && node dist/test.js",
13
+ "prepublishOnly": "npm run build:ts"
14
+ },
15
+ "keywords": [
16
+ "annoy",
17
+ "approximate-nearest-neighbors",
18
+ "nearest-neighbor",
19
+ "knn",
20
+ "vector-search",
21
+ "similarity-search",
22
+ "embeddings",
23
+ "native",
24
+ "n-api",
25
+ "node-addon"
26
+ ],
27
+ "author": "samyok",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/samyok/annoy.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/samyok/annoy/issues"
34
+ },
35
+ "homepage": "https://github.com/samyok/annoy#readme",
36
+ "engines": {
37
+ "node": ">=16.0.0"
38
+ },
39
+ "dependencies": {
40
+ "node-addon-api": "^8.3.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^25.7.0",
44
+ "node-gyp": "^11.2.0",
45
+ "typescript": "^5.8.3"
46
+ },
47
+ "files": [
48
+ "dist/**/*.js",
49
+ "dist/**/*.d.ts",
50
+ "src/addon.cc",
51
+ "deps/annoy/src/annoylib.h",
52
+ "deps/annoy/src/kissrandom.h",
53
+ "deps/annoy/src/mman.h",
54
+ "binding.gyp",
55
+ "README.md",
56
+ "LICENSE"
57
+ ],
58
+ "license": "Apache-2.0"
59
+ }
package/src/addon.cc ADDED
@@ -0,0 +1,353 @@
1
+ #include <napi.h>
2
+ #include "annoylib.h"
3
+ #include "kissrandom.h"
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ using namespace Annoy;
8
+
9
+ template <typename Distance>
10
+ class AnnoyIndexWrapper : public Napi::ObjectWrap<AnnoyIndexWrapper<Distance>> {
11
+ public:
12
+ using IndexType = AnnoyIndex<int32_t, float, Distance, Kiss64Random, AnnoyIndexMultiThreadedBuildPolicy>;
13
+
14
+ static Napi::FunctionReference constructor;
15
+
16
+ static Napi::Function Init(Napi::Env env, const char* name) {
17
+ Napi::Function func = AnnoyIndexWrapper::DefineClass(env, name, {
18
+ AnnoyIndexWrapper::InstanceMethod("addItem", &AnnoyIndexWrapper::AddItem),
19
+ AnnoyIndexWrapper::InstanceMethod("build", &AnnoyIndexWrapper::Build),
20
+ AnnoyIndexWrapper::InstanceMethod("unbuild", &AnnoyIndexWrapper::Unbuild),
21
+ AnnoyIndexWrapper::InstanceMethod("save", &AnnoyIndexWrapper::Save),
22
+ AnnoyIndexWrapper::InstanceMethod("load", &AnnoyIndexWrapper::Load),
23
+ AnnoyIndexWrapper::InstanceMethod("unload", &AnnoyIndexWrapper::Unload),
24
+ AnnoyIndexWrapper::InstanceMethod("getDistance", &AnnoyIndexWrapper::GetDistance),
25
+ AnnoyIndexWrapper::InstanceMethod("getNnsByItem", &AnnoyIndexWrapper::GetNnsByItem),
26
+ AnnoyIndexWrapper::InstanceMethod("getNnsByVector", &AnnoyIndexWrapper::GetNnsByVector),
27
+ AnnoyIndexWrapper::InstanceMethod("getNItems", &AnnoyIndexWrapper::GetNItems),
28
+ AnnoyIndexWrapper::InstanceMethod("getNTrees", &AnnoyIndexWrapper::GetNTrees),
29
+ AnnoyIndexWrapper::InstanceMethod("getItem", &AnnoyIndexWrapper::GetItem),
30
+ AnnoyIndexWrapper::InstanceMethod("setSeed", &AnnoyIndexWrapper::SetSeed),
31
+ AnnoyIndexWrapper::InstanceMethod("onDiskBuild", &AnnoyIndexWrapper::OnDiskBuild),
32
+ AnnoyIndexWrapper::InstanceMethod("verbose", &AnnoyIndexWrapper::Verbose),
33
+ AnnoyIndexWrapper::InstanceMethod("getF", &AnnoyIndexWrapper::GetF),
34
+ });
35
+ constructor = Napi::Persistent(func);
36
+ constructor.SuppressDestruct();
37
+ return func;
38
+ }
39
+
40
+ AnnoyIndexWrapper(const Napi::CallbackInfo& info)
41
+ : Napi::ObjectWrap<AnnoyIndexWrapper<Distance>>(info) {
42
+ Napi::Env env = info.Env();
43
+ if (info.Length() < 1 || !info[0].IsNumber()) {
44
+ Napi::TypeError::New(env, "Expected number of dimensions as first argument").ThrowAsJavaScriptException();
45
+ return;
46
+ }
47
+ int f = info[0].As<Napi::Number>().Int32Value();
48
+ index_ = new IndexType(f);
49
+ }
50
+
51
+ ~AnnoyIndexWrapper() {
52
+ delete index_;
53
+ }
54
+
55
+ private:
56
+ IndexType* index_;
57
+
58
+ Napi::Value AddItem(const Napi::CallbackInfo& info) {
59
+ Napi::Env env = info.Env();
60
+ if (info.Length() < 2 || !info[0].IsNumber()) {
61
+ Napi::TypeError::New(env, "Expected (index: number, vector: number[])").ThrowAsJavaScriptException();
62
+ return env.Undefined();
63
+ }
64
+
65
+ int32_t item = info[0].As<Napi::Number>().Int32Value();
66
+ std::vector<float> vec = jsArrayToVector(env, info[1]);
67
+
68
+ char* error = nullptr;
69
+ if (!index_->add_item(item, vec.data(), &error)) {
70
+ std::string msg = error ? error : "Failed to add item";
71
+ free(error);
72
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
73
+ return env.Undefined();
74
+ }
75
+ return env.Undefined();
76
+ }
77
+
78
+ Napi::Value Build(const Napi::CallbackInfo& info) {
79
+ Napi::Env env = info.Env();
80
+ if (info.Length() < 1 || !info[0].IsNumber()) {
81
+ Napi::TypeError::New(env, "Expected number of trees").ThrowAsJavaScriptException();
82
+ return env.Undefined();
83
+ }
84
+
85
+ int q = info[0].As<Napi::Number>().Int32Value();
86
+ int n_threads = -1;
87
+ if (info.Length() > 1 && info[1].IsNumber()) {
88
+ n_threads = info[1].As<Napi::Number>().Int32Value();
89
+ }
90
+
91
+ char* error = nullptr;
92
+ if (!index_->build(q, n_threads, &error)) {
93
+ std::string msg = error ? error : "Failed to build";
94
+ free(error);
95
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
96
+ return env.Undefined();
97
+ }
98
+ return env.Undefined();
99
+ }
100
+
101
+ Napi::Value Unbuild(const Napi::CallbackInfo& info) {
102
+ Napi::Env env = info.Env();
103
+ char* error = nullptr;
104
+ if (!index_->unbuild(&error)) {
105
+ std::string msg = error ? error : "Failed to unbuild";
106
+ free(error);
107
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
108
+ }
109
+ return env.Undefined();
110
+ }
111
+
112
+ Napi::Value Save(const Napi::CallbackInfo& info) {
113
+ Napi::Env env = info.Env();
114
+ if (info.Length() < 1 || !info[0].IsString()) {
115
+ Napi::TypeError::New(env, "Expected filename").ThrowAsJavaScriptException();
116
+ return env.Undefined();
117
+ }
118
+
119
+ std::string filename = info[0].As<Napi::String>().Utf8Value();
120
+ bool prefault = false;
121
+ if (info.Length() > 1 && info[1].IsBoolean()) {
122
+ prefault = info[1].As<Napi::Boolean>().Value();
123
+ }
124
+
125
+ char* error = nullptr;
126
+ if (!index_->save(filename.c_str(), prefault, &error)) {
127
+ std::string msg = error ? error : "Failed to save";
128
+ free(error);
129
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
130
+ }
131
+ return env.Undefined();
132
+ }
133
+
134
+ Napi::Value Load(const Napi::CallbackInfo& info) {
135
+ Napi::Env env = info.Env();
136
+ if (info.Length() < 1 || !info[0].IsString()) {
137
+ Napi::TypeError::New(env, "Expected filename").ThrowAsJavaScriptException();
138
+ return env.Undefined();
139
+ }
140
+
141
+ std::string filename = info[0].As<Napi::String>().Utf8Value();
142
+ bool prefault = false;
143
+ if (info.Length() > 1 && info[1].IsBoolean()) {
144
+ prefault = info[1].As<Napi::Boolean>().Value();
145
+ }
146
+
147
+ char* error = nullptr;
148
+ if (!index_->load(filename.c_str(), prefault, &error)) {
149
+ std::string msg = error ? error : "Failed to load";
150
+ free(error);
151
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
152
+ }
153
+ return env.Undefined();
154
+ }
155
+
156
+ Napi::Value Unload(const Napi::CallbackInfo& info) {
157
+ index_->unload();
158
+ return info.Env().Undefined();
159
+ }
160
+
161
+ Napi::Value GetDistance(const Napi::CallbackInfo& info) {
162
+ Napi::Env env = info.Env();
163
+ if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
164
+ Napi::TypeError::New(env, "Expected (i: number, j: number)").ThrowAsJavaScriptException();
165
+ return env.Undefined();
166
+ }
167
+
168
+ int32_t i = info[0].As<Napi::Number>().Int32Value();
169
+ int32_t j = info[1].As<Napi::Number>().Int32Value();
170
+ return Napi::Number::New(env, index_->get_distance(i, j));
171
+ }
172
+
173
+ Napi::Value GetNnsByItem(const Napi::CallbackInfo& info) {
174
+ Napi::Env env = info.Env();
175
+ if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
176
+ Napi::TypeError::New(env, "Expected (item: number, n: number, searchK?: number, includeDistances?: boolean)")
177
+ .ThrowAsJavaScriptException();
178
+ return env.Undefined();
179
+ }
180
+
181
+ int32_t item = info[0].As<Napi::Number>().Int32Value();
182
+ size_t n = info[1].As<Napi::Number>().Uint32Value();
183
+ int search_k = -1;
184
+ bool include_distances = false;
185
+
186
+ if (info.Length() > 2 && info[2].IsNumber()) {
187
+ search_k = info[2].As<Napi::Number>().Int32Value();
188
+ }
189
+ if (info.Length() > 3 && info[3].IsBoolean()) {
190
+ include_distances = info[3].As<Napi::Boolean>().Value();
191
+ }
192
+
193
+ std::vector<int32_t> result;
194
+ std::vector<float> distances;
195
+ index_->get_nns_by_item(item, n, search_k,
196
+ &result,
197
+ include_distances ? &distances : nullptr);
198
+
199
+ if (include_distances) {
200
+ Napi::Object obj = Napi::Object::New(env);
201
+ obj.Set("neighbors", vectorToJsArray(env, result));
202
+ obj.Set("distances", floatVectorToJsArray(env, distances));
203
+ return obj;
204
+ }
205
+ return vectorToJsArray(env, result);
206
+ }
207
+
208
+ Napi::Value GetNnsByVector(const Napi::CallbackInfo& info) {
209
+ Napi::Env env = info.Env();
210
+ if (info.Length() < 2 || !info[1].IsNumber()) {
211
+ Napi::TypeError::New(env, "Expected (vector: number[], n: number, searchK?: number, includeDistances?: boolean)")
212
+ .ThrowAsJavaScriptException();
213
+ return env.Undefined();
214
+ }
215
+
216
+ std::vector<float> vec = jsArrayToVector(env, info[0]);
217
+ size_t n = info[1].As<Napi::Number>().Uint32Value();
218
+ int search_k = -1;
219
+ bool include_distances = false;
220
+
221
+ if (info.Length() > 2 && info[2].IsNumber()) {
222
+ search_k = info[2].As<Napi::Number>().Int32Value();
223
+ }
224
+ if (info.Length() > 3 && info[3].IsBoolean()) {
225
+ include_distances = info[3].As<Napi::Boolean>().Value();
226
+ }
227
+
228
+ std::vector<int32_t> result;
229
+ std::vector<float> distances;
230
+ index_->get_nns_by_vector(vec.data(), n, search_k,
231
+ &result,
232
+ include_distances ? &distances : nullptr);
233
+
234
+ if (include_distances) {
235
+ Napi::Object obj = Napi::Object::New(env);
236
+ obj.Set("neighbors", vectorToJsArray(env, result));
237
+ obj.Set("distances", floatVectorToJsArray(env, distances));
238
+ return obj;
239
+ }
240
+ return vectorToJsArray(env, result);
241
+ }
242
+
243
+ Napi::Value GetNItems(const Napi::CallbackInfo& info) {
244
+ return Napi::Number::New(info.Env(), index_->get_n_items());
245
+ }
246
+
247
+ Napi::Value GetNTrees(const Napi::CallbackInfo& info) {
248
+ return Napi::Number::New(info.Env(), index_->get_n_trees());
249
+ }
250
+
251
+ Napi::Value GetItem(const Napi::CallbackInfo& info) {
252
+ Napi::Env env = info.Env();
253
+ if (info.Length() < 1 || !info[0].IsNumber()) {
254
+ Napi::TypeError::New(env, "Expected item index").ThrowAsJavaScriptException();
255
+ return env.Undefined();
256
+ }
257
+
258
+ int32_t item = info[0].As<Napi::Number>().Int32Value();
259
+ int f = index_->get_f();
260
+ std::vector<float> vec(f);
261
+ index_->get_item(item, vec.data());
262
+ return floatVectorToJsArray(env, vec);
263
+ }
264
+
265
+ Napi::Value SetSeed(const Napi::CallbackInfo& info) {
266
+ Napi::Env env = info.Env();
267
+ if (info.Length() < 1 || !info[0].IsNumber()) {
268
+ Napi::TypeError::New(env, "Expected seed number").ThrowAsJavaScriptException();
269
+ return env.Undefined();
270
+ }
271
+ index_->set_seed(info[0].As<Napi::Number>().Int64Value());
272
+ return env.Undefined();
273
+ }
274
+
275
+ Napi::Value OnDiskBuild(const Napi::CallbackInfo& info) {
276
+ Napi::Env env = info.Env();
277
+ if (info.Length() < 1 || !info[0].IsString()) {
278
+ Napi::TypeError::New(env, "Expected filename").ThrowAsJavaScriptException();
279
+ return env.Undefined();
280
+ }
281
+
282
+ std::string filename = info[0].As<Napi::String>().Utf8Value();
283
+ char* error = nullptr;
284
+ if (!index_->on_disk_build(filename.c_str(), &error)) {
285
+ std::string msg = error ? error : "Failed to set on-disk build";
286
+ free(error);
287
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
288
+ }
289
+ return env.Undefined();
290
+ }
291
+
292
+ Napi::Value Verbose(const Napi::CallbackInfo& info) {
293
+ Napi::Env env = info.Env();
294
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
295
+ Napi::TypeError::New(env, "Expected boolean").ThrowAsJavaScriptException();
296
+ return env.Undefined();
297
+ }
298
+ index_->verbose(info[0].As<Napi::Boolean>().Value());
299
+ return env.Undefined();
300
+ }
301
+
302
+ Napi::Value GetF(const Napi::CallbackInfo& info) {
303
+ return Napi::Number::New(info.Env(), index_->get_f());
304
+ }
305
+
306
+ // Helpers
307
+ static std::vector<float> jsArrayToVector(Napi::Env env, Napi::Value val) {
308
+ if (!val.IsArray()) {
309
+ Napi::TypeError::New(env, "Expected an array of numbers").ThrowAsJavaScriptException();
310
+ return {};
311
+ }
312
+ Napi::Array arr = val.As<Napi::Array>();
313
+ uint32_t len = arr.Length();
314
+ std::vector<float> vec(len);
315
+ for (uint32_t i = 0; i < len; i++) {
316
+ vec[i] = arr.Get(i).As<Napi::Number>().FloatValue();
317
+ }
318
+ return vec;
319
+ }
320
+
321
+ static Napi::Array vectorToJsArray(Napi::Env env, const std::vector<int32_t>& vec) {
322
+ Napi::Array arr = Napi::Array::New(env, vec.size());
323
+ for (size_t i = 0; i < vec.size(); i++) {
324
+ arr.Set(i, Napi::Number::New(env, vec[i]));
325
+ }
326
+ return arr;
327
+ }
328
+
329
+ static Napi::Array floatVectorToJsArray(Napi::Env env, const std::vector<float>& vec) {
330
+ Napi::Array arr = Napi::Array::New(env, vec.size());
331
+ for (size_t i = 0; i < vec.size(); i++) {
332
+ arr.Set(i, Napi::Number::New(env, vec[i]));
333
+ }
334
+ return arr;
335
+ }
336
+ };
337
+
338
+ template <typename Distance>
339
+ Napi::FunctionReference AnnoyIndexWrapper<Distance>::constructor;
340
+
341
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
342
+ exports.Set("AnnoyIndexAngular",
343
+ AnnoyIndexWrapper<Angular>::Init(env, "AnnoyIndexAngular"));
344
+ exports.Set("AnnoyIndexEuclidean",
345
+ AnnoyIndexWrapper<Euclidean>::Init(env, "AnnoyIndexEuclidean"));
346
+ exports.Set("AnnoyIndexManhattan",
347
+ AnnoyIndexWrapper<Manhattan>::Init(env, "AnnoyIndexManhattan"));
348
+ exports.Set("AnnoyIndexDotProduct",
349
+ AnnoyIndexWrapper<DotProduct>::Init(env, "AnnoyIndexDotProduct"));
350
+ return exports;
351
+ }
352
+
353
+ NODE_API_MODULE(annoy, Init)