usearch 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +47 -8
- package/conanfile.py +1 -1
- package/docs/wolfram/index.rst +6 -0
- package/javascript/lib.cpp +62 -0
- package/javascript/test.js +1 -0
- package/javascript/usearch.d.ts +17 -2
- package/package.json +1 -1
- package/python/lib.cpp +5 -2
- package/rust/lib.cpp +17 -8
- package/rust/lib.hpp +3 -0
- package/rust/lib.rs +5 -4
- package/wasmer.toml +1 -1
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ Vector Search Engine<br/>
|
|
|
20
20
|
<p align="center">
|
|
21
21
|
Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Metrics
|
|
22
22
|
<br/>
|
|
23
|
-
<a href="https://unum-cloud.github.io/usearch/cpp">C++11</a> •
|
|
24
|
-
<a href="https://unum-cloud.github.io/usearch/python">Python</a> •
|
|
23
|
+
<a href="https://unum-cloud.github.io/usearch/cpp">C++ 11</a> •
|
|
24
|
+
<a href="https://unum-cloud.github.io/usearch/python">Python 3</a> •
|
|
25
25
|
<a href="https://unum-cloud.github.io/usearch/javascript">JavaScript</a> •
|
|
26
26
|
<a href="https://unum-cloud.github.io/usearch/java">Java</a> •
|
|
27
27
|
<a href="https://unum-cloud.github.io/usearch/rust">Rust</a> •
|
|
28
|
-
<a href="https://unum-cloud.github.io/usearch/c">
|
|
28
|
+
<a href="https://unum-cloud.github.io/usearch/c">C 99</a> •
|
|
29
29
|
<a href="https://unum-cloud.github.io/usearch/objective-c">Objective-C</a> •
|
|
30
30
|
<a href="https://unum-cloud.github.io/usearch/swift">Swift</a> •
|
|
31
31
|
<a href="https://unum-cloud.github.io/usearch/golang">GoLang</a> •
|
|
@@ -159,7 +159,46 @@ other_view = Index(ndim=..., metric=CompiledMetric(...))
|
|
|
159
159
|
other_view.view("index.usearch")
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
##
|
|
162
|
+
## Exact, Approximate, and Multi-Index Lookups
|
|
163
|
+
|
|
164
|
+
Approximate search methods, such as HNSW, are predominantly used when an exact brute-force search becomes too resource-intensive.
|
|
165
|
+
This typically occurs when you have millions of entries in a collection.
|
|
166
|
+
For smaller collections, we offer a more direct approach with the `search` method.
|
|
167
|
+
|
|
168
|
+
```py
|
|
169
|
+
from usearch.index import search, MetricKind, Matches, BatchMatches
|
|
170
|
+
import numpy as np
|
|
171
|
+
|
|
172
|
+
# Generate 10'000 random vectors with 1024 dimensions
|
|
173
|
+
vectors = np.random.rand(10_000, 1024).astype(np.float32)
|
|
174
|
+
vector = np.random.rand(1024).astype(np.float32)
|
|
175
|
+
|
|
176
|
+
one_in_many: Matches = search(vectors, vector, 50, MetricKind.L2sq, exact=True)
|
|
177
|
+
many_in_many: BatchMatches = search(vectors, vectors, 50, MetricKind.L2sq, exact=True)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
By passing the `exact=True` argument, the system bypasses indexing altogether and performs a brute-force search through the entire dataset using SIMD-optimized similarity metrics from [SimSIMD](https://github.com/ashvardanian/simsimd).
|
|
181
|
+
When compared to FAISS's `IndexFlatL2` in Google Colab, **[USearch may offer up to a 20x performance improvement](https://github.com/unum-cloud/usearch/issues/176#issuecomment-1666650778)**:
|
|
182
|
+
|
|
183
|
+
- `faiss.IndexFlatL2`: **55.3 ms**.
|
|
184
|
+
- `usearch.index.search`: **2.54 ms**.
|
|
185
|
+
|
|
186
|
+
For larger workloads targeting billions or even trillions of vectors, parallel multi-index lookups become invaluable.
|
|
187
|
+
These lookups prevent the need to construct a single, massive index, allowing users to query multiple smaller ones instead.
|
|
188
|
+
|
|
189
|
+
```py
|
|
190
|
+
from usearch.index import Indexes
|
|
191
|
+
|
|
192
|
+
multi_index = Indexes(
|
|
193
|
+
indexes: Iterable[usearch.index.Index] = [...],
|
|
194
|
+
paths: Iterable[os.PathLike] = [...],
|
|
195
|
+
view: bool = False,
|
|
196
|
+
threads: int = 0,
|
|
197
|
+
)
|
|
198
|
+
multi_index.search(...)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Joins, One-to-One, One-to-Many, and Many-to-Many Mappings
|
|
163
202
|
|
|
164
203
|
One of the big questions these days is how will AI change the world of databases and data management.
|
|
165
204
|
Most databases are still struggling to implement high-quality fuzzy search, and the only kind of joins they know are deterministic.
|
|
@@ -189,9 +228,9 @@ Broader functionality is ported per request.
|
|
|
189
228
|
| :---------------------- | :----: | :------: | :---: | :---: | :--------: | :---: | :----: | :---: |
|
|
190
229
|
| Add, search | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
191
230
|
| Save, load, view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
192
|
-
| Join | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
193
231
|
| User-defined metrics | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
194
|
-
|
|
|
232
|
+
| Joins | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
233
|
+
| Variable-length vectors | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
195
234
|
| 4B+ capacities | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
196
235
|
|
|
197
236
|
## Application Examples
|
|
@@ -281,9 +320,9 @@ matches = index.search(fingerprints, 10)
|
|
|
281
320
|
## Integrations
|
|
282
321
|
|
|
283
322
|
- [x] GPT-Cache.
|
|
284
|
-
- [
|
|
285
|
-
- [ ] Microsoft Semantic Kernel.
|
|
323
|
+
- [x] LangChain.
|
|
286
324
|
- [ ] ClickHouse.
|
|
325
|
+
- [ ] Microsoft Semantic Kernel.
|
|
287
326
|
|
|
288
327
|
## Citations
|
|
289
328
|
|
package/conanfile.py
CHANGED
|
@@ -4,7 +4,7 @@ from conans import ConanFile
|
|
|
4
4
|
class USearchConan(ConanFile):
|
|
5
5
|
|
|
6
6
|
name = 'USearch'
|
|
7
|
-
version = '1.
|
|
7
|
+
version = '1.1.1'
|
|
8
8
|
license = 'Apache 2.0'
|
|
9
9
|
url = 'https://github.com/unum-cloud/usearch'
|
|
10
10
|
description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
|
package/javascript/lib.cpp
CHANGED
|
@@ -37,6 +37,8 @@ class Index : public Napi::ObjectWrap<Index> {
|
|
|
37
37
|
|
|
38
38
|
void Add(Napi::CallbackInfo const& ctx);
|
|
39
39
|
Napi::Value Search(Napi::CallbackInfo const& ctx);
|
|
40
|
+
Napi::Value Remove(Napi::CallbackInfo const& ctx);
|
|
41
|
+
Napi::Value Contains(Napi::CallbackInfo const& ctx);
|
|
40
42
|
|
|
41
43
|
std::unique_ptr<index_dense_t> native_;
|
|
42
44
|
};
|
|
@@ -51,6 +53,8 @@ Napi::Object Index::Init(Napi::Env env, Napi::Object exports) {
|
|
|
51
53
|
InstanceMethod("connectivity", &Index::GetConnectivity),
|
|
52
54
|
InstanceMethod("add", &Index::Add),
|
|
53
55
|
InstanceMethod("search", &Index::Search),
|
|
56
|
+
InstanceMethod("remove", &Index::Remove),
|
|
57
|
+
InstanceMethod("contains", &Index::Contains),
|
|
54
58
|
InstanceMethod("save", &Index::Save),
|
|
55
59
|
InstanceMethod("load", &Index::Load),
|
|
56
60
|
InstanceMethod("view", &Index::View),
|
|
@@ -291,6 +295,64 @@ Napi::Value Index::Search(Napi::CallbackInfo const& ctx) {
|
|
|
291
295
|
}
|
|
292
296
|
}
|
|
293
297
|
|
|
298
|
+
Napi::Value Index::Remove(Napi::CallbackInfo const& ctx) {
|
|
299
|
+
Napi::Env env = ctx.Env();
|
|
300
|
+
if (ctx.Length() < 1 || !ctx[0].IsBigInt()) {
|
|
301
|
+
Napi::TypeError::New(env, "Expects an entry identifier").ThrowAsJavaScriptException();
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
Napi::BigInt key_js = ctx[0].As<Napi::BigInt>();
|
|
306
|
+
bool lossless = true;
|
|
307
|
+
std::uint64_t key = key_js.Uint64Value(&lossless);
|
|
308
|
+
if (!lossless) {
|
|
309
|
+
Napi::TypeError::New(env, "Identifier must be an unsigned integer").ThrowAsJavaScriptException();
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
auto result = native_->remove(key);
|
|
315
|
+
if (!result) {
|
|
316
|
+
Napi::TypeError::New(env, "Removal has failed").ThrowAsJavaScriptException();
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
return Napi::Boolean::New(env, result.completed);
|
|
320
|
+
} catch (std::bad_alloc const&) {
|
|
321
|
+
Napi::TypeError::New(env, "Out of memory").ThrowAsJavaScriptException();
|
|
322
|
+
return {};
|
|
323
|
+
} catch (...) {
|
|
324
|
+
Napi::TypeError::New(env, "Search failed").ThrowAsJavaScriptException();
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
Napi::Value Index::Contains(Napi::CallbackInfo const& ctx) {
|
|
330
|
+
Napi::Env env = ctx.Env();
|
|
331
|
+
if (ctx.Length() < 1 || !ctx[0].IsBigInt()) {
|
|
332
|
+
Napi::TypeError::New(env, "Expects an entry identifier").ThrowAsJavaScriptException();
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
Napi::BigInt key_js = ctx[0].As<Napi::BigInt>();
|
|
337
|
+
bool lossless = true;
|
|
338
|
+
std::uint64_t key = key_js.Uint64Value(&lossless);
|
|
339
|
+
if (!lossless) {
|
|
340
|
+
Napi::TypeError::New(env, "Identifier must be an unsigned integer").ThrowAsJavaScriptException();
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
bool result = native_->contains(key);
|
|
346
|
+
return Napi::Boolean::New(env, result);
|
|
347
|
+
} catch (std::bad_alloc const&) {
|
|
348
|
+
Napi::TypeError::New(env, "Out of memory").ThrowAsJavaScriptException();
|
|
349
|
+
return {};
|
|
350
|
+
} catch (...) {
|
|
351
|
+
Napi::TypeError::New(env, "Search failed").ThrowAsJavaScriptException();
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
294
356
|
Napi::Object InitAll(Napi::Env env, Napi::Object exports) { return Index::Init(env, exports); }
|
|
295
357
|
|
|
296
358
|
NODE_API_MODULE(usearch, InitAll)
|
package/javascript/test.js
CHANGED
|
@@ -11,6 +11,7 @@ assert.equal(index.size(), 0n, 'initial size should be 0');
|
|
|
11
11
|
index.add(15n, new Float32Array([10, 20]));
|
|
12
12
|
index.add(16n, new Float32Array([10, 25]));
|
|
13
13
|
assert.equal(index.size(), 2n, 'size after adding elements should be 2');
|
|
14
|
+
assert.equal(index.contains(15n), true, 'entry must be present after insertion');
|
|
14
15
|
|
|
15
16
|
var results = index.search(new Float32Array([13, 14]), 2n);
|
|
16
17
|
assert.deepEqual(results.keys, new BigUint64Array([15n, 16n]), 'keys should be 15 and 16');
|
package/javascript/usearch.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface Matches {
|
|
|
11
11
|
|
|
12
12
|
/** K-Approximate Nearest Neighbors search index. */
|
|
13
13
|
export class Index {
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
/**
|
|
16
16
|
* Constructs a new index.
|
|
17
17
|
*
|
|
@@ -42,7 +42,7 @@ export class Index {
|
|
|
42
42
|
* @return {bigints} The capacity of index.
|
|
43
43
|
*/
|
|
44
44
|
capacity(): bigint;
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
/**
|
|
47
47
|
* Returns connectivity.
|
|
48
48
|
* @return {bigint} The connectivity of index.
|
|
@@ -84,4 +84,19 @@ export class Index {
|
|
|
84
84
|
* @return {Matches} Output of the search result.
|
|
85
85
|
*/
|
|
86
86
|
search(mat: Float32Array, k: bigint): Matches;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if an entry is contained in the index.
|
|
90
|
+
*
|
|
91
|
+
* @param {bigint} key Identifier to look up.
|
|
92
|
+
*/
|
|
93
|
+
contains(key: bigint): boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove a vector from the index.
|
|
97
|
+
*
|
|
98
|
+
* @param {bigint} key Input identifier for every vector to be removed.
|
|
99
|
+
*/
|
|
100
|
+
remove(key: bigint): boolean;
|
|
101
|
+
|
|
87
102
|
}
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -298,7 +298,10 @@ static void search_typed( //
|
|
|
298
298
|
if (!threads)
|
|
299
299
|
threads = std::thread::hardware_concurrency();
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
visits_bitset_t query_mutexes(static_cast<std::size_t>(vectors_count));
|
|
302
|
+
if (!query_mutexes)
|
|
303
|
+
throw std::bad_alloc();
|
|
304
|
+
|
|
302
305
|
executor_default_t{threads}.execute_bulk(indexes.shards_.size(), [&](std::size_t, std::size_t task_idx) {
|
|
303
306
|
dense_index_py_t& index = *indexes.shards_[task_idx].get();
|
|
304
307
|
|
|
@@ -318,7 +321,7 @@ static void search_typed( //
|
|
|
318
321
|
dense_search_result_t result = index.search(vector, wanted, config);
|
|
319
322
|
result.error.raise();
|
|
320
323
|
{
|
|
321
|
-
|
|
324
|
+
auto lock = query_mutexes.lock(vector_idx);
|
|
322
325
|
counts_py1d(vector_idx) = static_cast<Py_ssize_t>(result.merge_into( //
|
|
323
326
|
&keys_py2d(vector_idx, 0), //
|
|
324
327
|
&distances_py2d(vector_idx, 0), //
|
package/rust/lib.cpp
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
using namespace unum::usearch;
|
|
5
5
|
using namespace unum;
|
|
6
6
|
|
|
7
|
-
using index_t =
|
|
7
|
+
using index_t = index_dense_t;
|
|
8
8
|
using add_result_t = typename index_t::add_result_t;
|
|
9
9
|
using search_result_t = typename index_t::search_result_t;
|
|
10
|
+
using labeling_result_t = typename index_t::labeling_result_t;
|
|
10
11
|
|
|
11
12
|
Index::Index(std::unique_ptr<index_t> index) : index_(std::move(index)) {}
|
|
12
13
|
|
|
@@ -28,14 +29,22 @@ Matches Index::search_in_thread(rust::Slice<float const> vector, size_t count, s
|
|
|
28
29
|
config.expansion = index_->expansion_search();
|
|
29
30
|
search_result_t result = index_->search(vector.data(), count, config);
|
|
30
31
|
result.error.raise();
|
|
31
|
-
|
|
32
|
-
matches.keys.truncate(
|
|
33
|
-
matches.distances.truncate(
|
|
32
|
+
count = result.dump_to(matches.keys.data(), matches.distances.data());
|
|
33
|
+
matches.keys.truncate(count);
|
|
34
|
+
matches.distances.truncate(count);
|
|
34
35
|
return matches;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
void Index::add(key_t key, rust::Slice<float const> vector) const { index_->add(key, vector.data()).error.raise(); }
|
|
38
39
|
|
|
40
|
+
bool Index::remove(key_t key) const {
|
|
41
|
+
labeling_result_t result = index_->remove(key);
|
|
42
|
+
result.error.raise();
|
|
43
|
+
return result.completed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bool Index::contains(key_t key) const { return index_->contains(key); }
|
|
47
|
+
|
|
39
48
|
Matches Index::search(rust::Slice<float const> vector, size_t count) const {
|
|
40
49
|
Matches matches;
|
|
41
50
|
matches.keys.reserve(count);
|
|
@@ -44,9 +53,9 @@ Matches Index::search(rust::Slice<float const> vector, size_t count) const {
|
|
|
44
53
|
matches.keys.push_back(0), matches.distances.push_back(0);
|
|
45
54
|
search_result_t result = index_->search(vector.data(), count);
|
|
46
55
|
result.error.raise();
|
|
47
|
-
|
|
48
|
-
matches.keys.truncate(
|
|
49
|
-
matches.distances.truncate(
|
|
56
|
+
count = result.dump_to(matches.keys.data(), matches.distances.data());
|
|
57
|
+
matches.keys.truncate(count);
|
|
58
|
+
matches.distances.truncate(count);
|
|
50
59
|
return matches;
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -74,7 +83,7 @@ std::unique_ptr<Index> wrap(index_t&& index) {
|
|
|
74
83
|
metric_kind_t rust_to_cpp_metric(MetricKind value) {
|
|
75
84
|
switch (value) {
|
|
76
85
|
case MetricKind::IP: return metric_kind_t::ip_k;
|
|
77
|
-
case MetricKind::
|
|
86
|
+
case MetricKind::L2sq: return metric_kind_t::l2sq_k;
|
|
78
87
|
case MetricKind::Cos: return metric_kind_t::cos_k;
|
|
79
88
|
case MetricKind::Pearson: return metric_kind_t::pearson_k;
|
|
80
89
|
case MetricKind::Haversine: return metric_kind_t::haversine_k;
|
package/rust/lib.hpp
CHANGED
|
@@ -27,6 +27,9 @@ class Index {
|
|
|
27
27
|
Matches search(rust::Slice<float const> vector, size_t count) const;
|
|
28
28
|
Matches search_in_thread(rust::Slice<float const> vector, size_t count, size_t thread) const;
|
|
29
29
|
|
|
30
|
+
bool remove(key_t key) const;
|
|
31
|
+
bool contains(key_t key) const;
|
|
32
|
+
|
|
30
33
|
size_t dimensions() const;
|
|
31
34
|
size_t connectivity() const;
|
|
32
35
|
size_t size() const;
|
package/rust/lib.rs
CHANGED
|
@@ -3,14 +3,13 @@ pub mod ffi {
|
|
|
3
3
|
|
|
4
4
|
// Shared structs with fields visible to both languages.
|
|
5
5
|
struct Matches {
|
|
6
|
-
count: usize,
|
|
7
6
|
keys: Vec<u64>,
|
|
8
7
|
distances: Vec<f32>,
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
enum MetricKind {
|
|
12
11
|
IP,
|
|
13
|
-
|
|
12
|
+
L2sq,
|
|
14
13
|
Cos,
|
|
15
14
|
Pearson,
|
|
16
15
|
Haversine,
|
|
@@ -53,6 +52,8 @@ pub mod ffi {
|
|
|
53
52
|
|
|
54
53
|
pub fn add(self: &Index, key: u64, vector: &[f32]) -> Result<()>;
|
|
55
54
|
pub fn search(self: &Index, query: &[f32], count: usize) -> Result<Matches>;
|
|
55
|
+
pub fn remove(self: &Index, key: u64) -> Result<bool>;
|
|
56
|
+
pub fn contains(self: &Index, key: u64) -> bool;
|
|
56
57
|
|
|
57
58
|
pub fn save(self: &Index, path: &str) -> Result<()>;
|
|
58
59
|
pub fn load(self: &Index, path: &str) -> Result<()>;
|
|
@@ -97,7 +98,7 @@ mod tests {
|
|
|
97
98
|
|
|
98
99
|
// Read back the tags
|
|
99
100
|
let results = index.search(&first, 10).unwrap();
|
|
100
|
-
assert_eq!(results.
|
|
101
|
+
assert_eq!(results.keys.len(), 2);
|
|
101
102
|
|
|
102
103
|
// Validate serialization
|
|
103
104
|
assert!(index.save("index.rust.usearch").is_ok());
|
|
@@ -106,7 +107,7 @@ mod tests {
|
|
|
106
107
|
|
|
107
108
|
// Make sure every function is called at least once
|
|
108
109
|
assert!(new_index(&options).is_ok());
|
|
109
|
-
options.metric = MetricKind::
|
|
110
|
+
options.metric = MetricKind::L2sq;
|
|
110
111
|
assert!(new_index(&options).is_ok());
|
|
111
112
|
options.metric = MetricKind::Cos;
|
|
112
113
|
assert!(new_index(&options).is_ok());
|