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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "1.0.0"
3
+ version = "1.1.1"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
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">C99</a> •
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
- ## Joins
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
- | Variable-length vectors | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
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
- - [ ] LangChain.
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.0.0'
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'
@@ -0,0 +1,6 @@
1
+ =====================
2
+ Wolfram SDK
3
+ =====================
4
+
5
+
6
+ .. mdinclude:: ../../wolfram/README.md
@@ -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)
@@ -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');
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
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
- std::vector<std::mutex> query_mutexes(static_cast<std::size_t>(vectors_count));
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
- std::unique_lock<std::mutex> lock(query_mutexes[vector_idx]);
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 = typename Index::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
- matches.count = result.dump_to(matches.keys.data(), matches.distances.data());
32
- matches.keys.truncate(matches.count);
33
- matches.distances.truncate(matches.count);
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
- matches.count = result.dump_to(matches.keys.data(), matches.distances.data());
48
- matches.keys.truncate(matches.count);
49
- matches.distances.truncate(matches.count);
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::L2Sq: return metric_kind_t::l2sq_k;
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
- L2Sq,
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.count, 2);
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::L2Sq;
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());
package/wasmer.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="1.0.0"
3
+ version="1.1.1"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
5
  license="Apache-2.0"
6
6
  readme="README.md"