expo-vector-search 0.5.0 → 0.5.1

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.
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.vectorsearch'
4
- version = '0.2.0'
4
+ version = '0.5.1'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -35,8 +35,8 @@ android {
35
35
  }
36
36
 
37
37
  defaultConfig {
38
- versionCode 2
39
- versionName "0.2.0"
38
+ versionCode 3
39
+ versionName "0.5.1"
40
40
 
41
41
 
42
42
  externalNativeBuild {
@@ -8,6 +8,7 @@
8
8
  #include <fstream>
9
9
  #include <jsi/jsi.h>
10
10
  #include <memory>
11
+ #include <mutex>
11
12
  #include <string>
12
13
  #include <thread>
13
14
  #include <unordered_set>
@@ -192,11 +193,17 @@ public:
192
193
  jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override {
193
194
  std::string methodName = name.utf8(runtime);
194
195
 
195
- if (methodName == "dimensions")
196
+ if (methodName == "dimensions") {
197
+ std::lock_guard<std::mutex> lock(_mutex);
196
198
  return jsi::Value((double)_index->dimensions());
199
+ }
197
200
  if (methodName == "count")
201
+ {
202
+ std::lock_guard<std::mutex> lock(_mutex);
198
203
  return jsi::Value(_index ? (double)_index->size() : 0);
204
+ }
199
205
  if (methodName == "memoryUsage") {
206
+ std::lock_guard<std::mutex> lock(_mutex);
200
207
  if (!_index)
201
208
  return jsi::Value(0);
202
209
  // We calculate memory usage manually to avoid a race condition in
@@ -216,6 +223,7 @@ public:
216
223
  return jsi::Value((double)(vectorBytes + graphOverhead + baseMemory));
217
224
  }
218
225
  if (methodName == "isa") {
226
+ std::lock_guard<std::mutex> lock(_mutex);
219
227
  const char *isa = _index ? _index->metric().isa_name() : "unknown";
220
228
  return jsi::String::createFromUtf8(runtime, isa);
221
229
  }
@@ -237,6 +245,7 @@ public:
237
245
  runtime, name, 0,
238
246
  [this](jsi::Runtime &runtime, const jsi::Value &thisValue,
239
247
  const jsi::Value *arguments, size_t count) -> jsi::Value {
248
+ std::lock_guard<std::mutex> lock(_mutex);
240
249
  if (!_lastResult.error.empty()) {
241
250
  std::string err = _lastResult.error;
242
251
  _lastResult.error = ""; // Clear after reporting
@@ -254,6 +263,7 @@ public:
254
263
  runtime, name, 0,
255
264
  [this](jsi::Runtime &runtime, const jsi::Value &thisValue,
256
265
  const jsi::Value *arguments, size_t count) -> jsi::Value {
266
+ std::lock_guard<std::mutex> lock(_mutex);
257
267
  _index.reset();
258
268
  return jsi::Value::undefined();
259
269
  });
@@ -267,15 +277,14 @@ public:
267
277
  if (count < 2)
268
278
  throw jsi::JSError(runtime,
269
279
  "add expects 2 arguments: key, vector");
270
- if (!_index)
271
- throw jsi::JSError(runtime, "VectorIndex has been deleted.");
272
280
 
273
281
  default_key_t key =
274
282
  static_cast<default_key_t>(arguments[0].asNumber());
275
283
  auto [vecData, vecSize] = getRawVector(runtime, arguments[1]);
276
284
 
277
- // LOGD("add called: key=%llu, vecSize=%zu, capacity=%zu",
278
- // (unsigned long long)key, vecSize, _index->capacity());
285
+ std::lock_guard<std::mutex> lock(_mutex);
286
+ if (!_index)
287
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
279
288
 
280
289
  if (vecSize != _index->dimensions()) {
281
290
  LOGE("Dimension mismatch: expected %zu, got %zu",
@@ -349,9 +358,12 @@ public:
349
358
  throw jsi::JSError(runtime, "Batch mismatch: keys and vectors "
350
359
  "must have compatible sizes.");
351
360
 
352
- if (_index->size() + batchCount > _index->capacity()) {
353
- size_t newCapacity = _index->size() + batchCount + 100;
354
- _index->reserve(index_limits_t(newCapacity, _threads));
361
+ {
362
+ std::lock_guard<std::mutex> lock(_mutex);
363
+ if (_index->size() + batchCount > _index->capacity()) {
364
+ size_t newCapacity = _index->size() + batchCount + 100;
365
+ _index->reserve(index_limits_t(newCapacity, _threads));
366
+ }
355
367
  }
356
368
 
357
369
  // Copy data safely for background thread
@@ -369,6 +381,7 @@ public:
369
381
  auto start = std::chrono::high_resolution_clock::now();
370
382
  try {
371
383
  for (size_t i = 0; i < batchCount; ++i) {
384
+ std::lock_guard<std::mutex> lock(self->_mutex);
372
385
  if (!self->_index)
373
386
  break; // Safety check
374
387
  auto result = self->_index->add((default_key_t)keys[i],
@@ -382,12 +395,16 @@ public:
382
395
  self->_currentIndexingCount++;
383
396
  }
384
397
  auto end = std::chrono::high_resolution_clock::now();
385
- self->_lastResult.duration =
386
- std::chrono::duration<double, std::milli>(end - start)
387
- .count();
388
- self->_lastResult.count = batchCount;
389
- self->_lastResult.error = "";
398
+ {
399
+ std::lock_guard<std::mutex> lock(self->_mutex);
400
+ self->_lastResult.duration =
401
+ std::chrono::duration<double, std::milli>(end - start)
402
+ .count();
403
+ self->_lastResult.count = batchCount;
404
+ self->_lastResult.error = "";
405
+ }
390
406
  } catch (const std::exception &e) {
407
+ std::lock_guard<std::mutex> lock(self->_mutex);
391
408
  self->_lastResult.error = e.what();
392
409
  }
393
410
  self->_isIndexing = false;
@@ -404,12 +421,14 @@ public:
404
421
  const jsi::Value *arguments, size_t count) -> jsi::Value {
405
422
  if (count < 1)
406
423
  throw jsi::JSError(runtime, "remove expects 1 argument: key");
407
- if (!_index)
408
- throw jsi::JSError(runtime, "VectorIndex has been deleted.");
409
424
 
410
425
  default_key_t key =
411
426
  static_cast<default_key_t>(arguments[0].asNumber());
412
427
 
428
+ std::lock_guard<std::mutex> lock(_mutex);
429
+ if (!_index)
430
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
431
+
413
432
  auto result = _index->remove(key);
414
433
  if (!result) {
415
434
  LOGE("Failed to remove vector: %s", result.error.what());
@@ -429,13 +448,15 @@ public:
429
448
  if (count < 2)
430
449
  throw jsi::JSError(runtime,
431
450
  "update expects 2 arguments: key, vector");
432
- if (!_index)
433
- throw jsi::JSError(runtime, "VectorIndex has been deleted.");
434
451
 
435
452
  default_key_t key =
436
453
  static_cast<default_key_t>(arguments[0].asNumber());
437
454
  auto [vecData, vecSize] = getRawVector(runtime, arguments[1]);
438
455
 
456
+ std::lock_guard<std::mutex> lock(_mutex);
457
+ if (!_index)
458
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
459
+
439
460
  if (vecSize != _index->dimensions()) {
440
461
  throw jsi::JSError(runtime, "Incorrect dimension for update.");
441
462
  }
@@ -464,8 +485,6 @@ public:
464
485
  if (count < 2)
465
486
  throw jsi::JSError(runtime,
466
487
  "search expects 2 arguments: vector, count");
467
- if (!_index)
468
- throw jsi::JSError(runtime, "VectorIndex has been deleted.");
469
488
 
470
489
  LOGD("search: starting...");
471
490
  auto [queryData, querySize] = getRawVector(runtime, arguments[0]);
@@ -495,6 +514,10 @@ public:
495
514
  }
496
515
  }
497
516
 
517
+ std::lock_guard<std::mutex> lock(_mutex);
518
+ if (!_index)
519
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
520
+
498
521
  if (querySize != _index->dimensions()) {
499
522
  LOGE("Search dimension mismatch: expected %zu, got %zu",
500
523
  _index->dimensions(), querySize);
@@ -536,6 +559,11 @@ public:
536
559
 
537
560
  default_key_t key =
538
561
  static_cast<default_key_t>(arguments[0].asNumber());
562
+
563
+ std::lock_guard<std::mutex> lock(_mutex);
564
+ if (!_index)
565
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
566
+
539
567
  size_t dims = _index->dimensions();
540
568
 
541
569
  jsi::ArrayBuffer buffer =
@@ -545,24 +573,15 @@ public:
545
573
  .getObject(runtime)
546
574
  .getArrayBuffer(runtime);
547
575
 
548
- // We need raw access to write to it
549
- // JSI ArrayBuffer doesn't give direct mutable pointer easily
550
- // without a TypedArray view? Actually getArrayBuffer ->
551
- // data(runtime) gives pointer.
552
-
553
576
  uint8_t *data = buffer.data(runtime);
554
577
  float *vecData = reinterpret_cast<float *>(data);
555
578
 
556
- // USearch get() signature: bool get(key_t key, scalar_t* vector)
557
- // const
558
579
  bool found = _index->get(key, vecData);
559
580
 
560
581
  if (!found) {
561
582
  return jsi::Value::undefined();
562
583
  }
563
584
 
564
- // Return Float32Array view
565
- // Float32Array constructor: new Float32Array(buffer)
566
585
  jsi::Object float32ArrayCtor =
567
586
  runtime.global().getPropertyAsObject(runtime, "Float32Array");
568
587
  jsi::Object float32Array = float32ArrayCtor.asFunction(runtime)
@@ -582,6 +601,9 @@ public:
582
601
  throw jsi::JSError(runtime, "save expects path");
583
602
  std::string path = normalizePath(
584
603
  runtime, arguments[0].asString(runtime).utf8(runtime));
604
+ std::lock_guard<std::mutex> lock(_mutex);
605
+ if (!_index)
606
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
585
607
  if (!_index->save(path.c_str()))
586
608
  throw jsi::JSError(
587
609
  runtime, "Critical error saving index to disk: " + path);
@@ -626,27 +648,35 @@ public:
626
648
  file.read(reinterpret_cast<char *>(vectorData.data()),
627
649
  numVectors * dims * sizeof(float));
628
650
 
629
- if (!self->_index)
630
- return;
631
- if (self->_index->size() + numVectors >
632
- self->_index->capacity()) {
633
- self->_index->reserve(self->_index->size() + numVectors +
634
- 100);
651
+ {
652
+ std::lock_guard<std::mutex> lock(self->_mutex);
653
+ if (!self->_index)
654
+ return;
655
+ if (self->_index->size() + numVectors >
656
+ self->_index->capacity()) {
657
+ self->_index->reserve(self->_index->size() + numVectors +
658
+ 100);
659
+ }
635
660
  }
636
661
 
637
662
  for (size_t i = 0; i < numVectors; ++i) {
663
+ std::lock_guard<std::mutex> lock(self->_mutex);
638
664
  self->_index->add((default_key_t)i,
639
665
  vectorData.data() + (i * dims));
640
666
  self->_currentIndexingCount++;
641
667
  }
642
668
 
643
669
  auto end = std::chrono::high_resolution_clock::now();
644
- self->_lastResult.duration =
645
- std::chrono::duration<double, std::milli>(end - start)
646
- .count();
647
- self->_lastResult.count = numVectors;
648
- self->_lastResult.error = "";
670
+ {
671
+ std::lock_guard<std::mutex> lock(self->_mutex);
672
+ self->_lastResult.duration =
673
+ std::chrono::duration<double, std::milli>(end - start)
674
+ .count();
675
+ self->_lastResult.count = numVectors;
676
+ self->_lastResult.error = "";
677
+ }
649
678
  } catch (const std::exception &e) {
679
+ std::lock_guard<std::mutex> lock(self->_mutex);
650
680
  self->_lastResult.error = e.what();
651
681
  }
652
682
  self->_isIndexing = false;
@@ -665,6 +695,9 @@ public:
665
695
  throw jsi::JSError(runtime, "load expects path");
666
696
  std::string path = normalizePath(
667
697
  runtime, arguments[0].asString(runtime).utf8(runtime));
698
+ std::lock_guard<std::mutex> lock(_mutex);
699
+ if (!_index)
700
+ throw jsi::JSError(runtime, "VectorIndex has been deleted.");
668
701
  if (!_index->load(path.c_str()))
669
702
  throw jsi::JSError(
670
703
  runtime, "Critical error loading index from disk: " + path);
@@ -677,6 +710,7 @@ public:
677
710
 
678
711
  private:
679
712
  std::shared_ptr<Index> _index;
713
+ mutable std::mutex _mutex;
680
714
  std::atomic<bool> _isIndexing{false};
681
715
  std::atomic<size_t> _currentIndexingCount{0};
682
716
  std::atomic<size_t> _totalIndexingCount{0};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-vector-search",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A high-performance vector search module for Expo powered by USearch",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",