expo-vector-search 0.4.0 → 0.5.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 CHANGED
@@ -110,10 +110,11 @@ Inserts a vector into the index.
110
110
  - `key`: A unique numeric identifier.
111
111
  - `vector`: A `Float32Array` containing the embeddings.
112
112
 
113
- #### `addBatch(keys: Int32Array, vectors: Float32Array): void`
114
- High-performance batch insertion. Significantly reduces JSI overhead by processing multiple vectors in a single native call.
113
+ #### `async addBatch(keys: Int32Array, vectors: Float32Array): Promise<VectorAddBatchResult>`
114
+ High-performance **asynchronous** batch insertion. Runs in a background thread to prevent UI freezing.
115
115
  - `keys`: An `Int32Array` of unique identifiers.
116
116
  - `vectors`: A single `Float32Array` containing all vectors concatenated (must match `keys.length * dimensions`).
117
+ - **Returns**: A promise resolving to `{ duration: number, count: number }`.
117
118
 
118
119
  #### `search(vector: Float32Array, count: number, options?: SearchOptions): SearchResult[]`
119
120
  Performs an ANN search.
@@ -140,10 +141,10 @@ Deserializes an index from a file path.
140
141
  #### `delete(): void`
141
142
  Manually releases native memory resources. The index instance becomes unusable after this call.
142
143
 
143
- #### `loadVectorsFromFile(path: string): number`
144
- Loads raw vectors directly from a binary file into the index.
144
+ #### `async loadVectorsFromFile(path: string): Promise<VectorLoadResult>`
145
+ **Asynchronously** loads raw vectors directly from a binary file into the index.
145
146
  - `path`: Absolute path to the binary file containing packed floats.
146
- - **Returns**: The number of vectors successfully loaded.
147
+ - **Returns**: A promise resolving to `{ duration: number, count: number }`.
147
148
  - **Note**: This is significantly faster than parsing JSON/Base64 in JavaScript and adding vectors loop by loop.
148
149
 
149
150
  #### `getItemVector(key: number): Float32Array | undefined`
@@ -164,6 +165,12 @@ Returns the estimated memory usage of the native index in bytes.
164
165
  #### `isa: string` (readonly)
165
166
  Returns the active SIMD instruction set name (e.g., `'NEON'`, `'AVX2'`, `'SVE'`, or `'Serial'`). Useful for verifying hardware acceleration at runtime.
166
167
 
168
+ #### `isIndexing: boolean` (readonly)
169
+ Returns `true` if a background indexing operation (`addBatch` or `loadVectorsFromFile`) is currently in progress.
170
+
171
+ #### `indexingProgress: { current: number, total: number, percentage: number }` (readonly)
172
+ Returns real-time progress of the current background indexing operation.
173
+
167
174
  ## Example Usage
168
175
 
169
176
  ```typescript
@@ -180,10 +187,10 @@ index.add(1, myVector);
180
187
  const query = new Float32Array(384).fill(0.48);
181
188
  const results = index.search(query, 5);
182
189
 
183
- // High-Performance Batch Insertion
190
+ // High-Performance Async Batch Insertion
184
191
  const manyKeys = new Int32Array([10, 11, 12]);
185
192
  const manyVectors = new Float32Array(384 * 3).fill(0.1);
186
- index.addBatch(manyKeys, manyVectors);
193
+ await index.addBatch(manyKeys, manyVectors);
187
194
 
188
195
  console.log(`Found ${results.length} neighbors`);
189
196
  results.forEach(res => {
@@ -217,12 +224,14 @@ Recent benchmarks show that Int8 indexing is actually ~4x faster than F32 precis
217
224
  ### Future Roadmap
218
225
  - [x] **Dynamic CRUD Support**: Implemented `remove(key)` and `update(key, vector)`.
219
226
  - [x] **Metadata Filtering**: Support for `allowedKeys` filtering during search.
220
- - [x] **Architecture-Specific SIMD**: Enabled NEON/AVX optimizations via SimSIMD for Android/iOS.
221
- - [ ] **Hybrid Search**: Integration with a keywords-based engine for hybrid results.
222
- - [ ] **Background Indexing**: True multithreaded ingestion to avoid JS bridge/thread locks.
227
+ - [x] **Simplified React Hooks**: Abstractions like `useVectorSearch` for automatic resource management.
228
+ - [x] **Architecture-Specific SIMD**: Enabled NEON/AVX optimizations via SimSIMD for Android and iOS.
229
+ - [x] **Background Indexing**: True multithreaded ingestion to avoid JS thread locks.
223
230
  - [x] **Extended Distance Metrics**: Support for L2, IP, Hamming, and Jaccard.
224
231
  - [x] **USearch Upgrade**: Migration to `v2.23.0+` for enhanced performance.
225
- - [ ] **Incremental Persistence**: Local storage optimizations for large datasets.
232
+ - [ ] **Hybrid Search**: Combine vector similarity with traditional keyword-based search.
233
+ - [ ] **SQLite Synchronization**: Built-in utilities to sync vector indices with `expo-sqlite`.
234
+
226
235
 
227
236
  ## License
228
237
  MIT
@@ -7,12 +7,30 @@ export interface VectorIndexOptions {
7
7
  export interface SearchOptions {
8
8
  allowedKeys?: number[] | Int32Array | Uint32Array;
9
9
  }
10
+ export type AddResult = {
11
+ duration: number;
12
+ };
13
+ export type VectorAddBatchResult = {
14
+ duration: number;
15
+ count: number;
16
+ };
17
+ export type VectorLoadResult = {
18
+ duration: number;
19
+ count: number;
20
+ };
21
+ export type IndexingProgress = {
22
+ current: number;
23
+ total: number;
24
+ percentage: number;
25
+ };
10
26
  interface VectorIndexHostObject {
11
27
  dimensions: number;
12
28
  count: number;
13
29
  memoryUsage: number;
14
30
  isa: string;
15
- add(key: number, vector: Vector): void;
31
+ isIndexing: boolean;
32
+ indexingProgress: IndexingProgress;
33
+ add(key: number, vector: Vector): AddResult;
16
34
  remove(key: number): void;
17
35
  update(key: number, vector: Vector): void;
18
36
  search(vector: Vector, count: number, options?: SearchOptions): SearchResult[];
@@ -20,8 +38,9 @@ interface VectorIndexHostObject {
20
38
  load(path: string): void;
21
39
  delete(): void;
22
40
  addBatch(keys: Int32Array, vectors: Float32Array): void;
23
- loadVectorsFromFile(path: string): number;
41
+ loadVectorsFromFile(path: string): void;
24
42
  getItemVector(key: number): Float32Array | undefined;
43
+ getLastResult(): VectorLoadResult;
25
44
  }
26
45
  interface ExpoVectorSearchFactory {
27
46
  createIndex(dimensions: number, options?: VectorIndexOptions): VectorIndexHostObject;
@@ -59,21 +78,32 @@ export declare class VectorIndex {
59
78
  * The SIMD Instruction Set Architecture being used (e.g. 'neon', 'avx2', 'serial').
60
79
  */
61
80
  get isa(): string;
81
+ /**
82
+ * Whether the index is currently processing an asynchronous operation (like addBatch).
83
+ */
84
+ get isIndexing(): boolean;
85
+ /**
86
+ * The real-time progress of an ongoing indexing operation.
87
+ */
88
+ get indexingProgress(): IndexingProgress;
62
89
  /**
63
90
  * Adds a vector to the index.
64
91
  * @param key A unique numeric identifier for the vector.
65
92
  * @param vector A Float32Array containing the vector data.
66
93
  * @throws Error if the vector dimension doesn't match or memory allocation fails.
67
94
  */
68
- add(key: number, vector: Vector): void;
95
+ add(key: number, vector: Vector): AddResult;
69
96
  /**
70
97
  * Adds multiple vectors in a single high-performance batch operation.
71
98
  * This is significantly faster than calling `.add()` in a loop.
72
99
  * @param keys An Int32Array of unique numeric identifiers.
73
- * @param vectors A single Float32Array containing all vectors concatenated.
74
100
  * @throws Error if buffer sizes or alignment do not match.
75
101
  */
76
- addBatch(keys: Int32Array, vectors: Float32Array): void;
102
+ addBatch(keys: Int32Array, vectors: Float32Array): Promise<VectorAddBatchResult>;
103
+ /**
104
+ * Internal helper to poll for operation completion.
105
+ */
106
+ private _waitForOperation;
77
107
  /**
78
108
  * Removes a vector from the index.
79
109
  * @param key The unique numeric identifier of the vector to remove.
@@ -111,9 +141,9 @@ export declare class VectorIndex {
111
141
  * Loads raw vectors directly from a binary file.
112
142
  * This avoids JS parsing overhead and is much faster for initialization.
113
143
  * @param path The absolute path to the binary file containing packed floats.
114
- * @returns The number of vectors loaded.
144
+ * @returns An object containing the number of vectors loaded and the duration.
115
145
  */
116
- loadVectorsFromFile(path: string): number;
146
+ loadVectorsFromFile(path: string): Promise<VectorLoadResult>;
117
147
  /**
118
148
  * Retrieves the vector associated with a specific key from the index.
119
149
  * Useful when vectors are stored only in native memory (e.g., after loadVectorsFromFile).
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoVectorSearchModule.d.ts","sourceRoot":"","sources":["../../src/ExpoVectorSearchModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoVectorSearchModule.d.ts","sourceRoot":"","sources":["../../src/ExpoVectorSearchModule.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,18 @@ export class VectorIndex {
44
44
  get isa() {
45
45
  return this._index.isa;
46
46
  }
47
+ /**
48
+ * Whether the index is currently processing an asynchronous operation (like addBatch).
49
+ */
50
+ get isIndexing() {
51
+ return this._index.isIndexing;
52
+ }
53
+ /**
54
+ * The real-time progress of an ongoing indexing operation.
55
+ */
56
+ get indexingProgress() {
57
+ return this._index.indexingProgress;
58
+ }
47
59
  /**
48
60
  * Adds a vector to the index.
49
61
  * @param key A unique numeric identifier for the vector.
@@ -51,17 +63,26 @@ export class VectorIndex {
51
63
  * @throws Error if the vector dimension doesn't match or memory allocation fails.
52
64
  */
53
65
  add(key, vector) {
54
- this._index.add(key, vector);
66
+ return this._index.add(key, vector);
55
67
  }
56
68
  /**
57
69
  * Adds multiple vectors in a single high-performance batch operation.
58
70
  * This is significantly faster than calling `.add()` in a loop.
59
71
  * @param keys An Int32Array of unique numeric identifiers.
60
- * @param vectors A single Float32Array containing all vectors concatenated.
61
72
  * @throws Error if buffer sizes or alignment do not match.
62
73
  */
63
- addBatch(keys, vectors) {
74
+ async addBatch(keys, vectors) {
64
75
  this._index.addBatch(keys, vectors);
76
+ return this._waitForOperation();
77
+ }
78
+ /**
79
+ * Internal helper to poll for operation completion.
80
+ */
81
+ async _waitForOperation() {
82
+ while (this._index.isIndexing) {
83
+ await new Promise((resolve) => setTimeout(resolve, 50));
84
+ }
85
+ return this._index.getLastResult();
65
86
  }
66
87
  /**
67
88
  * Removes a vector from the index.
@@ -110,10 +131,11 @@ export class VectorIndex {
110
131
  * Loads raw vectors directly from a binary file.
111
132
  * This avoids JS parsing overhead and is much faster for initialization.
112
133
  * @param path The absolute path to the binary file containing packed floats.
113
- * @returns The number of vectors loaded.
134
+ * @returns An object containing the number of vectors loaded and the duration.
114
135
  */
115
- loadVectorsFromFile(path) {
116
- return this._index.loadVectorsFromFile(path);
136
+ async loadVectorsFromFile(path) {
137
+ this._index.loadVectorsFromFile(path);
138
+ return this._waitForOperation();
117
139
  }
118
140
  /**
119
141
  * Retrieves the vector associated with a specific key from the index.
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoVectorSearchModule.js","sourceRoot":"","sources":["../../src/ExpoVectorSearchModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAG3C,2EAA2E;AAC3E,mBAAmB,CAAC,kBAAkB,CAAC,CAAC;AA6CxC;;;GAGG;AACH,MAAM,OAAO,WAAW;IACd,MAAM,CAAwB;IAEtC;;;;;OAKG;IACH,YAAY,UAAkB,EAAE,OAA4B;QAC1D,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,gBAAgB,CAAC,WAAW,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC7E,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;IACjC,CAAC;IAED;;OAEG;IACH,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAC,GAAW,EAAE,MAAc;QAC7B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,IAAgB,EAAE,OAAqB;QAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,GAAW,EAAE,MAAc;QAChC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CACJ,MAAc,EACd,KAAa,EACb,OAAuB;QAEvB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED;;;;;OAKG;IACH,mBAAmB,CAAC,IAAY;QAC9B,OAAO,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;;OAKG;IACH,aAAa,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED;;;OAGG;IACH,MAAM;QACJ,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;IACvB,CAAC;CACF;AAED,eAAe,WAAW,CAAC","sourcesContent":["import { requireNativeModule } from 'expo';\nimport { DistanceMetric, SearchResult, Vector } from './ExpoVectorSearch.types';\n\n// The native module is loaded to ensure JSI installation occurs (OnCreate)\nrequireNativeModule('ExpoVectorSearch');\n\n// Index creation options\nexport type QuantizationMode = 'f32' | 'f16' | 'i8';\n\nexport interface VectorIndexOptions {\n quantization?: QuantizationMode;\n metric?: DistanceMetric;\n}\n\nexport interface SearchOptions {\n allowedKeys?: number[] | Int32Array | Uint32Array;\n}\n\n// C++ HostObject Interface (Index Instance)\ninterface VectorIndexHostObject {\n dimensions: number;\n count: number;\n memoryUsage: number;\n isa: string;\n add(key: number, vector: Vector): void;\n remove(key: number): void;\n update(key: number, vector: Vector): void;\n search(\n vector: Vector,\n count: number,\n options?: SearchOptions\n ): SearchResult[];\n save(path: string): void;\n load(path: string): void;\n delete(): void;\n addBatch(keys: Int32Array, vectors: Float32Array): void;\n loadVectorsFromFile(path: string): number;\n getItemVector(key: number): Float32Array | undefined;\n}\n\n// Global Module Interface (Factory)\ninterface ExpoVectorSearchFactory {\n createIndex(dimensions: number, options?: VectorIndexOptions): VectorIndexHostObject;\n}\n\ndeclare global {\n var ExpoVectorSearch: ExpoVectorSearchFactory;\n}\n\n/**\n * High-performance Vector Index powered by USearch (C++ JSI).\n * Allows for ultra-fast, on-device semantic search and similarity matching.\n */\nexport class VectorIndex {\n private _index: VectorIndexHostObject;\n\n /**\n * Creates a new vector index.\n * @param dimensions The dimensionality of the vectors (e.g., 768 or 1536).\n * @param options Configuration options for the index.\n * @throws Error if the native JSI module is not available.\n */\n constructor(dimensions: number, options?: VectorIndexOptions) {\n if (!globalThis.ExpoVectorSearch) {\n throw new Error(\"ExpoVectorSearch JSI module is not available.\");\n }\n this._index = globalThis.ExpoVectorSearch.createIndex(dimensions, options);\n }\n\n /**\n * The dimensionality of the vectors in this index.\n */\n get dimensions(): number {\n return this._index.dimensions;\n }\n\n /**\n * The total number of vectors currently stored in the index.\n */\n get count(): number {\n return this._index.count;\n }\n\n /**\n * The estimated memory usage of the native index in bytes.\n * Does not include JavaScript object overhead.\n */\n get memoryUsage(): number {\n return this._index.memoryUsage;\n }\n\n /**\n * The SIMD Instruction Set Architecture being used (e.g. 'neon', 'avx2', 'serial').\n */\n get isa(): string {\n return this._index.isa;\n }\n\n /**\n * Adds a vector to the index.\n * @param key A unique numeric identifier for the vector.\n * @param vector A Float32Array containing the vector data.\n * @throws Error if the vector dimension doesn't match or memory allocation fails.\n */\n add(key: number, vector: Vector): void {\n this._index.add(key, vector);\n }\n\n /**\n * Adds multiple vectors in a single high-performance batch operation.\n * This is significantly faster than calling `.add()` in a loop.\n * @param keys An Int32Array of unique numeric identifiers.\n * @param vectors A single Float32Array containing all vectors concatenated.\n * @throws Error if buffer sizes or alignment do not match.\n */\n addBatch(keys: Int32Array, vectors: Float32Array): void {\n this._index.addBatch(keys, vectors);\n }\n\n /**\n * Removes a vector from the index.\n * @param key The unique numeric identifier of the vector to remove.\n * @throws Error if the key is not found or removal fails.\n */\n remove(key: number): void {\n this._index.remove(key);\n }\n\n /**\n * Updates an existing vector in the index.\n * This is equivalent to removing the old vector and adding a new one.\n * @param key The unique numeric identifier.\n * @param vector The new vector data.\n * @throws Error if dimensions mismatch or update fails.\n */\n update(key: number, vector: Vector): void {\n this._index.update(key, vector);\n }\n\n /**\n * Performs an Approximate Nearest Neighbor (ANN) search.\n * @param vector The query vector.\n * @param count The number of nearest neighbors to return.\n * @param options Optional SearchOptions (e.g., allowedKeys for filtering).\n * @returns An array of SearchResult objects (key and distance).\n * @throws Error if dimensions mismatch or search fails.\n */\n search(\n vector: Vector,\n count: number,\n options?: SearchOptions\n ): SearchResult[] {\n return this._index.search(vector, count, options);\n }\n\n /**\n * Saves the index to a file.\n * @param path The absolute path to the file (e.g., in Expo.FileSystem.documentDirectory).\n */\n save(path: string): void {\n this._index.save(path);\n }\n\n /**\n * Loads the index from a file.\n * @param path The absolute path to the file.\n */\n load(path: string): void {\n this._index.load(path);\n }\n\n /**\n * Loads raw vectors directly from a binary file.\n * This avoids JS parsing overhead and is much faster for initialization.\n * @param path The absolute path to the binary file containing packed floats.\n * @returns The number of vectors loaded.\n */\n loadVectorsFromFile(path: string): number {\n return this._index.loadVectorsFromFile(path);\n }\n\n /**\n * Retrieves the vector associated with a specific key from the index.\n * Useful when vectors are stored only in native memory (e.g., after loadVectorsFromFile).\n * @param key The unique key of the item.\n * @returns The vector as a Float32Array, or undefined if not found.\n */\n getItemVector(key: number): Float32Array | undefined {\n return this._index.getItemVector(key);\n }\n\n /**\n * Explicitly releases the native memory associated with this index.\n * Once called, the index can no longer be used.\n */\n delete(): void {\n this._index.delete();\n }\n}\n\nexport default VectorIndex;"]}
1
+ {"version":3,"file":"ExpoVectorSearchModule.js","sourceRoot":"","sources":["../../src/ExpoVectorSearchModule.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireNativeModule } from 'expo';\nimport { DistanceMetric, SearchResult, Vector } from './ExpoVectorSearch.types';\n\n// The native module is loaded to ensure JSI installation occurs (OnCreate)\nrequireNativeModule('ExpoVectorSearch');\n\n// Index creation options\nexport type QuantizationMode = 'f32' | 'f16' | 'i8';\n\nexport interface VectorIndexOptions {\n quantization?: QuantizationMode;\n metric?: DistanceMetric;\n}\n\nexport interface SearchOptions {\n allowedKeys?: number[] | Int32Array | Uint32Array;\n}\n\nexport type AddResult = {\n duration: number; // in milliseconds\n};\n\nexport type VectorAddBatchResult = {\n duration: number; // in milliseconds\n count: number;\n};\n\nexport type VectorLoadResult = {\n duration: number; // in milliseconds\n count: number;\n};\n\nexport type IndexingProgress = {\n current: number;\n total: number;\n percentage: number;\n};\n\n// C++ HostObject Interface (Index Instance)\ninterface VectorIndexHostObject {\n dimensions: number;\n count: number;\n memoryUsage: number;\n isa: string;\n isIndexing: boolean;\n indexingProgress: IndexingProgress;\n add(key: number, vector: Vector): AddResult;\n remove(key: number): void;\n update(key: number, vector: Vector): void;\n search(\n vector: Vector,\n count: number,\n options?: SearchOptions\n ): SearchResult[];\n save(path: string): void;\n load(path: string): void;\n delete(): void;\n addBatch(keys: Int32Array, vectors: Float32Array): void;\n loadVectorsFromFile(path: string): void;\n getItemVector(key: number): Float32Array | undefined;\n getLastResult(): VectorLoadResult;\n}\n\n// Global Module Interface (Factory)\ninterface ExpoVectorSearchFactory {\n createIndex(dimensions: number, options?: VectorIndexOptions): VectorIndexHostObject;\n}\n\ndeclare global {\n var ExpoVectorSearch: ExpoVectorSearchFactory;\n}\n\n/**\n * High-performance Vector Index powered by USearch (C++ JSI).\n * Allows for ultra-fast, on-device semantic search and similarity matching.\n */\nexport class VectorIndex {\n private _index: VectorIndexHostObject;\n\n /**\n * Creates a new vector index.\n * @param dimensions The dimensionality of the vectors (e.g., 768 or 1536).\n * @param options Configuration options for the index.\n * @throws Error if the native JSI module is not available.\n */\n constructor(dimensions: number, options?: VectorIndexOptions) {\n if (!globalThis.ExpoVectorSearch) {\n throw new Error(\"ExpoVectorSearch JSI module is not available.\");\n }\n this._index = globalThis.ExpoVectorSearch.createIndex(dimensions, options);\n }\n\n /**\n * The dimensionality of the vectors in this index.\n */\n get dimensions(): number {\n return this._index.dimensions;\n }\n\n /**\n * The total number of vectors currently stored in the index.\n */\n get count(): number {\n return this._index.count;\n }\n\n /**\n * The estimated memory usage of the native index in bytes.\n * Does not include JavaScript object overhead.\n */\n get memoryUsage(): number {\n return this._index.memoryUsage;\n }\n\n /**\n * The SIMD Instruction Set Architecture being used (e.g. 'neon', 'avx2', 'serial').\n */\n get isa(): string {\n return this._index.isa;\n }\n\n /**\n * Whether the index is currently processing an asynchronous operation (like addBatch).\n */\n get isIndexing(): boolean {\n return this._index.isIndexing;\n }\n\n /**\n * The real-time progress of an ongoing indexing operation.\n */\n get indexingProgress(): IndexingProgress {\n return this._index.indexingProgress;\n }\n\n /**\n * Adds a vector to the index.\n * @param key A unique numeric identifier for the vector.\n * @param vector A Float32Array containing the vector data.\n * @throws Error if the vector dimension doesn't match or memory allocation fails.\n */\n add(key: number, vector: Vector): AddResult {\n return this._index.add(key, vector);\n }\n\n /**\n * Adds multiple vectors in a single high-performance batch operation.\n * This is significantly faster than calling `.add()` in a loop.\n * @param keys An Int32Array of unique numeric identifiers.\n * @throws Error if buffer sizes or alignment do not match.\n */\n async addBatch(\n keys: Int32Array,\n vectors: Float32Array\n ): Promise<VectorAddBatchResult> {\n this._index.addBatch(keys, vectors);\n return this._waitForOperation();\n }\n\n /**\n * Internal helper to poll for operation completion.\n */\n private async _waitForOperation(): Promise<VectorLoadResult> {\n while (this._index.isIndexing) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n return this._index.getLastResult();\n }\n\n /**\n * Removes a vector from the index.\n * @param key The unique numeric identifier of the vector to remove.\n * @throws Error if the key is not found or removal fails.\n */\n remove(key: number): void {\n this._index.remove(key);\n }\n\n /**\n * Updates an existing vector in the index.\n * This is equivalent to removing the old vector and adding a new one.\n * @param key The unique numeric identifier.\n * @param vector The new vector data.\n * @throws Error if dimensions mismatch or update fails.\n */\n update(key: number, vector: Vector): void {\n this._index.update(key, vector);\n }\n\n /**\n * Performs an Approximate Nearest Neighbor (ANN) search.\n * @param vector The query vector.\n * @param count The number of nearest neighbors to return.\n * @param options Optional SearchOptions (e.g., allowedKeys for filtering).\n * @returns An array of SearchResult objects (key and distance).\n * @throws Error if dimensions mismatch or search fails.\n */\n search(\n vector: Vector,\n count: number,\n options?: SearchOptions\n ): SearchResult[] {\n return this._index.search(vector, count, options);\n }\n\n /**\n * Saves the index to a file.\n * @param path The absolute path to the file (e.g., in Expo.FileSystem.documentDirectory).\n */\n save(path: string): void {\n this._index.save(path);\n }\n\n /**\n * Loads the index from a file.\n * @param path The absolute path to the file.\n */\n load(path: string): void {\n this._index.load(path);\n }\n\n /**\n * Loads raw vectors directly from a binary file.\n * This avoids JS parsing overhead and is much faster for initialization.\n * @param path The absolute path to the binary file containing packed floats.\n * @returns An object containing the number of vectors loaded and the duration.\n */\n async loadVectorsFromFile(path: string): Promise<VectorLoadResult> {\n this._index.loadVectorsFromFile(path);\n return this._waitForOperation();\n }\n\n /**\n * Retrieves the vector associated with a specific key from the index.\n * Useful when vectors are stored only in native memory (e.g., after loadVectorsFromFile).\n * @param key The unique key of the item.\n * @returns The vector as a Float32Array, or undefined if not found.\n */\n getItemVector(key: number): Float32Array | undefined {\n return this._index.getItemVector(key);\n }\n\n /**\n * Explicitly releases the native memory associated with this index.\n * Once called, the index can no longer be used.\n */\n delete(): void {\n this._index.delete();\n }\n}\n\nexport default VectorIndex;"]}
@@ -1,6 +1,8 @@
1
1
  #pragma once
2
2
 
3
3
  #ifdef __cplusplus
4
+ #include <atomic>
5
+ #include <chrono>
4
6
  #include <cstdio>
5
7
  #include <cstdlib>
6
8
  #include <fstream>
@@ -135,7 +137,15 @@ inline float jaccard_f32(const float *a, const float *b, std::size_t n,
135
137
  return 1.0f - (intersection / union_count);
136
138
  }
137
139
 
138
- class VectorIndexHostObject : public jsi::HostObject {
140
+ struct OperationResult {
141
+ double duration = 0;
142
+ size_t count = 0;
143
+ std::string error = "";
144
+ };
145
+
146
+ class VectorIndexHostObject
147
+ : public jsi::HostObject,
148
+ public std::enable_shared_from_this<VectorIndexHostObject> {
139
149
  public:
140
150
  using Index = index_dense_t;
141
151
 
@@ -146,20 +156,21 @@ public:
146
156
  _threads = std::thread::hardware_concurrency();
147
157
  if (_threads == 0)
148
158
  _threads = 1;
159
+ _quantized = quantized;
149
160
 
150
161
  scalar_kind_t scalar_kind =
151
162
  quantized ? scalar_kind_t::i8_k : scalar_kind_t::f32_k;
152
163
 
153
164
  // Special case: Jaccard with f32 (not bitsets)
154
165
  if (metric_kind == metric_kind_t::jaccard_k && !quantized) {
155
- metric_punned_t metric(dimensions,
156
- reinterpret_cast<std::uintptr_t>(&jaccard_f32),
157
- metric_punned_signature_t::array_array_size_k,
158
- metric_kind_t::jaccard_k, scalar_kind_t::f32_k);
159
- _index = std::make_unique<Index>(Index::make(metric));
166
+ metric_punned_t metric_j(dimensions,
167
+ reinterpret_cast<std::uintptr_t>(&jaccard_f32),
168
+ metric_punned_signature_t::array_array_size_k,
169
+ metric_kind_t::jaccard_k, scalar_kind_t::f32_k);
170
+ _index = std::make_shared<Index>(Index::make(metric_j));
160
171
  } else {
161
172
  metric_punned_t metric(dimensions, metric_kind, scalar_kind);
162
- _index = std::make_unique<Index>(Index::make(metric));
173
+ _index = std::make_shared<Index>(Index::make(metric));
163
174
  }
164
175
 
165
176
  LOGD("Initializing Index HostObject: dims=%d, quantized=%d, metric=%d",
@@ -185,13 +196,58 @@ public:
185
196
  return jsi::Value((double)_index->dimensions());
186
197
  if (methodName == "count")
187
198
  return jsi::Value(_index ? (double)_index->size() : 0);
188
- if (methodName == "memoryUsage")
189
- return jsi::Value(_index ? (double)_index->memory_usage() : 0);
199
+ if (methodName == "memoryUsage") {
200
+ if (!_index)
201
+ return jsi::Value(0);
202
+ // We calculate memory usage manually to avoid a race condition in
203
+ // USearch's stats() when called during background indexing. This is also
204
+ // much faster and safer.
205
+ size_t count = _index->size();
206
+ size_t dims = _index->dimensions();
207
+ // Estimation:
208
+ // 1. Vector data (the largest part)
209
+ size_t vectorBytes = count * dims * (_quantized ? 1 : 4);
210
+ // 2. Graph overhead (nodes + neighbors). USearch node is ~64 bytes +
211
+ // connectivity * 4. We assume an average connectivity of 32 for the
212
+ // estimation.
213
+ size_t graphOverhead = count * (64 + 32 * 4);
214
+ // 3. Buffer base (metadata, threads, etc)
215
+ size_t baseMemory = 1024 * 1024; // 1MB base
216
+ return jsi::Value((double)(vectorBytes + graphOverhead + baseMemory));
217
+ }
190
218
  if (methodName == "isa") {
191
219
  const char *isa = _index ? _index->metric().isa_name() : "unknown";
192
- LOGD("isa property accessed: %s", isa);
193
220
  return jsi::String::createFromUtf8(runtime, isa);
194
221
  }
222
+ if (methodName == "isIndexing") {
223
+ return jsi::Value(_isIndexing.load());
224
+ }
225
+ if (methodName == "indexingProgress") {
226
+ jsi::Object res(runtime);
227
+ size_t current = _currentIndexingCount.load();
228
+ size_t total = _totalIndexingCount.load();
229
+ double percentage = (total > 0) ? (double)current / total : 0;
230
+ res.setProperty(runtime, "current", (double)current);
231
+ res.setProperty(runtime, "total", (double)total);
232
+ res.setProperty(runtime, "percentage", percentage);
233
+ return res;
234
+ }
235
+ if (methodName == "getLastResult") {
236
+ return jsi::Function::createFromHostFunction(
237
+ runtime, name, 0,
238
+ [this](jsi::Runtime &runtime, const jsi::Value &thisValue,
239
+ const jsi::Value *arguments, size_t count) -> jsi::Value {
240
+ if (!_lastResult.error.empty()) {
241
+ std::string err = _lastResult.error;
242
+ _lastResult.error = ""; // Clear after reporting
243
+ throw jsi::JSError(runtime, err);
244
+ }
245
+ jsi::Object res(runtime);
246
+ res.setProperty(runtime, "duration", _lastResult.duration);
247
+ res.setProperty(runtime, "count", (double)_lastResult.count);
248
+ return res;
249
+ });
250
+ }
195
251
 
196
252
  if (methodName == "delete") {
197
253
  return jsi::Function::createFromHostFunction(
@@ -234,17 +290,22 @@ public:
234
290
  LOGD("Resizing index to: %zu", newCapacity);
235
291
  _index->reserve(index_limits_t(newCapacity, _threads));
236
292
  }
237
- LOGD("Attempting to add: key=%llu, ptr=%p, dim=%zu",
238
- (unsigned long long)key, vecData, _index->dimensions());
293
+ auto start = std::chrono::high_resolution_clock::now();
239
294
  auto result = _index->add(key, vecData);
240
- LOGD("Add completed: key=%llu", (unsigned long long)key);
295
+ auto end = std::chrono::high_resolution_clock::now();
296
+
241
297
  if (!result) {
242
298
  LOGE("Failed to add vector: %s", result.error.what());
243
299
  throw jsi::JSError(runtime, "Error adding: " +
244
300
  std::string(result.error.what()));
245
301
  }
246
302
 
247
- return jsi::Value::undefined();
303
+ double durationMs =
304
+ std::chrono::duration<double, std::milli>(end - start).count();
305
+
306
+ jsi::Object res(runtime);
307
+ res.setProperty(runtime, "duration", durationMs);
308
+ return res;
248
309
  });
249
310
  }
250
311
 
@@ -253,12 +314,13 @@ public:
253
314
  runtime, name, 2,
254
315
  [this](jsi::Runtime &runtime, const jsi::Value &thisValue,
255
316
  const jsi::Value *arguments, size_t count) -> jsi::Value {
256
- LOGD("addBatch called");
257
317
  if (count < 2)
258
318
  throw jsi::JSError(runtime,
259
319
  "addBatch expects 2 arguments: keys, vectors");
260
320
  if (!_index)
261
321
  throw jsi::JSError(runtime, "VectorIndex has been deleted.");
322
+ if (_isIndexing)
323
+ throw jsi::JSError(runtime, "Index is already busy.");
262
324
 
263
325
  jsi::Object keysArray = arguments[0].asObject(runtime);
264
326
  auto keysBuffer = keysArray.getProperty(runtime, "buffer")
@@ -283,37 +345,54 @@ public:
283
345
  size_t dims = _index->dimensions();
284
346
  size_t batchCount = vecTotalElements / dims;
285
347
 
286
- LOGD("addBatch processing: keysCount=%zu, batchCount=%zu, "
287
- "dims=%zu",
288
- keysCount, batchCount, dims);
289
-
290
348
  if (batchCount != keysCount)
291
349
  throw jsi::JSError(runtime, "Batch mismatch: keys and vectors "
292
350
  "must have compatible sizes.");
293
351
 
294
352
  if (_index->size() + batchCount > _index->capacity()) {
295
- size_t newCapacity = _index->capacity() + batchCount;
296
- // Double it if small to avoid frequent realloc
297
- if (newCapacity < _index->capacity() * 2)
298
- newCapacity = _index->capacity() * 2;
299
- if (newCapacity < _index->size() + batchCount)
300
- newCapacity = _index->size() + batchCount + 100;
301
-
302
- LOGD("Resizing index for batch to: %zu", newCapacity);
353
+ size_t newCapacity = _index->size() + batchCount + 100;
303
354
  _index->reserve(index_limits_t(newCapacity, _threads));
304
355
  }
305
356
 
306
- for (size_t i = 0; i < batchCount; ++i) {
307
- auto result =
308
- _index->add((default_key_t)keysData[i], vecData + (i * dims));
309
- if (!result) {
310
- LOGE("Error adding in batch at index %zu: %s", i,
311
- result.error.what());
312
- throw jsi::JSError(runtime, "Error adding in batch at index " +
313
- std::to_string(i));
357
+ // Copy data safely for background thread
358
+ std::vector<int32_t> keys(keysData, keysData + batchCount);
359
+ std::vector<float> vectors(vecData, vecData + (batchCount * dims));
360
+
361
+ _isIndexing = true;
362
+ _currentIndexingCount = 0;
363
+ _totalIndexingCount = batchCount;
364
+
365
+ // Capture self to keep HostObject alive during background thread
366
+ std::thread([self = shared_from_this(), keys = std::move(keys),
367
+ vectors = std::move(vectors), batchCount,
368
+ dims]() mutable {
369
+ auto start = std::chrono::high_resolution_clock::now();
370
+ try {
371
+ for (size_t i = 0; i < batchCount; ++i) {
372
+ if (!self->_index)
373
+ break; // Safety check
374
+ auto result = self->_index->add((default_key_t)keys[i],
375
+ vectors.data() + (i * dims));
376
+ if (!result) {
377
+ self->_lastResult.error =
378
+ "Error adding at index " + std::to_string(i);
379
+ self->_isIndexing = false;
380
+ return;
381
+ }
382
+ self->_currentIndexingCount++;
383
+ }
384
+ 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 = "";
390
+ } catch (const std::exception &e) {
391
+ self->_lastResult.error = e.what();
314
392
  }
315
- }
316
- LOGD("addBatch completed successfully");
393
+ self->_isIndexing = false;
394
+ }).detach();
395
+
317
396
  return jsi::Value::undefined();
318
397
  });
319
398
  }
@@ -517,6 +596,9 @@ public:
517
596
  const jsi::Value *arguments, size_t count) -> jsi::Value {
518
597
  if (count < 1 || !arguments[0].isString())
519
598
  throw jsi::JSError(runtime, "loadVectorsFromFile expects path");
599
+ if (_isIndexing)
600
+ throw jsi::JSError(runtime, "Index is already busy.");
601
+
520
602
  std::string path = normalizePath(
521
603
  runtime, arguments[0].asString(runtime).utf8(runtime));
522
604
 
@@ -525,39 +607,52 @@ public:
525
607
  throw jsi::JSError(runtime, "Could not open file: " + path);
526
608
 
527
609
  std::streamsize size = file.tellg();
528
- file.seekg(0, std::ios::beg);
529
-
530
610
  if (size <= 0)
531
611
  return jsi::Value::undefined();
532
612
 
533
613
  size_t dims = _index->dimensions();
534
- if (size % (dims * sizeof(float)) != 0) {
535
- throw jsi::JSError(runtime,
536
- "File size is not multiple of dimension");
537
- }
538
-
539
614
  size_t numVectors = size / (dims * sizeof(float));
540
- std::vector<char> buffer(size);
541
- if (!file.read(buffer.data(), size))
542
- throw jsi::JSError(runtime, "Failed to read file");
543
-
544
- const float *vectorData =
545
- reinterpret_cast<const float *>(buffer.data());
546
-
547
- // Reserve capacity
548
- if (_index->size() + numVectors > _index->capacity()) {
549
- size_t newCap = _index->size() + numVectors + 100;
550
- LOGD("Resizing index for large binary load: %zu", newCap);
551
- _index->reserve(newCap);
552
- }
553
615
 
554
- // Batch insert assuming keys 0..N
555
- for (size_t i = 0; i < numVectors; ++i) {
556
- _index->add((default_key_t)i, vectorData + (i * dims));
557
- }
616
+ _isIndexing = true;
617
+ _currentIndexingCount = 0;
618
+ _totalIndexingCount = numVectors;
619
+
620
+ // Capture self to keep HostObject alive during background thread
621
+ std::thread([self = shared_from_this(), path, numVectors, dims]() {
622
+ auto start = std::chrono::high_resolution_clock::now();
623
+ try {
624
+ std::ifstream file(path, std::ios::binary);
625
+ std::vector<float> vectorData(numVectors * dims);
626
+ file.read(reinterpret_cast<char *>(vectorData.data()),
627
+ numVectors * dims * sizeof(float));
628
+
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);
635
+ }
636
+
637
+ for (size_t i = 0; i < numVectors; ++i) {
638
+ self->_index->add((default_key_t)i,
639
+ vectorData.data() + (i * dims));
640
+ self->_currentIndexingCount++;
641
+ }
642
+
643
+ 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 = "";
649
+ } catch (const std::exception &e) {
650
+ self->_lastResult.error = e.what();
651
+ }
652
+ self->_isIndexing = false;
653
+ }).detach();
558
654
 
559
- LOGD("Appended %zu vectors from file", numVectors);
560
- return jsi::Value((double)numVectors);
655
+ return jsi::Value::undefined();
561
656
  });
562
657
  }
563
658
 
@@ -581,7 +676,12 @@ public:
581
676
  }
582
677
 
583
678
  private:
584
- std::unique_ptr<Index> _index;
679
+ std::shared_ptr<Index> _index;
680
+ std::atomic<bool> _isIndexing{false};
681
+ std::atomic<size_t> _currentIndexingCount{0};
682
+ std::atomic<size_t> _totalIndexingCount{0};
683
+ bool _quantized;
684
+ OperationResult _lastResult;
585
685
  };
586
686
 
587
687
  inline void install(jsi::Runtime &rt) {
@@ -1592,8 +1592,16 @@ public:
1592
1592
  case simsimd_cap_serial_k:
1593
1593
  return "serial";
1594
1594
  case simsimd_cap_neon_k:
1595
+ case simsimd_cap_neon_f16_k:
1596
+ case simsimd_cap_neon_bf16_k:
1597
+ case simsimd_cap_neon_i8_k:
1595
1598
  return "neon";
1596
1599
  case simsimd_cap_sve_k:
1600
+ case simsimd_cap_sve_f16_k:
1601
+ case simsimd_cap_sve_bf16_k:
1602
+ case simsimd_cap_sve_i8_k:
1603
+ case simsimd_cap_sve2_k:
1604
+ case simsimd_cap_sve2p1_k:
1597
1605
  return "sve";
1598
1606
  case simsimd_cap_haswell_k:
1599
1607
  return "avx2";
@@ -1601,10 +1609,14 @@ public:
1601
1609
  return "avx512";
1602
1610
  case simsimd_cap_ice_k:
1603
1611
  return "avx512+popcnt";
1612
+ case simsimd_cap_genoa_k:
1613
+ return "avx512+bf16";
1604
1614
  case simsimd_cap_sapphire_k:
1605
1615
  return "avx512+f16";
1606
- case simsimd_cap_sve2_k:
1607
- return "sve2";
1616
+ case simsimd_cap_turin_k:
1617
+ return "avx512+conflict";
1618
+ case simsimd_cap_sierra_k:
1619
+ return "avx2+vnni";
1608
1620
  default:
1609
1621
  return "unknown";
1610
1622
  }