usearch 0.2.3 → 0.4.0

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/blob/main/docs/benchmarks.md
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.
58
-
59
- ![USearch: Vector Search Approaches](assets/usearch-approaches-white.png)
60
-
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
67
-
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
- ```
76
-
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.
79
-
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.
85
-
86
- ![USearch uint40_t support](assets/usearch-neighbor-types.png)
87
-
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.
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.
93
94
 
94
- ### View Larger Indexes from Disk
95
+ ![USearch: Vector Search Approaches](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-approaches-white.png?raw=true)
95
96
 
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.
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.
98
100
 
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 |
101
+ [haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
102
+ [obscure]: https://ashvardanian.com/posts/abusing-vector-search
105
103
 
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.
104
+ ## Memory Efficiency, Downcasting, and Quantization
108
105
 
109
- > Construct on large machines, deploy on the cheap ones.
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.
110
108
 
111
- ### Quantize on the Fly
109
+ ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
112
110
 
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.
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.
115
114
 
116
- > Making vectors smaller will help pack more of them in-memory, while also increasing performance on CPUs implementing native support for target type.
115
+ ## View Larger Indexes from Disk
117
116
 
118
- ## Performance
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.
119
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.
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 |
122
125
 
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:
163
-
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.
148
+ #### Quickstart
171
149
 
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
+ ```
186
165
 
187
- index.save("index.usearch"); // Serializing to disk
188
- index.load("index.usearch"); // Reconstructing from disk
166
+ The `add` is thread-safe for concurrent index construction.
167
+
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
+ #### Multi-Threading
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}, add_config_t { .thread = 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
263
314
 
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%).
315
+ #### Tooling
316
+
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,39 @@ 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
+ ```
385
+
386
+ #### Multi-Threading
317
387
 
318
- // Validate serialization
388
+ ```rust
389
+ assert!(index.add_in_thread(42, &first, 0).is_ok());
390
+ assert!(index.add_in_thread(43, &second, 0).is_ok());
391
+ let results = index.search_in_thread(&first, 10, 0).unwrap();
392
+ ```
393
+
394
+ Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
395
+ 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.
396
+
397
+ #### Serialization
398
+
399
+ ```rust
319
400
  assert!(index.save("index.usearch").is_ok());
320
401
  assert!(index.load("index.usearch").is_ok());
321
402
  assert!(index.view("index.usearch").is_ok());
403
+ ```
404
+
405
+ #### Metrics
322
406
 
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());
407
+ ```rust
408
+ assert!(new_l2(3, &quant, 0, 0, 0).is_ok());
409
+ assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
326
410
  assert!(new_haversine(&quant, 0, 0, 0).is_ok());
327
411
  ```
328
412
 
329
413
  ### Java
330
414
 
415
+ #### Installation
416
+
331
417
  ```xml
332
418
  <dependency>
333
419
  <groupId>cloud.unum.usearch</groupId>
@@ -338,6 +424,8 @@ assert!(new_haversine(&quant, 0, 0, 0).is_ok());
338
424
 
339
425
  Add that snippet to your `pom.xml` and hit `mvn install`.
340
426
 
427
+ #### Quickstart
428
+
341
429
  ```java
342
430
  Index index = new Index.Config().metric("cos").dimensions(2).build();
343
431
  float vec[] = {10, 20};
@@ -347,10 +435,18 @@ int[] labels = index.search(vec, 5);
347
435
 
348
436
  ### Swift
349
437
 
438
+ #### Installation
439
+
440
+ ```txt
441
+ https://github.com/unum-cloud/usearch
442
+ ```
443
+
444
+ #### Quickstart
445
+
350
446
  ```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]
447
+ let index = Index.l2(dimensions: 3, connectivity: 8)
448
+ let vectorA: [Float32] = [0.3, 0.5, 1.2]
449
+ let vectorB: [Float32] = [0.4, 0.2, 1.2]
354
450
  index.add(label: 42, vector: vectorA[...])
355
451
  index.add(label: 43, vector: vectorB[...])
356
452
 
@@ -362,81 +458,11 @@ assert(results.0[0] == 42)
362
458
 
363
459
  ### Wolfram
364
460
 
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
461
  ## TODO
437
462
 
438
463
  - JavaScript: Allow calling from "worker threads".
439
464
  - Rust: Allow passing a custom thread ID.
465
+ - C# .NET bindings.
440
466
 
441
467
  ## AI + Vector Search = Semantic Search
442
468
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.4.0