usearch 0.22.3 → 1.0.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.
Files changed (93) hide show
  1. package/.github/workflows/prerelease.yml +3 -3
  2. package/.releaserc +10 -2
  3. package/.vscode/launch.json +10 -10
  4. package/.vscode/settings.json +9 -1
  5. package/.vscode/tasks.json +4 -4
  6. package/CMakeLists.txt +6 -1
  7. package/Cargo.toml +2 -2
  8. package/README.md +35 -41
  9. package/build.rs +2 -2
  10. package/c/CMakeLists.txt +48 -0
  11. package/c/README.md +34 -0
  12. package/c/lib.cpp +83 -72
  13. package/c/test.c +20 -20
  14. package/c/usearch.h +132 -32
  15. package/conanfile.py +1 -1
  16. package/cpp/CMakeLists.txt +3 -1
  17. package/cpp/README.md +16 -16
  18. package/cpp/bench.cpp +50 -126
  19. package/cpp/test.cpp +164 -80
  20. package/docs/benchmarks.md +26 -5
  21. package/docs/c/reference.rst +3 -3
  22. package/docs/compilation.md +31 -49
  23. package/docs/cpp/index.rst +1 -1
  24. package/docs/cpp/reference.rst +4 -4
  25. package/golang/lib.go +275 -141
  26. package/golang/lib_test.go +105 -0
  27. package/include/usearch/index.hpp +1681 -1656
  28. package/include/usearch/index_dense.hpp +1400 -0
  29. package/include/usearch/index_plugins.hpp +1263 -0
  30. package/java/README.md +1 -1
  31. package/java/cloud/unum/usearch/Index.java +10 -10
  32. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +34 -37
  33. package/java/test/IndexTest.java +1 -1
  34. package/javascript/README.md +6 -6
  35. package/javascript/lib.cpp +94 -55
  36. package/javascript/test.js +25 -11
  37. package/javascript/usearch.d.ts +87 -0
  38. package/javascript/usearch.js +2 -118
  39. package/objc/USearchObjective.mm +57 -31
  40. package/objc/include/USearchObjective.h +32 -22
  41. package/package.json +5 -3
  42. package/python/README.md +23 -14
  43. package/python/lib.cpp +468 -444
  44. package/python/scripts/bench.ipynb +3 -2
  45. package/python/scripts/bench.py +4 -4
  46. package/python/scripts/index_faiss.py +5 -5
  47. package/python/scripts/join.py +4 -4
  48. package/python/scripts/test.py +152 -61
  49. package/python/scripts/test_jit.py +10 -12
  50. package/python/scripts/test_sparse.py +32 -0
  51. package/python/usearch/client.py +26 -28
  52. package/python/usearch/eval.py +73 -23
  53. package/python/usearch/index.py +520 -228
  54. package/python/usearch/io.py +57 -25
  55. package/python/usearch/numba.py +4 -4
  56. package/python/usearch/server.py +10 -10
  57. package/rust/lib.cpp +22 -27
  58. package/rust/lib.hpp +11 -12
  59. package/rust/lib.rs +3 -3
  60. package/setup.py +5 -2
  61. package/simsimd/.github/workflows/prerelease.yml +73 -0
  62. package/simsimd/.github/workflows/release.yml +110 -0
  63. package/simsimd/.github/workflows/update_version.sh +3 -0
  64. package/simsimd/.releaserc +77 -0
  65. package/simsimd/.vscode/launch.json +27 -1
  66. package/simsimd/.vscode/settings.json +22 -1
  67. package/simsimd/.vscode/tasks.json +6 -0
  68. package/simsimd/CMakeLists.txt +3 -2
  69. package/simsimd/README.md +82 -46
  70. package/simsimd/bench.cxx +63 -24
  71. package/simsimd/include/simsimd/simsimd.h +88 -66
  72. package/simsimd/include/simsimd/simsimd_chem.h +126 -0
  73. package/simsimd/package.json +8 -0
  74. package/simsimd/pyproject.toml +42 -0
  75. package/simsimd/python/python.c +81 -0
  76. package/simsimd/python/run.sh +1 -0
  77. package/simsimd/python/test.c +109 -0
  78. package/simsimd/python/test.py +23 -0
  79. package/simsimd/setup.py +44 -0
  80. package/swift/Index+Sugar.swift +75 -23
  81. package/swift/README.md +26 -2
  82. package/swift/Test.swift +19 -7
  83. package/swift/USearch.swift +2 -2
  84. package/tsconfig.json +8 -0
  85. package/wasm/CMakeLists.txt +1 -0
  86. package/wasmer.toml +1 -1
  87. package/wolfram/CMakeLists.txt +1 -0
  88. package/wolfram/lib.cpp +11 -13
  89. package/golang/usearch.h +0 -9
  90. package/golang/usearch_par_test.go +0 -40
  91. package/golang/usearch_test.go +0 -82
  92. package/include/usearch/index_punned_dense.hpp +0 -1001
  93. package/include/usearch/index_punned_helpers.hpp +0 -676
@@ -8,7 +8,14 @@ from math import ceil
8
8
  import numpy as np
9
9
 
10
10
  from usearch.io import load_matrix
11
- from usearch.index import Index, Matches, MetricKind, MetricKindBitwise, Label
11
+ from usearch.index import (
12
+ Index,
13
+ Matches,
14
+ BatchMatches,
15
+ MetricKind,
16
+ MetricKindBitwise,
17
+ Key,
18
+ )
12
19
 
13
20
 
14
21
  def random_vectors(
@@ -48,28 +55,71 @@ def random_vectors(
48
55
  return x
49
56
 
50
57
 
51
- def recall_members(index: Index, sample: float = 1, **kwargs) -> float:
58
+ @dataclass
59
+ class SearchStats:
60
+ """
61
+ Contains statistics for one or more search runs, including the number of
62
+ internal nodes that were fetched (`visited_members`) and the number
63
+ of times the distance metric was invoked (`computed_distances`).
64
+
65
+ Other derivative metrics include the `mean_recall` and `mean_efficiency`.
66
+ Recall is the share of queried vectors, that were successfully found.
67
+ Efficiency describes the number of distances that had to be computed for
68
+ each query, normalized to size of the `index`. Highest efficiency is 0.(9),
69
+ lowest is zero. Highest is achieved, when the distance metric was computed
70
+ just once per query. Lowest happens during exact search, when every distance
71
+ to every present vector had to be computed.
72
+ """
73
+
74
+ index_size: int
75
+ count_queries: int
76
+ count_matches: int
77
+
78
+ visited_members: int
79
+ computed_distances: int
80
+
81
+ @property
82
+ def mean_efficiency(self) -> float:
83
+ return 1 - float(self.computed_distances) / (
84
+ self.count_queries * self.index_size
85
+ )
86
+
87
+ @property
88
+ def mean_recall(self) -> float:
89
+ return self.count_matches / self.count_queries
90
+
91
+
92
+ def self_recall(index: Index, sample: float = 1, **kwargs) -> SearchStats:
52
93
  """Simplest benchmark for a quality of search, which queries every
53
94
  existing member of the index, to make sure approximate search finds
54
95
  the point itself.
55
96
 
56
97
  :param index: Non-empty pre-constructed index
57
98
  :type index: Index
58
- :return: Value from 0 to 1, for the share of found self-references
59
- :rtype: float
99
+ :param sample: Share of vectors to search, defaults to 1
100
+ :type sample: float
101
+ :return: Evaluation report with key metrics
102
+ :rtype: SearchStats
60
103
  """
61
104
  if len(index) == 0:
62
105
  return 0
63
106
  if "k" not in kwargs:
64
107
  kwargs["k"] = 1
65
108
 
66
- labels = index.labels
109
+ keys = index.keys
67
110
  if sample != 1:
68
- labels = np.random.choice(labels, int(ceil(len(labels) * sample)))
111
+ keys = np.random.choice(keys, int(ceil(len(keys) * sample)))
69
112
 
70
- queries = index.get_vectors(labels, index.dtype)
71
- matches: Matches = index.search(queries, **kwargs)
72
- return matches.recall_first(labels)
113
+ queries = index.get_vectors(keys, index.dtype)
114
+ matches: BatchMatches = index.search(queries, **kwargs)
115
+ count_matches: float = matches.count_matches(keys)
116
+ return SearchStats(
117
+ index_size=len(index),
118
+ count_queries=len(keys),
119
+ count_matches=count_matches,
120
+ visited_members=matches.visited_members,
121
+ computed_distances=matches.computed_distances,
122
+ )
73
123
 
74
124
 
75
125
  def measure_seconds(f: Callable) -> Tuple[float, Any]:
@@ -131,9 +181,9 @@ def relevance(
131
181
  ) -> np.ndarray:
132
182
  """Calculate relevance scores. Binary relevance scores
133
183
 
134
- :param expected: ground-truth labels
184
+ :param expected: ground-truth keys
135
185
  :type expected: np.ndarray
136
- :param predicted: predicted labels
186
+ :param predicted: predicted keys
137
187
  :type predicted: np.ndarray
138
188
  """
139
189
  expected = expected[:k]
@@ -143,7 +193,7 @@ def relevance(
143
193
 
144
194
  @dataclass
145
195
  class Dataset:
146
- labels: np.ndarray
196
+ keys: np.ndarray
147
197
  vectors: np.ndarray
148
198
  queries: np.ndarray
149
199
  neighbors: np.ndarray
@@ -189,7 +239,7 @@ class Dataset:
189
239
  else d.vectors.shape[0]
190
240
  )
191
241
  d.vectors = d.vectors[:count, :]
192
- d.labels = np.arange(count, dtype=Label)
242
+ d.keys = np.arange(count, dtype=Key)
193
243
 
194
244
  if queries is not None:
195
245
  d.queries = load_matrix(queries)
@@ -202,7 +252,7 @@ class Dataset:
202
252
  d.neighbors = d.neighbors[:, :k]
203
253
  else:
204
254
  assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
205
- d.neighbors = np.reshape(d.labels, (count, 1))
255
+ d.neighbors = np.reshape(d.keys, (count, 1))
206
256
 
207
257
  else:
208
258
  assert ndim is not None
@@ -211,8 +261,8 @@ class Dataset:
211
261
 
212
262
  d.vectors = random_vectors(count=count, ndim=ndim)
213
263
  d.queries = d.vectors
214
- d.labels = np.arange(count, dtype=Label)
215
- d.neighbors = np.reshape(d.labels, (count, 1))
264
+ d.keys = np.arange(count, dtype=Key)
265
+ d.neighbors = np.reshape(d.keys, (count, 1))
216
266
 
217
267
  return d
218
268
 
@@ -276,13 +326,13 @@ class TaskResult:
276
326
 
277
327
  @dataclass
278
328
  class AddTask:
279
- labels: np.ndarray
329
+ keys: np.ndarray
280
330
  vectors: np.ndarray
281
331
 
282
332
  def __call__(self, index: Index) -> TaskResult:
283
333
  batch_size: int = self.vectors.shape[0]
284
334
  old_size: int = len(index)
285
- dt, _ = measure_seconds(lambda: index.add(self.labels, self.vectors))
335
+ dt, _ = measure_seconds(lambda: index.add(self.keys, self.vectors))
286
336
 
287
337
  assert len(index) == old_size + batch_size
288
338
  return TaskResult(
@@ -299,11 +349,11 @@ class AddTask:
299
349
  return self.vectors.shape[0]
300
350
 
301
351
  def inplace_shuffle(self):
302
- """Rorders the `vectors` and `labels`. Often used for robustness benchmarks."""
352
+ """Rorders the `vectors` and `keys`. Often used for robustness benchmarks."""
303
353
 
304
354
  new_order = np.arange(self.count)
305
355
  np.random.shuffle(new_order)
306
- self.labels = self.labels[new_order]
356
+ self.keys = self.keys[new_order]
307
357
  self.vectors = self.vectors[new_order, :]
308
358
 
309
359
  def slices(self, batch_size: int) -> List[AddTask]:
@@ -311,7 +361,7 @@ class AddTask:
311
361
 
312
362
  return [
313
363
  AddTask(
314
- labels=self.labels[start_row : start_row + batch_size],
364
+ keys=self.keys[start_row : start_row + batch_size],
315
365
  vectors=self.vectors[start_row : start_row + batch_size, :],
316
366
  )
317
367
  for start_row in range(0, self.count, batch_size)
@@ -334,7 +384,7 @@ class AddTask:
334
384
 
335
385
  return [
336
386
  AddTask(
337
- labels=self.labels[rows],
387
+ keys=self.keys[rows],
338
388
  vectors=self.vectors[rows, :],
339
389
  )
340
390
  for rows in partitioning.values()
@@ -379,7 +429,7 @@ class Evaluation:
379
429
  dataset: Dataset, batch_size: int = 0, clusters: int = 1
380
430
  ) -> Evaluation:
381
431
  tasks = []
382
- add = AddTask(vectors=dataset.vectors, labels=dataset.labels)
432
+ add = AddTask(vectors=dataset.vectors, keys=dataset.keys)
383
433
  search = SearchTask(queries=dataset.queries, neighbors=dataset.neighbors)
384
434
 
385
435
  if batch_size: