@faiss-node/native 0.1.4

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.
@@ -0,0 +1,969 @@
1
+ #include <napi.h>
2
+ // Include FAISS headers for idx_t
3
+ #include <faiss/MetricType.h>
4
+ #include "faiss_index.h"
5
+ #include <vector>
6
+ #include <memory>
7
+ #include <cstring>
8
+ #include <string>
9
+
10
+ // Forward declaration
11
+ class FaissIndexWrapperJS;
12
+
13
+ // ============================================================================
14
+ // Async Workers for Non-Blocking Operations
15
+ // ============================================================================
16
+
17
+ // Add Worker
18
+ class AddWorker : public Napi::AsyncWorker {
19
+ public:
20
+ AddWorker(FaissIndexWrapper* wrapper, const float* vectors, size_t n, int dims, Napi::Promise::Deferred deferred)
21
+ : Napi::AsyncWorker(deferred.Env(), "AddWorker"),
22
+ wrapper_(wrapper),
23
+ vectors_(vectors, vectors + n * dims),
24
+ n_(n),
25
+ deferred_(deferred) {
26
+ }
27
+
28
+ void Execute() override {
29
+ try {
30
+ if (wrapper_->IsDisposed()) {
31
+ SetError("Index has been disposed");
32
+ return;
33
+ }
34
+ wrapper_->Add(vectors_.data(), n_);
35
+ } catch (const std::exception& e) {
36
+ SetError(std::string("FAISS error: ") + e.what());
37
+ }
38
+ }
39
+
40
+ void OnOK() override {
41
+ deferred_.Resolve(Env().Undefined());
42
+ }
43
+
44
+ void OnError(const Napi::Error& e) override {
45
+ deferred_.Reject(e.Value());
46
+ }
47
+
48
+ private:
49
+ FaissIndexWrapper* wrapper_;
50
+ std::vector<float> vectors_;
51
+ size_t n_;
52
+ Napi::Promise::Deferred deferred_;
53
+ };
54
+
55
+ // Train Worker
56
+ class TrainWorker : public Napi::AsyncWorker {
57
+ public:
58
+ TrainWorker(FaissIndexWrapper* wrapper, const float* vectors, size_t n, int dims, Napi::Promise::Deferred deferred)
59
+ : Napi::AsyncWorker(deferred.Env(), "TrainWorker"),
60
+ wrapper_(wrapper),
61
+ vectors_(vectors, vectors + n * dims),
62
+ n_(n),
63
+ deferred_(deferred) {
64
+ }
65
+
66
+ void Execute() override {
67
+ try {
68
+ if (wrapper_->IsDisposed()) {
69
+ SetError("Index has been disposed");
70
+ return;
71
+ }
72
+ wrapper_->Train(vectors_.data(), n_);
73
+ } catch (const std::exception& e) {
74
+ SetError(std::string("FAISS error: ") + e.what());
75
+ }
76
+ }
77
+
78
+ void OnOK() override {
79
+ deferred_.Resolve(Env().Undefined());
80
+ }
81
+
82
+ void OnError(const Napi::Error& e) override {
83
+ deferred_.Reject(e.Value());
84
+ }
85
+
86
+ private:
87
+ FaissIndexWrapper* wrapper_;
88
+ std::vector<float> vectors_;
89
+ size_t n_;
90
+ Napi::Promise::Deferred deferred_;
91
+ };
92
+
93
+ // Search Worker
94
+ class SearchWorker : public Napi::AsyncWorker {
95
+ public:
96
+ SearchWorker(FaissIndexWrapper* wrapper, const float* query, int k, Napi::Promise::Deferred deferred)
97
+ : Napi::AsyncWorker(deferred.Env(), "SearchWorker"),
98
+ wrapper_(wrapper),
99
+ query_(query, query + wrapper->GetDimensions()),
100
+ k_(k),
101
+ deferred_(deferred) {
102
+ }
103
+
104
+ void Execute() override {
105
+ try {
106
+ if (wrapper_->IsDisposed()) {
107
+ SetError("Index has been disposed");
108
+ return;
109
+ }
110
+
111
+ size_t ntotal = wrapper_->GetTotalVectors();
112
+ if (ntotal == 0) {
113
+ SetError("Cannot search empty index");
114
+ return;
115
+ }
116
+
117
+ int actual_k = (k_ > static_cast<int>(ntotal)) ? static_cast<int>(ntotal) : k_;
118
+ distances_.resize(actual_k);
119
+ labels_.resize(actual_k);
120
+
121
+ wrapper_->Search(query_.data(), actual_k, distances_.data(), labels_.data());
122
+ } catch (const std::exception& e) {
123
+ SetError(std::string("FAISS error: ") + e.what());
124
+ }
125
+ }
126
+
127
+ void OnOK() override {
128
+ Napi::Env env = Env();
129
+ Napi::Object result = Napi::Object::New(env);
130
+
131
+ Napi::Float32Array distances = Napi::Float32Array::New(env, distances_.size());
132
+ memcpy(distances.Data(), distances_.data(), distances_.size() * sizeof(float));
133
+
134
+ Napi::Int32Array labels = Napi::Int32Array::New(env, labels_.size());
135
+ int32_t* labelsData = labels.Data();
136
+ for (size_t i = 0; i < labels_.size(); i++) {
137
+ labelsData[i] = static_cast<int32_t>(labels_[i]);
138
+ }
139
+
140
+ result.Set("distances", distances);
141
+ result.Set("labels", labels);
142
+ deferred_.Resolve(result);
143
+ }
144
+
145
+ void OnError(const Napi::Error& e) override {
146
+ deferred_.Reject(e.Value());
147
+ }
148
+
149
+ private:
150
+ FaissIndexWrapper* wrapper_;
151
+ std::vector<float> query_;
152
+ int k_;
153
+ std::vector<float> distances_;
154
+ std::vector<faiss::idx_t> labels_;
155
+ Napi::Promise::Deferred deferred_;
156
+ };
157
+
158
+ // SearchBatch Worker
159
+ class SearchBatchWorker : public Napi::AsyncWorker {
160
+ public:
161
+ SearchBatchWorker(FaissIndexWrapper* wrapper, const float* queries, size_t nq, int k, Napi::Promise::Deferred deferred)
162
+ : Napi::AsyncWorker(deferred.Env(), "SearchBatchWorker"),
163
+ wrapper_(wrapper),
164
+ queries_(queries, queries + nq * wrapper->GetDimensions()),
165
+ nq_(nq),
166
+ k_(k),
167
+ deferred_(deferred) {
168
+ }
169
+
170
+ void Execute() override {
171
+ try {
172
+ if (wrapper_->IsDisposed()) {
173
+ SetError("Index has been disposed");
174
+ return;
175
+ }
176
+
177
+ size_t ntotal = wrapper_->GetTotalVectors();
178
+ if (ntotal == 0) {
179
+ SetError("Cannot search empty index");
180
+ return;
181
+ }
182
+
183
+ int actual_k = (k_ > static_cast<int>(ntotal)) ? static_cast<int>(ntotal) : k_;
184
+ distances_.resize(nq_ * actual_k);
185
+ labels_.resize(nq_ * actual_k);
186
+
187
+ wrapper_->SearchBatch(queries_.data(), nq_, actual_k, distances_.data(), labels_.data());
188
+ } catch (const std::exception& e) {
189
+ SetError(std::string("FAISS error: ") + e.what());
190
+ }
191
+ }
192
+
193
+ void OnOK() override {
194
+ Napi::Env env = Env();
195
+ Napi::Object result = Napi::Object::New(env);
196
+
197
+ Napi::Float32Array distances = Napi::Float32Array::New(env, distances_.size());
198
+ memcpy(distances.Data(), distances_.data(), distances_.size() * sizeof(float));
199
+
200
+ Napi::Int32Array labels = Napi::Int32Array::New(env, labels_.size());
201
+ int32_t* labelsData = labels.Data();
202
+ for (size_t i = 0; i < labels_.size(); i++) {
203
+ labelsData[i] = static_cast<int32_t>(labels_[i]);
204
+ }
205
+
206
+ result.Set("distances", distances);
207
+ result.Set("labels", labels);
208
+ result.Set("nq", Napi::Number::New(env, nq_));
209
+ result.Set("k", Napi::Number::New(env, static_cast<int>(distances_.size() / nq_)));
210
+ deferred_.Resolve(result);
211
+ }
212
+
213
+ void OnError(const Napi::Error& e) override {
214
+ deferred_.Reject(e.Value());
215
+ }
216
+
217
+ private:
218
+ FaissIndexWrapper* wrapper_;
219
+ std::vector<float> queries_;
220
+ size_t nq_;
221
+ int k_;
222
+ std::vector<float> distances_;
223
+ std::vector<faiss::idx_t> labels_;
224
+ Napi::Promise::Deferred deferred_;
225
+ };
226
+
227
+ // Save Worker
228
+ class SaveWorker : public Napi::AsyncWorker {
229
+ public:
230
+ SaveWorker(FaissIndexWrapper* wrapper, const std::string& filename, Napi::Promise::Deferred deferred)
231
+ : Napi::AsyncWorker(deferred.Env(), "SaveWorker"),
232
+ wrapper_(wrapper),
233
+ filename_(filename),
234
+ deferred_(deferred) {
235
+ }
236
+
237
+ void Execute() override {
238
+ try {
239
+ if (wrapper_->IsDisposed()) {
240
+ SetError("Index has been disposed");
241
+ return;
242
+ }
243
+ wrapper_->Save(filename_);
244
+ } catch (const std::exception& e) {
245
+ SetError(std::string("FAISS error: ") + e.what());
246
+ }
247
+ }
248
+
249
+ void OnOK() override {
250
+ deferred_.Resolve(Env().Undefined());
251
+ }
252
+
253
+ void OnError(const Napi::Error& e) override {
254
+ deferred_.Reject(e.Value());
255
+ }
256
+
257
+ private:
258
+ FaissIndexWrapper* wrapper_;
259
+ std::string filename_;
260
+ Napi::Promise::Deferred deferred_;
261
+ };
262
+
263
+ // ToBuffer Worker
264
+ class ToBufferWorker : public Napi::AsyncWorker {
265
+ public:
266
+ ToBufferWorker(FaissIndexWrapper* wrapper, Napi::Promise::Deferred deferred)
267
+ : Napi::AsyncWorker(deferred.Env(), "ToBufferWorker"),
268
+ wrapper_(wrapper),
269
+ deferred_(deferred) {
270
+ }
271
+
272
+ void Execute() override {
273
+ try {
274
+ if (wrapper_->IsDisposed()) {
275
+ SetError("Index has been disposed");
276
+ return;
277
+ }
278
+ buffer_ = wrapper_->ToBuffer();
279
+ } catch (const std::exception& e) {
280
+ SetError(std::string("FAISS error: ") + e.what());
281
+ }
282
+ }
283
+
284
+ void OnOK() override {
285
+ Napi::Env env = Env();
286
+ Napi::Buffer<uint8_t> nodeBuffer = Napi::Buffer<uint8_t>::Copy(env, buffer_.data(), buffer_.size());
287
+ deferred_.Resolve(nodeBuffer);
288
+ }
289
+
290
+ void OnError(const Napi::Error& e) override {
291
+ deferred_.Reject(e.Value());
292
+ }
293
+
294
+ private:
295
+ FaissIndexWrapper* wrapper_;
296
+ std::vector<uint8_t> buffer_;
297
+ Napi::Promise::Deferred deferred_;
298
+ };
299
+
300
+ // MergeFrom Worker
301
+ class MergeFromWorker : public Napi::AsyncWorker {
302
+ public:
303
+ MergeFromWorker(FaissIndexWrapper* target, FaissIndexWrapper* source, Napi::Promise::Deferred deferred)
304
+ : Napi::AsyncWorker(deferred.Env(), "MergeFromWorker"),
305
+ target_(target),
306
+ source_(source),
307
+ deferred_(deferred) {
308
+ }
309
+
310
+ void Execute() override {
311
+ try {
312
+ if (target_->IsDisposed()) {
313
+ SetError("Target index has been disposed");
314
+ return;
315
+ }
316
+ if (source_->IsDisposed()) {
317
+ SetError("Source index has been disposed");
318
+ return;
319
+ }
320
+ target_->MergeFrom(*source_);
321
+ } catch (const std::exception& e) {
322
+ SetError(std::string("FAISS error: ") + e.what());
323
+ }
324
+ }
325
+
326
+ void OnOK() override {
327
+ deferred_.Resolve(Env().Undefined());
328
+ }
329
+
330
+ void OnError(const Napi::Error& e) override {
331
+ deferred_.Reject(e.Value());
332
+ }
333
+
334
+ private:
335
+ FaissIndexWrapper* target_;
336
+ FaissIndexWrapper* source_;
337
+ Napi::Promise::Deferred deferred_;
338
+ };
339
+
340
+ // Wrapper class that bridges N-API and our C++ wrapper
341
+ class FaissIndexWrapperJS : public Napi::ObjectWrap<FaissIndexWrapperJS> {
342
+ public:
343
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
344
+ FaissIndexWrapperJS(const Napi::CallbackInfo& info);
345
+ ~FaissIndexWrapperJS();
346
+
347
+ private:
348
+ static Napi::FunctionReference constructor;
349
+ std::unique_ptr<FaissIndexWrapper> wrapper_;
350
+ int dims_;
351
+
352
+ // Methods
353
+ Napi::Value Add(const Napi::CallbackInfo& info);
354
+ Napi::Value Train(const Napi::CallbackInfo& info);
355
+ Napi::Value Search(const Napi::CallbackInfo& info);
356
+ Napi::Value SearchBatch(const Napi::CallbackInfo& info);
357
+ Napi::Value GetStats(const Napi::CallbackInfo& info);
358
+ Napi::Value Dispose(const Napi::CallbackInfo& info);
359
+ Napi::Value Save(const Napi::CallbackInfo& info);
360
+ Napi::Value ToBuffer(const Napi::CallbackInfo& info);
361
+ Napi::Value MergeFrom(const Napi::CallbackInfo& info);
362
+ Napi::Value SetNprobe(const Napi::CallbackInfo& info);
363
+
364
+ // Static methods
365
+ static Napi::Value Load(const Napi::CallbackInfo& info);
366
+ static Napi::Value FromBuffer(const Napi::CallbackInfo& info);
367
+
368
+ // Helper methods
369
+ void ValidateNotDisposed(Napi::Env env) const;
370
+ Napi::Float32Array CreateFloat32Array(Napi::Env env, size_t length, const float* data);
371
+ Napi::Int32Array CreateInt32Array(Napi::Env env, size_t length, const faiss::idx_t* data);
372
+ };
373
+
374
+ // Static constructor reference
375
+ Napi::FunctionReference FaissIndexWrapperJS::constructor;
376
+
377
+ Napi::Object FaissIndexWrapperJS::Init(Napi::Env env, Napi::Object exports) {
378
+ Napi::Function func = DefineClass(env, "FaissIndexWrapper", {
379
+ InstanceMethod("add", &FaissIndexWrapperJS::Add),
380
+ InstanceMethod("train", &FaissIndexWrapperJS::Train),
381
+ InstanceMethod("search", &FaissIndexWrapperJS::Search),
382
+ InstanceMethod("searchBatch", &FaissIndexWrapperJS::SearchBatch),
383
+ InstanceMethod("getStats", &FaissIndexWrapperJS::GetStats),
384
+ InstanceMethod("dispose", &FaissIndexWrapperJS::Dispose),
385
+ InstanceMethod("save", &FaissIndexWrapperJS::Save),
386
+ InstanceMethod("toBuffer", &FaissIndexWrapperJS::ToBuffer),
387
+ InstanceMethod("mergeFrom", &FaissIndexWrapperJS::MergeFrom),
388
+ InstanceMethod("setNprobe", &FaissIndexWrapperJS::SetNprobe),
389
+ StaticMethod("load", &FaissIndexWrapperJS::Load),
390
+ StaticMethod("fromBuffer", &FaissIndexWrapperJS::FromBuffer),
391
+ });
392
+
393
+ constructor = Napi::Persistent(func);
394
+ constructor.SuppressDestruct();
395
+
396
+ exports.Set("FaissIndexWrapper", func);
397
+ return exports;
398
+ }
399
+
400
+ FaissIndexWrapperJS::FaissIndexWrapperJS(const Napi::CallbackInfo& info)
401
+ : Napi::ObjectWrap<FaissIndexWrapperJS>(info), dims_(0) {
402
+ Napi::Env env = info.Env();
403
+
404
+ try {
405
+ // Validate constructor arguments
406
+ if (info.Length() < 1) {
407
+ throw Napi::TypeError::New(env, "Expected 1 argument: { dims: number }");
408
+ }
409
+
410
+ if (!info[0].IsObject()) {
411
+ throw Napi::TypeError::New(env, "Expected object with 'dims' property");
412
+ }
413
+
414
+ Napi::Object config = info[0].As<Napi::Object>();
415
+
416
+ if (!config.Has("dims") || !config.Get("dims").IsNumber()) {
417
+ throw Napi::TypeError::New(env, "Config must have 'dims' as a number");
418
+ }
419
+
420
+ dims_ = config.Get("dims").As<Napi::Number>().Int32Value();
421
+
422
+ if (dims_ <= 0) {
423
+ throw Napi::RangeError::New(env, "Dimensions must be positive");
424
+ }
425
+
426
+ // Get index type (default to "FLAT_L2" -> "Flat")
427
+ std::string indexDescription = "Flat"; // Default: IndexFlatL2
428
+ int metric = 1; // Default: METRIC_L2
429
+
430
+ if (config.Has("type") && config.Get("type").IsString()) {
431
+ std::string type = config.Get("type").As<Napi::String>().Utf8Value();
432
+
433
+ if (type == "FLAT_L2") {
434
+ indexDescription = "Flat";
435
+ metric = 1; // METRIC_L2
436
+ } else if (type == "IVF_FLAT") {
437
+ // Build IVF string: "IVF{nlist},Flat"
438
+ int nlist = 100; // Default number of clusters
439
+ if (config.Has("nlist") && config.Get("nlist").IsNumber()) {
440
+ nlist = config.Get("nlist").As<Napi::Number>().Int32Value();
441
+ }
442
+ indexDescription = "IVF" + std::to_string(nlist) + ",Flat";
443
+ metric = 1; // METRIC_L2
444
+ } else if (type == "HNSW") {
445
+ // Build HNSW string: "HNSW{M}"
446
+ int M = 16; // Default connections per node
447
+ if (config.Has("M") && config.Get("M").IsNumber()) {
448
+ M = config.Get("M").As<Napi::Number>().Int32Value();
449
+ }
450
+ indexDescription = "HNSW" + std::to_string(M);
451
+ metric = 1; // METRIC_L2
452
+ } else {
453
+ throw Napi::TypeError::New(env, "Unsupported index type: " + type + ". Supported: FLAT_L2, IVF_FLAT, HNSW");
454
+ }
455
+ }
456
+
457
+ // Create the C++ wrapper with index_factory
458
+ wrapper_ = std::make_unique<FaissIndexWrapper>(dims_, indexDescription, metric);
459
+
460
+ // Set nprobe for IVF indexes
461
+ if (config.Has("nprobe") && config.Get("nprobe").IsNumber()) {
462
+ int nprobe = config.Get("nprobe").As<Napi::Number>().Int32Value();
463
+ wrapper_->SetNprobe(nprobe);
464
+ }
465
+
466
+ } catch (const Napi::Error& e) {
467
+ throw; // Re-throw N-API errors
468
+ } catch (const std::exception& e) {
469
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
470
+ } catch (...) {
471
+ throw Napi::Error::New(env, "Unknown error creating index");
472
+ }
473
+ }
474
+
475
+ FaissIndexWrapperJS::~FaissIndexWrapperJS() {
476
+ // RAII: wrapper_ will be automatically destroyed
477
+ // But we can explicitly dispose if needed
478
+ if (wrapper_ && !wrapper_->IsDisposed()) {
479
+ wrapper_->Dispose();
480
+ }
481
+ }
482
+
483
+ void FaissIndexWrapperJS::ValidateNotDisposed(Napi::Env env) const {
484
+ if (!wrapper_ || wrapper_->IsDisposed()) {
485
+ throw Napi::Error::New(env, "Index has been disposed");
486
+ }
487
+ }
488
+
489
+ Napi::Value FaissIndexWrapperJS::Add(const Napi::CallbackInfo& info) {
490
+ Napi::Env env = info.Env();
491
+
492
+ try {
493
+ ValidateNotDisposed(env);
494
+
495
+ if (info.Length() < 1) {
496
+ throw Napi::TypeError::New(env, "Expected at least 1 argument: vectors (Float32Array)");
497
+ }
498
+
499
+ if (!info[0].IsTypedArray()) {
500
+ throw Napi::TypeError::New(env, "Expected Float32Array");
501
+ }
502
+
503
+ Napi::TypedArray arr = info[0].As<Napi::TypedArray>();
504
+
505
+ if (arr.TypedArrayType() != napi_float32_array) {
506
+ throw Napi::TypeError::New(env, "Expected Float32Array");
507
+ }
508
+
509
+ Napi::Float32Array floatArr = arr.As<Napi::Float32Array>();
510
+ size_t length = floatArr.ElementLength();
511
+
512
+ // Validate dimensions
513
+ if (length % dims_ != 0) {
514
+ throw Napi::RangeError::New(env,
515
+ "Vector length must be a multiple of dimensions. Got " +
516
+ std::to_string(length) + ", expected multiple of " + std::to_string(dims_));
517
+ }
518
+
519
+ size_t n = length / dims_;
520
+
521
+ // Get pointer to data (zero-copy read) - copy data for async worker
522
+ const float* data = floatArr.Data();
523
+
524
+ // Create promise and async worker
525
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
526
+ AddWorker* worker = new AddWorker(wrapper_.get(), data, n, dims_, deferred);
527
+ worker->Queue();
528
+
529
+ return deferred.Promise();
530
+
531
+ } catch (const Napi::Error& e) {
532
+ throw; // Re-throw N-API errors
533
+ } catch (const std::exception& e) {
534
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
535
+ } catch (...) {
536
+ throw Napi::Error::New(env, "Unknown error in add()");
537
+ }
538
+ }
539
+
540
+ Napi::Value FaissIndexWrapperJS::Train(const Napi::CallbackInfo& info) {
541
+ Napi::Env env = info.Env();
542
+
543
+ try {
544
+ ValidateNotDisposed(env);
545
+
546
+ if (info.Length() < 1) {
547
+ throw Napi::TypeError::New(env, "Expected 1 argument: vectors (Float32Array)");
548
+ }
549
+
550
+ if (!info[0].IsTypedArray()) {
551
+ throw Napi::TypeError::New(env, "Expected Float32Array");
552
+ }
553
+
554
+ Napi::TypedArray arr = info[0].As<Napi::TypedArray>();
555
+
556
+ if (arr.TypedArrayType() != napi_float32_array) {
557
+ throw Napi::TypeError::New(env, "Expected Float32Array");
558
+ }
559
+
560
+ Napi::Float32Array floatArr = arr.As<Napi::Float32Array>();
561
+ size_t length = floatArr.ElementLength();
562
+
563
+ // Validate dimensions
564
+ if (length % dims_ != 0) {
565
+ throw Napi::RangeError::New(env,
566
+ "Vector length must be a multiple of dimensions. Got " +
567
+ std::to_string(length) + ", expected multiple of " + std::to_string(dims_));
568
+ }
569
+
570
+ size_t n = length / dims_;
571
+
572
+ // Get pointer to data (zero-copy read) - copy data for async worker
573
+ const float* data = floatArr.Data();
574
+
575
+ // Create promise and async worker
576
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
577
+ TrainWorker* worker = new TrainWorker(wrapper_.get(), data, n, dims_, deferred);
578
+ worker->Queue();
579
+
580
+ return deferred.Promise();
581
+
582
+ } catch (const Napi::Error& e) {
583
+ throw; // Re-throw N-API errors
584
+ } catch (const std::exception& e) {
585
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
586
+ } catch (...) {
587
+ throw Napi::Error::New(env, "Unknown error in train()");
588
+ }
589
+ }
590
+
591
+ Napi::Value FaissIndexWrapperJS::SetNprobe(const Napi::CallbackInfo& info) {
592
+ Napi::Env env = info.Env();
593
+
594
+ try {
595
+ ValidateNotDisposed(env);
596
+
597
+ if (info.Length() < 1) {
598
+ throw Napi::TypeError::New(env, "Expected 1 argument: nprobe (number)");
599
+ }
600
+
601
+ if (!info[0].IsNumber()) {
602
+ throw Napi::TypeError::New(env, "Expected number for nprobe");
603
+ }
604
+
605
+ int nprobe = info[0].As<Napi::Number>().Int32Value();
606
+
607
+ if (nprobe <= 0) {
608
+ throw Napi::RangeError::New(env, "nprobe must be positive");
609
+ }
610
+
611
+ wrapper_->SetNprobe(nprobe);
612
+ return env.Undefined();
613
+
614
+ } catch (const Napi::Error& e) {
615
+ throw; // Re-throw N-API errors
616
+ } catch (const std::exception& e) {
617
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
618
+ } catch (...) {
619
+ throw Napi::Error::New(env, "Unknown error in setNprobe()");
620
+ }
621
+ }
622
+
623
+ Napi::Value FaissIndexWrapperJS::Search(const Napi::CallbackInfo& info) {
624
+ Napi::Env env = info.Env();
625
+
626
+ try {
627
+ ValidateNotDisposed(env);
628
+
629
+ if (info.Length() < 2) {
630
+ throw Napi::TypeError::New(env, "Expected 2 arguments: query (Float32Array), k (number)");
631
+ }
632
+
633
+ if (!info[0].IsTypedArray()) {
634
+ throw Napi::TypeError::New(env, "Expected Float32Array for query");
635
+ }
636
+
637
+ if (!info[1].IsNumber()) {
638
+ throw Napi::TypeError::New(env, "Expected number for k");
639
+ }
640
+
641
+ Napi::TypedArray arr = info[0].As<Napi::TypedArray>();
642
+ if (arr.TypedArrayType() != napi_float32_array) {
643
+ throw Napi::TypeError::New(env, "Expected Float32Array for query");
644
+ }
645
+
646
+ Napi::Float32Array queryArr = arr.As<Napi::Float32Array>();
647
+ int k = info[1].As<Napi::Number>().Int32Value();
648
+
649
+ if (static_cast<int>(queryArr.ElementLength()) != dims_) {
650
+ throw Napi::RangeError::New(env,
651
+ "Query vector length must match index dimensions. Got " +
652
+ std::to_string(queryArr.ElementLength()) + ", expected " + std::to_string(dims_));
653
+ }
654
+
655
+ if (k <= 0) {
656
+ throw Napi::RangeError::New(env, "k must be positive");
657
+ }
658
+
659
+ // Get query pointer (zero-copy read) - copy data for async worker
660
+ const float* query = queryArr.Data();
661
+
662
+ // Create promise and async worker
663
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
664
+ SearchWorker* worker = new SearchWorker(wrapper_.get(), query, k, deferred);
665
+ worker->Queue();
666
+
667
+ return deferred.Promise();
668
+
669
+ } catch (const Napi::Error& e) {
670
+ throw; // Re-throw N-API errors
671
+ } catch (const std::exception& e) {
672
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
673
+ } catch (...) {
674
+ throw Napi::Error::New(env, "Unknown error in search()");
675
+ }
676
+ }
677
+
678
+ Napi::Value FaissIndexWrapperJS::SearchBatch(const Napi::CallbackInfo& info) {
679
+ Napi::Env env = info.Env();
680
+
681
+ try {
682
+ ValidateNotDisposed(env);
683
+
684
+ if (info.Length() < 2) {
685
+ throw Napi::TypeError::New(env, "Expected 2 arguments: queries (Float32Array), k (number)");
686
+ }
687
+
688
+ if (!info[0].IsTypedArray()) {
689
+ throw Napi::TypeError::New(env, "Expected Float32Array for queries");
690
+ }
691
+
692
+ if (!info[1].IsNumber()) {
693
+ throw Napi::TypeError::New(env, "Expected number for k");
694
+ }
695
+
696
+ Napi::TypedArray arr = info[0].As<Napi::TypedArray>();
697
+ if (arr.TypedArrayType() != napi_float32_array) {
698
+ throw Napi::TypeError::New(env, "Expected Float32Array for queries");
699
+ }
700
+
701
+ Napi::Float32Array queriesArr = arr.As<Napi::Float32Array>();
702
+ size_t totalElements = queriesArr.ElementLength();
703
+ int k = info[1].As<Napi::Number>().Int32Value();
704
+
705
+ if (totalElements == 0) {
706
+ throw Napi::RangeError::New(env, "Queries array cannot be empty");
707
+ }
708
+
709
+ if (totalElements % dims_ != 0) {
710
+ throw Napi::RangeError::New(env,
711
+ "Queries array length must be a multiple of index dimensions. Got " +
712
+ std::to_string(totalElements) + ", expected multiple of " + std::to_string(dims_));
713
+ }
714
+
715
+ size_t nq = totalElements / dims_;
716
+
717
+ if (k <= 0) {
718
+ throw Napi::RangeError::New(env, "k must be positive");
719
+ }
720
+
721
+ // Get queries pointer (zero-copy read) - copy data for async worker
722
+ const float* queries = queriesArr.Data();
723
+
724
+ // Create promise and async worker
725
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
726
+ SearchBatchWorker* worker = new SearchBatchWorker(wrapper_.get(), queries, nq, k, deferred);
727
+ worker->Queue();
728
+
729
+ return deferred.Promise();
730
+
731
+ } catch (const Napi::Error& e) {
732
+ throw; // Re-throw N-API errors
733
+ } catch (const std::exception& e) {
734
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
735
+ } catch (...) {
736
+ throw Napi::Error::New(env, "Unknown error in searchBatch()");
737
+ }
738
+ }
739
+
740
+ Napi::Value FaissIndexWrapperJS::GetStats(const Napi::CallbackInfo& info) {
741
+ Napi::Env env = info.Env();
742
+
743
+ try {
744
+ ValidateNotDisposed(env);
745
+
746
+ Napi::Object stats = Napi::Object::New(env);
747
+ stats.Set("ntotal", Napi::Number::New(env, wrapper_->GetTotalVectors()));
748
+ stats.Set("dims", Napi::Number::New(env, wrapper_->GetDimensions()));
749
+ stats.Set("isTrained", Napi::Boolean::New(env, wrapper_->IsTrained()));
750
+ stats.Set("type", Napi::String::New(env, "FLAT_L2"));
751
+
752
+ return stats;
753
+
754
+ } catch (const Napi::Error& e) {
755
+ throw;
756
+ } catch (const std::exception& e) {
757
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
758
+ } catch (...) {
759
+ throw Napi::Error::New(env, "Unknown error in getStats()");
760
+ }
761
+ }
762
+
763
+ Napi::Value FaissIndexWrapperJS::Dispose(const Napi::CallbackInfo& info) {
764
+ Napi::Env env = info.Env();
765
+
766
+ if (wrapper_ && !wrapper_->IsDisposed()) {
767
+ wrapper_->Dispose();
768
+ }
769
+
770
+ return env.Undefined();
771
+ }
772
+
773
+ Napi::Float32Array FaissIndexWrapperJS::CreateFloat32Array(Napi::Env env, size_t length, const float* data) {
774
+ Napi::Float32Array arr = Napi::Float32Array::New(env, length);
775
+ memcpy(arr.Data(), data, length * sizeof(float));
776
+ return arr;
777
+ }
778
+
779
+ Napi::Int32Array FaissIndexWrapperJS::CreateInt32Array(Napi::Env env, size_t length, const faiss::idx_t* data) {
780
+ Napi::Int32Array arr = Napi::Int32Array::New(env, length);
781
+ // faiss::idx_t is typically int64_t, but we'll cast to int32_t for JS
782
+ // Use direct pointer access for better performance
783
+ int32_t* arrData = arr.Data();
784
+ for (size_t i = 0; i < length; i++) {
785
+ arrData[i] = static_cast<int32_t>(data[i]);
786
+ }
787
+ return arr;
788
+ }
789
+
790
+ Napi::Value FaissIndexWrapperJS::Save(const Napi::CallbackInfo& info) {
791
+ Napi::Env env = info.Env();
792
+
793
+ try {
794
+ ValidateNotDisposed(env);
795
+
796
+ if (info.Length() < 1) {
797
+ throw Napi::TypeError::New(env, "Expected 1 argument: filename (string)");
798
+ }
799
+
800
+ if (!info[0].IsString()) {
801
+ throw Napi::TypeError::New(env, "Expected string for filename");
802
+ }
803
+
804
+ std::string filename = info[0].As<Napi::String>().Utf8Value();
805
+
806
+ // Create promise and async worker
807
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
808
+ SaveWorker* worker = new SaveWorker(wrapper_.get(), filename, deferred);
809
+ worker->Queue();
810
+
811
+ return deferred.Promise();
812
+
813
+ } catch (const Napi::Error& e) {
814
+ throw;
815
+ } catch (const std::exception& e) {
816
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
817
+ } catch (...) {
818
+ throw Napi::Error::New(env, "Unknown error in save()");
819
+ }
820
+ }
821
+
822
+ Napi::Value FaissIndexWrapperJS::ToBuffer(const Napi::CallbackInfo& info) {
823
+ Napi::Env env = info.Env();
824
+
825
+ try {
826
+ ValidateNotDisposed(env);
827
+
828
+ // Create promise and async worker
829
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
830
+ ToBufferWorker* worker = new ToBufferWorker(wrapper_.get(), deferred);
831
+ worker->Queue();
832
+
833
+ return deferred.Promise();
834
+
835
+ } catch (const Napi::Error& e) {
836
+ throw;
837
+ } catch (const std::exception& e) {
838
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
839
+ } catch (...) {
840
+ throw Napi::Error::New(env, "Unknown error in toBuffer()");
841
+ }
842
+ }
843
+
844
+ Napi::Value FaissIndexWrapperJS::MergeFrom(const Napi::CallbackInfo& info) {
845
+ Napi::Env env = info.Env();
846
+
847
+ try {
848
+ ValidateNotDisposed(env);
849
+
850
+ if (info.Length() < 1) {
851
+ throw Napi::TypeError::New(env, "Expected 1 argument: otherIndex (FaissIndex)");
852
+ }
853
+
854
+ if (!info[0].IsObject()) {
855
+ throw Napi::TypeError::New(env, "Expected FaissIndex object");
856
+ }
857
+
858
+ // Get the other index instance
859
+ Napi::Object otherObj = info[0].As<Napi::Object>();
860
+ FaissIndexWrapperJS* otherInstance = Napi::ObjectWrap<FaissIndexWrapperJS>::Unwrap(otherObj);
861
+
862
+ if (!otherInstance || !otherInstance->wrapper_) {
863
+ throw Napi::Error::New(env, "Invalid index object");
864
+ }
865
+
866
+ if (otherInstance->wrapper_->IsDisposed()) {
867
+ throw Napi::Error::New(env, "Cannot merge from disposed index");
868
+ }
869
+
870
+ // Create promise and async worker
871
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
872
+ MergeFromWorker* worker = new MergeFromWorker(wrapper_.get(), otherInstance->wrapper_.get(), deferred);
873
+ worker->Queue();
874
+
875
+ return deferred.Promise();
876
+
877
+ } catch (const Napi::Error& e) {
878
+ throw; // Re-throw N-API errors
879
+ } catch (const std::exception& e) {
880
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
881
+ } catch (...) {
882
+ throw Napi::Error::New(env, "Unknown error in mergeFrom()");
883
+ }
884
+ }
885
+
886
+ Napi::Value FaissIndexWrapperJS::Load(const Napi::CallbackInfo& info) {
887
+ Napi::Env env = info.Env();
888
+
889
+ try {
890
+ if (info.Length() < 1) {
891
+ throw Napi::TypeError::New(env, "Expected 1 argument: filename (string)");
892
+ }
893
+
894
+ if (!info[0].IsString()) {
895
+ throw Napi::TypeError::New(env, "Expected string for filename");
896
+ }
897
+
898
+ std::string filename = info[0].As<Napi::String>().Utf8Value();
899
+ auto loaded_wrapper = FaissIndexWrapper::Load(filename);
900
+
901
+ // Create new JS instance with dummy config (will be replaced)
902
+ int dims = loaded_wrapper->GetDimensions();
903
+ Napi::Object config = Napi::Object::New(env);
904
+ config.Set("dims", Napi::Number::New(env, dims));
905
+ Napi::Object obj = constructor.New({config});
906
+ FaissIndexWrapperJS* instance = Napi::ObjectWrap<FaissIndexWrapperJS>::Unwrap(obj);
907
+
908
+ // Replace the wrapper with the loaded one
909
+ instance->wrapper_ = std::move(loaded_wrapper);
910
+ instance->dims_ = dims;
911
+
912
+ return obj;
913
+
914
+ } catch (const Napi::Error& e) {
915
+ throw;
916
+ } catch (const std::exception& e) {
917
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
918
+ } catch (...) {
919
+ throw Napi::Error::New(env, "Unknown error in load()");
920
+ }
921
+ }
922
+
923
+ Napi::Value FaissIndexWrapperJS::FromBuffer(const Napi::CallbackInfo& info) {
924
+ Napi::Env env = info.Env();
925
+
926
+ try {
927
+ if (info.Length() < 1) {
928
+ throw Napi::TypeError::New(env, "Expected 1 argument: buffer (Buffer)");
929
+ }
930
+
931
+ if (!info[0].IsBuffer()) {
932
+ throw Napi::TypeError::New(env, "Expected Buffer");
933
+ }
934
+
935
+ Napi::Buffer<uint8_t> buffer = info[0].As<Napi::Buffer<uint8_t>>();
936
+ const uint8_t* data = buffer.Data();
937
+ size_t length = buffer.Length();
938
+
939
+ auto loaded_wrapper = FaissIndexWrapper::FromBuffer(data, length);
940
+
941
+ // Create new JS instance with dummy config (will be replaced)
942
+ int dims = loaded_wrapper->GetDimensions();
943
+ Napi::Object config = Napi::Object::New(env);
944
+ config.Set("dims", Napi::Number::New(env, dims));
945
+ Napi::Object obj = constructor.New({config});
946
+ FaissIndexWrapperJS* instance = Napi::ObjectWrap<FaissIndexWrapperJS>::Unwrap(obj);
947
+
948
+ // Replace the wrapper with the loaded one
949
+ instance->wrapper_ = std::move(loaded_wrapper);
950
+ instance->dims_ = dims;
951
+
952
+ return obj;
953
+
954
+ } catch (const Napi::Error& e) {
955
+ throw;
956
+ } catch (const std::exception& e) {
957
+ throw Napi::Error::New(env, std::string("FAISS error: ") + e.what());
958
+ } catch (...) {
959
+ throw Napi::Error::New(env, "Unknown error in fromBuffer()");
960
+ }
961
+ }
962
+
963
+ // Module initialization
964
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
965
+ FaissIndexWrapperJS::Init(env, exports);
966
+ return exports;
967
+ }
968
+
969
+ NODE_API_MODULE(faiss_node, Init)