usearch 0.2.3 → 0.2.4

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
@@ -33,123 +33,110 @@ Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Met
33
33
  Linux • MacOS • Windows
34
34
  </p>
35
35
 
36
- - [x] [Single C++11 header][usearch-header] implementation, easily extendible.
37
- - [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
38
- - [x] [Half-precision](#quantize-on-the-fly) support with [`fp16`](https://github.com/maratyszcza/fp16).
36
+ ---
37
+
38
+ - [x] Industry-leading [performance](#performance).
39
+ - [x] Easily-extendible [single C++11 header][usearch-header] implementation.
40
+ - [x] [User-defined](#define-custom-metrics) and pre-packaged SIMD-accelerated metrics.
41
+ - [x] [Half-precision `f16` and Quarter-precision `f8`](#quantize-on-the-fly) support on any hardware.
39
42
  - [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
40
- - [x] [User-defined](#define-custom-metrics) and hardware-accelerated metrics with [`simsimd`](https://github.com/ashvardanian/simsimd).
41
- - [x] Variable dimensionality vectors.
42
- - [x] Don't copy vectors if not needed.
43
+ - [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
44
+ - [x] Variable dimensionality vectors - for [obscure use-cases][obscure-use-cases].
43
45
  - [x] [Bring your threads](#bring-your-threads), like OpenMP.
44
46
  - [x] Multiple vectors per label.
45
47
  - [ ] Thread-safe `reserve`.
46
- - [x] Compatible with Linux, MacOS, and Windows.
47
- - [x] AI + Vector Search = Semantic Search.
48
+ - [x] AI + Vector Search = [Semantic Search](#ai--vector-search--semantic-search).
48
49
 
49
50
  [usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
50
-
51
- ## Features
52
-
53
- ### Define Custom Metrics
51
+ [obscure-use-cases]: https://ashvardanian.com/posts/abusing-vector-search
52
+
53
+ ---
54
+
55
+ | | FAISS | USearch |
56
+ | :----------------- | :---------------------------- | :--------------------------------- |
57
+ | Implementation | 84 K [SLOC][sloc] in `faiss/` | 1 K [SLOC][sloc] in `usearch/` |
58
+ | Supported metrics | 9 fixed metrics | Any User-Defined metrics |
59
+ | Supported ID types | `uint32_t`, `uint64_t` | `uint32_t`, `uint40_t`, `uint64_t` |
60
+ | Dependencies | BLAS, OpenMP | None |
61
+ | Bindings | SWIG | Native |
62
+ | Acceleration | Learned Quantization | Downcasting |
63
+
64
+ FAISS is the industry standard for a high-performance batteries-included vector search engine.
65
+ Both USearch and FAISS implement the same HNSW algorithm.
66
+ But they differ in a lot of design decisions.
67
+ USearch is designed to be compact and broadly compatible without sacrificing performance.
68
+
69
+ | | FAISS, `f32` | USearch, `f32` | USearch, `f16` | USearch, `f8` |
70
+ | :--------- | -----------: | -------------: | -------------: | ------------: |
71
+ | Insertions | 76 K/s | 89 K/s | 73 K/s | **137 K/s** |
72
+ | Queries | 118 K/s | 167 K/s | 140 K/s | **281 K/s** |
73
+ | Recall @1 | 99% | 99.2% | 98% | **99.2%** |
74
+
75
+ > Dataset: 1M vectors sample of the Deep1B dataset.
76
+ > Hardware: `c7g.metal` AWS instance with 64 cores and DDR5 memory.
77
+ > HNSW was configured with identical hyper-parameters:
78
+ > connectivity `M=16`,
79
+ > expansion @ construction `efConstruction=128`,
80
+ > and expansion @ search `ef=64`.
81
+ > Both libraries were compiled for the target architecture.
82
+ > Jump to the [Performance Tuning][benchmarking] section to read about the effects of those hyper-parameters.
83
+
84
+ [sloc]: https://en.wikipedia.org/wiki/Source_lines_of_code
85
+ [benchmarking]: https://github.com/unum-cloud/usearch
86
+
87
+ ## User-Defined Functions
54
88
 
55
89
  Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
56
- The lack of dedicated "Cosine distance" can be justified with the simplicity of normalizing such vectors on the fly.
57
- But that hardly exhausts the list of possible metrics.
90
+ That only partially exhausts the list of possible metrics.
91
+ A good example would be the rare [Haversine][haversine] distance, used to compute the distance between geo-spatial coordinates, extending Vector Search into the GIS domain.
92
+ Another example would be designing a custom metric for **composite embeddings** concatenated from multiple AI models in real-world applications.
93
+ USearch supports that: [Python](#user-defined-functions-in-python) and [C++](#user-defined-functions-in-c) examples.
58
94
 
59
95
  ![USearch: Vector Search Approaches](assets/usearch-approaches-white.png)
60
96
 
61
- Older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing are hardly extendible to vectors/objects of variable length.
62
- Modern NSW-like approaches, however, only require two objects to be comparable.
63
- This means, the index can be extended to support fuzzy search with Levenstein distance over strings.
64
- USearch implements HNSW, and provides some uncommon-to-vector-search "similarity measures" and "distances" out of the box: Haversine, Jaccard, Hamming, etc.
65
-
66
- ### Bring your Threads
97
+ Unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
98
+ They only have to be comparable.
99
+ So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching.
67
100
 
68
- Most AI, HPC, or Big Data packages use some form of a thread pool.
69
- Instead of spawning additional threads within USearch, we focus on thread-safety of the `add` function.
70
-
71
- ```cpp
72
- #pragma omp parallel for
73
- for (std::size_t i = 0; i < n; ++i)
74
- native.add(label, span_t{vector, dims}, omp_get_thread_num());
75
- ```
101
+ [haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
102
+ [obscure]: https://ashvardanian.com/posts/abusing-vector-search
76
103
 
77
- During initialization we allocate enough temporary memory for all the cores on the machine.
78
- On call, the user can simply supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
104
+ ## Memory Efficiency, Downcasting, and Quantization
79
105
 
80
- ### Go Beyond 4B Entries
81
-
82
- Constructing a vector index is a memory-intensive task.
83
- 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.
84
- Choosing the right data-structures and numeric types can have profound implications on both.
106
+ Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
107
+ Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
85
108
 
86
109
  ![USearch uint40_t support](assets/usearch-neighbor-types.png)
87
110
 
88
- Internally, some point "label" is mapped into an integer identifier for every stored vector.
89
- Most implementations hard-code it to `uint32_t`, as people rarely store more than 4 Billion vectors.
90
- But when they do, software becomes unusable.
91
- Others hard-code it to `uint64_t`, which hardly space-efficient.
92
- We added a tiny class - `uint40_t`, that should be enough for collections up to 1 Trillion vectors.
93
-
94
- ### View Larger Indexes from Disk
95
-
96
- 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.
97
- Those are, however, pricy, and would cost you over $200/hour.
98
-
99
- | | To Build | To Serve |
100
- | :------- | :-----------: | :------------: |
101
- | Instance | u-24tb1.metal | is4gen.8xlarge |
102
- | Price | ~ $200/h | ~$4.5/h |
103
- | RAM | 24 TB | 192 GB |
104
- | SSD | EBS | 30 TB |
105
-
106
- You wouldn't want to have them running all the time.
107
- 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.
108
-
109
- > Construct on large machines, deploy on the cheap ones.
111
+ Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
112
+ The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f32_t`, `f16_t`, `f64_t`, and `f8_t` representations, even if the hardware doesn't natively support it.
113
+ Continuing the topic of memory-efficiency, we provide a `uint40_t` to allow collection with over 4B+ vectors without allocating 8 bytes for every neighbor reference in the proximity graph.
110
114
 
111
- ### Quantize on the Fly
115
+ ## View Larger Indexes from Disk
112
116
 
113
- Most modern CPU have at least partial support for `half`-precision `f16_t` arithmetic.
114
- USearch supports automatic down-casting and up-casting between `f32_t`, `f16_t`, `f64_t`, and `f8_t` representations.
117
+ Modern search systems often suggest using different servers to maximize indexing speed and minimize serving costs.
118
+ Memory-optimized for the first task, and storage-optimized for the second, if the index can be served from external memory, which USearch can.
115
119
 
116
- > Making vectors smaller will help pack more of them in-memory, while also increasing performance on CPUs implementing native support for target type.
120
+ | | To Build | To Serve |
121
+ | :------- | :-------------: | :--------------------: |
122
+ | Instance | u-24tb1.metal | is4gen.8xlarge |
123
+ | Price | ~ $200/h | ~$4.5/h |
124
+ | Memory | 24 TB RAM + EBS | 192 GB RAM + 30 TB SSD |
117
125
 
118
- ## Performance
119
-
120
- Below are the performance numbers for a benchmark running on the 64 cores of AWS `c7g.metal` "Graviton 3"-based instances.
121
- We fix the default configuration in the top line and show the affects of various parameters by changing one parameter at a time.
122
-
123
- | Vectors | Connectivity | EF @ A | EF @ S | **Add**, QPS | **Search**, QPS | **Recall @1** |
124
- | :--------: | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
125
- | `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
126
- | | | | | | | |
127
- | `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
128
- | `f32` x256 | 32 | 128 | 64 | 64'368 | 104'050 | 99.4% |
129
- | | | | | | | |
130
- | `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
131
- | `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
132
- | | | | | | | |
133
- | `f16` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
134
- | `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
135
-
136
- The main columns are:
137
-
138
- - Add: Number of insertion Queries Per Second.
139
- - Search: Number search Queries Per Second.
140
- - Recall @1: How often does approximate search yield the exact best match?
141
-
142
- To read more and reproduce, jump to [benchmarking section](#benchmarking).
126
+ There is a 50x difference between the cost of such instances for identical capacity.
127
+ Of course, the latency of external memory access will be higher, but it is in part compensated with an excellent prefetching mechanism.
143
128
 
144
129
  ## Usage
145
130
 
146
131
  There are two usage patters:
147
132
 
148
- - Bare-bones with `usearch/usearch.hpp`, only available in C++.
149
- - Full-fat version with it's own threads, mutexes, type-punning, quantization, that is available both in C++ and is wrapped for higher-level bindings.
133
+ 1. Bare-bones with `usearch/usearch.hpp`, only available in C++.
134
+ 2. Full-fat version with it's own threads, mutexes, type-punning, quantization, that is available both in C++ and is wrapped for higher-level bindings.
150
135
 
151
136
  ### C++
152
137
 
138
+ #### Installation
139
+
153
140
  To use in a C++ project simply copy the `include/usearch/usearch.hpp` header into your project.
154
141
  Alternatively fetch it with CMake:
155
142
 
@@ -158,19 +145,10 @@ FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearc
158
145
  FetchContent_MakeAvailable(usearch)
159
146
  ```
160
147
 
161
- The simple usage example would require including the `unum::usearch` namespace and choosing the right "distance" function.
162
- That can be one of the following templates:
148
+ #### Quickstart
163
149
 
164
- - `cos_gt<float>` for "Cosine" or "Angular" distance.
165
- - `ip_gt<float>` for "Inner Product" or "Dot Product" distance.
166
- - `l2sq_gt<float>` for the squared "L2" or "Euclidean" distance.
167
- - `jaccard_gt<int>` for "Jaccard" distance between two ordered sets of unique elements.
168
- - `bit_hamming_gt<uint>` for "Hamming" distance, as the number of shared bits in hashes.
169
- - `pearson_correlation_gt<float>` for "Pearson" correlation between probability distributions.
170
- - `haversine_gt<float>` for "Haversine" or "Great Circle" distance between coordinates.
171
-
172
- That list is easily extendible, and can include similarity measures for vectors that have a different number of elements/dimensions.
173
- The minimal example would be.
150
+ Once included, the low-level C++11 interface is as simple as it gets: `reserve()`, `add()`, `search()`, `size()`, `capacity()`, `save()`, `load()`, `view()`.
151
+ This covers 90% of use-cases.
174
152
 
175
153
  ```c++
176
154
  using namespace unum::usearch;
@@ -183,14 +161,21 @@ index.add(/* label: */ 42, /* vector: */ {&vec, 3});
183
161
  index.search(
184
162
  /* query: */ {&vec, 3}, /* top */ 5 /* results */,
185
163
  /* with callback: */ [](std::size_t label, float distance) { });
164
+ ```
165
+
166
+ The `add` is thread-safe for concurrent index construction.
186
167
 
187
- index.save("index.usearch"); // Serializing to disk
188
- index.load("index.usearch"); // Reconstructing from disk
168
+ #### Serialization
169
+
170
+ ```c++
171
+ index.save("index.usearch");
172
+ index.load("index.usearch"); // Copying from disk
189
173
  index.view("index.usearch"); // Memory-mapping from disk
190
174
  ```
191
175
 
192
- The `add` is thread-safe for concurrent index construction.
193
- For advanced users, more compile-time abstraction are available.
176
+ #### User-Defined Metrics in C++
177
+
178
+ For advanced users, more compile-time abstractions are available.
194
179
 
195
180
  ```cpp
196
181
  template <typename metric_at = ip_gt<float>, //
@@ -201,8 +186,8 @@ template <typename metric_at = ip_gt<float>, //
201
186
  class index_gt;
202
187
  ```
203
188
 
204
- One may also define a custom metric, such as Damerau–Levenshtein distance, to compute the similarity between variable length strings.
205
- The only constraint is the function signature:
189
+ You may want to use a custom memory allocator or a rare scalar type, but most often, you would start by defining a custom similarity measure.
190
+ The function object should have the following signature to support different-length vectors.
206
191
 
207
192
  ```cpp
208
193
  struct custom_metric_t {
@@ -210,102 +195,184 @@ struct custom_metric_t {
210
195
  };
211
196
  ```
212
197
 
198
+ The following distances are pre-packaged:
199
+
200
+ - `cos_gt<scalar_t>` for "Cosine" or "Angular" distance.
201
+ - `ip_gt<scalar_t>` for "Inner Product" or "Dot Product" distance.
202
+ - `l2sq_gt<scalar_t>` for the squared "L2" or "Euclidean" distance.
203
+ - `jaccard_gt<scalar_t>` for "Jaccard" distance between two ordered sets of unique elements.
204
+ - `bit_hamming_gt<scalar_t>` for "Hamming" distance, as the number of shared bits in hashes.
205
+ - `pearson_correlation_gt<scalar_t>` for "Pearson" correlation between probability distributions.
206
+ - `haversine_gt<scalar_t>` for "Haversine" or "Great Circle" distance between coordinates.
207
+
208
+ #### Bring your Threads
209
+
210
+ Most AI, HPC, or Big Data packages use some form of a thread pool.
211
+ Instead of spawning additional threads within USearch, we focus on the thread safety of `add()` function, simplifying resource management.
212
+
213
+ ```cpp
214
+ #pragma omp parallel for
215
+ for (std::size_t i = 0; i < n; ++i)
216
+ native.add(label, span_t{vector, dims}, omp_get_thread_num());
217
+ ```
218
+
219
+ During initialization, we allocate enough temporary memory for all the cores on the machine.
220
+ On the call, the user can supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
221
+
213
222
  ### Python
214
223
 
224
+ #### Installation
225
+
215
226
  ```sh
216
227
  pip install usearch
217
228
  ```
218
229
 
219
- Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
220
- Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
230
+ #### Quickstart
221
231
 
222
232
  ```python
223
233
  import numpy as np
224
234
  from usearch.index import Index
225
235
 
226
236
  index = Index(
227
- ndim=256, # Define the number of dimensions in input vectors
228
- metric='cos', # Choose the "metric" or "distance", default = 'ip', optional
229
- dtype='f32', # Quantize to 'f16' or 'f8' if needed, default = 'f32', optional
237
+ ndim=3, # Define the number of dimensions in input vectors
238
+ metric='cos', # Choose 'l2', 'haversine' or other metric, default = 'ip'
239
+ dtype='f32', # Quantize to 'f16' or 'f8' if needed, default = 'f32'
230
240
  connectivity=16, # How frequent should the connections in the graph be, optional
231
241
  expansion_add=128, # Control the recall of indexing, optional
232
242
  expansion_search=64, # Control the quality of search, optional
233
243
  )
234
244
 
235
- vector = np.random.uniform(0, 0.3, (index.ndim)).astype(np.float32)
245
+ vector = np.array([0.2, 0.6, 0.4], dtype=np.float32)
236
246
  index.add(42, vector)
237
247
  matches, distances, count = index.search(vector, 10)
248
+
249
+ assert len(index) == 1
250
+ assert count == 1
251
+ assert matches[0] == 42
252
+ assert distances[0] <= 0.001
253
+ ```
254
+
255
+ Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
256
+ Assuming the presence of Global Interpreter Lock in Python, we spawn threads in the C++ layer on large insertions.
257
+
258
+ #### Serialization
259
+
260
+ ```py
261
+ index.save('index.usearch')
262
+ index.load('index.usearch') # Copy the whole index into memory
263
+ index.view('index.usearch') # View from disk without loading in memory
238
264
  ```
239
265
 
240
266
  #### Batch Operations
241
267
 
242
- Same can be done with batches, rather than single entries.
243
- Under the hood, worker threads will be spawned, to parallelize the procedure.
268
+ Adding or querying a batch of entries is identical to adding a single vector.
269
+ The difference would be in the shape of the tensors.
244
270
 
245
271
  ```py
246
272
  n = 100
247
273
  labels = np.array(range(n), dtype=np.longlong)
248
274
  vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
249
275
 
250
- index.add(labels, vectors)
251
- matches, distances, counts = index.search(vectors, 10)
276
+ index.add(labels, vectors, threads=..., copy=...)
277
+ matches, distances, counts = index.search(vectors, 10, threads=...)
278
+
279
+ assert matches.shape[0] == vectors.shape[0]
280
+ assert counts[0] <= 10
252
281
  ```
253
282
 
254
- #### Performance
283
+ You can also override the default `threads` and `copy` arguments in bulk workloads.
284
+ The first controls the number of threads spawned for the task.
285
+ The second controls whether the vector itself will be persisted inside the index.
286
+ If you can preserve the lifetime of the vector somewhere else, you can avoid the copy.
255
287
 
256
- To maximize performance, you can:
288
+ #### User-Defined Metrics in Python
257
289
 
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=)`.
290
+ Assuming the language boundary exists between Python user code and C++ implementation, there are more efficient solutions than passing a Python callable to the engine.
291
+ Luckily, with the help of [Numba][numba], we can JIT compile a function with a matching signature and pass it down to the engine.
261
292
 
262
- For a dataset with 1 Million 256-dimensional embeddings at single precision, the performance would be:
293
+ ```py
294
+ from numba import cfunc, types, carray
295
+
296
+ signature = types.float32(
297
+ types.CPointer(types.float32),
298
+ types.CPointer(types.float32),
299
+ types.size_t, types.size_t)
300
+
301
+ @cfunc(signature)
302
+ def python_dot(a, b, n, m):
303
+ a_array = carray(a, n)
304
+ b_array = carray(b, n)
305
+ c = 0.0
306
+ for i in range(n):
307
+ c += a_array[i] * b_array[i]
308
+ return c
309
+
310
+ index = Index(ndim=ndim, metric_pointer=python_dot.address)
311
+ ```
312
+
313
+ [numba]: https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
314
+
315
+ #### Tooling
263
316
 
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%).
317
+ ```py
318
+ from usearch.index import Index
319
+ from usearch.io import load_matrix, save_matrix
320
+
321
+ vectors = load_matrix('deep1B.fbin')
322
+ index = Index(ndim=vectors.shape[1])
323
+ index.add(labels, vectors)
324
+ ```
267
325
 
268
326
  ### JavaScript
269
327
 
328
+ #### Installation
329
+
270
330
  ```sh
271
331
  npm install usearch
272
332
  ```
273
333
 
334
+ #### Quickstart
335
+
274
336
  ```js
275
- var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 2 })
276
- assert.equal(index.connectivity(), 16)
277
- assert.equal(index.dimensions(), 2)
278
- assert.equal(index.size(), 0)
337
+ var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 3 })
338
+ index.add(42, new Float32Array([0.2, 0.6, 0.4]))
339
+ var results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10)
340
+
341
+ assert.equal(index.size(), 1)
342
+ assert.deepEqual(results.labels, new Uint32Array([42]))
343
+ assert.deepEqual(results.distances, new Float32Array([0]))
344
+ ```
279
345
 
280
- index.add(15, new Float32Array([10, 20]))
281
- assert.equal(index.size(), 2)
346
+ #### Serialization
282
347
 
283
- var results = index.search(new Float32Array([13, 14]), 2)
284
- assert.deepEqual(results.labels, new Uint32Array([15, 16]))
285
- assert.deepEqual(results.distances, new Float32Array([45, 130]))
348
+ ```js
349
+ index.save('index.usearch')
350
+ index.load('index.usearch')
351
+ index.view('index.usearch')
286
352
  ```
287
353
 
288
354
  ### Rust
289
355
 
356
+ #### Installation
357
+
290
358
  ```sh
291
359
  cargo add usearch
292
360
  ```
293
361
 
294
- Being a systems-programming language, Rust has better control over memory management and concurrency, but lacks function overloading.
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.
362
+ #### Quickstart
296
363
 
297
364
  ```rust
298
365
  let quant: &str = "f16";
299
- let index = new_ip(5, &quant, 0, 0, 0).unwrap();
366
+ let index = new_ip(3, &quant, 0, 0, 0).unwrap();
300
367
 
301
368
  assert!(index.reserve(10).is_ok());
302
369
  assert!(index.capacity() >= 10);
303
370
  assert!(index.connectivity() != 0);
304
- assert_eq!(index.dimensions(), 5);
371
+ assert_eq!(index.dimensions(), 3);
305
372
  assert_eq!(index.size(), 0);
306
373
 
307
- let first: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
308
- let second: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
374
+ let first: [f32; 3] = [0.2, 0.1, 0.2];
375
+ let second: [f32; 3] = [0.2, 0.1, 0.2];
309
376
 
310
377
  assert!(index.add(42, &first).is_ok());
311
378
  assert!(index.add(43, &second).is_ok());
@@ -314,20 +381,31 @@ assert_eq!(index.size(), 2);
314
381
  // Read back the tags
315
382
  let results = index.search(&first, 10).unwrap();
316
383
  assert_eq!(results.count, 2);
384
+ ```
317
385
 
318
- // Validate serialization
386
+ Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
387
+ Aside from the `add` and `search`, USearch Rust binding also provides `add_in_thread` and `search_in_thread`, which let users identify the calling thread to use underlying temporary memory more efficiently.
388
+
389
+ #### Serialization
390
+
391
+ ```rust
319
392
  assert!(index.save("index.usearch").is_ok());
320
393
  assert!(index.load("index.usearch").is_ok());
321
394
  assert!(index.view("index.usearch").is_ok());
395
+ ```
396
+
397
+ #### Metrics
322
398
 
323
- // There are more "metrics" available
324
- assert!(new_l2(5, &quant, 0, 0, 0).is_ok());
325
- assert!(new_cos(5, &quant, 0, 0, 0).is_ok());
399
+ ```rust
400
+ assert!(new_l2(3, &quant, 0, 0, 0).is_ok());
401
+ assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
326
402
  assert!(new_haversine(&quant, 0, 0, 0).is_ok());
327
403
  ```
328
404
 
329
405
  ### Java
330
406
 
407
+ #### Installation
408
+
331
409
  ```xml
332
410
  <dependency>
333
411
  <groupId>cloud.unum.usearch</groupId>
@@ -338,6 +416,8 @@ assert!(new_haversine(&quant, 0, 0, 0).is_ok());
338
416
 
339
417
  Add that snippet to your `pom.xml` and hit `mvn install`.
340
418
 
419
+ #### Quickstart
420
+
341
421
  ```java
342
422
  Index index = new Index.Config().metric("cos").dimensions(2).build();
343
423
  float vec[] = {10, 20};
@@ -347,10 +427,18 @@ int[] labels = index.search(vec, 5);
347
427
 
348
428
  ### Swift
349
429
 
430
+ #### Installation
431
+
432
+ ```txt
433
+ https://github.com/unum-cloud/usearch
434
+ ```
435
+
436
+ #### Quickstart
437
+
350
438
  ```swift
351
- let index = Index.l2(dimensions: 4, connectivity: 8)
352
- let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
353
- let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
439
+ let index = Index.l2(dimensions: 3, connectivity: 8)
440
+ let vectorA: [Float32] = [0.3, 0.5, 1.2]
441
+ let vectorB: [Float32] = [0.4, 0.2, 1.2]
354
442
  index.add(label: 42, vector: vectorA[...])
355
443
  index.add(label: 43, vector: vectorB[...])
356
444
 
@@ -362,81 +450,11 @@ assert(results.0[0] == 42)
362
450
 
363
451
  ### Wolfram
364
452
 
365
- ## Benchmarking
366
-
367
- All major HNSW implementation share an identical list of hyper-parameters:
368
-
369
- - connectivity (often called `M`),
370
- - expansion on additions (often called `efConstruction`),
371
- - expansion on search (often called `ef`).
372
-
373
- The default values vary drastically.
374
-
375
- | Library | Connectivity | EF @ A | EF @ S |
376
- | :-------: | :----------: | :----: | :----: |
377
- | `hnswlib` | 16 | 200 | 10 |
378
- | `FAISS` | 32 | 40 | 16 |
379
- | `USearch` | 16 | 128 | 64 |
380
-
381
-
382
- To achieve best results, please compile locally and check out various configuration options.
383
-
384
- ```sh
385
- cmake -B ./build_release \
386
- -DCMAKE_BUILD_TYPE=Release \
387
- -DUSEARCH_USE_OPENMP=1 \
388
- -DUSEARCH_USE_JEMALLOC=1 && \
389
- make -C ./build_release -j
390
-
391
- ./build_release/bench --help
392
- ```
393
-
394
- Which would print the following instructions:
395
-
396
- ```txt
397
- SYNOPSIS
398
- ./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
399
- <integer>] [-c <integer>] [--expansion-add <integer>]
400
- [--expansion-search <integer>] [--native|--f16quant|--i8quant]
401
- [--ip|--l2|--cos|--haversine] [-h]
402
-
403
- OPTIONS
404
- --vectors <path>
405
- .fbin file path to construct the index
406
-
407
- --queries <path>
408
- .fbin file path to query the index
409
-
410
- --neighbors <path>
411
- .ibin file path with ground truth
412
-
413
- -b, --big Will switch to uint40_t for neighbors lists with over 4B entries
414
- -j, --threads <integer>
415
- Uses all available cores by default
416
-
417
- -c, --connectivity <integer>
418
- Index granularity
419
-
420
- --expansion-add <integer>
421
- Affects indexing depth
422
-
423
- --expansion-search <integer>
424
- Affects search depth
425
-
426
- --native Use raw templates instead of type-punned classes
427
- --f16quant Enable `f16_t` quantization
428
- --i8quant Enable `int8_t` quantization
429
- --ip Choose Inner Product metric
430
- --l2 Choose L2 Euclidean metric
431
- --cos Choose Angular metric
432
- --haversine Choose Haversine metric
433
- -h, --help Print this help information on this tool and exit
434
- ```
435
-
436
453
  ## TODO
437
454
 
438
455
  - JavaScript: Allow calling from "worker threads".
439
456
  - Rust: Allow passing a custom thread ID.
457
+ - C# .NET bindings.
440
458
 
441
459
  ## AI + Vector Search = Semantic Search
442
460
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.2.4
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 = '0.2.3'
7
+ version = '0.2.4'
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'