usearch 0.10.0 → 0.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.
Files changed (43) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/.gitmodules +3 -0
  3. package/.vscode/settings.json +2 -0
  4. package/Cargo.toml +1 -1
  5. package/Package.swift +1 -0
  6. package/README.md +1 -1
  7. package/VERSION +1 -1
  8. package/binding.gyp +4 -3
  9. package/build.gradle +1 -1
  10. package/build.rs +1 -0
  11. package/conanfile.py +1 -1
  12. package/docs/compilation.md +6 -3
  13. package/include/usearch/usearch.hpp +133 -43
  14. package/package.json +1 -1
  15. package/python/lib.cpp +36 -24
  16. package/python/scripts/bench.py +2 -2
  17. package/python/scripts/test.py +1 -1
  18. package/python/usearch/client.py +8 -9
  19. package/python/usearch/index.py +306 -0
  20. package/python/usearch/server.py +6 -16
  21. package/robin-map/.codecov.yml +5 -0
  22. package/robin-map/.github/workflows/ci.yml +136 -0
  23. package/robin-map/CMakeLists.txt +86 -0
  24. package/robin-map/LICENSE +21 -0
  25. package/robin-map/README.md +483 -0
  26. package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +9 -0
  27. package/robin-map/doxygen.conf +2483 -0
  28. package/robin-map/include/tsl/robin_growth_policy.h +406 -0
  29. package/robin-map/include/tsl/robin_hash.h +1639 -0
  30. package/robin-map/include/tsl/robin_map.h +807 -0
  31. package/robin-map/include/tsl/robin_set.h +660 -0
  32. package/robin-map/tests/CMakeLists.txt +26 -0
  33. package/robin-map/tests/custom_allocator_tests.cpp +138 -0
  34. package/robin-map/tests/main.cpp +26 -0
  35. package/robin-map/tests/policy_tests.cpp +97 -0
  36. package/robin-map/tests/robin_map_tests.cpp +1451 -0
  37. package/robin-map/tests/robin_set_tests.cpp +164 -0
  38. package/robin-map/tests/utils.h +443 -0
  39. package/robin-map/tsl-robin-map.natvis +78 -0
  40. package/setup.py +4 -2
  41. package/src/CMakeLists.txt +1 -0
  42. package/src/punned.hpp +50 -6
  43. package/src/test.cpp +29 -8
package/python/lib.cpp CHANGED
@@ -169,7 +169,7 @@ static std::unique_ptr<hash_index_py_t> make_hash_index( //
169
169
  return std::unique_ptr<hash_index_py_t>(new hash_index_py_t(config, bits));
170
170
  }
171
171
 
172
- static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy) {
172
+ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
173
173
 
174
174
  py::buffer_info vector_info = vector.request();
175
175
  if (vector_info.ndim != 1)
@@ -191,9 +191,9 @@ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vecto
191
191
  index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config);
192
192
  else if (vector_info.format == "e")
193
193
  index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config);
194
- else if (vector_info.format == "f")
194
+ else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
195
195
  index.add(label, reinterpret_cast<float const*>(vector_data), config);
196
- else if (vector_info.format == "d")
196
+ else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
197
197
  index.add(label, reinterpret_cast<double const*>(vector_data), config);
198
198
  else
199
199
  throw std::invalid_argument("Incompatible scalars in the vector!");
@@ -201,7 +201,10 @@ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vecto
201
201
 
202
202
  static void add_many_to_index( //
203
203
  punned_py_t& index, py::buffer labels, py::buffer vectors, //
204
- bool copy, std::size_t threads = 0) {
204
+ bool copy, std::size_t threads) {
205
+
206
+ if (index.config().max_threads_add < threads)
207
+ throw std::invalid_argument("Can't use that many threads!");
205
208
 
206
209
  py::buffer_info labels_info = labels.request();
207
210
  py::buffer_info vectors_info = vectors.request();
@@ -232,7 +235,7 @@ static void add_many_to_index( //
232
235
 
233
236
  // https://docs.python.org/3/library/struct.html#format-characters
234
237
  if (vectors_info.format == "c" || vectors_info.format == "b")
235
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
238
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
236
239
  add_config_t config;
237
240
  config.store_vector = copy;
238
241
  config.thread = thread_idx;
@@ -242,7 +245,7 @@ static void add_many_to_index( //
242
245
  index.add(label, vector, config);
243
246
  });
244
247
  else if (vectors_info.format == "e")
245
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
248
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
246
249
  add_config_t config;
247
250
  config.store_vector = copy;
248
251
  config.thread = thread_idx;
@@ -251,8 +254,8 @@ static void add_many_to_index( //
251
254
  reinterpret_cast<f16_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
252
255
  index.add(label, vector, config);
253
256
  });
254
- else if (vectors_info.format == "f")
255
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
257
+ else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
258
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
256
259
  add_config_t config;
257
260
  config.store_vector = copy;
258
261
  config.thread = thread_idx;
@@ -260,8 +263,8 @@ static void add_many_to_index( //
260
263
  float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
261
264
  index.add(label, vector, config);
262
265
  });
263
- else if (vectors_info.format == "d")
264
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
266
+ else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
267
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
265
268
  add_config_t config;
266
269
  config.store_vector = copy;
267
270
  config.thread = thread_idx;
@@ -299,11 +302,11 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
299
302
  count = index //
300
303
  .search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config)
301
304
  .dump_to(&labels_py1d(0), &distances_py1d(0));
302
- else if (vector_info.format == "f")
305
+ else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
303
306
  count = index //
304
307
  .search(reinterpret_cast<float const*>(vector_data), wanted, config)
305
308
  .dump_to(&labels_py1d(0), &distances_py1d(0));
306
- else if (vector_info.format == "d")
309
+ else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
307
310
  count = index //
308
311
  .search(reinterpret_cast<double const*>(vector_data), wanted, config)
309
312
  .dump_to(&labels_py1d(0), &distances_py1d(0));
@@ -329,11 +332,15 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
329
332
  * 2. matrix of distances,
330
333
  * 3. array with match counts.
331
334
  */
332
- static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, std::size_t wanted, bool exact) {
335
+ static py::tuple search_many_in_index( //
336
+ punned_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
333
337
 
334
338
  if (wanted == 0)
335
339
  return py::tuple(3);
336
340
 
341
+ if (index.config().max_threads_add < threads)
342
+ throw std::invalid_argument("Can't use that many threads!");
343
+
337
344
  py::buffer_info vectors_info = vectors.request();
338
345
  if (vectors_info.ndim == 1)
339
346
  return search_one_in_index(index, vectors, wanted, exact);
@@ -355,7 +362,7 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
355
362
 
356
363
  // https://docs.python.org/3/library/struct.html#format-characters
357
364
  if (vectors_info.format == "c" || vectors_info.format == "b")
358
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
365
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
359
366
  search_config_t config;
360
367
  config.thread = thread_idx;
361
368
  config.exact = exact;
@@ -364,7 +371,7 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
364
371
  index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
365
372
  });
366
373
  else if (vectors_info.format == "e")
367
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
374
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
368
375
  search_config_t config;
369
376
  config.thread = thread_idx;
370
377
  config.exact = exact;
@@ -372,8 +379,8 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
372
379
  counts_py1d(task_idx) = static_cast<Py_ssize_t>(
373
380
  index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
374
381
  });
375
- else if (vectors_info.format == "f")
376
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
382
+ else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
383
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
377
384
  search_config_t config;
378
385
  config.thread = thread_idx;
379
386
  config.exact = exact;
@@ -381,8 +388,8 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
381
388
  counts_py1d(task_idx) = static_cast<Py_ssize_t>(
382
389
  index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
383
390
  });
384
- else if (vectors_info.format == "d")
385
- multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
391
+ else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
392
+ multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
386
393
  search_config_t config;
387
394
  config.thread = thread_idx;
388
395
  config.exact = exact;
@@ -407,7 +414,6 @@ template <typename index_at> void view_index(index_at& index, std::string const&
407
414
  template <typename index_at> void clear_index(index_at& index) { index.clear(); }
408
415
  template <typename index_at> std::size_t get_expansion_add(index_at const &index) { return index.config().expansion_add; }
409
416
  template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
410
-
411
417
  // clang-format on
412
418
 
413
419
  template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
@@ -476,8 +482,12 @@ void hash_buffer(hash_index_py_t& index, py::buffer vector) {
476
482
  throw std::invalid_argument("Array elements must be 16, 32, or 64 bit hashable integers!");
477
483
  }
478
484
 
479
- PYBIND11_MODULE(index, m) {
480
- m.doc() = "Unum USearch Python bindings";
485
+ PYBIND11_MODULE(compiled, m) {
486
+ m.doc() = "Smaller & Faster Single-File Vector Search Engine from Unum";
487
+
488
+ m.attr("DEFAULT_CONNECTIVITY") = py::int_(default_connectivity());
489
+ m.attr("DEFAULT_EXPANSION_ADD") = py::int_(default_expansion_add());
490
+ m.attr("DEFAULT_EXPANSION_SEARCH") = py::int_(default_expansion_search());
481
491
 
482
492
  auto i = py::class_<punned_py_t>(m, "Index");
483
493
 
@@ -508,14 +518,16 @@ PYBIND11_MODULE(index, m) {
508
518
  py::arg("label"), //
509
519
  py::arg("vector"), //
510
520
  py::kw_only(), //
511
- py::arg("copy") = true //
521
+ py::arg("copy") = true, //
522
+ py::arg("threads") = 0 //
512
523
  );
513
524
 
514
525
  i.def( //
515
526
  "search", &search_many_in_index, //
516
527
  py::arg("query"), //
517
528
  py::arg("count") = 10, //
518
- py::arg("exact") = false //
529
+ py::arg("exact") = false, //
530
+ py::arg("threads") = 0 //
519
531
  );
520
532
 
521
533
  i.def("__len__", &punned_py_t::size);
@@ -142,7 +142,7 @@ def main(
142
142
  expansion_add=expansion_add,
143
143
  expansion_search=expansion_search,
144
144
  connectivity=connectivity,
145
- metric_pointer=inner_product_f32.address,
145
+ metric=inner_product_f32.address,
146
146
  )
147
147
  bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
148
148
 
@@ -153,7 +153,7 @@ def main(
153
153
  expansion_add=expansion_add,
154
154
  expansion_search=expansion_search,
155
155
  connectivity=connectivity,
156
- metric_pointer=cos_f8.address,
156
+ metric=cos_f8.address,
157
157
  )
158
158
  bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
159
159
  except ImportError:
@@ -86,7 +86,7 @@ def test_user_defined_function(ndim: int, size: int):
86
86
  c += a_array[i] * b_array[i]
87
87
  return c
88
88
 
89
- index = Index(ndim=ndim, metric_pointer=python_dot.address)
89
+ index = Index(ndim=ndim, metric=python_dot.address)
90
90
 
91
91
  vectors = np.random.uniform(
92
92
  0, 0.3, (size, index.ndim)).astype(np.float32)
@@ -1,8 +1,10 @@
1
- from typing import Union, Optional
1
+ from typing import Union, Optional, List
2
2
 
3
3
  import numpy as np
4
4
  from ucall.client import Client
5
5
 
6
+ from usearch.index import Matches
7
+
6
8
 
7
9
  def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
8
10
  if vector.dtype != np.int8 and vector.dtype != np.uint8 and vector.dtype != np.byte:
@@ -18,9 +20,6 @@ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
18
20
  return ascii
19
21
 
20
22
 
21
- Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
22
-
23
-
24
23
  class IndexClient:
25
24
 
26
25
  def __init__(
@@ -53,8 +52,8 @@ class IndexClient:
53
52
  else:
54
53
  return self.add_many(labels, vectors)
55
54
 
56
- def search_one(self, vector: np.ndarray, count: int) -> Triplet:
57
- matches: list[dict] = []
55
+ def search_one(self, vector: np.ndarray, count: int) -> Matches:
56
+ matches: List[dict] = []
58
57
  vector = vector.flatten()
59
58
  ascii = _vector_to_ascii(vector)
60
59
  if ascii:
@@ -75,9 +74,9 @@ class IndexClient:
75
74
 
76
75
  return labels, distances, counts
77
76
 
78
- def search_many(self, vectors: np.ndarray, count: int) -> Triplet:
77
+ def search_many(self, vectors: np.ndarray, count: int) -> Matches:
79
78
  batch_size: int = vectors.shape[0]
80
- list_of_matches: list[list[dict]] = self.client.search_many(
79
+ list_of_matches: List[List[dict]] = self.client.search_many(
81
80
  vectors=vectors, count=count)
82
81
 
83
82
  labels = np.array((batch_size, count), dtype=np.uint32)
@@ -91,7 +90,7 @@ class IndexClient:
91
90
 
92
91
  return labels, distances, counts
93
92
 
94
- def search(self, vectors: np.ndarray, count: int) -> Triplet:
93
+ def search(self, vectors: np.ndarray, count: int) -> Matches:
95
94
  if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
96
95
  return self.search_one(vectors, count)
97
96
  else:
@@ -0,0 +1,306 @@
1
+ # The purpose of this file is to provide Pythonic wrapper on top
2
+ # the native precompiled CPython module. It improves compatibility
3
+ # Python tooling, linters, and static analyzers. It also embeds JIT
4
+ # into the primary `Index` class, connecting USearch with Numba.
5
+ import os
6
+ from math import sqrt
7
+ from typing import Optional, Callable, Union, NamedTuple, List
8
+
9
+ import numpy as np
10
+
11
+ from usearch.compiled import Index as _CompiledIndex
12
+ from usearch.compiled import SetsIndex as _CompiledSetsIndex
13
+ from usearch.compiled import HashIndex as _CompiledHashIndex
14
+ from usearch.compiled import DEFAULT_CONNECTIVITY, DEFAULT_EXPANSION_ADD, DEFAULT_EXPANSION_SEARCH
15
+
16
+ SetsIndex = _CompiledSetsIndex
17
+ HashIndex = _CompiledHashIndex
18
+
19
+
20
+ class Matches(NamedTuple):
21
+ labels: np.ndarray
22
+ distances: np.ndarray
23
+ counts: np.ndarray
24
+
25
+
26
+ def list_matches(results: Matches, row: int) -> List[dict]:
27
+
28
+ count = results[2][row]
29
+ labels = results[0][row, :count]
30
+ distances = results[1][row, :count]
31
+ return [
32
+ {'label': int(label), 'distance': float(distance)}
33
+ for label, distance in zip(labels, distances)
34
+ ]
35
+
36
+
37
+ def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
38
+
39
+ try:
40
+ from numba import cfunc, types, carray
41
+ except ImportError:
42
+ raise ModuleNotFoundError(
43
+ 'To use JIT install Numba with `pip install numba`.'
44
+ 'Alternatively, reinstall usearch with `pip install usearch[jit]`')
45
+
46
+ # Showcases how to use Numba to JIT-compile similarity measures for USearch.
47
+ # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
48
+
49
+ if accuracy == 'f32':
50
+ signature = types.float32(
51
+ types.CPointer(types.float32),
52
+ types.CPointer(types.float32),
53
+ types.size_t, types.size_t)
54
+
55
+ if metric_name == 'ip':
56
+
57
+ def numba_ip(a, b, _n, _m):
58
+ a_array = carray(a, ndim)
59
+ b_array = carray(b, ndim)
60
+ ab = 0.0
61
+ for i in range(ndim):
62
+ ab += a_array[i] * b_array[i]
63
+ return ab
64
+
65
+ return cfunc(numba_ip, signature)
66
+
67
+ if metric_name == 'cos':
68
+
69
+ def numba_cos(a, b, _n, _m):
70
+ a_array = carray(a, ndim)
71
+ b_array = carray(b, ndim)
72
+ ab = 0.0
73
+ a_sq = 0.0
74
+ b_sq = 0.0
75
+ for i in range(ndim):
76
+ ab += a_array[i] * b_array[i]
77
+ a_sq += a_array[i] * a_array[i]
78
+ b_sq += b_array[i] * b_array[i]
79
+ return ab / (sqrt(a_sq) * sqrt(b_sq))
80
+
81
+ return cfunc(numba_cos, signature)
82
+
83
+ if metric_name == 'l2sq':
84
+
85
+ def numba_l2sq(a, b, _n, _m):
86
+ a_array = carray(a, ndim)
87
+ b_array = carray(b, ndim)
88
+ ab_delta_sq = 0.0
89
+ for i in range(ndim):
90
+ ab_delta_sq += (a_array[i] - b_array[i]) * \
91
+ (a_array[i] - b_array[i])
92
+ return ab_delta_sq
93
+
94
+ return cfunc(numba_l2sq, signature)
95
+
96
+ return None
97
+
98
+
99
+ class Index:
100
+ """Fast JIT-compiled index for equi-dimensional embeddings.
101
+
102
+ Vector labels must be integers.
103
+ Vectors must have the same number of dimensions within the index.
104
+ Supports Inner Product, Cosine Distance, Ln measures
105
+ like the Euclidean metric, as well as automatic downcasting
106
+ and quantization.
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ ndim: int,
112
+ dtype: str = 'f32',
113
+ metric: Union[str, int] = 'ip',
114
+ jit: bool = False,
115
+
116
+ connectivity: int = DEFAULT_CONNECTIVITY,
117
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
118
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
119
+ tune: bool = False,
120
+
121
+ path: Optional[os.PathLike] = None,
122
+ view: bool = False,
123
+ ) -> None:
124
+ """Construct the index and compiles the functions, if requested (expensive).
125
+
126
+ :param ndim: Number of vector dimensions
127
+ :type ndim: int
128
+ :param dtype: Scalar type for vectors, defaults to 'f32'
129
+ :type dtype: str, optional
130
+ Example: you can use the `f16` index with `f32` vectors,
131
+ which will be automatically downcasted.
132
+
133
+ :param metric: Distance function, defaults to 'ip'
134
+ :type metric: Union[str, int], optional
135
+ Name of distance function, or the address of the
136
+ Numba `cfunc` JIT-compiled object.
137
+
138
+ :param jit: Enable Numba to JIT compile the metric, defaults to False
139
+ :type jit: bool, optional
140
+ This can result in up-to 3x performance difference on very large vectors
141
+ and very recent hardware, as the Python module is compiled with high
142
+ compatibility in mind and avoids very fancy assembly instructions.
143
+
144
+ :param connectivity: Connections per node in HNSW, defaults to None
145
+ :type connectivity: Optional[int], optional
146
+ Hyper-parameter for the number of Graph connections
147
+ per layer of HNSW. The original paper calls it "M".
148
+ Optional, but can't be changed after construction.
149
+
150
+ :param expansion_add: Traversal depth on insertions, defaults to None
151
+ :type expansion_add: Optional[int], optional
152
+ Hyper-parameter for the search depth when inserting new
153
+ vectors. The original paper calls it "efConstruction".
154
+ Can be changed afterwards, as the `.expansion_add`.
155
+
156
+ :param expansion_search: Traversal depth on queries, defaults to None
157
+ :type expansion_search: Optional[int], optional
158
+ Hyper-parameter for the search depth when querying
159
+ nearest neighbors. The original paper calls it "ef".
160
+ Can be changed afterwards, as the `.expansion_search`.
161
+
162
+ :param tune: Automatically adjusts hyper-parameters, defaults to False
163
+ :type tune: bool, optional
164
+
165
+ :param path: Where to store the index, defaults to None
166
+ :type path: Optional[os.PathLike], optional
167
+ :param view: Are we simply viewing an immutable index, defaults to False
168
+ :type view: bool, optional
169
+ """
170
+ if jit:
171
+ assert isinstance(metric, str), 'Name the metric to JIT'
172
+ self._metric_name = metric
173
+ self._metric_jit = jit_metric(
174
+ ndim=ndim,
175
+ metric_name=metric,
176
+ dtype=dtype,
177
+ )
178
+ self._metric_pointer = self._metric_jit.address if \
179
+ self._metric_jit else 0
180
+
181
+ elif isinstance(metric, int):
182
+ self._metric_name = ''
183
+ self._metric_jit = None
184
+ self._metric_pointer = metric
185
+
186
+ else:
187
+ if metric is None:
188
+ metric = 'ip'
189
+ assert isinstance(metric, str)
190
+ self._metric_name = metric
191
+ self._metric_jit = None
192
+ self._metric_pointer = 0
193
+
194
+ self._compiled = _CompiledIndex(
195
+ ndim=ndim,
196
+ metric=self._metric_name,
197
+ metric_pointer=self._metric_pointer,
198
+ dtype=dtype,
199
+ connectivity=connectivity,
200
+ expansion_add=expansion_add,
201
+ expansion_search=expansion_search,
202
+ tune=tune,
203
+ )
204
+
205
+ self.path = path
206
+ if path and os.path.exists(path):
207
+ if view:
208
+ self._compiled.view(path)
209
+ else:
210
+ self._compiled.load(path)
211
+
212
+ def add(
213
+ self, labels, vectors, *,
214
+ copy: bool = True, threads: int = 0):
215
+ """Inserts one or move vectors into the index.
216
+
217
+ For maximal performance the `labels` and `vectors`
218
+ should conform to the Python's "buffer protocol" spec.
219
+
220
+ To index a single entry:
221
+ labels: int, vectors: np.ndarray.
222
+ To index many entries:
223
+ labels: np.ndarray, vectors: np.ndarray.
224
+
225
+ When working with extremely large indexes, you may want to
226
+ pass `copy=False`, if you can guarantee the lifetime of the
227
+ primary vectors store during the process of construction.
228
+
229
+ :param labels: Unique identifier for passed vectors.
230
+ :type labels: Buffer
231
+ :param vectors: Collection of vectors.
232
+ :type vectors: Buffer
233
+ :param copy: Should the index store a copy of vectors, defaults to True
234
+ :type copy: bool, optional
235
+ :param threads: Optimal number of cores to use, defaults to 0
236
+ :type threads: int, optional
237
+ """
238
+ if isinstance(labels, np.ndarray):
239
+ labels = labels.astype(np.longlong)
240
+ self._compiled.add(labels, vectors, copy=copy, threads=threads)
241
+
242
+ def search(
243
+ self, vectors, k: int = 10, *,
244
+ threads: int = 0, exact: bool = False) -> Matches:
245
+ tuple_ = self._compiled.search(
246
+ vectors, k,
247
+ exact=exact, threads=threads)
248
+ return Matches(*tuple_)
249
+
250
+ def __len__(self) -> int:
251
+ return len(self._compiled)
252
+
253
+ @property
254
+ def size(self) -> int:
255
+ return self._compiled.size
256
+
257
+ @property
258
+ def ndim(self) -> int:
259
+ return self._compiled.ndim
260
+
261
+ @property
262
+ def dtype(self) -> str:
263
+ return self._compiled.dtype
264
+
265
+ @property
266
+ def connectivity(self) -> int:
267
+ return self._compiled.connectivity
268
+
269
+ @property
270
+ def capacity(self) -> int:
271
+ return self._compiled.capacity
272
+
273
+ @property
274
+ def memory_usage(self) -> int:
275
+ return self._compiled.memory_usage
276
+
277
+ @property
278
+ def expansion_add(self) -> int:
279
+ return self._compiled.expansion_add
280
+
281
+ @property
282
+ def expansion_search(self) -> int:
283
+ return self._compiled.expansion_search
284
+
285
+ @expansion_add.setter
286
+ def change_expansion_add(self, v: int):
287
+ self._compiled.expansion_add = v
288
+
289
+ @expansion_search.setter
290
+ def change_expansion_search(self, v: int):
291
+ self._compiled.expansion_search = v
292
+
293
+ def save(self, path: os.PathLike):
294
+ self._compiled.save(path)
295
+
296
+ def load(self, path: os.PathLike):
297
+ self._compiled.load(path)
298
+
299
+ def view(self, path: os.PathLike):
300
+ self._compiled.view(path)
301
+
302
+ def clear(self):
303
+ self._compiled.clear()
304
+
305
+ def remove(self, label: int):
306
+ pass
@@ -4,20 +4,10 @@
4
4
  import os
5
5
  import argparse
6
6
  import numpy as np
7
+ from typing import List
7
8
 
8
9
  from ucall.rich_posix import Server
9
- from usearch.index import Index
10
-
11
- Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
12
-
13
-
14
- def _results_to_json(results: Triplet, row: int) -> list[dict]:
15
-
16
- count = results[2][row]
17
- labels = results[0][row, :count]
18
- distances = results[1][row, :count]
19
- return [{'label': int(label), 'distance': float(distance)}
20
- for label, distance in zip(labels, distances)]
10
+ from usearch.index import Index, list_matches
21
11
 
22
12
 
23
13
  def _ascii_to_vector(string: str) -> np.ndarray:
@@ -78,16 +68,16 @@ def serve(
78
68
  index.add(labels, vectors, threads=threads)
79
69
 
80
70
  @server
81
- def search_one(vector: np.ndarray, count: int) -> list[dict]:
71
+ def search_one(vector: np.ndarray, count: int) -> List[dict]:
82
72
  print('search', vector, count)
83
73
  vectors = vector.reshape(vector.shape[0], 1)
84
74
  results = index.search(vectors, count)
85
- return _results_to_json(results, 0)
75
+ return list_matches(results, 0)
86
76
 
87
77
  @server
88
- def search_many(vectors: np.ndarray, count: int) -> list[list[dict]]:
78
+ def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
89
79
  results = index.search(vectors, count)
90
- return [_results_to_json(results, i) for i in range(vectors.shape[0])]
80
+ return [list_matches(results, i) for i in range(vectors.shape[0])]
91
81
 
92
82
  @server
93
83
  def add_ascii(label: int, string: str):
@@ -0,0 +1,5 @@
1
+ comment: off
2
+ coverage:
3
+ status:
4
+ project: off
5
+ patch: off