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.
- package/README.md +103 -11
- package/android/CMakeLists.txt +6 -5
- package/android/build.gradle +3 -3
- package/android/src/main/cpp/ExpoVectorSearchModule.cpp +7 -301
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/src/ExpoVectorSearch.types.d.ts +7 -0
- package/build/src/ExpoVectorSearch.types.d.ts.map +1 -0
- package/build/src/ExpoVectorSearch.types.js +2 -0
- package/build/src/ExpoVectorSearch.types.js.map +1 -0
- package/build/src/ExpoVectorSearchModule.d.ts +126 -0
- package/build/src/ExpoVectorSearchModule.d.ts.map +1 -0
- package/build/src/ExpoVectorSearchModule.js +130 -0
- package/build/src/ExpoVectorSearchModule.js.map +1 -0
- package/build/src/useVectorSearch.d.ts +20 -0
- package/build/src/useVectorSearch.d.ts.map +1 -0
- package/build/src/useVectorSearch.js +70 -0
- package/build/src/useVectorSearch.js.map +1 -0
- package/cpp/ExpoVectorSearch.h +619 -0
- package/cpp/lib/index.hpp +4577 -0
- package/cpp/usearch/index.hpp +4577 -0
- package/cpp/usearch/index_dense.hpp +2221 -0
- package/cpp/usearch/index_plugins.hpp +2568 -0
- package/index.ts +1 -0
- package/ios/ExpoVectorSearch.podspec +10 -8
- package/ios/ExpoVectorSearchModule.h +7 -0
- package/ios/ExpoVectorSearchModule.mm +18 -0
- package/ios/ExpoVectorSearchModule.swift +4 -31
- package/package.json +4 -3
- package/src/ExpoVectorSearch.types.ts +2 -0
- package/src/ExpoVectorSearchModule.ts +66 -7
- package/src/useVectorSearch.ts +85 -0
- 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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
package/android/CMakeLists.txt
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
#
|
|
19
|
+
# Make usearch available to the project
|
|
20
20
|
FetchContent_MakeAvailable(usearch)
|
|
21
21
|
|
|
22
|
-
# 3.
|
|
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.
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'expo.modules.vectorsearch'
|
|
4
|
-
version = '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
|
|
39
|
-
versionName "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(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
}
|
package/build/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"]}
|