usearch 0.19.0 → 0.19.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/Cargo.toml +1 -1
- package/README.md +11 -34
- package/c/lib.cpp +71 -51
- package/c/test.c +182 -0
- package/c/usearch.h +11 -5
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +4 -4
- package/docs/compilation.md +1 -0
- package/include/usearch/index.hpp +22 -5
- package/include/usearch/index_punned_dense.hpp +5 -3
- package/include/usearch/index_punned_helpers.hpp +2 -0
- package/objc/USearchObjective.mm +4 -0
- package/objc/include/USearchObjective.h +5 -0
- package/package.json +2 -2
- package/python/lib.cpp +125 -166
- package/python/scripts/test.py +51 -11
- package/python/usearch/index.py +152 -65
- package/python/usearch/io.py +2 -2
- package/setup.py +56 -59
- package/swift/Test.swift +13 -9
package/objc/USearchObjective.mm
CHANGED
|
@@ -206,6 +206,10 @@ scalar_kind_t to_native_scalar(USearchScalar m) {
|
|
|
206
206
|
_native->clear();
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
- (void)reserve:(UInt32)count {
|
|
210
|
+
_native->reserve(static_cast<std::size_t>(count));
|
|
211
|
+
}
|
|
212
|
+
|
|
209
213
|
- (void)save:(NSString *)path {
|
|
210
214
|
char const *path_c = [path UTF8String];
|
|
211
215
|
|
|
@@ -51,6 +51,11 @@ API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0), watchos(6.0))
|
|
|
51
51
|
*/
|
|
52
52
|
+ (instancetype)make:(USearchMetric)metric dimensions:(UInt32)dimensions connectivity:(UInt32)connectivity quantization:(USearchScalar)quantization NS_SWIFT_NAME(make(metric:dimensions:connectivity:quantization:));
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @brief Pre-allocates space in the index for the given number of vectors.
|
|
56
|
+
*/
|
|
57
|
+
- (void)reserve:(UInt32)count NS_SWIFT_NAME(reserve(_:));
|
|
58
|
+
|
|
54
59
|
/**
|
|
55
60
|
* @brief Adds a labeled vector to the index.
|
|
56
61
|
* @param vector Single-precision vector.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usearch",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
5
5
|
"author": "Ashot Vardanian",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"@semantic-release/exec": "^6.0.3",
|
|
25
25
|
"@semantic-release/git": "^10.0.1",
|
|
26
26
|
"conventional-changelog-eslint": "^3.0.9",
|
|
27
|
-
"semantic-release": "^21.0.
|
|
27
|
+
"semantic-release": "^21.0.6"
|
|
28
28
|
}
|
|
29
29
|
}
|
package/python/lib.cpp
CHANGED
|
@@ -76,10 +76,12 @@ struct sparse_index_py_t : public sparse_index_t {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
template <typename scalar_at>
|
|
79
|
-
metric_t
|
|
79
|
+
metric_t typed_udf( //
|
|
80
|
+
metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
|
|
80
81
|
//
|
|
81
82
|
metric_t result;
|
|
82
83
|
result.kind_ = kind;
|
|
84
|
+
result.scalar_kind_ = accuracy;
|
|
83
85
|
switch (signature) {
|
|
84
86
|
case metric_signature_t::array_array_k:
|
|
85
87
|
result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
|
|
@@ -110,10 +112,11 @@ metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t me
|
|
|
110
112
|
|
|
111
113
|
metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
|
|
112
114
|
switch (accuracy) {
|
|
113
|
-
case scalar_kind_t::
|
|
114
|
-
case scalar_kind_t::
|
|
115
|
-
case scalar_kind_t::
|
|
116
|
-
case scalar_kind_t::
|
|
115
|
+
case scalar_kind_t::b1x8_k: return typed_udf<b1x8_t>(kind, signature, metric_uintptr, accuracy);
|
|
116
|
+
case scalar_kind_t::f8_k: return typed_udf<f8_bits_t>(kind, signature, metric_uintptr, accuracy);
|
|
117
|
+
case scalar_kind_t::f16_k: return typed_udf<f16_t>(kind, signature, metric_uintptr, accuracy);
|
|
118
|
+
case scalar_kind_t::f32_k: return typed_udf<f32_t>(kind, signature, metric_uintptr, accuracy);
|
|
119
|
+
case scalar_kind_t::f64_k: return typed_udf<f64_t>(kind, signature, metric_uintptr, accuracy);
|
|
117
120
|
default: return {};
|
|
118
121
|
}
|
|
119
122
|
}
|
|
@@ -154,6 +157,22 @@ static std::unique_ptr<sparse_index_py_t> make_sparse_index( //
|
|
|
154
157
|
return std::unique_ptr<sparse_index_py_t>(new sparse_index_py_t(sparse_index_t(config)));
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
scalar_kind_t numpy_string_to_kind(std::string const& name) {
|
|
161
|
+
// https://docs.python.org/3/library/struct.html#format-characters
|
|
162
|
+
if (name == "B" || name == "<B" || name == "u1" || name == "|u1")
|
|
163
|
+
return scalar_kind_t::b1x8_k;
|
|
164
|
+
else if (name == "b" || name == "<b" || name == "i1" || name == "|i1")
|
|
165
|
+
return scalar_kind_t::f8_k;
|
|
166
|
+
else if (name == "e" || name == "<e" || name == "f2" || name == "<f2")
|
|
167
|
+
return scalar_kind_t::f16_k;
|
|
168
|
+
else if (name == "f" || name == "<f" || name == "f4" || name == "<f4")
|
|
169
|
+
return scalar_kind_t::f32_k;
|
|
170
|
+
else if (name == "d" || name == "<d" || name == "f8" || name == "<f8")
|
|
171
|
+
return scalar_kind_t::f64_k;
|
|
172
|
+
else
|
|
173
|
+
return scalar_kind_t::unknown_k;
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
|
|
158
177
|
|
|
159
178
|
py::buffer_info vector_info = vector.request();
|
|
@@ -171,19 +190,37 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
|
|
|
171
190
|
add_config_t config;
|
|
172
191
|
config.store_vector = copy;
|
|
173
192
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
|
|
182
|
-
index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
|
|
183
|
-
else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
|
|
184
|
-
index.add(label, reinterpret_cast<double const*>(vector_data), config).error.raise();
|
|
185
|
-
else
|
|
193
|
+
switch (numpy_string_to_kind(vector_info.format)) {
|
|
194
|
+
case scalar_kind_t::b1x8_k: index.add(label, (b1x8_t const*)(vector_data), config).error.raise(); break;
|
|
195
|
+
case scalar_kind_t::f8_k: index.add(label, (f8_bits_t const*)(vector_data), config).error.raise(); break;
|
|
196
|
+
case scalar_kind_t::f16_k: index.add(label, (f16_t const*)(vector_data), config).error.raise(); break;
|
|
197
|
+
case scalar_kind_t::f32_k: index.add(label, (f32_t const*)(vector_data), config).error.raise(); break;
|
|
198
|
+
case scalar_kind_t::f64_k: index.add(label, (f64_t const*)(vector_data), config).error.raise(); break;
|
|
199
|
+
case scalar_kind_t::unknown_k:
|
|
186
200
|
throw std::invalid_argument("Incompatible scalars in the vector: " + vector_info.format);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
template <typename scalar_at>
|
|
205
|
+
static void add_typed_to_index( //
|
|
206
|
+
punned_index_py_t& index, //
|
|
207
|
+
py::buffer_info const& labels_info, py::buffer_info const& vectors_info, //
|
|
208
|
+
bool copy, std::size_t threads) {
|
|
209
|
+
|
|
210
|
+
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
211
|
+
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
212
|
+
char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
|
|
213
|
+
|
|
214
|
+
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
215
|
+
add_config_t config;
|
|
216
|
+
config.store_vector = copy;
|
|
217
|
+
config.thread = thread_idx;
|
|
218
|
+
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
219
|
+
scalar_at const* vector = reinterpret_cast<scalar_at const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
220
|
+
index.add(label, vector, config).error.raise();
|
|
221
|
+
if (PyErr_CheckSignals() != 0)
|
|
222
|
+
throw py::error_already_set();
|
|
223
|
+
});
|
|
187
224
|
}
|
|
188
225
|
|
|
189
226
|
static void add_many_to_index( //
|
|
@@ -217,58 +254,15 @@ static void add_many_to_index( //
|
|
|
217
254
|
if (index.size() + vectors_count >= index.capacity())
|
|
218
255
|
index.reserve(ceil2(index.size() + vectors_count));
|
|
219
256
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
config.store_vector = copy;
|
|
228
|
-
config.thread = thread_idx;
|
|
229
|
-
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
230
|
-
b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
231
|
-
index.add(label, vector, config).error.raise();
|
|
232
|
-
});
|
|
233
|
-
else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
|
|
234
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
235
|
-
add_config_t config;
|
|
236
|
-
config.store_vector = copy;
|
|
237
|
-
config.thread = thread_idx;
|
|
238
|
-
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
239
|
-
f8_bits_t const* vector =
|
|
240
|
-
reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
241
|
-
index.add(label, vector, config).error.raise();
|
|
242
|
-
});
|
|
243
|
-
else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
|
|
244
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
245
|
-
add_config_t config;
|
|
246
|
-
config.store_vector = copy;
|
|
247
|
-
config.thread = thread_idx;
|
|
248
|
-
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
249
|
-
f16_t const* vector = reinterpret_cast<f16_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
250
|
-
index.add(label, vector, config).error.raise();
|
|
251
|
-
});
|
|
252
|
-
else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
|
|
253
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
254
|
-
add_config_t config;
|
|
255
|
-
config.store_vector = copy;
|
|
256
|
-
config.thread = thread_idx;
|
|
257
|
-
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
258
|
-
float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
259
|
-
index.add(label, vector, config).error.raise();
|
|
260
|
-
});
|
|
261
|
-
else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
|
|
262
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
263
|
-
add_config_t config;
|
|
264
|
-
config.store_vector = copy;
|
|
265
|
-
config.thread = thread_idx;
|
|
266
|
-
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
267
|
-
double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
268
|
-
index.add(label, vector, config).error.raise();
|
|
269
|
-
});
|
|
270
|
-
else
|
|
257
|
+
switch (numpy_string_to_kind(vectors_info.format)) {
|
|
258
|
+
case scalar_kind_t::b1x8_k: add_typed_to_index<b1x8_t>(index, labels_info, vectors_info, copy, threads); break;
|
|
259
|
+
case scalar_kind_t::f8_k: add_typed_to_index<f8_bits_t>(index, labels_info, vectors_info, copy, threads); break;
|
|
260
|
+
case scalar_kind_t::f16_k: add_typed_to_index<f16_t>(index, labels_info, vectors_info, copy, threads); break;
|
|
261
|
+
case scalar_kind_t::f32_k: add_typed_to_index<f32_t>(index, labels_info, vectors_info, copy, threads); break;
|
|
262
|
+
case scalar_kind_t::f64_k: add_typed_to_index<f64_t>(index, labels_info, vectors_info, copy, threads); break;
|
|
263
|
+
case scalar_kind_t::unknown_k:
|
|
271
264
|
throw std::invalid_argument("Incompatible scalars in the vectors matrix: " + vectors_info.format);
|
|
265
|
+
}
|
|
272
266
|
}
|
|
273
267
|
|
|
274
268
|
static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
|
|
@@ -288,29 +282,20 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
|
|
|
288
282
|
search_config_t config;
|
|
289
283
|
config.exact = exact;
|
|
290
284
|
|
|
291
|
-
|
|
292
|
-
if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1") {
|
|
293
|
-
punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
|
|
294
|
-
result.error.raise();
|
|
295
|
-
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
296
|
-
} else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1") {
|
|
297
|
-
punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
|
|
285
|
+
auto raise_and_dump = [&](punned_search_result_t result) {
|
|
298
286
|
result.error.raise();
|
|
299
287
|
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
punned_search_result_t result = index.search(reinterpret_cast<double const*>(vector_data), wanted, config);
|
|
310
|
-
result.error.raise();
|
|
311
|
-
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
312
|
-
} else
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
switch (numpy_string_to_kind(vector_info.format)) {
|
|
291
|
+
case scalar_kind_t::b1x8_k: raise_and_dump(index.search((b1x8_t const*)(vector_data), wanted, config)); break;
|
|
292
|
+
case scalar_kind_t::f8_k: raise_and_dump(index.search((f8_bits_t const*)(vector_data), wanted, config)); break;
|
|
293
|
+
case scalar_kind_t::f16_k: raise_and_dump(index.search((f16_t const*)(vector_data), wanted, config)); break;
|
|
294
|
+
case scalar_kind_t::f32_k: raise_and_dump(index.search((f32_t const*)(vector_data), wanted, config)); break;
|
|
295
|
+
case scalar_kind_t::f64_k: raise_and_dump(index.search((f64_t const*)(vector_data), wanted, config)); break;
|
|
296
|
+
case scalar_kind_t::unknown_k:
|
|
313
297
|
throw std::invalid_argument("Incompatible scalars in the query vector: " + vector_info.format);
|
|
298
|
+
}
|
|
314
299
|
|
|
315
300
|
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
316
301
|
distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
@@ -322,6 +307,33 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
|
|
|
322
307
|
return results;
|
|
323
308
|
}
|
|
324
309
|
|
|
310
|
+
template <typename scalar_at>
|
|
311
|
+
static void search_typed( //
|
|
312
|
+
punned_index_py_t& index, py::buffer_info& vectors_info, //
|
|
313
|
+
std::size_t wanted, bool exact, std::size_t threads, //
|
|
314
|
+
py::array_t<label_t>& labels_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py) {
|
|
315
|
+
|
|
316
|
+
auto labels_py2d = labels_py.template mutable_unchecked<2>();
|
|
317
|
+
auto distances_py2d = distances_py.template mutable_unchecked<2>();
|
|
318
|
+
auto counts_py1d = counts_py.template mutable_unchecked<1>();
|
|
319
|
+
|
|
320
|
+
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
321
|
+
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
322
|
+
|
|
323
|
+
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
324
|
+
search_config_t config;
|
|
325
|
+
config.thread = thread_idx;
|
|
326
|
+
config.exact = exact;
|
|
327
|
+
scalar_at const* vector = (scalar_at const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
328
|
+
punned_search_result_t result = index.search(vector, wanted, config);
|
|
329
|
+
result.error.raise();
|
|
330
|
+
counts_py1d(task_idx) =
|
|
331
|
+
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
332
|
+
if (PyErr_CheckSignals() != 0)
|
|
333
|
+
throw py::error_already_set();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
325
337
|
/**
|
|
326
338
|
* @param vectors Matrix of vectors to search for.
|
|
327
339
|
* @param wanted Number of matches per request.
|
|
@@ -337,7 +349,7 @@ static py::tuple search_many_in_index( //
|
|
|
337
349
|
if (wanted == 0)
|
|
338
350
|
return py::tuple(3);
|
|
339
351
|
|
|
340
|
-
if (index.limits().
|
|
352
|
+
if (index.limits().threads_search < threads)
|
|
341
353
|
throw std::invalid_argument("Can't use that many threads!");
|
|
342
354
|
|
|
343
355
|
py::buffer_info vectors_info = vectors.request();
|
|
@@ -352,76 +364,24 @@ static py::tuple search_many_in_index( //
|
|
|
352
364
|
if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
|
|
353
365
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
354
366
|
|
|
355
|
-
py::array_t<label_t>
|
|
356
|
-
py::array_t<distance_t>
|
|
357
|
-
py::array_t<Py_ssize_t>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
config.thread = thread_idx;
|
|
367
|
-
config.exact = exact;
|
|
368
|
-
b1x8_t const* vector = (b1x8_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
369
|
-
punned_search_result_t result = index.search(vector, wanted, config);
|
|
370
|
-
result.error.raise();
|
|
371
|
-
counts_py1d(task_idx) =
|
|
372
|
-
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
373
|
-
});
|
|
374
|
-
else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
|
|
375
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
376
|
-
search_config_t config;
|
|
377
|
-
config.thread = thread_idx;
|
|
378
|
-
config.exact = exact;
|
|
379
|
-
f8_bits_t const* vector = (f8_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
380
|
-
punned_search_result_t result = index.search(vector, wanted, config);
|
|
381
|
-
result.error.raise();
|
|
382
|
-
counts_py1d(task_idx) =
|
|
383
|
-
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
384
|
-
});
|
|
385
|
-
else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
|
|
386
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
387
|
-
search_config_t config;
|
|
388
|
-
config.thread = thread_idx;
|
|
389
|
-
config.exact = exact;
|
|
390
|
-
f16_t const* vector = (f16_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
391
|
-
punned_search_result_t result = index.search(vector, wanted, config);
|
|
392
|
-
result.error.raise();
|
|
393
|
-
counts_py1d(task_idx) =
|
|
394
|
-
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
395
|
-
});
|
|
396
|
-
else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
|
|
397
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
398
|
-
search_config_t config;
|
|
399
|
-
config.thread = thread_idx;
|
|
400
|
-
config.exact = exact;
|
|
401
|
-
float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
402
|
-
punned_search_result_t result = index.search(vector, wanted, config);
|
|
403
|
-
result.error.raise();
|
|
404
|
-
counts_py1d(task_idx) =
|
|
405
|
-
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
406
|
-
});
|
|
407
|
-
else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
|
|
408
|
-
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
409
|
-
search_config_t config;
|
|
410
|
-
config.thread = thread_idx;
|
|
411
|
-
config.exact = exact;
|
|
412
|
-
double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
413
|
-
punned_search_result_t result = index.search(vector, wanted, config);
|
|
414
|
-
result.error.raise();
|
|
415
|
-
counts_py1d(task_idx) =
|
|
416
|
-
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
417
|
-
});
|
|
418
|
-
else
|
|
367
|
+
py::array_t<label_t> ls({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
368
|
+
py::array_t<distance_t> ds({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
369
|
+
py::array_t<Py_ssize_t> cs(vectors_count);
|
|
370
|
+
|
|
371
|
+
switch (numpy_string_to_kind(vectors_info.format)) {
|
|
372
|
+
case scalar_kind_t::b1x8_k: search_typed<b1x8_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
|
|
373
|
+
case scalar_kind_t::f8_k: search_typed<f8_bits_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
|
|
374
|
+
case scalar_kind_t::f16_k: search_typed<f16_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
|
|
375
|
+
case scalar_kind_t::f32_k: search_typed<f32_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
|
|
376
|
+
case scalar_kind_t::f64_k: search_typed<f64_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
|
|
377
|
+
case scalar_kind_t::unknown_k:
|
|
419
378
|
throw std::invalid_argument("Incompatible scalars in the query matrix: " + vectors_info.format);
|
|
379
|
+
}
|
|
420
380
|
|
|
421
381
|
py::tuple results(3);
|
|
422
|
-
results[0] =
|
|
423
|
-
results[1] =
|
|
424
|
-
results[2] =
|
|
382
|
+
results[0] = ls;
|
|
383
|
+
results[1] = ds;
|
|
384
|
+
results[2] = cs;
|
|
425
385
|
return results;
|
|
426
386
|
}
|
|
427
387
|
|
|
@@ -447,7 +407,7 @@ template <typename index_at> py::object get_member(index_at const& index, label_
|
|
|
447
407
|
else if (scalar_kind == scalar_kind_t::f64_k)
|
|
448
408
|
return get_typed_member<f64_t>(index, label);
|
|
449
409
|
else if (scalar_kind == scalar_kind_t::f16_k)
|
|
450
|
-
return get_typed_member<f16_t>(index, label);
|
|
410
|
+
return get_typed_member<f16_t, std::uint16_t>(index, label);
|
|
451
411
|
else if (scalar_kind == scalar_kind_t::f8_k)
|
|
452
412
|
return get_typed_member<f8_bits_t, std::int8_t>(index, label);
|
|
453
413
|
else if (scalar_kind == scalar_kind_t::b1x8_k)
|
|
@@ -678,10 +638,10 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
678
638
|
i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
|
|
679
639
|
i.def_property_readonly("capacity", &punned_index_py_t::capacity);
|
|
680
640
|
i.def_property_readonly( //
|
|
681
|
-
"dtype",
|
|
682
|
-
[](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.metric().scalar_kind_); });
|
|
641
|
+
"dtype", [](punned_index_py_t const& index) -> scalar_kind_t { return index.metric().scalar_kind_; });
|
|
683
642
|
i.def_property_readonly( //
|
|
684
|
-
"memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); }
|
|
643
|
+
"memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); },
|
|
644
|
+
py::call_guard<py::gil_scoped_release>());
|
|
685
645
|
|
|
686
646
|
i.def_property("expansion_add", &punned_index_py_t::expansion_add, &punned_index_py_t::change_expansion_add);
|
|
687
647
|
i.def_property("expansion_search", &punned_index_py_t::expansion_search,
|
|
@@ -690,13 +650,12 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
690
650
|
i.def_property_readonly("labels", &get_all_labels<punned_index_py_t>);
|
|
691
651
|
i.def("get_labels", &get_labels<punned_index_py_t>, py::arg("limit") = std::numeric_limits<std::size_t>::max());
|
|
692
652
|
i.def("__contains__", &punned_index_py_t::contains);
|
|
693
|
-
i.def(
|
|
694
|
-
"__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
|
|
653
|
+
i.def("__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
|
|
695
654
|
|
|
696
|
-
i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
|
|
697
|
-
i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
|
|
698
|
-
i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
|
|
699
|
-
i.def("clear", &punned_index_py_t::clear);
|
|
655
|
+
i.def("save", &save_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
656
|
+
i.def("load", &load_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
657
|
+
i.def("view", &view_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
658
|
+
i.def("clear", &punned_index_py_t::clear, py::call_guard<py::gil_scoped_release>());
|
|
700
659
|
|
|
701
660
|
auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
|
|
702
661
|
|
package/python/scripts/test.py
CHANGED
|
@@ -24,7 +24,7 @@ from usearch.index import (
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
dimensions = [3, 97, 256]
|
|
27
|
-
batch_sizes = [1,
|
|
27
|
+
batch_sizes = [1, 77]
|
|
28
28
|
index_types = [
|
|
29
29
|
ScalarKind.F32,
|
|
30
30
|
ScalarKind.F64,
|
|
@@ -75,7 +75,7 @@ def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
|
75
75
|
def test_index(
|
|
76
76
|
ndim: int,
|
|
77
77
|
metric: MetricKind,
|
|
78
|
-
index_type:
|
|
78
|
+
index_type: ScalarKind,
|
|
79
79
|
numpy_type: str,
|
|
80
80
|
connectivity: int,
|
|
81
81
|
jit: bool,
|
|
@@ -135,7 +135,11 @@ def test_index(
|
|
|
135
135
|
@pytest.mark.parametrize("index_type", index_types)
|
|
136
136
|
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
137
137
|
def test_index_batch(
|
|
138
|
-
ndim: int,
|
|
138
|
+
ndim: int,
|
|
139
|
+
metric: MetricKind,
|
|
140
|
+
batch_size: int,
|
|
141
|
+
index_type: ScalarKind,
|
|
142
|
+
numpy_type: str,
|
|
139
143
|
):
|
|
140
144
|
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
141
145
|
|
|
@@ -151,8 +155,50 @@ def test_index_batch(
|
|
|
151
155
|
assert matches.counts.shape[0] == batch_size
|
|
152
156
|
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
index.save("tmp.usearch")
|
|
159
|
+
index.clear()
|
|
160
|
+
assert len(index) == 0
|
|
161
|
+
|
|
162
|
+
index.load("tmp.usearch")
|
|
163
|
+
assert len(index) == batch_size
|
|
164
|
+
assert len(index[0]) == ndim
|
|
165
|
+
|
|
166
|
+
index = Index.restore("tmp.usearch")
|
|
167
|
+
assert len(index) == batch_size
|
|
168
|
+
assert len(index[0]) == ndim
|
|
169
|
+
|
|
170
|
+
# Cleanup
|
|
171
|
+
os.remove("tmp.usearch")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@pytest.mark.parametrize("metric", [MetricKind.L2sq])
|
|
175
|
+
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
176
|
+
@pytest.mark.parametrize("index_type", index_types)
|
|
177
|
+
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
178
|
+
def test_exact_recall(
|
|
179
|
+
metric: MetricKind, batch_size: int, index_type: ScalarKind, numpy_type: str
|
|
180
|
+
):
|
|
181
|
+
ndim: int = batch_size
|
|
182
|
+
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
183
|
+
|
|
184
|
+
vectors = np.zeros((batch_size, ndim), dtype=numpy_type)
|
|
185
|
+
for i in range(batch_size):
|
|
186
|
+
vectors[i, i] = 1
|
|
187
|
+
labels = np.arange(batch_size)
|
|
188
|
+
index.add(labels, vectors)
|
|
189
|
+
assert len(index) == batch_size
|
|
190
|
+
|
|
191
|
+
# Search one at a time
|
|
192
|
+
for i in range(batch_size):
|
|
193
|
+
matches: Matches = index.search(vectors[i], 10, exact=True)
|
|
194
|
+
found_labels = matches.labels
|
|
195
|
+
assert found_labels[0] == i
|
|
196
|
+
|
|
197
|
+
# Search the whole batch
|
|
198
|
+
matches: Matches = index.search(vectors, 10, exact=True)
|
|
199
|
+
found_labels = matches.labels
|
|
200
|
+
for i in range(batch_size):
|
|
201
|
+
assert found_labels[i, 0] == i
|
|
156
202
|
|
|
157
203
|
|
|
158
204
|
@pytest.mark.parametrize("ndim", dimensions)
|
|
@@ -234,8 +280,6 @@ def test_index_numba(ndim: int, batch_size: int):
|
|
|
234
280
|
assert matches.shape[0] == distances.shape[0]
|
|
235
281
|
assert count.shape[0] == batch_size
|
|
236
282
|
|
|
237
|
-
assert recall_members(index, exact=True) == 1
|
|
238
|
-
|
|
239
283
|
|
|
240
284
|
@pytest.mark.parametrize("ndim", dimensions[-1:])
|
|
241
285
|
@pytest.mark.parametrize("batch_size", batch_sizes[-1:])
|
|
@@ -300,8 +344,6 @@ def test_index_cppyy(ndim: int, batch_size: int):
|
|
|
300
344
|
assert matches.shape[0] == distances.shape[0]
|
|
301
345
|
assert count.shape[0] == batch_size
|
|
302
346
|
|
|
303
|
-
assert recall_members(index, exact=True) == 1
|
|
304
|
-
|
|
305
347
|
|
|
306
348
|
@pytest.mark.parametrize("ndim", [8])
|
|
307
349
|
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
@@ -388,8 +430,6 @@ def test_index_peachpy(ndim: int, batch_size: int):
|
|
|
388
430
|
assert matches.shape[0] == distances.shape[0]
|
|
389
431
|
assert count.shape[0] == batch_size
|
|
390
432
|
|
|
391
|
-
assert recall_members(index, exact=True) == 1
|
|
392
|
-
|
|
393
433
|
|
|
394
434
|
@pytest.mark.parametrize("bits", dimensions)
|
|
395
435
|
@pytest.mark.parametrize("metric", hash_metrics)
|