usearch 2.8.7 → 2.8.9

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 (113) hide show
  1. package/CITATION.cff +1 -1
  2. package/CMakeLists.txt +3 -3
  3. package/CONTRIBUTING.md +26 -3
  4. package/Cargo.toml +1 -1
  5. package/Dockerfile +16 -0
  6. package/README.md +41 -38
  7. package/VERSION +1 -0
  8. package/binding.gyp +57 -8
  9. package/conanfile.py +1 -1
  10. package/include/usearch/index.hpp +1 -1
  11. package/include/usearch/index_plugins.hpp +28 -24
  12. package/javascript/README.md +64 -12
  13. package/javascript/lib.cpp +11 -3
  14. package/javascript/usearch.js +4 -2
  15. package/javascript/usearch.test.js +15 -0
  16. package/package.json +1 -2
  17. package/setup.py +76 -15
  18. package/simsimd/README.md +14 -13
  19. package/simsimd/include/simsimd/probability.h +2 -2
  20. package/simsimd/package.json +1 -1
  21. package/wasmer.toml +1 -1
  22. package/. npmignore +0 -14
  23. package/.gitignore +0 -95
  24. package/.vscode/launch.json +0 -181
  25. package/.vscode/settings.json +0 -180
  26. package/.vscode/tasks.json +0 -71
  27. package/Package.swift +0 -41
  28. package/csharp/.editorconfig +0 -361
  29. package/csharp/Cloud.Unum.USearch.sln +0 -34
  30. package/csharp/DEV_SETUP.md +0 -51
  31. package/csharp/Directory.Build.props +0 -29
  32. package/csharp/README.md +0 -74
  33. package/csharp/build_and_test.cmd +0 -31
  34. package/csharp/build_and_test.sh +0 -30
  35. package/csharp/nuget/nuget-package.props +0 -33
  36. package/csharp/pack.sh +0 -56
  37. package/csharp/src/Cloud.Unum.USearch.Tests/Cloud.Unum.USearch.Tests.csproj +0 -39
  38. package/csharp/src/Cloud.Unum.USearch.Tests/USearchIndexTests.cs +0 -1013
  39. package/csharp/src/Cloud.Unum.USearch.Tests/Usings.cs +0 -1
  40. package/docs/_static/custom.css +0 -26
  41. package/docs/_static/custom.js +0 -7
  42. package/docs/benchmarks.md +0 -245
  43. package/docs/benchmarks.rst +0 -5
  44. package/docs/c/index.rst +0 -11
  45. package/docs/c/reference.rst +0 -7
  46. package/docs/conf.dox +0 -2668
  47. package/docs/conf.py +0 -53
  48. package/docs/contributing.rst +0 -5
  49. package/docs/cpp/index.rst +0 -11
  50. package/docs/cpp/reference.rst +0 -17
  51. package/docs/csharp/index.rst +0 -6
  52. package/docs/format.md +0 -86
  53. package/docs/format.rst +0 -5
  54. package/docs/golang/index.rst +0 -6
  55. package/docs/index.rst +0 -33
  56. package/docs/java/index.rst +0 -11
  57. package/docs/java/reference.rst +0 -7
  58. package/docs/javascript/index.rst +0 -11
  59. package/docs/javascript/reference.rst +0 -12
  60. package/docs/python/index.rst +0 -11
  61. package/docs/python/reference.rst +0 -42
  62. package/docs/rust/index.rst +0 -6
  63. package/docs/swift/index.rst +0 -6
  64. package/docs/wolfram/index.rst +0 -6
  65. package/golang/README.md +0 -95
  66. package/golang/go.mod +0 -3
  67. package/golang/lib.go +0 -379
  68. package/golang/lib_test.go +0 -108
  69. package/java/README.md +0 -22
  70. package/java/cloud/unum/usearch/Index.java +0 -337
  71. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +0 -197
  72. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +0 -125
  73. package/java/test/IndexTest.java +0 -30
  74. package/objc/USearchObjective.mm +0 -299
  75. package/objc/include/USearchObjective.h +0 -154
  76. package/python/README.md +0 -292
  77. package/python/lib.cpp +0 -1216
  78. package/python/scripts/bench.ipynb +0 -193
  79. package/python/scripts/bench.py +0 -137
  80. package/python/scripts/bench_exact.py +0 -44
  81. package/python/scripts/cluster.ipynb +0 -160
  82. package/python/scripts/index_faiss.py +0 -100
  83. package/python/scripts/join.py +0 -155
  84. package/python/scripts/test_index.py +0 -247
  85. package/python/scripts/test_jit.py +0 -231
  86. package/python/scripts/test_sparse.py +0 -32
  87. package/python/scripts/test_tooling.py +0 -133
  88. package/python/usearch/__init__.py +0 -0
  89. package/python/usearch/client.py +0 -124
  90. package/python/usearch/eval.py +0 -470
  91. package/python/usearch/index.py +0 -1401
  92. package/python/usearch/io.py +0 -113
  93. package/python/usearch/numba.py +0 -121
  94. package/python/usearch/server.py +0 -137
  95. package/rust/README.md +0 -66
  96. package/rust/lib.cpp +0 -136
  97. package/rust/lib.hpp +0 -69
  98. package/rust/lib.rs +0 -610
  99. package/simsimd/CMakeLists.txt +0 -67
  100. package/simsimd/cpp/bench.cxx +0 -206
  101. package/simsimd/package-ci.json +0 -10
  102. package/simsimd/setup.py +0 -86
  103. package/swift/Index+Sugar.swift +0 -153
  104. package/swift/README.md +0 -48
  105. package/swift/Test.swift +0 -37
  106. package/swift/USearch.swift +0 -8
  107. package/wasm/CMakeLists.txt +0 -7
  108. package/wasm/README.md +0 -13
  109. package/wasm/lib.cpp +0 -5
  110. package/wolfram/CMakeLists.txt +0 -18
  111. package/wolfram/README.md +0 -13
  112. package/wolfram/lib.cpp +0 -160
  113. package/wolfram/test.wls +0 -27
@@ -1,470 +0,0 @@
1
- from __future__ import annotations
2
- from time import time_ns
3
- from typing import Tuple, Any, Callable, Union, Optional, List
4
- from dataclasses import dataclass, asdict
5
- from collections import defaultdict
6
- from math import ceil
7
-
8
- import numpy as np
9
-
10
- from usearch.io import load_matrix
11
- from usearch.index import (
12
- Index,
13
- Matches,
14
- BatchMatches,
15
- MetricKind,
16
- MetricKindBitwise,
17
- Key,
18
- )
19
-
20
-
21
- def random_vectors(
22
- count: int,
23
- metric: MetricKind = MetricKind.IP,
24
- dtype: np.generic = np.float32,
25
- ndim: Optional[int] = None,
26
- index: Optional[Index] = None,
27
- ) -> np.ndarray:
28
- """Produces a collection of random vectors normalized for the provided `metric`
29
- and matching wanted `dtype`, which can both be inferred from an existing `index`.
30
- """
31
-
32
- # Infer default parameters from the `index`, if passed
33
- if index is not None:
34
- if not isinstance(index, Index):
35
- raise ValueError("Unsupported `index` type")
36
-
37
- ndim = index.ndim
38
- dtype = index.numpy_dtype
39
- metric = index.metric
40
-
41
- # Produce data
42
- if metric in MetricKindBitwise:
43
- bit_vectors = np.random.randint(2, size=(count, ndim))
44
- bit_vectors = np.packbits(bit_vectors, axis=1)
45
- return bit_vectors
46
-
47
- else:
48
- x = np.random.rand(count, ndim)
49
- if dtype == np.int8:
50
- x = (x * 100).astype(np.int8)
51
- else:
52
- x = x.astype(dtype)
53
- if metric == MetricKind.IP:
54
- return x / np.linalg.norm(x, axis=1, keepdims=True)
55
- return x
56
-
57
-
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:
93
- """Simplest benchmark for a quality of search, which queries every
94
- existing member of the index, to make sure approximate search finds
95
- the point itself.
96
-
97
- :param index: Non-empty pre-constructed index
98
- :type index: Index
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
103
- """
104
- if len(index) == 0:
105
- return 0
106
- if "count" not in kwargs:
107
- kwargs["count"] = 1
108
-
109
- if "keys" in kwargs:
110
- keys = kwargs["keys"]
111
- else:
112
- keys = np.array(index.keys)
113
-
114
- if sample != 1:
115
- keys = np.random.choice(keys, int(ceil(len(keys) * sample)))
116
-
117
- queries = index.get(keys, index.dtype)
118
- matches: BatchMatches = index.search(queries, **kwargs)
119
- count_matches: float = matches.count_matches(keys)
120
- return SearchStats(
121
- index_size=len(index),
122
- count_queries=len(keys),
123
- count_matches=count_matches,
124
- visited_members=matches.visited_members,
125
- computed_distances=matches.computed_distances,
126
- )
127
-
128
-
129
- def measure_seconds(f: Callable) -> Tuple[float, Any]:
130
- """Simple function profiling decorator.
131
-
132
- :param f: Function to be profiled
133
- :type f: Callable
134
- :return: Time elapsed in seconds and the result of the execution
135
- :rtype: Tuple[float, Any]
136
- """
137
- a = time_ns()
138
- result = f()
139
- b = time_ns()
140
- c = b - a
141
- secs = c / (10**9)
142
- return secs, result
143
-
144
-
145
- def dcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
146
- """Calculate DCG (Discounted Cumulative Gain) up to position k.
147
-
148
- :param relevances: List of true relevance scores (in the order as they are ranked)
149
- :type relevances: list
150
- :param k: Position up to which DCG is computed
151
- :type k: int
152
- :return: The DCG score at position k
153
- :rtype: float
154
- """
155
- if k:
156
- relevances = np.asarray(relevances)[:k]
157
-
158
- n_relevances = len(relevances)
159
- if n_relevances == 0:
160
- return 0.0
161
-
162
- discounts = np.log2(np.arange(n_relevances) + 2)
163
- return np.sum(relevances / discounts)
164
-
165
-
166
- def ndcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
167
- """Calculate NDCG (Normalized Discounted Cumulative Gain) at position k.
168
-
169
- :param relevances: List of true relevance scores (in the order as they are ranked)
170
- :type relevances: list
171
- :param k: Position up to which NDCG is computed
172
- :type k: int
173
- :return: The NDCG score at position k
174
- :rtype: float
175
- """
176
- best_dcg = dcg(sorted(relevances, reverse=True), k)
177
- if best_dcg == 0:
178
- return 0.0
179
-
180
- return dcg(relevances, k) / best_dcg
181
-
182
-
183
- def relevance(
184
- expected: np.ndarray, predicted: np.ndarray, k: Optional[int] = None
185
- ) -> np.ndarray:
186
- """Calculate relevance scores. Binary relevance scores
187
-
188
- :param expected: ground-truth keys
189
- :type expected: np.ndarray
190
- :param predicted: predicted keys
191
- :type predicted: np.ndarray
192
- """
193
- expected = expected[:k]
194
- predicted = predicted[:k]
195
- return [1 if i in expected else 0 for i in predicted]
196
-
197
-
198
- @dataclass
199
- class Dataset:
200
- keys: np.ndarray
201
- vectors: np.ndarray
202
- queries: np.ndarray
203
- neighbors: np.ndarray
204
-
205
- def crop_neighbors(self, k: int):
206
- self.neighbors = self.neighbors[:, k]
207
-
208
- @staticmethod
209
- def build(
210
- vectors: Optional[str] = None,
211
- queries: Optional[str] = None,
212
- neighbors: Optional[str] = None,
213
- count: Optional[int] = None,
214
- ndim: Optional[int] = None,
215
- k: Optional[int] = None,
216
- ):
217
- """Either loads an existing dataset from disk, or generates one on the fly.
218
-
219
- :param vectors: _description_, defaults to None
220
- :type vectors: Optional[str], optional
221
- :param queries: _description_, defaults to None
222
- :type queries: Optional[str], optional
223
- :param neighbors: _description_, defaults to None
224
- :type neighbors: Optional[str], optional
225
- :param count: _description_, defaults to None
226
- :type count: Optional[int], optional
227
- :param ndim: _description_, defaults to None
228
- :type ndim: Optional[int], optional
229
- :param k: _description_, defaults to None
230
- :type k: Optional[int], optional
231
- """
232
-
233
- d = Dataset(None, None, None, None)
234
-
235
- if vectors is not None:
236
- assert ndim is None
237
-
238
- d.vectors = load_matrix(vectors)
239
- ndim = d.vectors.shape[1]
240
- count = (
241
- min(d.vectors.shape[0], count)
242
- if count is not None
243
- else d.vectors.shape[0]
244
- )
245
- d.vectors = d.vectors[:count, :]
246
- d.keys = np.arange(count, dtype=Key)
247
-
248
- if queries is not None:
249
- d.queries = load_matrix(queries)
250
- else:
251
- d.queries = d.vectors
252
-
253
- if neighbors is not None:
254
- d.neighbors = load_matrix(neighbors)
255
- if k is not None:
256
- d.neighbors = d.neighbors[:, :k]
257
- else:
258
- assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
259
- d.neighbors = np.reshape(d.keys, (count, 1))
260
-
261
- else:
262
- assert ndim is not None
263
- assert count is not None
264
- assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
265
-
266
- d.vectors = random_vectors(count=count, ndim=ndim)
267
- d.queries = d.vectors
268
- d.keys = np.arange(count, dtype=Key)
269
- d.neighbors = np.reshape(d.keys, (count, 1))
270
-
271
- return d
272
-
273
-
274
- @dataclass
275
- class TaskResult:
276
- add_operations: Optional[int] = None
277
- add_per_second: Optional[float] = None
278
-
279
- search_operations: Optional[int] = None
280
- search_per_second: Optional[float] = None
281
- recall_at_one: Optional[float] = None
282
-
283
- def __repr__(self) -> str:
284
- parts = []
285
- if self.add_per_second:
286
- parts.append(f"{self.add_per_second:.2f} add/s")
287
- if self.search_per_second:
288
- parts.append(f"{self.search_per_second:.2f} search/s")
289
- if self.recall_at_one:
290
- parts.append(f"{self.recall_at_one * 100:.2f}% recall@1")
291
- return ", ".join(parts)
292
-
293
- @property
294
- def add_seconds(self) -> float:
295
- return self.add_operations / self.add_per_second
296
-
297
- @property
298
- def search_seconds(self) -> float:
299
- return self.search_operations / self.search_per_second
300
-
301
- def __add__(self, other: TaskResult):
302
- result = TaskResult()
303
- if self.add_operations and other.add_operations:
304
- result.add_operations = self.add_operations + other.add_operations
305
- result.add_per_second = result.add_operations / (
306
- self.add_seconds + other.add_seconds
307
- )
308
- else:
309
- base = self if self.add_operations else other
310
- result.add_operations = base.add_operations
311
- result.add_per_second = base.add_per_second
312
-
313
- if self.search_operations and other.search_operations:
314
- result.search_operations = self.search_operations + other.search_operations
315
- result.recall_at_one = (
316
- self.recall_at_one * self.search_operations
317
- + other.recall_at_one * other.search_operations
318
- ) / (self.search_operations + other.search_operations)
319
- result.search_per_second = result.search_operations / (
320
- self.search_seconds + other.search_seconds
321
- )
322
- else:
323
- base = self if self.search_operations else other
324
- result.search_operations = base.search_operations
325
- result.search_per_second = base.search_per_second
326
- result.recall_at_one = base.recall_at_one
327
-
328
- return result
329
-
330
-
331
- @dataclass
332
- class AddTask:
333
- keys: np.ndarray
334
- vectors: np.ndarray
335
-
336
- def __call__(self, index: Index) -> TaskResult:
337
- batch_size: int = self.vectors.shape[0]
338
- old_size: int = len(index)
339
- dt, _ = measure_seconds(lambda: index.add(self.keys, self.vectors))
340
-
341
- assert len(index) == old_size + batch_size
342
- return TaskResult(
343
- add_operations=batch_size,
344
- add_per_second=batch_size / dt,
345
- )
346
-
347
- @property
348
- def ndim(self):
349
- return self.vectors.shape[1]
350
-
351
- @property
352
- def count(self):
353
- return self.vectors.shape[0]
354
-
355
- def inplace_shuffle(self):
356
- """Rorders the `vectors` and `keys`. Often used for robustness benchmarks."""
357
-
358
- new_order = np.arange(self.count)
359
- np.random.shuffle(new_order)
360
- self.keys = self.keys[new_order]
361
- self.vectors = self.vectors[new_order, :]
362
-
363
- def slices(self, batch_size: int) -> List[AddTask]:
364
- """Splits this dataset into smaller chunks."""
365
-
366
- return [
367
- AddTask(
368
- keys=self.keys[start_row : start_row + batch_size],
369
- vectors=self.vectors[start_row : start_row + batch_size, :],
370
- )
371
- for start_row in range(0, self.count, batch_size)
372
- ]
373
-
374
- def clusters(self, number_of_clusters: int) -> List[AddTask]:
375
- """Splits this dataset into smaller chunks."""
376
-
377
- from sklearn.cluster import KMeans
378
-
379
- clustering = KMeans(
380
- n_clusters=number_of_clusters,
381
- random_state=0,
382
- n_init="auto",
383
- ).fit(self.vectors)
384
-
385
- partitioning = defaultdict(list)
386
- for row, cluster in enumerate(clustering.labels_):
387
- partitioning[cluster].append(row)
388
-
389
- return [
390
- AddTask(
391
- keys=self.keys[rows],
392
- vectors=self.vectors[rows, :],
393
- )
394
- for rows in partitioning.values()
395
- ]
396
-
397
-
398
- @dataclass
399
- class SearchTask:
400
- queries: np.ndarray
401
- neighbors: np.ndarray
402
-
403
- def __call__(self, index: Index) -> TaskResult:
404
- dt, results = measure_seconds(
405
- lambda: index.search(self.queries, self.neighbors.shape[1])
406
- )
407
-
408
- return TaskResult(
409
- search_per_second=self.queries.shape[0] / dt,
410
- recall_at_one=results.mean_recall(self.neighbors[:, 0].flatten()),
411
- )
412
-
413
- def slices(self, batch_size: int) -> List[SearchTask]:
414
- """Splits this dataset into smaller chunks."""
415
-
416
- return [
417
- SearchTask(
418
- queries=self.queries[start_row : start_row + batch_size, :],
419
- neighbors=self.neighbors[start_row : start_row + batch_size, :],
420
- )
421
- for start_row in range(0, self.queries.shape[0], batch_size)
422
- ]
423
-
424
-
425
- @dataclass
426
- class Evaluation:
427
- tasks: List[Union[AddTask, SearchTask]]
428
- count: int
429
- ndim: int
430
-
431
- @staticmethod
432
- def for_dataset(
433
- dataset: Dataset, batch_size: int = 0, clusters: int = 1
434
- ) -> Evaluation:
435
- tasks = []
436
- add = AddTask(vectors=dataset.vectors, keys=dataset.keys)
437
- search = SearchTask(queries=dataset.queries, neighbors=dataset.neighbors)
438
-
439
- if batch_size:
440
- tasks.extend(add.slices(batch_size))
441
- tasks.extend(search.slices(batch_size))
442
- elif clusters != 1:
443
- tasks.extend(add.clusters(clusters))
444
- print(tasks)
445
- tasks.append(search)
446
- else:
447
- tasks.append(add)
448
- tasks.append(search)
449
-
450
- return Evaluation(
451
- tasks=tasks,
452
- count=add.count,
453
- ndim=add.ndim,
454
- )
455
-
456
- def __call__(self, index: Index, post_clean: bool = True) -> dict:
457
- task_result = TaskResult()
458
-
459
- try:
460
- for task in self.tasks:
461
- task_result = task_result + task(index)
462
- except KeyboardInterrupt:
463
- pass
464
-
465
- if post_clean:
466
- index.clear()
467
- return {
468
- **index.specs,
469
- **asdict(task_result),
470
- }