usearch 2.10.5 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,9 +94,10 @@ USearch is compact and broadly compatible without sacrificing performance, prima
94
94
  | Supported metrics ² | 9 fixed metrics | any metric | extendible |
95
95
  | Supported languages ³ | C++, Python | 10 languages | portable |
96
96
  | Supported ID types ⁴ | 32-bit, 64-bit | 32-bit, 40-bit, 64-bit | efficient |
97
- | Required dependencies ⁵ | BLAS, OpenMP | - | light-weight |
98
- | Bindings ⁶ | SWIG | Native | low-latency |
99
- | Python binding size ⁷ | [~ 10 MB][faiss-weight] | [< 1 MB][usearch-weight] | deployable |
97
+ | Filtering ⁵ | ban-lists | any predicates | composable |
98
+ | Required dependencies ⁶ | BLAS, OpenMP | - | light-weight |
99
+ | Bindings ⁷ | SWIG | Native | low-latency |
100
+ | Python binding size ⁸ | [~ 10 MB][faiss-weight] | [< 1 MB][usearch-weight] | deployable |
100
101
 
101
102
  [sloc]: https://en.wikipedia.org/wiki/Source_lines_of_code
102
103
  [faiss-weight]: https://pypi.org/project/faiss-cpu/#files
@@ -107,9 +108,10 @@ USearch is compact and broadly compatible without sacrificing performance, prima
107
108
  > ² User-defined metrics allow you to customize your search for various applications, from GIS to creating custom metrics for composite embeddings from multiple AI models or hybrid full-text and semantic search.
108
109
  > ³ With USearch, you can reuse the same preconstructed index in various programming languages.
109
110
  > ⁴ The 40-bit integer allows you to store 4B+ vectors without allocating 8 bytes for every neighbor reference in the proximity graph.
110
- > ⁵ Lack of obligatory dependencies makes USearch much more portable.
111
- > ⁶ Native bindings introduce lower call latencies than more straightforward approaches.
112
- > ⁷ Lighter bindings make downloads and deployments faster.
111
+ > ⁵ With USearch the index can be combined with arbitrary external containers, like Bloom filters or third-party databases, to filter out irrelevant keys during index traversal.
112
+ > ⁶ Lack of obligatory dependencies makes USearch much more portable.
113
+ > ⁷ Native bindings introduce lower call latencies than more straightforward approaches.
114
+ > ⁸ Lighter bindings make downloads and deployments faster.
113
115
 
114
116
  [intel-benchmarks]: https://www.unum.cloud/blog/2023-11-07-scaling-vector-search-with-intel
115
117
 
@@ -192,23 +194,59 @@ When compared to FAISS's `IndexFlatL2` in Google Colab, __[USearch may offer up
192
194
  - `faiss.IndexFlatL2`: __55.3 ms__.
193
195
  - `usearch.index.search`: __2.54 ms__.
194
196
 
195
- ## User-Defined Functions
197
+ ## User-Defined Metrics
196
198
 
197
- While most vector search packages concentrate on just a few metrics - "Inner Product distance" and "Euclidean distance," USearch extends this list to include any user-defined metrics.
198
- This flexibility allows you to customize your search for various applications, from computing geospatial coordinates with the rare [Haversine][haversine] distance to creating custom metrics for composite embeddings from multiple AI models.
199
+ While most vector search packages concentrate on just two metrics, "Inner Product distance" and "Euclidean distance", USearch allows arbitrary user-defined metrics.
200
+ This flexibility allows you to customize your search for various applications, from computing geospatial coordinates with the rare [Haversine][haversine] distance to creating custom metrics for composite embeddings from multiple AI models, like joint image-text embeddings.
201
+ You can use [Numba][numba], [Cppyy][cppyy], or [PeachPy][peachpy] to define your [custom metric even in Python](https://unum-cloud.github.io/usearch/python#user-defined-metrics-and-jit-in-python):
199
202
 
200
- ![USearch: Vector Search Approaches](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-approaches-white.png?raw=true)
203
+ ```py
204
+ from numba import cfunc, types, carray
205
+ from usearch.index import Index, MetricKind, MetricSignature, CompiledMetric
206
+
207
+ @cfunc(types.float32(types.CPointer(types.float32), types.CPointer(types.float32)))
208
+ def python_inner_product(a, b):
209
+ a_array = carray(a, ndim)
210
+ b_array = carray(b, ndim)
211
+ c = 0.0
212
+ for i in range(ndim):
213
+ c += a_array[i] * b_array[i]
214
+ return 1 - c
215
+
216
+ metric = CompiledMetric(pointer=python_inner_product.address, kind=MetricKind.IP, signature=MetricSignature.ArrayArray)
217
+ index = Index(ndim=ndim, metric=metric, dtype=np.float32)
218
+ ```
201
219
 
202
- Unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
220
+ Similar effect is even easier to achieve in C, C++, and Rust interfaces.
221
+ Moreover, unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
203
222
  They only have to be comparable.
204
- So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching, using [GZip][gzip-similarity] as a distance function.
205
-
206
- > Read more about [JIT and UDF in USearch Python SDK](https://unum-cloud.github.io/usearch/python#user-defined-metrics-and-jit-in-python).
223
+ So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching, using [GZip][gzip-similarity] compression-ratio as a distance function.
207
224
 
208
225
  [haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
209
226
  [obscure]: https://ashvardanian.com/posts/abusing-vector-search
210
227
  [gzip-similarity]: https://twitter.com/LukeGessler/status/1679211291292889100?s=20
211
228
 
229
+ [numba]: https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
230
+ [cppyy]: https://cppyy.readthedocs.io/en/latest/
231
+ [peachpy]: https://github.com/Maratyszcza/PeachPy
232
+
233
+ ## Filtering and Predicate Functions
234
+
235
+ Sometimes you may want to cross-reference search-results against some external database or filter them based on some criteria.
236
+ In most engines, you'd have to manually perform paging requests, successively filtering the results.
237
+ In USearch you can simply pass a predicate function to the search method, which will be applied directly during graph traversal.
238
+ In Rust that would look like this:
239
+
240
+ ```rust
241
+ let is_odd = |key: Key| key % 2 == 1;
242
+ let query = vec![0.2, 0.1, 0.2, 0.1, 0.3];
243
+ let results = index.filtered_search(&query, 10, is_odd).unwrap();
244
+ assert!(
245
+ results.keys.iter().all(|&key| key % 2 == 1),
246
+ "All keys must be odd"
247
+ );
248
+ ```
249
+
212
250
  ## Memory Efficiency, Downcasting, and Quantization
213
251
 
214
252
  Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
@@ -315,6 +353,7 @@ In some cases, like Batch operations, feature parity is meaningless, as the host
315
353
  | Save, load, view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
316
354
  | User-defined metrics | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
317
355
  | Batch operations | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
356
+ | Filter predicates | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
318
357
  | Joins | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
319
358
  | Variable-length vectors | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
320
359
  | 4B+ capacities | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
@@ -432,7 +471,7 @@ doi = {10.5281/zenodo.7949416},
432
471
  author = {Vardanian, Ash},
433
472
  title = {{USearch by Unum Cloud}},
434
473
  url = {https://github.com/unum-cloud/usearch},
435
- version = {2.10.5},
474
+ version = {2.11.1},
436
475
  year = {2023},
437
476
  month = oct,
438
477
  }
package/c/README.md CHANGED
@@ -2,33 +2,246 @@
2
2
 
3
3
  ## Installation
4
4
 
5
- The simplest form to integrate is to copy the contents of `usearch/c/` into your project.
6
- To build the library `libusearch_static_c` and `libusearch_c`, pass enable the `USEARCH_BUILD_LIB_C` CMake option:
5
+ USearch vector-search engine can be integrated into your project using CMake.
6
+ Alternatively, you can download one of the precompiled binaries from the [releases page](https://github.com/unum-cloud/usearch/releases).
7
7
 
8
- ```bash
9
- cmake -DCMAKE_BUILD_TYPE=Release -DUSEARCH_BUILD_LIB_C=1 -DUSEARCH_BUILD_TEST_C=1 -DUSEARCH_BUILD_TEST_CPP=0 -DUSEARCH_BUILD_BENCH_CPP=0 -B ./build_release
10
- cmake --build ./build_release --config Release
11
- ./build_release/test_c
8
+ ## Quickstart
9
+
10
+ ```c
11
+ #include <stdio.h> // For printing.
12
+ #include <assert.h> // For assertions!
13
+ #include <usearch/usearch.h> // For the win 🚀
14
+
15
+ int main() {
16
+
17
+ // Construct:
18
+ size_t dimensions = 128;
19
+ usearch_error_t error = NULL;
20
+ usearch_init_options_t opts = {
21
+ .metric_kind = usearch_metric_cos_k,
22
+ .scalar_kind = usearch_scalar_f16_k,
23
+ .dimensions = dimensions,
24
+ .expansion_add = 0, // for defaults
25
+ .expansion_search = 0 // for defaults
26
+ };
27
+ usearch_index_t index = usearch_init(&opts, &error);
28
+
29
+ size_t vectors_count = 1000;
30
+ usearch_reserve(index, vectors_count, &error);
31
+ if (error) goto cleanup;
32
+
33
+ // Populate:
34
+ float vector[dimensions]; // don't forget to fill the vector with data
35
+ usearch_add(index, 42, &vector[0], usearch_scalar_f32_k, &error);
36
+ if (error) goto cleanup;
37
+
38
+ // Check up:
39
+ assert(usearch_size(index, &error) == vectors_count);
40
+ assert(usearch_capacity(index, &error) == vectors_count);
41
+ assert(usearch_contains(index, 42, &error));
42
+
43
+ // Search:
44
+ usearch_key_t found_keys[10];
45
+ usearch_distance_t found_distances[10];
46
+ size_t found_count = usearch_search(
47
+ index, &vector[0], usearch_scalar_f32_k, 10,
48
+ &found_keys[0], &found_distances[0], &error);
49
+
50
+ usearch_free(index, &error);
51
+
52
+ cleanup:
53
+ if (error) fprintf(stderr, "Error: %s\n", error);
54
+ if (index) usearch_free(index, &error);
55
+ return error ? 1 : 0;
56
+ }
12
57
  ```
13
58
 
14
- ## Quickstart
59
+ ## Serialization
60
+
61
+ To save and load the index from disk, use the following methods:
62
+
63
+ ```c
64
+ usearch_save(index, "index.usearch", &error);
65
+ usearch_load(index, "index.usearch", &error);
66
+ usearch_view(index, "index.usearch", &error);
67
+ ```
68
+
69
+ Before saving them do disk, you can determine exactly how much disk space the index will take:
70
+
71
+ ```c
72
+ size_t bytes = usearch_serialized_length(index, &error);
73
+ ```
74
+
75
+ With a known size, you can serialize the index into an in-memory a buffer:
76
+
77
+ ```c
78
+ void* buffer = malloc(bytes);
79
+ usearch_save_buffer(index, buffer, bytes, &error);
80
+ usearch_load_buffer(index, buffer, bytes, &error);
81
+ usearch_view_buffer(index, buffer, bytes, &error);
82
+ ```
83
+
84
+ One can also retrieve index metadata from the buffer or from disk:
15
85
 
16
86
  ```c
17
- usearch_error_t error = NULL;
18
- usearch_init_options_t opts = create_options(dimensions);
19
- usearch_index_t index = usearch_init(&opts, &error);
20
- usearch_reserve(index, vectors_count, &error);
87
+ usearch_init_options_t opts;
88
+ usearch_metadata("index.usearch", &opts, &error);
89
+ usearch_metadata_buffer(buffer, bytes, &opts, &error);
90
+ ```
91
+
92
+ ## Metrics
93
+
94
+ USearch comes pre-packaged with SimSIMD, bringing over 100 SIMD-accelerated distance kernels for x86 and ARM architectures.
95
+ That includes:
96
+
97
+ - `usearch_metric_cos_k` - Cosine Similarity metric, defined as `Cos = 1 - sum(a[i] * b[i]) / (sqrt(sum(a[i]^2) * sqrt(sum(b[i]^2)))`.
98
+ - `usearch_metric_ip_k` - Inner Product metric, defined as `IP = 1 - sum(a[i] * b[i])`.
99
+ - `usearch_metric_l2sq_k` - Squared Euclidean Distance metric, defined as `L2 = sum((a[i] - b[i])^2)`.
100
+ - `usearch_metric_haversine_k` - Haversine (Great Circle) Distance metric.
101
+ - `usearch_metric_divergence_k` - Jensen Shannon Divergence metric.
102
+ - `usearch_metric_pearson_k` - Pearson Correlation metric.
103
+ - `usearch_metric_hamming_k` - Bit-level Hamming Distance metric, defined as the number of differing bits.
104
+ - `usearch_metric_tanimoto_k` - Bit-level Tanimoto (Jaccard) metric, defined as the number of intersecting bits divided by the number of union bits.
105
+ - `usearch_metric_sorensen_k` - Bit-level Sorensen metric.
106
+
107
+ ### User-Defined Metrics
108
+
109
+ You can also define your own metrics by implementing the `usearch_metric_t` interface:
110
+
111
+ ```c
112
+ simsimd_distance_t callback(void const* a, void const* b, void* state) {
113
+ // Your custom metric implementation here
114
+ }
115
+
116
+ void callback_state = NULL;
117
+ usearch_change_metric(index, callback, callback_state, usearch_metric_unknown_k, &error);
118
+ ```
119
+
120
+ You can always revert back to one of the native metrics by calling:
121
+
122
+ ```c
123
+ usearch_change_metric_kind(index, usearch_metric_cos_k, &error);
124
+ ```
21
125
 
22
- float vector[dimensions] = {...};
23
- usearch_add(index, 42, &vector[0], usearch_scalar_f32_k, &error);
126
+ ## Filtering with Predicates
24
127
 
25
- ASSERT(!error, error);
26
- ASSERT(usearch_size(index, &error) == vectors_count, error);
27
- ASSERT(usearch_capacity(index, &error) == vectors_count, error);
128
+ Sometimes you may want to cross-reference search-results against some external database or filter them based on some criteria.
129
+ In most engines, you'd have to manually perform paging requests, successively filtering the results.
130
+ In USearch you can simply pass a predicate function to the search method, which will be applied directly during graph traversal.
131
+
132
+ ```c
133
+ int is_odd(usearch_key_t key, void* state) {
134
+ return key % 2;
135
+ }
28
136
 
29
137
  usearch_key_t found_keys[10];
30
- float found_distances[10];
31
- size_t found_count = usearch_search(index, &vector[0], usearch_scalar_f32_k, 10, &found_keys[0], &found_distances[0], &error);
138
+ usearch_distance_t found_distances[10];
139
+ usearch_filtered_search(
140
+ index, &query[0], usearch_scalar_f32_k, 10,
141
+ &is_odd, NULL, // no state needed for this callback
142
+ &found_keys[0], &found_distances[0], &error);
143
+ ```
144
+
145
+ ## Extracting, Updating, and Removing Values
146
+
147
+ It is generally not recommended to use HNSW indexes in case of frequent removals or major distribution shifts.
148
+ For small updates, you can use the following methods:
149
+
150
+ ```c
151
+ float recovered_vector[dimensions];
152
+ size_t count_retrieved = usearch_get(index, 42, 1,
153
+ &recovered_vector[0], usearch_scalar_f32_k, &error);
154
+ assert(count_retrieved <= 1);
155
+
156
+ size_t count_renamed = usearch_rename(index, 42, 43, &error);
157
+ size_t count_removed = usearch_remove(index, 43, &error);
158
+ assert(count_renamed == count_removed);
159
+ ```
160
+
161
+ USearch can also be used for multi-indicies, where each key may map to multiple vectors.
162
+ That's a common case when implementing semantic search for long documents, where the entire documents can't fit into one input sequence in a transformer-model, and you have to split them into chunks.
163
+
164
+ ```c
165
+ float many_vectors[10][dimensions];
166
+ size_t count_retrieved = usearch_get(index, 42, 10,
167
+ &many_vectors[0][0], usearch_scalar_f32_k, &error);
168
+ assert(count_retrieved <= 10);
169
+ ```
170
+
171
+ ## Exact Search
172
+
173
+ USearch exposes its internal SIMD-accelerated distance functions for exact search.
174
+ To invoke them for a pair of vectors, use the following methods:
175
+
176
+ ```c
177
+ float vector_a[dimensions], vector_b[dimensions];
178
+ usearch_distance_t distance = usearch_distance(
179
+ &vector_a[0], &vector_b[0], usearch_scalar_f32_k, dimensions, usearch_metric_cos_k, &error);
180
+ ```
181
+
182
+ Alternatively, you can benefir from faster thread-pools and priority queues for parallel exact batch-search.
32
183
 
33
- usearch_free(index, &error);
184
+ ```c
185
+ size_t threads = 0;
186
+ size_t top_k = 10;
187
+ size_t dataset_count = 1000, queries_count = 10;
188
+ simsimd_f16_t dataset[dataset_count][dimensions];
189
+ simsimd_f16_t queries[queries_count][dimensions];
190
+
191
+ usearch_key_t resulting_keys[queries_count][top_k];
192
+ usearch_distance_t resulting_distances[queries_count][top_k];
193
+
194
+ usearch_exact_search(
195
+ &dataset[0][0], dataset_count, dimensions * sizeof(simsimd_f16_t),
196
+ &queries[0][0], queries_count, dimensions * sizeof(simsimd_f16_t),
197
+ usearch_scalar_f16_k, top_k, threads,
198
+ &resulting_keys[0][0], sizeof(usearch_key_t) * top_k,
199
+ &resulting_distances[0][0], sizeof(usearch_distance_t) * top_k,
200
+ &error);
201
+ ```
202
+
203
+ ## Concurrency and Parallelism
204
+
205
+ USearch manages a pool of "thread local" data-structures to avoid contention and improve performance.
206
+ By default, as many of these structures are created as there are logical cores on the machine.
207
+ Then, one can construct the index in parallel using many cores using third-party thread-pools or tools like OpenACC and OpenMP.
208
+
209
+ ```c
210
+ #pragma omp parallel for
211
+ for (size_t i = 0; i < 1000; i++) {
212
+ usearch_add(index, i, &vector[0], usearch_scalar_f32_k, &error);
213
+ }
214
+
215
+ #pragma omp parallel for
216
+ for (size_t i = 0; i < 1000; i++) {
217
+ usearch_key_t found_keys[10];
218
+ usearch_distance_t found_distances[10];
219
+ size_t found_matches = usearch_search(index,
220
+ &vector[0], usearch_scalar_f32_k, 10,
221
+ &found_keys[0], &found_distances[0], &error);
222
+ }
34
223
  ```
224
+
225
+ ## Performance Tuning
226
+
227
+ To optimize the performance of the index, you can adjust the expansion values used during index creation and search operations.
228
+ Higher expansion values will lead to better search accuracy at the cost of slightly increased memory usage, but potentially much higher search times.
229
+ Following methods are available to adjust the expansion values:
230
+
231
+ ```c
232
+ printf("Connectivity: %zu\n", usearch_connectivity(index, &error)); // can't change
233
+ printf("Add expansion: %zu\n", usearch_expansion_add(index, &error)); // can change
234
+ printf("Search expansion: %zu\n", usearch_expansion_search(index, &error)); // can change
235
+ usearch_change_expansion_add(index, 32, &error);
236
+ usearch_change_expansion_search(index, 32, &error);
237
+ ```
238
+
239
+ Optimizing hardware utilization, you may want to check the SIMD hardware acceleration capabilities of the index and memory consumption.
240
+ The first will print the codename of the most advanced SIMD instruction set supported by the CPU and used by the index.
241
+ The second will print the memory usage of the index in bytes.
242
+
243
+ ```c
244
+ printf("Hardware acceleration: %s\n", usearch_hardware_acceleration(index, &error));
245
+ printf("Memory usage: %zu\n", usearch_memory_usage(index, &error));
246
+ ```
247
+