usearch 0.2.2 → 0.2.3

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.
@@ -32,7 +32,7 @@ jobs:
32
32
  # needs: versioning
33
33
  strategy:
34
34
  matrix:
35
- os: [ubuntu-20.04, macOS-11, windows-2022]
35
+ os: [ubuntu-20.04, macOS-11]
36
36
 
37
37
  steps:
38
38
  - uses: actions/checkout@v3
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  authors = ["Ashot 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
@@ -17,30 +17,33 @@ Vector Search Engine<br/>
17
17
  <a href="https://github.com/unum-cloud/usearch"><img height="25" src="https://github.com/unum-cloud/.github/raw/main/assets/github.svg" alt="GitHub"></a>
18
18
  </p>
19
19
 
20
+ <p align="center">
21
+ Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Metrics
22
+ <br/>
23
+ <a href="#c++">C++11</a> •
24
+ <a href="#python">Python</a> •
25
+ <a href="#javascript">JavaScript</a> •
26
+ <a href="#java">Java</a> •
27
+ <a href="#rust">Rust</a> •
28
+ <a href="#objective-c">Objective-C</a> •
29
+ <a href="#swift">Swift</a> •
30
+ <a href="#golang">GoLang</a> •
31
+ <a href="#wolfram">Wolfram</a>
32
+ <br/>
33
+ Linux • MacOS • Windows
34
+ </p>
35
+
20
36
  - [x] [Single C++11 header][usearch-header] implementation, easily extendible.
21
37
  - [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
22
- - [x] [Half-precision](#quantize-on-the-fly) support with [`maratyszcza/fp16`](https://github.com/maratyszcza/fp16).
38
+ - [x] [Half-precision](#quantize-on-the-fly) support with [`fp16`](https://github.com/maratyszcza/fp16).
23
39
  - [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
24
- - [x] [Any metric](#define-custom-metrics), includes:
25
- - [x] Euclidean, Dot-product, Cosine,
26
- - [x] Jaccard, Hamming, Haversine.
27
- - [x] Hardware-accelerated [`ashvardanian/simsimd`](https://github.com/ashvardanian/simsimd).
40
+ - [x] [User-defined](#define-custom-metrics) and hardware-accelerated metrics with [`simsimd`](https://github.com/ashvardanian/simsimd).
28
41
  - [x] Variable dimensionality vectors.
29
42
  - [x] Don't copy vectors if not needed.
30
43
  - [x] [Bring your threads](#bring-your-threads), like OpenMP.
31
44
  - [x] Multiple vectors per label.
32
- - [x] [Python](#python) bindings: `pip install usearch`.
33
- - [x] [JavaScript](#javascript) bindings: `npm install usearch`.
34
- - [x] [Rust](#rust) bindings: `cargo add usearch`.
35
- - [x] [Java](#java) bindings: `cloud.unum:usearch` on GitHub.
36
- - [ ] GoLang bindings.
37
- - [x] Wolfram language bindings.
38
- - [x] For Linux: GCC, Clang.
39
- - [x] For MacOS: Apple Clang.
40
- - [ ] For Windows.
41
- - [ ] Multi-index lookups in Python.
42
45
  - [ ] Thread-safe `reserve`.
43
- - [ ] Distributed construction with MPI.
46
+ - [x] Compatible with Linux, MacOS, and Windows.
44
47
  - [x] AI + Vector Search = Semantic Search.
45
48
 
46
49
  [usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
@@ -209,12 +212,14 @@ struct custom_metric_t {
209
212
 
210
213
  ### Python
211
214
 
215
+ ```sh
216
+ pip install usearch
217
+ ```
218
+
212
219
  Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
213
220
  Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
214
221
 
215
222
  ```python
216
- $ pip install usearch
217
-
218
223
  import numpy as np
219
224
  from usearch.index import Index
220
225
 
@@ -232,6 +237,8 @@ index.add(42, vector)
232
237
  matches, distances, count = index.search(vector, 10)
233
238
  ```
234
239
 
240
+ #### Batch Operations
241
+
235
242
  Same can be done with batches, rather than single entries.
236
243
  Under the hood, worker threads will be spawned, to parallelize the procedure.
237
244
 
@@ -240,17 +247,31 @@ n = 100
240
247
  labels = np.array(range(n), dtype=np.longlong)
241
248
  vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
242
249
 
243
- # You can avoid copying the data
244
- # Handy when build 1B+ indexes of memory-mapped files
245
- index.add(labels, vectors, copy=True)
250
+ index.add(labels, vectors)
246
251
  matches, distances, counts = index.search(vectors, 10)
247
252
  ```
248
253
 
254
+ #### Performance
255
+
256
+ To maximize performance, you can:
257
+
258
+ - adjust the number of threads passed to `index.add(threads=)`.
259
+ - downcast to half-precision with `Index(accuracy=)`.
260
+ - JIT-compile your metrics for hardware and pass to `Index(matric_pointer=)`.
261
+
262
+ For a dataset with 1 Million 256-dimensional embeddings at single precision, the performance would be:
263
+
264
+ - FAISS: 77'563 vectors/s.
265
+ - USearch precompiled for old hardware: 53'198 vectors/s (-32%).
266
+ - USearch with Numba JIT for target hardware: 88'658 vectors/s (+14%).
267
+
249
268
  ### JavaScript
250
269
 
251
- ```js
252
- // npm install usearch
270
+ ```sh
271
+ npm install usearch
272
+ ```
253
273
 
274
+ ```js
254
275
  var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 2 })
255
276
  assert.equal(index.connectivity(), 16)
256
277
  assert.equal(index.dimensions(), 2)
@@ -266,12 +287,14 @@ assert.deepEqual(results.distances, new Float32Array([45, 130]))
266
287
 
267
288
  ### Rust
268
289
 
290
+ ```sh
291
+ cargo add usearch
292
+ ```
293
+
269
294
  Being a systems-programming language, Rust has better control over memory management and concurrency, but lacks function overloading.
270
295
  Aside from the `add` and `search`, it also provides `add_in_thread` and `search_in_thread` which let users identify the calling thread to use underlying temporary memory more efficiently.
271
296
 
272
297
  ```rust
273
- // cargo add usearch
274
-
275
298
  let quant: &str = "f16";
276
299
  let index = new_ip(5, &quant, 0, 0, 0).unwrap();
277
300
 
@@ -305,6 +328,16 @@ assert!(new_haversine(&quant, 0, 0, 0).is_ok());
305
328
 
306
329
  ### Java
307
330
 
331
+ ```xml
332
+ <dependency>
333
+ <groupId>cloud.unum.usearch</groupId>
334
+ <artifactId>usearch</artifactId>
335
+ <version>0.2.3</version>
336
+ </dependency>
337
+ ```
338
+
339
+ Add that snippet to your `pom.xml` and hit `mvn install`.
340
+
308
341
  ```java
309
342
  Index index = new Index.Config().metric("cos").dimensions(2).build();
310
343
  float vec[] = {10, 20};
@@ -312,7 +345,7 @@ index.add(42, vec);
312
345
  int[] labels = index.search(vec, 5);
313
346
  ```
314
347
 
315
- ### Objective-C and Swift
348
+ ### Swift
316
349
 
317
350
  ```swift
318
351
  let index = Index.l2(dimensions: 4, connectivity: 8)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.2.3
package/conanfile.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from conans import ConanFile
2
2
 
3
+
3
4
  class USearchConan(ConanFile):
4
-
5
+
5
6
  name = 'USearch'
6
- version = '0.2.2'
7
+ version = '0.2.3'
7
8
  license = 'Apache 2.0'
8
9
  url = 'https://github.com/unum-cloud/usearch'
9
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
package/python/bench.py CHANGED
@@ -1,25 +1,28 @@
1
- import datetime
1
+ import time
2
2
  import numpy as np
3
3
  import fire
4
4
 
5
- import faiss
6
- import usearch
7
-
8
- import struct
9
5
  import numpy as np
6
+ from numba import cfunc, types, carray
7
+
8
+ from faiss import IndexHNSWFlat, IndexIVFPQ
9
+ from usearch.index import Index
10
+ from usearch.io import load_matrix
11
+
10
12
 
11
13
  def measure(f) -> float:
12
- a = datetime.datetime.now()
14
+ a = time.time_ns()
13
15
  f()
14
- b = datetime.datetime.now()
16
+ b = time.time_ns()
15
17
  c = b - a
16
- print(f'Took: {c.seconds:.2f} seconds')
17
- return c.seconds
18
+ secs = c / (10 ** 9)
19
+ print(f'- Took: {secs:.2f} seconds')
20
+ return secs
18
21
 
19
22
 
20
23
  def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
21
24
  dt = measure(lambda: index.add(vectors))
22
- print(f'- FAISS: {vectors.shape[0]/dt:.2f} vectors/s')
25
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} vectors/s')
23
26
 
24
27
 
25
28
  def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
@@ -27,7 +30,26 @@ def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.arr
27
30
  assert len(index) == 0
28
31
  dt = measure(lambda: index.add(labels, vectors))
29
32
  assert len(index) == vectors.shape[0]
30
- print(f'- USearch: {vectors.shape[0]/dt:.2f} vectors/s')
33
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} vectors/s')
34
+
35
+
36
+ # Showcases how to use Numba to JIT-compile similarity measures for USearch.
37
+ # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
38
+
39
+ signature_f32 = types.float32(
40
+ types.CPointer(types.float32),
41
+ types.CPointer(types.float32),
42
+ types.size_t, types.size_t)
43
+
44
+
45
+ @cfunc(signature_f32)
46
+ def inner_product_f32(a, b, n, m):
47
+ a_array = carray(a, n)
48
+ b_array = carray(b, n)
49
+ c = types.float32(0)
50
+ for i in range(n):
51
+ c += a_array[i] * b_array[i]
52
+ return types.float32(1 - c)
31
53
 
32
54
 
33
55
  def main(
@@ -44,31 +66,51 @@ def main(
44
66
  neighbors_mat = load_matrix(neighbors)
45
67
  dim = vectors_mat.shape[1]
46
68
 
47
- # Test FAISS
48
- index = faiss.IndexHNSWFlat(dim, connectivity)
69
+ print('USearch: HNSW')
70
+ index = Index(
71
+ ndim=dim,
72
+ expansion_add=expansion_add,
73
+ expansion_search=expansion_search,
74
+ connectivity=connectivity,
75
+ )
76
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
77
+
78
+ print('USearch: HNSW + Numba JIT')
79
+ index = Index(
80
+ ndim=dim,
81
+ expansion_add=expansion_add,
82
+ expansion_search=expansion_search,
83
+ connectivity=connectivity,
84
+ metric_pointer=inner_product_f32.address,
85
+ )
86
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
87
+
88
+ print('USearch: HNSW + Numba JIT + Half Precision')
89
+ index = Index(
90
+ ndim=dim,
91
+ dtype='f16',
92
+ expansion_add=expansion_add,
93
+ expansion_search=expansion_search,
94
+ connectivity=connectivity,
95
+ )
96
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
97
+
98
+ print('FAISS: HNSW')
99
+ index = IndexHNSWFlat(dim, connectivity)
49
100
  index.hnsw.efSearch = expansion_search
50
101
  index.hnsw.efConstruction = expansion_add
51
102
  bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
52
103
 
53
- # Test FAISS with default Product Quantization
104
+ print('FAISS: HNSW + IVFPQ')
54
105
  nlist = 10000 # Number of inverted lists (number of partitions or cells).
55
106
  nsegment = 16 # Number of segments for PQ (number of subquantizers).
56
107
  nbit = 8 # Number of bits to encode each segment.
57
- coarse_quantizer = faiss.IndexHNSWFlat(dim, connectivity)
58
- index = faiss.IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
108
+ coarse_quantizer = IndexHNSWFlat(dim, connectivity)
109
+ index = IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
59
110
  index.train(vectors_mat)
60
111
  index.nprobe = 10
61
112
  bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
62
113
 
63
- # Test USearch `Index`
64
- index = usearch.Index(
65
- ndim=dim,
66
- expansion_add=expansion_add,
67
- expansion_search=expansion_search,
68
- connectivity=connectivity,
69
- )
70
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
71
-
72
114
 
73
115
  if __name__ == '__main__':
74
116
  fire.Fire(main)
package/python/lib.cpp CHANGED
@@ -59,6 +59,25 @@ struct hash_index_w_meta_t : public hash_index_t {
59
59
  span_gt<hash_word_t const> buffer() const noexcept { return {buffer_.data(), buffer_.size()}; }
60
60
  };
61
61
 
62
+ template <typename scalar_at> punned_stateful_metric_t udf(std::size_t metric_uintptr) {
63
+ return [metric_uintptr](byte_t const* a_bytes, byte_t const* b_bytes, std::size_t n, std::size_t m) -> distance_t {
64
+ using metric_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, std::size_t, std::size_t);
65
+ metric_t metric_ptr = reinterpret_cast<metric_t>(metric_uintptr);
66
+ scalar_at const* a = reinterpret_cast<scalar_at const*>(a_bytes);
67
+ scalar_at const* b = reinterpret_cast<scalar_at const*>(b_bytes);
68
+ return metric_ptr(a, b, n / sizeof(scalar_at), m / sizeof(scalar_at));
69
+ };
70
+ }
71
+
72
+ punned_stateful_metric_t udf(std::size_t metric_uintptr, accuracy_t accuracy) {
73
+ switch (accuracy) {
74
+ case accuracy_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
75
+ case accuracy_t::f16_k: return udf<f16_bits_t>(metric_uintptr);
76
+ case accuracy_t::f32_k: return udf<f32_t>(metric_uintptr);
77
+ case accuracy_t::f64_k: return udf<f64_t>(metric_uintptr);
78
+ }
79
+ }
80
+
62
81
  static native_index_t make_index( //
63
82
  std::size_t dimensions, //
64
83
  std::size_t capacity, //
@@ -79,9 +98,8 @@ static native_index_t make_index( //
79
98
  config.max_threads_search = std::thread::hardware_concurrency();
80
99
 
81
100
  accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
82
- punned_metric_t metric_ptr = reinterpret_cast<punned_metric_t>(metric_uintptr);
83
- if (metric_ptr)
84
- return native_index_t::udf(dimensions, metric_ptr, accuracy, config);
101
+ if (metric_uintptr)
102
+ return native_index_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
85
103
  else
86
104
  return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
87
105
  }
@@ -146,7 +164,9 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
146
164
  throw std::invalid_argument("Incompatible scalars in the vector!");
147
165
  }
148
166
 
149
- static void add_many_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
167
+ static void add_many_to_index( //
168
+ native_index_t& index, py::buffer labels, py::buffer vectors, //
169
+ bool copy, std::size_t threads = 0) {
150
170
 
151
171
  py::buffer_info labels_info = labels.request();
152
172
  py::buffer_info vectors_info = vectors.request();
@@ -393,7 +413,8 @@ PYBIND11_MODULE(index, m) {
393
413
  py::arg("labels"), //
394
414
  py::arg("vectors"), //
395
415
  py::kw_only(), //
396
- py::arg("copy") = true //
416
+ py::arg("copy") = true, //
417
+ py::arg("threads") = 0 //
397
418
  );
398
419
 
399
420
  i.def( //
@@ -415,6 +436,8 @@ PYBIND11_MODULE(index, m) {
415
436
  i.def_property_readonly("ndim", &native_index_t::dimensions);
416
437
  i.def_property_readonly("connectivity", &native_index_t::connectivity);
417
438
  i.def_property_readonly("capacity", &native_index_t::capacity);
439
+ i.def_property_readonly( //
440
+ "dtype", [](native_index_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
418
441
 
419
442
  i.def("save", &save_index<native_index_t>, py::arg("path"));
420
443
  i.def("load", &load_index<native_index_t>, py::arg("path"));
package/setup.py CHANGED
@@ -1,13 +1,20 @@
1
1
  import os
2
2
  import sys
3
- from setuptools import setup, find_packages
3
+ from setuptools import setup
4
4
  from pybind11.setup_helpers import Pybind11Extension
5
5
 
6
6
 
7
7
  compile_args = ['-std=c++11', '-O3']
8
+ link_args = []
9
+
10
+ if sys.platform == 'linux':
11
+ compile_args.append('-fopenmp')
12
+ link_args.append('-lgomp')
8
13
 
9
14
  if sys.platform == 'darwin':
10
- compile_args.append('-mmacosx-version-min=10.13')
15
+ # MacOS 10.15 orhigher is needed for `aligned_alloc` support.
16
+ # https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
17
+ compile_args.append('-mmacosx-version-min=10.15')
11
18
 
12
19
  if sys.platform == 'win32':
13
20
  compile_args = ['/std:c++14', '/O2']
@@ -17,7 +24,8 @@ ext_modules = [
17
24
  Pybind11Extension(
18
25
  'usearch.index',
19
26
  ['python/lib.cpp'],
20
- extra_compile_args=compile_args
27
+ extra_compile_args=compile_args,
28
+ extra_link_args=link_args,
21
29
  ),
22
30
  ]
23
31
 
package/src/advanced.hpp CHANGED
@@ -14,6 +14,10 @@
14
14
  #include <thread> // `std::thread`
15
15
  #include <vector> // `std::vector`
16
16
 
17
+ #if defined(_OPENMP)
18
+ #include <omp.h> // `omp_get_num_threads()`
19
+ #endif
20
+
17
21
  #if __linux__
18
22
  #include <sys/auxv.h>
19
23
  #endif
@@ -179,12 +183,18 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
179
183
 
180
184
  if (threads == 0)
181
185
  threads = std::thread::hardware_concurrency();
182
- if (threads == 1) {
186
+ if (threads == 1 || tasks <= 128) {
183
187
  for (std::size_t task_idx = 0; task_idx < tasks; ++task_idx)
184
188
  callback(0, task_idx);
185
189
  return;
186
190
  }
187
191
 
192
+ #if defined(_OPENMP)
193
+ omp_set_num_threads(threads);
194
+ #pragma omp parallel for schedule(dynamic)
195
+ for (std::size_t i = 0; i < tasks; ++i)
196
+ callback(omp_get_thread_num(), i);
197
+ #else
188
198
  std::vector<std::thread> threads_pool;
189
199
  std::size_t tasks_per_thread = (tasks / threads) + ((tasks % threads) != 0);
190
200
  for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx) {
@@ -194,9 +204,9 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
194
204
  callback(thread_idx, task_idx);
195
205
  });
196
206
  }
197
-
198
207
  for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx)
199
208
  threads_pool[thread_idx].join();
209
+ #endif
200
210
  }
201
211
 
202
212
  /**
@@ -424,6 +434,7 @@ class auto_index_gt {
424
434
  config_t const& config() const noexcept { return root_config_; }
425
435
  void clear() noexcept { return index_->clear(); }
426
436
 
437
+ accuracy_t accuracy() const noexcept { return accuracy_; }
427
438
  isa_t acceleration() const noexcept { return acceleration_; }
428
439
  std::size_t concurrency() const noexcept {
429
440
  return (std::min)(root_config_.max_threads_add, root_config_.max_threads_search);
package/src/bench.cpp CHANGED
@@ -33,7 +33,7 @@
33
33
  #include <variant> // `std::monostate`
34
34
 
35
35
  #include <clipp.h> // Command Line Interface
36
- #include <omp.h> // `omp_get_num_threads()`
36
+ #include <omp.h> // `omp_set_num_threads()`
37
37
 
38
38
  #include <simsimd/simsimd.h>
39
39