expo-vector-search 0.1.0 → 0.3.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.
Files changed (35) hide show
  1. package/README.md +103 -11
  2. package/android/CMakeLists.txt +6 -5
  3. package/android/build.gradle +3 -3
  4. package/android/src/main/cpp/ExpoVectorSearchModule.cpp +7 -301
  5. package/build/index.d.ts +5 -0
  6. package/build/index.d.ts.map +1 -0
  7. package/build/index.js +6 -0
  8. package/build/index.js.map +1 -0
  9. package/build/src/ExpoVectorSearch.types.d.ts +7 -0
  10. package/build/src/ExpoVectorSearch.types.d.ts.map +1 -0
  11. package/build/src/ExpoVectorSearch.types.js +2 -0
  12. package/build/src/ExpoVectorSearch.types.js.map +1 -0
  13. package/build/src/ExpoVectorSearchModule.d.ts +126 -0
  14. package/build/src/ExpoVectorSearchModule.d.ts.map +1 -0
  15. package/build/src/ExpoVectorSearchModule.js +130 -0
  16. package/build/src/ExpoVectorSearchModule.js.map +1 -0
  17. package/build/src/useVectorSearch.d.ts +20 -0
  18. package/build/src/useVectorSearch.d.ts.map +1 -0
  19. package/build/src/useVectorSearch.js +70 -0
  20. package/build/src/useVectorSearch.js.map +1 -0
  21. package/cpp/ExpoVectorSearch.h +619 -0
  22. package/cpp/lib/index.hpp +4577 -0
  23. package/cpp/usearch/index.hpp +4577 -0
  24. package/cpp/usearch/index_dense.hpp +2221 -0
  25. package/cpp/usearch/index_plugins.hpp +2568 -0
  26. package/index.ts +1 -0
  27. package/ios/ExpoVectorSearch.podspec +10 -8
  28. package/ios/ExpoVectorSearchModule.h +7 -0
  29. package/ios/ExpoVectorSearchModule.mm +18 -0
  30. package/ios/ExpoVectorSearchModule.swift +4 -31
  31. package/package.json +4 -3
  32. package/src/ExpoVectorSearch.types.ts +2 -0
  33. package/src/ExpoVectorSearchModule.ts +66 -7
  34. package/src/useVectorSearch.ts +85 -0
  35. package/ios/ExpoVectorSearchView.swift +0 -38
package/README.md CHANGED
@@ -3,17 +3,29 @@
3
3
  `expo-vector-search` is a high-performance, on-device **vector search engine** module for Expo and React Native.
4
4
 
5
5
  > [!NOTE]
6
- > This module is currently **optimized and validated for Android**. The C++ JSI layer is fully functional, but the build configuration and native runtime have been primarily verified on Android devices. iOS support is on the roadmap.
6
+ > This module is **cross-platform** (Android & iOS). The C++ JSI core and build configurations have been validated on production devices (Galaxy S23 FE and iPhone 12).
7
7
 
8
- ## Performance (Real-world Benchmarks)
9
- Results obtained on a **Samsung Galaxy S23 FE**:
10
- - **Search (1000 items, 128 dims)**: **0.08ms** per query.
11
- - **Batch Ingest (1000 items)**: **74.44ms** total time.
12
- - **Memory Optimization (10k items, 384 dims)**:
13
- - F32 Footprint: 36,964.94 KB.
14
- - Int8 Footprint: 20,580.94 KB.
8
+ ## Performance (Release Benchmarks)
15
9
 
16
- ![Performance Lab Benchmarks on S23 FE](../../assets/images/perf_lab.jpg)
10
+ Benchmark results obtained using **Release builds** on physical devices (1,000 vectors, 128 dimensions for search/ingestion; 10,000 vectors, 384 dimensions for memory optimization).
11
+
12
+ ### JS vs. Native Engine Race
13
+ | Platform | JavaScript (Runtime Loop) | Expo Vector Search (Native) | Speedup |
14
+ | :--- | :--- | :--- | :--- |
15
+ | **Android** (S23 FE) | 6.20 ms | 0.15 ms | **~41x** |
16
+ | **iOS** (iPhone 12) | 12.06 ms | 0.10 ms | **~120x** |
17
+
18
+ ### Bulk Ingestion (1,000 items)
19
+ | Platform | Individual `.add` | Batch `.addBatch` |
20
+ | :--- | :--- | :--- |
21
+ | **Android** (S23 FE) | 79.87 ms | 76.70 ms |
22
+ | **iOS** (iPhone 12) | 107.94 ms | 102.59 ms |
23
+
24
+ ### Memory Optimization (10,000 items, 384 dims)
25
+ | Platform | Full Precision (F32) | Quantized (Int8) | Savings |
26
+ | :--- | :--- | :--- | :--- |
27
+ | **Android** (S23 FE) | 36,943.84 KB | 20,559.84 KB | **~44%** |
28
+ | **iOS** (iPhone 12) | 36,943.97 KB | 20,559.97 KB | **~44%** |
17
29
 
18
30
  ## Key Features
19
31
 
@@ -22,16 +34,43 @@ Results obtained on a **Samsung Galaxy S23 FE**:
22
34
  - **Production-Grade Quantization**: Support for Int8 quantization to reduce memory footprint by up to 4x with minimal impact on search accuracy.
23
35
  - **Disk Persistence**: Built-in methods to save and load vector indices to/from the local file system.
24
36
  - **Memory Safety**: Strict buffer alignment checks and type validation to ensure native stability.
37
+ - **Extended Distance Metrics**: Support for Cosine, Euclidean (L2), Inner Product, Hamming (binary), and Jaccard distances.
25
38
  - **Explicit Memory Management**: Provision for deterministic resource release via the `delete()` method.
26
39
 
27
40
  ## Installation
28
41
 
29
- This module contains custom native code. You must use development builds to use this module.
42
+ This module contains custom native code (C++/JSI). **It does not work in [Expo Go](https://expo.dev/go).** You must generate and build the native project.
30
43
 
44
+ ### 1. Install the package
31
45
  ```bash
32
46
  npx expo install expo-vector-search
33
47
  ```
34
48
 
49
+ ### 2. Generate Native Folders
50
+ To access the `ios` and `android` directories and include the C++ engine, run:
51
+ ```bash
52
+ npx expo prebuild
53
+ ```
54
+ > [!NOTE]
55
+ > This command generates the native projects based on your `app.json`. If you already have `ios` and `android` folders (Bare Workflow), you can skip this step.
56
+
57
+ ### 3. Build and Run
58
+ You must compile the native code to use the module. Use the following commands to build and launch the app:
59
+ ```bash
60
+ npx expo run:android
61
+ # or
62
+ npx expo run:ios
63
+ ```
64
+
65
+ ---
66
+
67
+ ### Compatibility
68
+
69
+ | Environment | Supported | Requirement |
70
+ | :--- | :--- | :--- |
71
+ | **Expo Go** | ❌ No | Requires custom native engine |
72
+ | **Bare Workflow** | ✅ Yes | Standard `ios`/`android` folders |
73
+
35
74
  ## Architecture
36
75
 
37
76
  The module is designed for performance-critical applications where latency and battery efficiency are paramount.
@@ -43,6 +82,17 @@ The module is designed for performance-critical applications where latency and b
43
82
 
44
83
  ## API Reference
45
84
 
85
+ ### useVectorSearch (React Hook)
86
+
87
+ A wrapper hook that manages the lifecycle of a `VectorIndex`, handling creation and cleanup automatically.
88
+
89
+ ```typescript
90
+ const { index, search, add } = useVectorSearch(384, {
91
+ quantization: 'i8',
92
+ metric: 'cos'
93
+ });
94
+ ```
95
+
46
96
  ### VectorIndex
47
97
 
48
98
  The primary class for managing a vector collection.
@@ -51,6 +101,7 @@ The primary class for managing a vector collection.
51
101
  Initializes a new vector index.
52
102
  - `dimensions`: The dimensionality of the vectors (e.g., 128, 384, 768).
53
103
  - `options.quantization`: Scaling mode (`'f32'` or `'i8'`). Use `'i8'` for significant memory savings.
104
+ - `options.metric`: Distance metric calculation (`'cos'`, `'l2sq'`, `'ip'`, `'hamming'`, `'jaccard'`). Default is `'cos'`.
54
105
 
55
106
  #### `add(key: number, vector: Float32Array): void`
56
107
  Inserts a vector into the index.
@@ -62,12 +113,22 @@ High-performance batch insertion. Significantly reduces JSI overhead by processi
62
113
  - `keys`: An `Int32Array` of unique identifiers.
63
114
  - `vectors`: A single `Float32Array` containing all vectors concatenated (must match `keys.length * dimensions`).
64
115
 
65
- #### `search(vector: Float32Array, count: number): SearchResult[]`
116
+ #### `search(vector: Float32Array, count: number, options?: SearchOptions): SearchResult[]`
66
117
  Performs an ANN search.
67
118
  - `vector`: The query embedding.
68
119
  - `count`: Number of nearest neighbors to retrieve.
120
+ - `options.allowedKeys`: Optional array of keys to restrict the search to (filtering).
69
121
  - **Returns**: An array of `SearchResult` objects `{ key: number, distance: number }`.
70
122
 
123
+ #### `remove(key: number): void`
124
+ Removes a vector from the index.
125
+ - `key`: The unique numeric identifier of the vector to remove.
126
+
127
+ #### `update(key: number, vector: Float32Array): void`
128
+ Updates an existing vector in the index (upsert operation).
129
+ - `key`: The unique numeric identifier.
130
+ - `vector`: The new vector data.
131
+
71
132
  #### `save(path: string): void`
72
133
  Serializes the current state of the index to a specified file path.
73
134
 
@@ -77,6 +138,18 @@ Deserializes an index from a file path.
77
138
  #### `delete(): void`
78
139
  Manually releases native memory resources. The index instance becomes unusable after this call.
79
140
 
141
+ #### `loadVectorsFromFile(path: string): number`
142
+ Loads raw vectors directly from a binary file into the index.
143
+ - `path`: Absolute path to the binary file containing packed floats.
144
+ - **Returns**: The number of vectors successfully loaded.
145
+ - **Note**: This is significantly faster than parsing JSON/Base64 in JavaScript and adding vectors loop by loop.
146
+
147
+ #### `getItemVector(key: number): Float32Array | undefined`
148
+ Retrieves the vector associated with a specific key.
149
+ - `key`: The unique numeric identifier.
150
+ - **Returns**: A `Float32Array` copy of the vector, or `undefined` if the key does not exist.
151
+ - **Use Case**: Allows you to store vectors ONLY in native memory (saving JS RAM) and fetch them only when needed (e.g., for "Find Similar" queries).
152
+
80
153
  #### `dimensions: number` (readonly)
81
154
  Returns the dimensionality of the index.
82
155
 
@@ -129,5 +202,24 @@ This module performs strict validation on input buffers.
129
202
  - **Alignment**: `Float32Array` buffers must be 4-byte aligned for safe native access.
130
203
  - **Type Safety**: Input vectors are validated against the index's defined dimensions to prevent out-of-bounds memory operations.
131
204
 
205
+ ## Known Limitations & Roadmap
206
+
207
+ ### Performance Considerations (F32 vs Int8)
208
+ While **Int8 Quantization** provides significant memory savings (~44% total index reduction and ~75% raw vector reduction), it currently involves a computational overhead during the ingestion process on Android:
209
+ - **Full Precision (F32)**: ~9,284 ms per 10k vectors (Standard).
210
+ - **Quantized (Int8)**: ~34,608 ms per 10k vectors (Slower due to real-time conversion).
211
+
212
+ This performance gap is a known characteristic of the current version. The Int8 path requires a conversion from `float` to `int8` for every dimension, which is not yet fully vectorized in the Android build.
213
+
214
+ ### Future Roadmap
215
+ - [x] **Dynamic CRUD Support**: Implemented `remove(key)` and `update(key, vector)`.
216
+ - [x] **Metadata Filtering**: Support for `allowedKeys` filtering during search.
217
+ - [ ] **Architecture-Specific SIMD**: Enable NEON/SVE optimizations for Android builds.
218
+ - [ ] **Hybrid Search**: Integration with a keywords-based engine for hybrid results.
219
+ - [ ] **Background Indexing**: True multithreaded ingestion to avoid JS bridge/thread locks.
220
+ - [x] **Extended Distance Metrics**: Support for L2, IP, Hamming, and Jaccard.
221
+ - [ ] **USearch Upgrade**: Migration to `v2.23.0+` for enhanced performance.
222
+ - [ ] **Incremental Persistence**: Local storage optimizations for large datasets.
223
+
132
224
  ## License
133
225
  MIT
@@ -2,12 +2,12 @@ cmake_minimum_required(VERSION 3.10.2)
2
2
  project(ExpoVectorSearch)
3
3
 
4
4
  # 1. React Native / Expo Configuration (Default)
5
- set(CMAKE_VERBOSE_MAKEFILE ON)
5
+ set(CMAKE_VERBOSE_MAKEFILE OFF)
6
6
  set(CMAKE_CXX_STANDARD 17)
7
7
  # Disable external FP16 library requirement (use native or fallback)
8
8
  add_compile_definitions(USEARCH_USE_FP16LIB=0)
9
9
 
10
- # 2. Baixar e Configurar o USearch
10
+ # 2. Download and Configure USearch
11
11
  include(FetchContent)
12
12
 
13
13
  FetchContent_Declare(
@@ -16,10 +16,10 @@ FetchContent_Declare(
16
16
  GIT_TAG v2.9.0 # Use a recent stable version
17
17
  )
18
18
 
19
- # Disponibiliza o usearch para o projeto
19
+ # Make usearch available to the project
20
20
  FetchContent_MakeAvailable(usearch)
21
21
 
22
- # 3. Definir sua biblioteca compartilhada
22
+ # 3. Define the shared library
23
23
  add_library(
24
24
  expo-vector-search
25
25
  SHARED
@@ -33,9 +33,10 @@ target_include_directories(
33
33
  expo-vector-search
34
34
  PUBLIC
35
35
  ${usearch_SOURCE_DIR}/include
36
+ ${CMAKE_CURRENT_SOURCE_DIR}/../cpp
36
37
  )
37
38
 
38
- # 5. Linkar bibliotecas (Log do Android e bibliotecas do JS Engine)
39
+ # 5. Link libraries (Android Log and JS Engine libraries)
39
40
  # Important: React Native uses Prefab, so we need to find the ReactAndroid package
40
41
  find_package(ReactAndroid REQUIRED CONFIG)
41
42
 
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.vectorsearch'
4
- version = '0.1.0'
4
+ version = '0.2.0'
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 1
39
- versionName "0.1.0"
38
+ versionCode 2
39
+ versionName "0.2.0"
40
40
 
41
41
 
42
42
  externalNativeBuild {
@@ -1,306 +1,12 @@
1
+ #include "ExpoVectorSearch.h"
1
2
  #include <jni.h>
2
- #include <string>
3
- #include <vector>
4
- #include <memory>
5
- #include <android/log.h>
6
-
7
- #define TAG "ExpoVectorSearch"
8
- #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
9
- #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
10
-
11
- // Include the correct USearch header (Dense Index)
12
- // Polyfill for aligned_alloc on Android API < 28
13
- #if defined(__ANDROID__) && __ANDROID_API__ < 28
14
- #include <stdlib.h>
15
- extern "C" void* aligned_alloc(size_t alignment, size_t size) {
16
- void* ptr = nullptr;
17
- if (posix_memalign(&ptr, alignment, size) != 0) {
18
- return nullptr;
19
- }
20
- return ptr;
21
- }
22
- #endif
23
-
24
- #include <usearch/index_dense.hpp>
25
-
26
- // JSI Headers
27
3
  #include <jsi/jsi.h>
28
4
 
29
- using namespace facebook;
30
- using namespace unum::usearch;
31
-
32
- // Helper function to get raw pointer from a Float32Array
33
- // Returns {float_pointer, element_count}
34
- std::pair<const float*, size_t> getRawVector(jsi::Runtime &runtime, const jsi::Value &val) {
35
- if (!val.isObject()) {
36
- throw jsi::JSError(runtime, "Invalid argument: Expected a Float32Array.");
37
- }
38
- jsi::Object obj = val.asObject(runtime);
39
-
40
- // Check if it has the 'buffer' property (indicates it's a TypedArray)
41
- if (!obj.hasProperty(runtime, "buffer")) {
42
- throw jsi::JSError(runtime, "Invalid argument: Object must have a 'buffer' (Float32Array).");
43
- }
44
-
45
- // Access the ArrayBuffer
46
- auto bufferValue = obj.getProperty(runtime, "buffer");
47
- if (!bufferValue.isObject() || !bufferValue.asObject(runtime).isArrayBuffer(runtime)) {
48
- throw jsi::JSError(runtime, "Internal failure: 'buffer' is not a valid ArrayBuffer.");
49
- }
50
- auto arrayBuffer = bufferValue.asObject(runtime).getArrayBuffer(runtime);
51
-
52
- // Google-grade Hardening: Buffer length validation
53
- if (arrayBuffer.size(runtime) == 0) {
54
- throw jsi::JSError(runtime, "Invalid argument: Float32Array is empty.");
55
- }
56
-
57
- // Read offset and length to support Views (sub-arrays)
58
- size_t byteOffset = 0;
59
- if (obj.hasProperty(runtime, "byteOffset")) {
60
- byteOffset = static_cast<size_t>(obj.getProperty(runtime, "byteOffset").asNumber());
61
- }
62
-
63
- size_t byteLength = arrayBuffer.size(runtime); // Default full size
64
- if (obj.hasProperty(runtime, "byteLength")) {
65
- byteLength = static_cast<size_t>(obj.getProperty(runtime, "byteLength").asNumber());
66
- }
67
-
68
- // ZERO-COPY access to the memory pointer
69
- uint8_t* rawBytes = arrayBuffer.data(runtime) + byteOffset;
70
-
71
- // Google-grade Hardening: Memory Alignment Check
72
- // Float32 needs 4-byte alignment. reinterpret_cast on unaligned memory is undefined behavior.
73
- if (reinterpret_cast<uintptr_t>(rawBytes) % sizeof(float) != 0) {
74
- throw jsi::JSError(runtime, "Memory Alignment Error: Float32Array buffer is not 4-byte aligned.");
75
- }
76
-
77
- // Cast to float*
78
- const float* floatPtr = reinterpret_cast<const float*>(rawBytes);
79
- size_t count = byteLength / sizeof(float);
80
-
81
- return {floatPtr, count};
82
- }
83
-
84
- // Helper to normalize and sanitize paths (strips file:// and prevents traversal)
85
- std::string normalizePath(jsi::Runtime &runtime, std::string path) {
86
- // Strips file:// prefix if present
87
- if (path.compare(0, 7, "file://") == 0) {
88
- path = path.substr(7);
89
- }
90
-
91
- // Security Audit: Basic prevention of path traversal
92
- if (path.find("..") != std::string::npos) {
93
- throw jsi::JSError(runtime, "Security violation: Path traversal is not allowed.");
94
- }
95
-
96
- return path;
97
- }
98
-
99
- // Class that encapsulates a USearch index instance
100
- class VectorIndex : public facebook::jsi::HostObject {
101
- public:
102
- using Index = index_dense_t;
103
-
104
- VectorIndex(int dimensions, bool quantized) {
105
- metric_kind_t metric_kind = metric_kind_t::cos_k;
106
- scalar_kind_t scalar_kind = quantized ? scalar_kind_t::i8_k : scalar_kind_t::f32_k;
107
- metric_punned_t metric(dimensions, metric_kind, scalar_kind);
108
-
109
- _index = std::make_unique<Index>(Index::make(metric));
110
- if (!_index) throw std::runtime_error("Failed to initialize USearch index");
111
-
112
- if (!_index->reserve(100)) LOGE("Failed to reserve initial capacity");
113
- }
114
-
115
- // Expose methods to JavaScript
116
- jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override {
117
- std::string methodName = name.utf8(runtime);
118
-
119
- // dimensions
120
- if (methodName == "dimensions") {
121
- return jsi::Value((double)_index->dimensions());
122
- }
123
-
124
- // count
125
- if (methodName == "count") {
126
- if (!_index) return jsi::Value(0);
127
- return jsi::Value((double)_index->size());
128
- }
129
-
130
- // memoryUsage (bytes)
131
- if (methodName == "memoryUsage") {
132
- if (!_index) return jsi::Value(0);
133
- return jsi::Value((double)_index->memory_usage());
134
- }
135
-
136
- // delete() - Explicit Memory Release
137
- if (methodName == "delete") {
138
- return jsi::Function::createFromHostFunction(runtime, name, 0,
139
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
140
- _index.reset(); // Destroy the unique_ptr and release memory
141
- return jsi::Value::undefined();
142
- });
143
- }
144
-
145
- // add(key: number, vector: Float32Array)
146
- if (methodName == "add") {
147
- return jsi::Function::createFromHostFunction(runtime, name, 2,
148
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
149
- if (count < 2) throw jsi::JSError(runtime, "add expects 2 arguments: key, vector");
150
- if (!_index) throw jsi::JSError(runtime, "VectorIndex has been deleted.");
151
-
152
- double keyDouble = arguments[0].asNumber();
153
- default_key_t key = static_cast<default_key_t>(keyDouble);
154
-
155
- auto [vecData, vecSize] = getRawVector(runtime, arguments[1]);
156
-
157
- if (vecSize != _index->dimensions()) {
158
- throw jsi::JSError(runtime, "Incorrect dimension.");
159
- }
160
-
161
- if (_index->size() >= _index->capacity()) _index->reserve(_index->capacity() * 2);
162
- auto result = _index->add(key, vecData);
163
- if (!result) throw jsi::JSError(runtime, "Error adding: " + std::string(result.error.what()));
164
-
165
- return jsi::Value::undefined();
166
- });
167
- }
168
-
169
- // addBatch(keys: Int32Array, vectors: Float32Array)
170
- if (methodName == "addBatch") {
171
- return jsi::Function::createFromHostFunction(runtime, name, 2,
172
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
173
- if (count < 2) throw jsi::JSError(runtime, "addBatch expects 2 arguments: keys, vectors");
174
- if (!_index) throw jsi::JSError(runtime, "VectorIndex has been deleted.");
175
-
176
- // 1. Get Keys (Int32Array)
177
- jsi::Object keysArray = arguments[0].asObject(runtime);
178
- auto keysBufferValue = keysArray.getProperty(runtime, "buffer");
179
- auto keysBuffer = keysBufferValue.asObject(runtime).getArrayBuffer(runtime);
180
-
181
- size_t keysByteOffset = keysArray.hasProperty(runtime, "byteOffset") ?
182
- (size_t)keysArray.getProperty(runtime, "byteOffset").asNumber() : 0;
183
- size_t keysCount = (keysArray.hasProperty(runtime, "byteLength") ?
184
- (size_t)keysArray.getProperty(runtime, "byteLength").asNumber() : keysBuffer.size(runtime)) / sizeof(int32_t);
185
-
186
- const int32_t* keysData = reinterpret_cast<const int32_t*>(keysBuffer.data(runtime) + keysByteOffset);
187
-
188
- // 2. Get Vectors (Float32Array)
189
- auto [vecData, vecTotalElements] = getRawVector(runtime, arguments[1]);
190
- size_t dims = _index->dimensions();
191
- size_t batchCount = vecTotalElements / dims;
192
-
193
- // 3. Validation
194
- if (batchCount != keysCount) {
195
- throw jsi::JSError(runtime, "Batch mismatch: keys and vectors must have compatible sizes.");
196
- }
197
-
198
- // 4. Optimization: Single Reserve
199
- if (_index->size() + batchCount > _index->capacity()) {
200
- _index->reserve(_index->size() + batchCount);
201
- }
202
-
203
- // 5. Bulk Add
204
- for (size_t i = 0; i < batchCount; ++i) {
205
- auto result = _index->add((default_key_t)keysData[i], vecData + (i * dims));
206
- if (!result) throw jsi::JSError(runtime, "Error adding in batch at index " + std::to_string(i));
207
- }
208
-
209
- return jsi::Value::undefined();
210
- });
211
- }
212
-
213
- // search(vector: Float32Array, count: number)
214
- if (methodName == "search") {
215
- return jsi::Function::createFromHostFunction(runtime, name, 2,
216
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
217
- if (count < 2) throw jsi::JSError(runtime, "search expects 2 arguments: vector, count");
218
- if (!_index) throw jsi::JSError(runtime, "VectorIndex has been deleted.");
219
-
220
- auto [queryData, querySize] = getRawVector(runtime, arguments[0]);
221
- int resultsCount = static_cast<int>(arguments[1].asNumber());
222
-
223
- if (querySize != _index->dimensions()) {
224
- throw jsi::JSError(runtime, "Query vector dimension mismatch.");
225
- }
226
-
227
- auto results = _index->search(queryData, resultsCount);
228
- jsi::Array returnArray(runtime, results.size());
229
- for (size_t i = 0; i < results.size(); ++i) {
230
- auto pair = results[i];
231
- jsi::Object resultObj(runtime);
232
- resultObj.setProperty(runtime, "key", static_cast<double>(pair.member.key));
233
- resultObj.setProperty(runtime, "distance", static_cast<double>(pair.distance));
234
- returnArray.setValueAtIndex(runtime, i, resultObj);
235
- }
236
- return returnArray;
237
- });
238
- }
239
-
240
- // save(path: string)
241
- if (methodName == "save") {
242
- return jsi::Function::createFromHostFunction(runtime, name, 1,
243
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
244
- if (count < 1 || !arguments[0].isString()) throw jsi::JSError(runtime, "save expects path");
245
- std::string path = normalizePath(runtime, arguments[0].asString(runtime).utf8(runtime));
246
-
247
- if (!_index->save(path.c_str())) throw jsi::JSError(runtime, "Critical error saving index to disk: " + path);
248
- return jsi::Value::undefined();
249
- });
250
- }
251
-
252
- // load(path: string)
253
- if (methodName == "load") {
254
- return jsi::Function::createFromHostFunction(runtime, name, 1,
255
- [this](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
256
- if (count < 1 || !arguments[0].isString()) throw jsi::JSError(runtime, "load expects path");
257
- std::string path = normalizePath(runtime, arguments[0].asString(runtime).utf8(runtime));
258
-
259
- if (!_index->load(path.c_str())) throw jsi::JSError(runtime, "Critical error loading index from disk: " + path);
260
- return jsi::Value::undefined();
261
- });
262
- }
263
-
264
- return jsi::Value::undefined();
265
- }
266
-
267
- private:
268
- std::unique_ptr<Index> _index;
269
- };
270
-
271
- // --------------------------------------------------------------------------
272
- // Module Installation (Factory)
273
- // --------------------------------------------------------------------------
274
-
275
5
  extern "C" JNIEXPORT void JNICALL
276
- Java_expo_modules_vectorsearch_ExpoVectorSearchModule_nativeInstall(JNIEnv* env, jobject thiz, jlong jsiPtr) {
277
- auto runtime = reinterpret_cast<jsi::Runtime*>(jsiPtr);
278
- if (!runtime) return;
279
- auto &rt = *runtime;
280
-
281
- auto global = rt.global();
282
- auto moduleObj = jsi::Object(rt);
283
-
284
- // ExpoVectorSearch.createIndex(dimensions: number, options?: { quantization: 'f32' | 'i8' }) -> VectorIndex
285
- moduleObj.setProperty(rt, "createIndex", jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "createIndex"), 1,
286
- [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
287
- if (count < 1 || !args[0].isNumber()) {
288
- throw jsi::JSError(rt, "createIndex expects at least 1 argument: dimensions");
289
- }
290
- int dims = static_cast<int>(args[0].asNumber());
291
-
292
- bool quantized = false;
293
- if (count > 1 && args[1].isObject()) {
294
- jsi::Object options = args[1].asObject(rt);
295
- if (options.hasProperty(rt, "quantization")) {
296
- std::string q = options.getProperty(rt, "quantization").asString(rt).utf8(rt);
297
- if (q == "i8") quantized = true;
298
- }
299
- }
300
-
301
- auto indexInstance = std::make_shared<VectorIndex>(dims, quantized);
302
- return jsi::Object::createFromHostObject(rt, indexInstance);
303
- }));
304
-
305
- global.setProperty(rt, "ExpoVectorSearch", moduleObj);
6
+ Java_expo_modules_vectorsearch_ExpoVectorSearchModule_nativeInstall(
7
+ JNIEnv *env, jobject thiz, jlong jsiPtr) {
8
+ auto runtime = reinterpret_cast<facebook::jsi::Runtime *>(jsiPtr);
9
+ if (runtime) {
10
+ expo::vectorsearch::install(*runtime);
11
+ }
306
12
  }
@@ -0,0 +1,5 @@
1
+ export { VectorIndex } from './src/ExpoVectorSearchModule';
2
+ export { useVectorSearch } from './src/useVectorSearch';
3
+ import { VectorIndex } from './src/ExpoVectorSearchModule';
4
+ export default VectorIndex;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,eAAe,WAAW,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { VectorIndex } from './src/ExpoVectorSearchModule';
2
+ export { useVectorSearch } from './src/useVectorSearch';
3
+ // Export default module (VectorIndex class)
4
+ import { VectorIndex } from './src/ExpoVectorSearchModule';
5
+ export default VectorIndex;
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,4CAA4C;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,eAAe,WAAW,CAAC","sourcesContent":["export { VectorIndex } from './src/ExpoVectorSearchModule';\nexport { useVectorSearch } from './src/useVectorSearch';\n\n// Export default module (VectorIndex class)\nimport { VectorIndex } from './src/ExpoVectorSearchModule';\nexport default VectorIndex;"]}
@@ -0,0 +1,7 @@
1
+ export type Vector = Float32Array;
2
+ export type DistanceMetric = 'cos' | 'l2sq' | 'ip' | 'hamming' | 'jaccard';
3
+ export type SearchResult = {
4
+ key: number;
5
+ distance: number;
6
+ };
7
+ //# sourceMappingURL=ExpoVectorSearch.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoVectorSearch.types.d.ts","sourceRoot":"","sources":["../../src/ExpoVectorSearch.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,YAAY,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;AAE3E,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoVectorSearch.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoVectorSearch.types.js","sourceRoot":"","sources":["../../src/ExpoVectorSearch.types.ts"],"names":[],"mappings":"","sourcesContent":["export type Vector = Float32Array;\n\nexport type DistanceMetric = 'cos' | 'l2sq' | 'ip' | 'hamming' | 'jaccard';\n\nexport type SearchResult = {\n key: number;\n distance: number;\n};\n"]}