usearch 0.1.8 → 0.1.10

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
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">USearch</h1>
2
2
  <h3 align="center">
3
- C++11 Single Header Vector Search<br/>
4
- Compact, yet Powerful<br/>
3
+ Smaller & Faster Single-File<br/>
4
+ Vector Search Engine<br/>
5
5
  </h3>
6
6
  <br/>
7
7
 
@@ -17,30 +17,126 @@ Compact, yet Powerful<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
- - [x] Single C++11 header implementation, easily extendible.
21
- - [x] 4B+ sized space efficient point-clouds with `uint40_t`.
22
- - [x] Half-precision support with [`maratyszcza/fp16`](https://github.com/maratyszcza/fp16).
23
- - [x] View from disk, without loading into RAM.
24
- - [x] Any metric, includes:
20
+ - [x] [Single C++11 header][usearch-header] implementation, easily extendible.
21
+ - [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).
23
+ - [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
24
+ - [x] [Any metric](#define-custom-metrics), includes:
25
25
  - [x] Euclidean, Dot-product, Cosine,
26
26
  - [x] Jaccard, Hamming, Haversine.
27
27
  - [x] Hardware-accelerated [`ashvardanian/simsimd`](https://github.com/ashvardanian/simsimd).
28
28
  - [x] Variable dimensionality vectors.
29
29
  - [x] Don't copy vectors if not needed.
30
- - [x] Bring your threads.
30
+ - [x] [Bring your threads](#bring-your-threads), like OpenMP.
31
31
  - [x] Multiple vectors per label.
32
- - [x] Python bindings: `pip install usearch`.
33
- - [x] JavaScript bindings: `npm install usearch`.
34
- - [x] Rust bindings: `cargo add usearch`.
35
- - [x] Java bindings: `cloud.unum:usearch` on GitHub.
32
+ - [x] [Python](#python) bindings: `pip install usearch`.
33
+ - [x] [JavaScript](#java) bindings: `npm install usearch`.
34
+ - [x] [Rust](#rust) bindings: `cargo add usearch`.
35
+ - [x] [Java](#java) bindings: `cloud.unum:usearch` on GitHub.
36
36
  - [ ] GoLang bindings.
37
- - [ ] Wolfram language bindings.
37
+ - [x] Wolfram language bindings.
38
38
  - [x] For Linux: GCC, Clang.
39
39
  - [x] For MacOS: Apple Clang.
40
40
  - [ ] For Windows.
41
41
  - [ ] Multi-index lookups in Python.
42
42
  - [ ] Thread-safe `reserve`.
43
- - [ ] Distributed construction.
43
+ - [ ] Distributed construction with MPI.
44
+ - [x] AI + Vector Search = Semantic Search.
45
+
46
+ [usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
47
+
48
+ ## Features
49
+
50
+ ### Define Custom Metrics
51
+
52
+ Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
53
+ The lack of dedicated "Cosine distance" can be justified with the simplicity of normalizing such vectors on the fly.
54
+ But that hardly exhausts the list of possible metrics.
55
+
56
+ ![USearch: Vector Search Approaches](assets/usearch-approaches-white.png)
57
+
58
+ Older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing are hardly extendible to vectors/objects of variable length.
59
+ Modern NSW-like approaches, however, only require two objects to be comparable.
60
+ This means, the index can be extended to support fuzzy search with Levenstein distance over strings.
61
+ USearch implements HNSW, and provides some uncommon-to-vector-search "similarity measures" and "distances" out of the box: Haversine, Jaccard, Hamming, etc.
62
+
63
+ ### Bring your Threads
64
+
65
+ Most AI, HPC, or Big Data packages use some form of a thread pool.
66
+ Instead of spawning additional threads within USearch, we focus on thread-safety of the `add` function.
67
+
68
+ ```cpp
69
+ #pragma omp parallel for
70
+ for (std::size_t i = 0; i < n; ++i)
71
+ native.add(label, span_t{vector, dims}, omp_get_thread_num());
72
+ ```
73
+
74
+ During initialization we allocate enough temporary memory for all the cores on the machine.
75
+ On call, the user can simply supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
76
+
77
+ ### Go Beyond 4B Entries
78
+
79
+ Constructing a vector index is a memory-intensive task.
80
+ Depending on your configuration parameters, part of the memory will be allocated towards copies of vectors, and another part to the Small World Graph, common to NSW-like algorithms.
81
+ Choosing the right data-structures and numeric types can have profound implications on both.
82
+
83
+ ![USearch uint40_t support](assets/usearch-neighbor-types.png)
84
+
85
+ Internally, some point "label" is mapped into an integer identifier for every stored vector.
86
+ Most implementations hard-code it to `uint32_t`, as people rarely store more than 4 Billion vectors.
87
+ But when they do, software becomes unusable.
88
+ Others hard-code it to `uint64_t`, which hardly space-efficient.
89
+ We added a tiny class - `uint40_t`, that should be enough for collections up to 1 Trillion vectors.
90
+
91
+ ### View Larger Indexes from Disk
92
+
93
+ To construct a large index one may use a beefy RAM-optimized machine, like the AWS `u-24tb1.metal` instances with 24 TB of RAM.
94
+ Those are, however, pricy, and would cost you over $200/hour.
95
+
96
+ | | To Build | To Serve |
97
+ | :------- | :-----------: | :------------: |
98
+ | Instance | u-24tb1.metal | is4gen.8xlarge |
99
+ | Price | ~ $200/h | ~$4.5/h |
100
+ | RAM | 24 TB | 192 GB |
101
+ | SSD | EBS | 30 TB |
102
+
103
+ You wouldn't want to have them running all the time.
104
+ So, if you are working on large datasets, but don't need the in-RAM throughput, you can simply view an existing dataset from disk, without ever loading it fully into memory.
105
+
106
+ > Construct on large machines, deploy on the cheap ones.
107
+
108
+ ### Quantize on the Fly
109
+
110
+ Most modern CPU have at least partial support for `half`-precision `f16_t` arithmetic.
111
+ USearch supports automatic down-casting and up-casting between `f32_t`, `f16_t`, `f64_t`, and `i8q100_t` representations.
112
+
113
+ > Making vectors smaller will help pack more of them in-memory, while also increasing performance on CPUs implementing native support for target type.
114
+
115
+ ## Performance
116
+
117
+ Below are the performance numbers for a benchmark running on the 64 cores of AWS `c7g.metal` "Graviton 3"-based instances.
118
+ We fix the default configuration in the top line and show the affects of various parameters by changing one parameter at a time.
119
+
120
+ | Vectors | Connectivity | EF @ A | EF @ S | **Add**, QPS | **Search**, QPS | **Recall @1** |
121
+ | :--------: | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
122
+ | `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
123
+ | | | | | | | |
124
+ | `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
125
+ | `f32` x256 | 32 | 128 | 64 | 64'368 | 104'050 | 99.4% |
126
+ | | | | | | | |
127
+ | `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
128
+ | `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
129
+ | | | | | | | |
130
+ | `f16` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
131
+ | `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
132
+
133
+ The main columns are:
134
+
135
+ - Add: Number of insertion Queries Per Second.
136
+ - Search: Number search Queries Per Second.
137
+ - Recall @1: How often does approximate search yield the exact best match?
138
+
139
+ To read more and reproduce, jump to [benchmarking section](#benchmarking).
44
140
 
45
141
  ## Usage
46
142
 
@@ -91,22 +187,41 @@ index.view("index.usearch"); // Memory-mapping from disk
91
187
  ```
92
188
 
93
189
  The `add` is thread-safe for concurrent index construction.
190
+ For advanced users, more compile-time abstraction are available.
191
+
192
+ ```cpp
193
+ template <typename metric_at = ip_gt<float>, //
194
+ typename label_at = std::size_t, // `uint32_t`, `uuid_t`...
195
+ typename id_at = std::uint32_t, // `uint40_t`, `uint64_t`...
196
+ typename scalar_at = float, // `double`, `half`, `char`...
197
+ typename allocator_at = std::allocator<char>> //
198
+ class index_gt;
199
+ ```
200
+
201
+ One may also define a custom metric, such as Damerau–Levenshtein distance, to compute the similarity between variable length strings.
202
+ The only constraint is the function signature:
203
+
204
+ ```cpp
205
+ struct custom_metric_t {
206
+ T operator()(T const* a, T const* b, std::size_t a_length, std::size_t b_length) const;
207
+ };
208
+ ```
94
209
 
95
210
  ### Python
96
211
 
97
212
  Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
98
213
  Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
99
214
 
100
- ```sh
215
+ ```python
101
216
  $ pip install usearch
102
217
 
103
218
  import numpy as np
104
219
  import usearch
105
220
 
106
221
  index = usearch.Index(
107
- dim=256, # Define the number of dimensions in input vectors
222
+ ndim=256, # Define the number of dimensions in input vectors
108
223
  metric='cos', # Choose the "metric" or "distance", default = 'ip', optional
109
- dtype='f16', # Quantize to 'f16' or 'i8q100' if needed, default = 'f32', optional
224
+ dtype='f32', # Quantize to 'f16' or 'i8q100' if needed, default = 'f32', optional
110
225
  connectivity=16, # How frequent should the connections in the graph be, optional
111
226
  expansion_add=128, # Control the recall of indexing, optional
112
227
  expansion_search=64, # Control the quality of search, optional
@@ -125,13 +240,184 @@ assert len(index) == n
125
240
  matches, distances, counts = index.search(vectors, 10)
126
241
  ```
127
242
 
128
- ## Features
243
+ ### JavaScript
129
244
 
130
- ### Bring your Threads
245
+ ```js
246
+ // npm install usearch
131
247
 
132
- ## Performance
248
+ var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 2 })
249
+ assert.equal(index.connectivity(), 16)
250
+ assert.equal(index.dimensions(), 2)
251
+ assert.equal(index.size(), 0)
252
+
253
+ index.add(15, new Float32Array([10, 20]))
254
+ assert.equal(index.size(), 2)
255
+
256
+ var results = index.search(new Float32Array([13, 14]), 2)
257
+ assert.deepEqual(results.labels, new Uint32Array([15, 16]))
258
+ assert.deepEqual(results.distances, new Float32Array([45, 130]))
259
+ ```
260
+
261
+ ### Rust
262
+
263
+ Being a systems-programming language, Rust has better control over memory management and concurrency, but lacks function overloading.
264
+ 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.
265
+
266
+ ```rust
267
+ // cargo add usearch
268
+
269
+ let quant: &str = "f16";
270
+ let index = new_ip(5, &quant, 0, 0, 0).unwrap();
271
+
272
+ assert!(index.reserve(10).is_ok());
273
+ assert!(index.capacity() >= 10);
274
+ assert!(index.connectivity() != 0);
275
+ assert_eq!(index.dimensions(), 5);
276
+ assert_eq!(index.size(), 0);
277
+
278
+ let first: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
279
+ let second: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
280
+
281
+ assert!(index.add(42, &first).is_ok());
282
+ assert!(index.add(43, &second).is_ok());
283
+ assert_eq!(index.size(), 2);
284
+
285
+ // Read back the tags
286
+ let results = index.search(&first, 10).unwrap();
287
+ assert_eq!(results.count, 2);
288
+
289
+ // Validate serialization
290
+ assert!(index.save("index.usearch").is_ok());
291
+ assert!(index.load("index.usearch").is_ok());
292
+ assert!(index.view("index.usearch").is_ok());
293
+
294
+ // There are more "metrics" available
295
+ assert!(new_l2(5, &quant, 0, 0, 0).is_ok());
296
+ assert!(new_cos(5, &quant, 0, 0, 0).is_ok());
297
+ assert!(new_haversine(&quant, 0, 0, 0).is_ok());
298
+ ```
299
+
300
+ ### Java
301
+
302
+ ```java
303
+ Index index = new Index.Config().metric("cos").dimensions(2).build();
304
+ float vec[] = {10, 20};
305
+ index.add(42, vec);
306
+ int[] labels = index.search(vec, 5);
307
+ ```
308
+
309
+ ### GoLang
310
+
311
+ ### Wolfram
312
+
313
+ ## Benchmarking
314
+
315
+ All major HNSW implementation share an identical list of hyper-parameters:
316
+
317
+ - connectivity (often called `M`),
318
+ - expansion on additions (often called `efConstruction`),
319
+ - expansion on search (often called `ef`).
320
+
321
+ The default values vary drastically.
322
+
323
+ | Library | Connectivity | EF @ A | EF @ S |
324
+ | :-------: | :----------: | :----: | :----: |
325
+ | `hnswlib` | 16 | 200 | 10 |
326
+ | `FAISS` | 32 | 40 | 16 |
327
+ | `USearch` | 16 | 128 | 64 |
328
+
329
+
330
+ To achieve best results, please compile locally and check out various configuration options.
331
+
332
+ ```sh
333
+ cmake -B ./build_release \
334
+ -DCMAKE_BUILD_TYPE=Release \
335
+ -DUSEARCH_USE_OPENMP=1 \
336
+ -DUSEARCH_USE_JEMALLOC=1 && \
337
+ make -C ./build_release -j
338
+
339
+ ./build_release/bench --help
340
+ ```
341
+
342
+ Which would print the following instructions:
343
+
344
+ ```txt
345
+ SYNOPSIS
346
+ ./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
347
+ <integer>] [-c <integer>] [--expansion-add <integer>]
348
+ [--expansion-search <integer>] [--native|--f16quant|--i8quant]
349
+ [--ip|--l2|--cos|--haversine] [-h]
350
+
351
+ OPTIONS
352
+ --vectors <path>
353
+ .fbin file path to construct the index
354
+
355
+ --queries <path>
356
+ .fbin file path to query the index
357
+
358
+ --neighbors <path>
359
+ .ibin file path with ground truth
360
+
361
+ -b, --big Will switch to uint40_t for neighbors lists with over 4B entries
362
+ -j, --threads <integer>
363
+ Uses all available cores by default
364
+
365
+ -c, --connectivity <integer>
366
+ Index granularity
367
+
368
+ --expansion-add <integer>
369
+ Affects indexing depth
370
+
371
+ --expansion-search <integer>
372
+ Affects search depth
373
+
374
+ --native Use raw templates instead of type-punned classes
375
+ --f16quant Enable `f16_t` quantization
376
+ --i8quant Enable `int8_t` quantization
377
+ --ip Choose Inner Product metric
378
+ --l2 Choose L2 Euclidean metric
379
+ --cos Choose Angular metric
380
+ --haversine Choose Haversine metric
381
+ -h, --help Print this help information on this tool and exit
382
+ ```
133
383
 
134
384
  ## TODO
135
385
 
136
386
  - JavaScript: Allow calling from "worker threads".
137
387
  - Rust: Allow passing a custom thread ID.
388
+
389
+ ## AI + Vector Search = Semantic Search
390
+
391
+ AI has a growing number of applications, but one of the coolest classic ideas is to use it for Semantic Search.
392
+ One can take an encoder model, like the multi-modal UForm, and a web-programming framework, like UCall, and build an image search platform in just 20 lines of Python.
393
+
394
+ ```python
395
+ import ucall.rich_posix as ucall
396
+ import uform
397
+ import usearch
398
+
399
+ import numpy as np
400
+ from PIL import Image
401
+
402
+ server = ucall.Server()
403
+ model = uform.get_model('unum-cloud/uform-vl-multilingual')
404
+ index = usearch.Index(ndim=256)
405
+
406
+ @server
407
+ def add(label: int, photo: Image.Image):
408
+ image = model.preprocess_image(photo)
409
+ vector = model.encode_image(image).detach().numpy()
410
+ labels = np.array([label], dtype=np.longlong)
411
+ index.add(labels, vector, copy=True)
412
+
413
+ @server
414
+ def search(query: str) -> np.ndarray:
415
+ tokens = model.preprocess_text(query)
416
+ vector = model.encode_text(tokens).detach().numpy()
417
+ neighbors = index.search(vector, 3)
418
+ return neighbors[0][:neighbors[2][0]]
419
+
420
+ server.run()
421
+ ```
422
+
423
+ Check [that](https://github.com/ashvardanian/image-search) and [other](https://github.com/unum-cloud/examples) examples on our corporate GitHub 🤗
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.8
1
+ 0.1.10
package/build.gradle CHANGED
@@ -90,7 +90,7 @@ publishing {
90
90
 
91
91
  pom {
92
92
  name = "USearch"
93
- description = "The Mighty Tiny Vector Search Engine with Automatic Quantization and Hardware Acceleration"
93
+ description = "Smaller & Faster Single-File Vector Search Engine from Unum"
94
94
  url = "https://github.com/unum-cloud/usearch"
95
95
  licenses {
96
96
  license {
@@ -136,6 +136,30 @@ template <typename scalar_at> struct l2_squared_gt {
136
136
  }
137
137
  };
138
138
 
139
+ /**
140
+ * @brief Hamming distance computes the number of differing elements in
141
+ * two arrays. An example would be a chess board.
142
+ */
143
+ template <typename scalar_at> struct hamming_gt {
144
+ using scalar_t = scalar_at;
145
+ using result_type = std::size_t;
146
+
147
+ inline std::size_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
148
+ std::size_t = 0) const noexcept {
149
+ std::size_t matches{};
150
+ #if defined(__GNUC__)
151
+ #pragma GCC ivdep
152
+ #elif defined(__clang__)
153
+ #pragma clang loop vectorize(enable)
154
+ #elif defined(_OPENMP)
155
+ #pragma omp simd reduction(+ : matches)
156
+ #endif
157
+ for (std::size_t i = 0; i != elements; ++i)
158
+ matches += a[i] != b[i];
159
+ return matches;
160
+ }
161
+ };
162
+
139
163
  /**
140
164
  * @brief Hamming distance computes the number of differing bits in
141
165
  * two arrays of integers. An example would be a textual document,
@@ -19,7 +19,12 @@ public class IndexTest {
19
19
  public void test() {
20
20
  String path = "./tmp/";
21
21
  deleteDirectoryFiles(path);
22
- // new Index("cos", "f32", 1, 10, 1, 1, 1);
22
+
23
+ // Index index = new Index.Config().metric("cos").dimensions(2).build();
24
+ // float vec[] = {10, 20};
25
+ // index.add(42, vec);
26
+ // int[] labels = index.search(vec, 5);
27
+
23
28
  System.out.println("Java Tests Passed!");
24
29
  }
25
30
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.1.8",
4
- "description": "The Mighty Tiny Vector Search Engine with Automatic Quantization and Hardware Acceleration",
3
+ "version": "0.1.10",
4
+ "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
7
7
  "repository": {
package/python/bench.py CHANGED
@@ -1,89 +1,62 @@
1
1
  import datetime
2
2
  import numpy as np
3
+ import fire
4
+
5
+ from usearchh.io import load_matrix
3
6
 
4
7
  import faiss
5
8
  import usearch
6
9
 
7
10
 
8
- print('Welcome to basic kANN benchmark!')
9
-
10
-
11
- def read_matrix(filename: str, start_row: int = 0, count_rows: int = None):
12
- """
13
- Read *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
14
- Args:
15
- :param filename (str): path to the matrix file
16
- :param start_row (int): start reading vectors from this index
17
- :param count_rows (int): number of vectors to read. If None, read all vectors
18
- Returns:
19
- Parsed matrix (numpy.ndarray)
20
- """
21
- dtype = np.float32
22
- scalar_size = 4
23
- if filename.endswith('.fbin'):
24
- dtype = np.float32
25
- scalar_size = 4
26
- elif filename.endswith('.dbin'):
27
- dtype = np.float64
28
- scalar_size = 8
29
- elif filename.endswith('.hbin'):
30
- dtype = np.float16
31
- scalar_size = 2
32
- elif filename.endswith('.ibin'):
33
- dtype = np.int32
34
- scalar_size = 4
35
- else:
36
- raise Exception('Unknown file type')
37
- with open(filename, 'rb') as f:
38
- rows, cols = np.fromfile(f, count=2, dtype=np.int32)
39
- rows = (rows - start_row) if count_rows is None else count_rows
40
- arr = np.fromfile(
41
- f, count=rows * cols, dtype=dtype,
42
- offset=start_row * scalar_size * cols)
43
- return arr.reshape(rows, cols)
44
-
45
-
46
- print('Will read datasets!')
47
- xb = read_matrix('datasets/wiki_1M/base.1M.fbin')
48
- xq = read_matrix('datasets/wiki_1M/query.public.100K.fbin')
49
-
50
- d = 256 # vector size
51
- M = 16
52
- efSearch = 32 # number of entry points (neighbors) we use on each layer
53
- efConstruction = 128 # number of entry points used on each layer
54
- labels = np.arange(xb.shape[0], dtype=np.longlong)
55
- print('Will benchmark algorithms!')
56
-
57
-
58
11
  def measure(f) -> float:
59
12
  a = datetime.datetime.now()
60
13
  f()
61
14
  b = datetime.datetime.now()
62
15
  c = b - a
63
- print(f'Took: {c.seconds} seconds')
16
+ print(f'Took: {c.seconds:.2f} seconds')
64
17
  return c.seconds
65
18
 
66
19
 
67
- def construct_both() -> tuple:
20
+ def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
21
+ dt = measure(lambda: index.add(vectors))
22
+ print(f'- FAISS: {vectors.shape[0]/dt:.2f} vectors/s')
68
23
 
69
- index_meta = faiss.IndexHNSWFlat(d, M)
70
- index_meta.hnsw.efSearch = efSearch
71
- index_meta.hnsw.efConstruction = efConstruction
72
- dt_meta = measure(lambda: index_meta.add(xb))
73
24
 
74
- index_unum = usearch.make_index(
75
- expansion_add=efConstruction,
76
- expansion_search=efSearch,
77
- connectivity=M,
78
- )
79
- dt_unum = measure(lambda: index_unum.add(labels, xb, copy=False))
25
+ def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
26
+ labels = np.arange(vectors.shape[0], dtype=np.longlong)
27
+ assert len(index) == 0
28
+ dt = measure(lambda: index.add(labels, vectors))
29
+ assert len(index) == vectors.shape[0]
30
+ print(f'- USearch: {vectors.shape[0]/dt:.2f} vectors/s')
31
+
32
+
33
+ def main(
34
+ vectors: str,
35
+ queries: str,
36
+ neighbors: str,
37
+ connectivity: int = 16,
38
+ expansion_add: int = 128,
39
+ expansion_search: int = 64,
40
+ ):
80
41
 
81
- return dt_meta, dt_unum
42
+ vectors_mat = load_matrix(vectors)
43
+ queries_mat = load_matrix(queries)
44
+ neighbors_mat = load_matrix(neighbors)
45
+ dim = vectors_mat.shape[1]
46
+
47
+ index = faiss.IndexHNSWFlat(dim, connectivity)
48
+ index.hnsw.efSearch = expansion_search
49
+ index.hnsw.efConstruction = expansion_add
50
+ bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
51
+
52
+ index = usearch.Index(
53
+ ndim=dim,
54
+ expansion_add=expansion_add,
55
+ expansion_search=expansion_search,
56
+ connectivity=connectivity,
57
+ )
58
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
82
59
 
83
60
 
84
- experiments = 10
85
- durations = [construct_both() for _ in range(experiments)]
86
- dt_meta = sum(x[0] for x in durations)
87
- dt_unum = sum(x[1] for x in durations)
88
- print(f'- FAISS vectors per second: {xb.shape[0]*experiments/dt_meta:.2f}')
89
- print(f'- USearch vectors per second: {xb.shape[0]*experiments/dt_unum:.2f}')
61
+ if __name__ == '__main__':
62
+ fire.Fire(main)
package/python/lib.cpp CHANGED
@@ -36,7 +36,8 @@ static native_index_t make_index( //
36
36
  std::string const& metric, //
37
37
  std::size_t connectivity, //
38
38
  std::size_t expansion_add, //
39
- std::size_t expansion_search //
39
+ std::size_t expansion_search, //
40
+ std::size_t metric_uintptr //
40
41
  ) {
41
42
 
42
43
  config_t config;
@@ -48,7 +49,11 @@ static native_index_t make_index( //
48
49
  config.max_threads_search = std::thread::hardware_concurrency();
49
50
 
50
51
  accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
51
- return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
52
+ punned_metric_t metric_ptr = reinterpret_cast<punned_metric_t>(metric_uintptr);
53
+ if (metric_ptr)
54
+ return native_index_t::udf(dimensions, metric_ptr, accuracy, config);
55
+ else
56
+ return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
52
57
  }
53
58
 
54
59
  static void add_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
@@ -173,15 +178,16 @@ PYBIND11_MODULE(usearch, m) {
173
178
 
174
179
  auto i = py::class_<native_index_t>(m, "Index");
175
180
 
176
- i.def(py::init(&make_index), //
177
- py::kw_only(), //
178
- py::arg("dim"), //
179
- py::arg("capacity") = 0, //
180
- py::arg("dtype") = std::string("f32"), //
181
- py::arg("metric") = std::string("ip"), //
182
- py::arg("connectivity") = config_t::connectivity_default_k, //
183
- py::arg("expansion_add") = config_t::expansion_add_default_k, //
184
- py::arg("expansion_search") = config_t::expansion_search_default_k //
181
+ i.def(py::init(&make_index), //
182
+ py::kw_only(), //
183
+ py::arg("ndim"), //
184
+ py::arg("capacity") = 0, //
185
+ py::arg("dtype") = std::string("f32"), //
186
+ py::arg("metric") = std::string("ip"), //
187
+ py::arg("connectivity") = config_t::connectivity_default_k, //
188
+ py::arg("expansion_add") = config_t::expansion_add_default_k, //
189
+ py::arg("expansion_search") = config_t::expansion_search_default_k, //
190
+ py::arg("metric_pointer") = 0 //
185
191
  );
186
192
 
187
193
  i.def( //