usearch 2.8.7 → 2.8.8

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 (102) hide show
  1. package/CITATION.cff +1 -1
  2. package/CMakeLists.txt +3 -3
  3. package/CONTRIBUTING.md +3 -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 +27 -23
  12. package/package.json +1 -2
  13. package/setup.py +76 -15
  14. package/wasmer.toml +1 -1
  15. package/. npmignore +0 -14
  16. package/.gitignore +0 -95
  17. package/.vscode/launch.json +0 -181
  18. package/.vscode/settings.json +0 -180
  19. package/.vscode/tasks.json +0 -71
  20. package/Package.swift +0 -41
  21. package/csharp/.editorconfig +0 -361
  22. package/csharp/Cloud.Unum.USearch.sln +0 -34
  23. package/csharp/DEV_SETUP.md +0 -51
  24. package/csharp/Directory.Build.props +0 -29
  25. package/csharp/README.md +0 -74
  26. package/csharp/build_and_test.cmd +0 -31
  27. package/csharp/build_and_test.sh +0 -30
  28. package/csharp/nuget/nuget-package.props +0 -33
  29. package/csharp/pack.sh +0 -56
  30. package/csharp/src/Cloud.Unum.USearch.Tests/Cloud.Unum.USearch.Tests.csproj +0 -39
  31. package/csharp/src/Cloud.Unum.USearch.Tests/USearchIndexTests.cs +0 -1013
  32. package/csharp/src/Cloud.Unum.USearch.Tests/Usings.cs +0 -1
  33. package/docs/_static/custom.css +0 -26
  34. package/docs/_static/custom.js +0 -7
  35. package/docs/benchmarks.md +0 -245
  36. package/docs/benchmarks.rst +0 -5
  37. package/docs/c/index.rst +0 -11
  38. package/docs/c/reference.rst +0 -7
  39. package/docs/conf.dox +0 -2668
  40. package/docs/conf.py +0 -53
  41. package/docs/contributing.rst +0 -5
  42. package/docs/cpp/index.rst +0 -11
  43. package/docs/cpp/reference.rst +0 -17
  44. package/docs/csharp/index.rst +0 -6
  45. package/docs/format.md +0 -86
  46. package/docs/format.rst +0 -5
  47. package/docs/golang/index.rst +0 -6
  48. package/docs/index.rst +0 -33
  49. package/docs/java/index.rst +0 -11
  50. package/docs/java/reference.rst +0 -7
  51. package/docs/javascript/index.rst +0 -11
  52. package/docs/javascript/reference.rst +0 -12
  53. package/docs/python/index.rst +0 -11
  54. package/docs/python/reference.rst +0 -42
  55. package/docs/rust/index.rst +0 -6
  56. package/docs/swift/index.rst +0 -6
  57. package/docs/wolfram/index.rst +0 -6
  58. package/golang/README.md +0 -95
  59. package/golang/go.mod +0 -3
  60. package/golang/lib.go +0 -379
  61. package/golang/lib_test.go +0 -108
  62. package/java/README.md +0 -22
  63. package/java/cloud/unum/usearch/Index.java +0 -337
  64. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +0 -197
  65. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +0 -125
  66. package/java/test/IndexTest.java +0 -30
  67. package/objc/USearchObjective.mm +0 -299
  68. package/objc/include/USearchObjective.h +0 -154
  69. package/python/README.md +0 -292
  70. package/python/lib.cpp +0 -1216
  71. package/python/scripts/bench.ipynb +0 -193
  72. package/python/scripts/bench.py +0 -137
  73. package/python/scripts/bench_exact.py +0 -44
  74. package/python/scripts/cluster.ipynb +0 -160
  75. package/python/scripts/index_faiss.py +0 -100
  76. package/python/scripts/join.py +0 -155
  77. package/python/scripts/test_index.py +0 -247
  78. package/python/scripts/test_jit.py +0 -231
  79. package/python/scripts/test_sparse.py +0 -32
  80. package/python/scripts/test_tooling.py +0 -133
  81. package/python/usearch/__init__.py +0 -0
  82. package/python/usearch/client.py +0 -124
  83. package/python/usearch/eval.py +0 -470
  84. package/python/usearch/index.py +0 -1401
  85. package/python/usearch/io.py +0 -113
  86. package/python/usearch/numba.py +0 -121
  87. package/python/usearch/server.py +0 -137
  88. package/rust/README.md +0 -66
  89. package/rust/lib.cpp +0 -136
  90. package/rust/lib.hpp +0 -69
  91. package/rust/lib.rs +0 -610
  92. package/swift/Index+Sugar.swift +0 -153
  93. package/swift/README.md +0 -48
  94. package/swift/Test.swift +0 -37
  95. package/swift/USearch.swift +0 -8
  96. package/wasm/CMakeLists.txt +0 -7
  97. package/wasm/README.md +0 -13
  98. package/wasm/lib.cpp +0 -5
  99. package/wolfram/CMakeLists.txt +0 -18
  100. package/wolfram/README.md +0 -13
  101. package/wolfram/lib.cpp +0 -160
  102. package/wolfram/test.wls +0 -27
@@ -1,1401 +0,0 @@
1
- from __future__ import annotations
2
- from inspect import signature
3
-
4
- # The purpose of this file is to provide Pythonic wrapper on top
5
- # the native precompiled CPython module. It improves compatibility
6
- # Python tooling, linters, and static analyzers. It also embeds JIT
7
- # into the primary `Index` class, connecting USearch with Numba.
8
- import os
9
- import math
10
- from dataclasses import dataclass
11
- from typing import (
12
- Any,
13
- Optional,
14
- Union,
15
- NamedTuple,
16
- List,
17
- Iterable,
18
- Tuple,
19
- Dict,
20
- Callable,
21
- )
22
-
23
- import numpy as np
24
- from tqdm import tqdm
25
-
26
- # Precompiled symbols that won't be exposed directly:
27
- from usearch.compiled import (
28
- Index as _CompiledIndex,
29
- Indexes as _CompiledIndexes,
30
- IndexStats as _CompiledIndexStats,
31
- index_dense_metadata_from_path as _index_dense_metadata_from_path,
32
- index_dense_metadata_from_buffer as _index_dense_metadata_from_buffer,
33
- exact_search as _exact_search,
34
- hardware_acceleration as _hardware_acceleration,
35
- )
36
-
37
- # Precompiled symbols that will be exposed
38
- from usearch.compiled import (
39
- MetricKind,
40
- ScalarKind,
41
- MetricSignature,
42
- DEFAULT_CONNECTIVITY,
43
- DEFAULT_EXPANSION_ADD,
44
- DEFAULT_EXPANSION_SEARCH,
45
- USES_OPENMP,
46
- USES_SIMSIMD,
47
- USES_NATIVE_F16,
48
- )
49
-
50
- MetricKindBitwise = (
51
- MetricKind.Hamming,
52
- MetricKind.Tanimoto,
53
- MetricKind.Sorensen,
54
- )
55
-
56
-
57
- class CompiledMetric(NamedTuple):
58
- pointer: int
59
- kind: MetricKind
60
- signature: MetricSignature
61
-
62
-
63
- Key = np.uint64
64
-
65
- KeyOrKeysLike = Union[Key, Iterable[Key], int, Iterable[int], np.ndarray, memoryview]
66
-
67
- VectorOrVectorsLike = Union[np.ndarray, Iterable[np.ndarray], memoryview]
68
-
69
- DTypeLike = Union[str, ScalarKind]
70
-
71
- MetricLike = Union[str, MetricKind, CompiledMetric]
72
-
73
- NoneType = type(None)
74
-
75
- PathOrBuffer = Union[str, os.PathLike, bytes]
76
-
77
- ProgressCallback = Callable[[int, int], bool]
78
-
79
-
80
- def _match_signature(
81
- func: Callable[[Any], Any], arg_types: List[type], ret_type: type
82
- ) -> bool:
83
- assert callable(func), "Not callable"
84
- sig = signature(func)
85
- param_types = [param.annotation for param in sig.parameters.values()]
86
- return param_types == arg_types and sig.return_annotation == ret_type
87
-
88
-
89
- def _normalize_dtype(
90
- dtype,
91
- ndim: int = 0,
92
- metric: MetricKind = MetricKind.Cos,
93
- ) -> ScalarKind:
94
- if dtype is None or dtype == "":
95
- if metric in MetricKindBitwise:
96
- return ScalarKind.B1
97
- if _hardware_acceleration(dtype=ScalarKind.F16, ndim=ndim, metric_kind=metric):
98
- return ScalarKind.F16
99
- return ScalarKind.F32
100
-
101
- if isinstance(dtype, ScalarKind):
102
- return dtype
103
-
104
- if isinstance(dtype, str):
105
- dtype = dtype.lower()
106
-
107
- _normalize = {
108
- "f64": ScalarKind.F64,
109
- "f32": ScalarKind.F32,
110
- "f16": ScalarKind.F16,
111
- "i8": ScalarKind.I8,
112
- "b1": ScalarKind.B1,
113
- "b1x8": ScalarKind.B1,
114
- "float64": ScalarKind.F64,
115
- "float32": ScalarKind.F32,
116
- "float16": ScalarKind.F16,
117
- "int8": ScalarKind.I8,
118
- np.float64: ScalarKind.F64,
119
- np.float32: ScalarKind.F32,
120
- np.float16: ScalarKind.F16,
121
- np.int8: ScalarKind.I8,
122
- np.uint8: ScalarKind.B1,
123
- }
124
- return _normalize[dtype]
125
-
126
-
127
- def _to_numpy_dtype(dtype: ScalarKind):
128
- _normalize = {
129
- ScalarKind.F64: np.float64,
130
- ScalarKind.F32: np.float32,
131
- ScalarKind.F16: np.float16,
132
- ScalarKind.I8: np.int8,
133
- ScalarKind.B1: np.uint8,
134
- }
135
- if dtype in _normalize.values():
136
- return dtype
137
- return _normalize[dtype]
138
-
139
-
140
- def _normalize_metric(metric) -> MetricKind:
141
- if metric is None:
142
- return MetricKind.Cos
143
-
144
- if isinstance(metric, str):
145
- _normalize = {
146
- "cos": MetricKind.Cos,
147
- "cosine": MetricKind.Cos,
148
- "ip": MetricKind.IP,
149
- "dot": MetricKind.IP,
150
- "inner_product": MetricKind.IP,
151
- "l2sq": MetricKind.L2sq,
152
- "l2_sq": MetricKind.L2sq,
153
- "haversine": MetricKind.Haversine,
154
- "divergence": MetricKind.Divergence,
155
- "pearson": MetricKind.Pearson,
156
- "hamming": MetricKind.Hamming,
157
- "tanimoto": MetricKind.Tanimoto,
158
- "sorensen": MetricKind.Sorensen,
159
- }
160
- return _normalize[metric.lower()]
161
-
162
- return metric
163
-
164
-
165
- def _search_in_compiled(
166
- compiled_callable: Callable,
167
- vectors: np.ndarray,
168
- *,
169
- log: Union[str, bool],
170
- progress: Optional[ProgressCallback],
171
- **kwargs,
172
- ) -> Union[Matches, BatchMatches]:
173
- #
174
- assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
175
- assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
176
- assert not progress or _match_signature(
177
- progress, [int, int], bool
178
- ), "Invalid callback"
179
-
180
- if vectors.ndim == 1:
181
- vectors = vectors.reshape(1, len(vectors))
182
- count_vectors = vectors.shape[0]
183
-
184
- def distill_batch(
185
- batch_matches: BatchMatches,
186
- ) -> Union[BatchMatches, Matches]:
187
- return batch_matches[0] if count_vectors == 1 else batch_matches
188
-
189
- # Create progress bar if needed
190
- if log:
191
- name = log if isinstance(log, str) else "Search"
192
- progress_bar = tqdm(
193
- desc=name,
194
- total=count_vectors,
195
- unit="vector",
196
- )
197
-
198
- def update_progress_bar(processed: int, total: int) -> bool:
199
- progress_bar.update(processed - progress_bar.n)
200
- return progress if progress else True
201
-
202
- tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
203
- progress_bar.close()
204
- else:
205
- tuple_ = compiled_callable(vectors, **kwargs)
206
-
207
- return distill_batch(BatchMatches(*tuple_))
208
-
209
-
210
- def _add_to_compiled(
211
- compiled,
212
- *,
213
- keys,
214
- vectors,
215
- copy: bool,
216
- threads: int,
217
- log: Union[str, bool],
218
- progress: Optional[ProgressCallback],
219
- ) -> Union[int, np.ndarray]:
220
- #
221
- assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
222
- assert not progress or _match_signature(
223
- progress, [int, int], bool
224
- ), "Invalid callback"
225
- assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
226
- if vectors.ndim == 1:
227
- vectors = vectors.reshape(1, len(vectors))
228
-
229
- # Validate or generate the keys
230
- count_vectors = vectors.shape[0]
231
- generate_labels = keys is None
232
- if generate_labels:
233
- start_id = len(compiled)
234
- keys = np.arange(start_id, start_id + count_vectors, dtype=Key)
235
- else:
236
- if not isinstance(keys, Iterable):
237
- assert count_vectors == 1, "Each vector must have a key"
238
- keys = [keys]
239
- keys = np.array(keys).astype(Key)
240
-
241
- assert len(keys) == count_vectors
242
-
243
- # Create progress bar if needed
244
- if log:
245
- name = log if isinstance(log, str) else "Add"
246
- pbar = tqdm(
247
- desc=name,
248
- total=count_vectors,
249
- unit="vector",
250
- )
251
-
252
- def update_progress_bar(processed: int, total: int) -> bool:
253
- pbar.update(processed - pbar.n)
254
- return progress(processed, total) if progress else True
255
-
256
- compiled.add_many(
257
- keys,
258
- vectors,
259
- copy=copy,
260
- threads=threads,
261
- progress=update_progress_bar,
262
- )
263
- pbar.close()
264
- else:
265
- compiled.add_many(keys, vectors, copy=copy, threads=threads, progress=progress)
266
-
267
- return keys
268
-
269
-
270
- @dataclass
271
- class Match:
272
- """This class contains information about retrieved vector."""
273
-
274
- key: int
275
- distance: float
276
-
277
- def to_tuple(self) -> tuple:
278
- return self.key, self.distance
279
-
280
-
281
- @dataclass
282
- class Matches:
283
- """This class contains information about multiple retrieved vectors for single query,
284
- i.e it is a set of `Match` instances."""
285
-
286
- keys: np.ndarray
287
- distances: np.ndarray
288
-
289
- visited_members: int = 0
290
- computed_distances: int = 0
291
-
292
- def __len__(self) -> int:
293
- return len(self.keys)
294
-
295
- def __getitem__(self, index: int) -> Match:
296
- if isinstance(index, int) and index < len(self):
297
- return Match(
298
- key=self.keys[index],
299
- distance=self.distances[index],
300
- )
301
- else:
302
- raise IndexError(f"`index` must be an integer under {len(self)}")
303
-
304
- def to_list(self) -> List[tuple]:
305
- """
306
- Convert matches to the list of tuples which contain matches' indices and distances to them.
307
- """
308
-
309
- return [(int(l), float(d)) for l, d in zip(self.keys, self.distances)]
310
-
311
- def __repr__(self) -> str:
312
- return f"usearch.Matches({len(self)})"
313
-
314
-
315
- @dataclass
316
- class BatchMatches:
317
- """This class contains information about multiple retrieved vectors for multiple queries,
318
- i.e it is a set of `Matches` instances."""
319
-
320
- keys: np.ndarray
321
- distances: np.ndarray
322
- counts: np.ndarray
323
-
324
- visited_members: int = 0
325
- computed_distances: int = 0
326
-
327
- def __len__(self) -> int:
328
- return len(self.counts)
329
-
330
- def __getitem__(self, index: int) -> Matches:
331
- if isinstance(index, int) and index < len(self):
332
- return Matches(
333
- keys=self.keys[index, : self.counts[index]],
334
- distances=self.distances[index, : self.counts[index]],
335
- visited_members=self.visited_members // len(self),
336
- computed_distances=self.computed_distances // len(self),
337
- )
338
- else:
339
- raise IndexError(f"`index` must be an integer under {len(self)}")
340
-
341
- def to_list(self) -> List[List[tuple]]:
342
- """Convert the result for each query to the list of tuples with information about its matches."""
343
- list_of_matches = [self.__getitem__(row) for row in range(self.__len__())]
344
- return [match.to_tuple() for matches in list_of_matches for match in matches]
345
-
346
- def mean_recall(self, expected: np.ndarray, count: Optional[int] = None) -> float:
347
- """Measures recall [0, 1] as of `Matches` that contain the corresponding
348
- `expected` entry anywhere among results."""
349
- return self.count_matches(expected, count=count) / len(expected)
350
-
351
- def count_matches(self, expected: np.ndarray, count: Optional[int] = None) -> int:
352
- """Measures recall [0, len(expected)] as of `Matches` that contain the corresponding
353
- `expected` entry anywhere among results.
354
- """
355
- assert len(expected) == len(self)
356
- recall = 0
357
- if count is None:
358
- count = self.keys.shape[1]
359
-
360
- if count == 1:
361
- recall = np.sum(self.keys[:, 0] == expected)
362
- else:
363
- for i in range(len(self)):
364
- recall += expected[i] in self.keys[i, :count]
365
- return recall
366
-
367
- def __repr__(self) -> str:
368
- return f"usearch.BatchMatches({np.sum(self.counts)} across {len(self)} queries)"
369
-
370
-
371
- @dataclass
372
- class Clustering:
373
- def __init__(
374
- self,
375
- index: Index,
376
- matches: BatchMatches,
377
- queries: Optional[np.ndarray] = None,
378
- ) -> None:
379
- if queries is None:
380
- queries = index._compiled.get_keys_in_slice()
381
- self.index = index
382
- self.queries = queries
383
- self.matches = matches
384
-
385
- def __repr__(self) -> str:
386
- return f"usearch.Clustering(for {len(self.queries)} queries)"
387
-
388
- @property
389
- def centroids_popularity(self) -> Tuple[np.ndarray, np.ndarray]:
390
- return np.unique(self.matches.keys, return_counts=True)
391
-
392
- def members_of(self, centroid: Key) -> np.ndarray:
393
- return self.queries[self.matches.keys.flatten() == centroid]
394
-
395
- def subcluster(self, centroid: Key, **clustering_kwards) -> Clustering:
396
- sub_keys = self.members_of(centroid)
397
- return self.index.cluster(keys=sub_keys, **clustering_kwards)
398
-
399
- def plot_centroids_popularity(self):
400
- from matplotlib import pyplot as plt
401
-
402
- _, sizes = self.centroids_popularity
403
- plt.yscale("log")
404
- plt.plot(sorted(sizes), np.arange(len(sizes)))
405
- plt.show()
406
-
407
- @property
408
- def network(self):
409
- import networkx as nx
410
-
411
- keys, sizes = self.centroids_popularity
412
-
413
- g = nx.Graph()
414
- for key, size in zip(keys, sizes):
415
- g.add_node(key, size=size)
416
-
417
- for i, i_key in enumerate(keys):
418
- for j_key in keys[:i]:
419
- d = self.index.pairwise_distance(i_key, j_key)
420
- g.add_edge(i_key, j_key, distance=d)
421
-
422
- return g
423
-
424
-
425
- class IndexedKeys:
426
- """Smart-reference for the range of keys present in a specific `Index`"""
427
-
428
- def __init__(self, index: Index) -> None:
429
- self.index = index
430
-
431
- def __len__(self) -> int:
432
- return len(self.index)
433
-
434
- def __getitem__(
435
- self,
436
- offset_offsets_or_slice: Union[int, np.ndarray, slice],
437
- ) -> Union[Key, np.ndarray]:
438
- if isinstance(offset_offsets_or_slice, slice):
439
- start, stop, step = offset_offsets_or_slice.indices(len(self))
440
- if step:
441
- raise
442
- return self.index._compiled.get_keys_in_slice(start, stop - start)
443
-
444
- elif isinstance(offset_offsets_or_slice, Iterable):
445
- offsets = np.array(offset_offsets_or_slice)
446
- return self.index._compiled.get_keys_at_offsets(offsets)
447
-
448
- else:
449
- offset = int(offset_offsets_or_slice)
450
- return self.index._compiled.get_key_at_offset(offset)
451
-
452
- def __array__(self, dtype=None) -> np.ndarray:
453
- if dtype is None:
454
- dtype = Key
455
- return self.index._compiled.get_keys_in_slice().astype(dtype)
456
-
457
-
458
- class Index:
459
- """Fast vector-search engine for dense equi-dimensional embeddings.
460
-
461
- Vector keys must be integers.
462
- Vectors must have the same number of dimensions within the index.
463
- Supports Inner Product, Cosine Distance, L^n measures like the Euclidean metric,
464
- as well as automatic downcasting to low-precision floating-point and integral
465
- representations.
466
- """
467
-
468
- def __init__(
469
- self,
470
- *,
471
- ndim: int = 0,
472
- metric: MetricLike = MetricKind.Cos,
473
- dtype: Optional[DTypeLike] = None,
474
- connectivity: Optional[int] = None,
475
- expansion_add: Optional[int] = None,
476
- expansion_search: Optional[int] = None,
477
- multi: bool = False,
478
- path: Optional[os.PathLike] = None,
479
- view: bool = False,
480
- enable_key_lookups: bool = True,
481
- ) -> None:
482
- """Construct the index and compiles the functions, if requested (expensive).
483
-
484
- :param ndim: Number of vector dimensions
485
- :type ndim: int
486
- Required for some metrics, pre-set for others.
487
- Haversine, for example, only applies to 2-dimensional latitude/longitude
488
- coordinates. Angular (Cos) and Euclidean (L2sq), obviously, apply to
489
- vectors with arbitrary number of dimensions.
490
-
491
- :param metric: Distance function
492
- :type metric: MetricLike, defaults to MetricKind.Cos
493
- Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
494
- Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
495
- Hamming, Tanimoto, Sorensen.
496
-
497
- :param dtype: Scalar type for internal vector storage
498
- :type dtype: Optional[DTypeLike], defaults to None
499
- For continuous metrics can be: f16, f32, f64, or i8.
500
- For bitwise metrics it's implementation-defined, and can't change.
501
- If nothing is provided, the optimal data type is selected based on the metric
502
- kind and hardware support.
503
- Example: you can use the `f16` index with `f32` vectors in Euclidean space,
504
- which will be automatically downcasted. Moreover, if `dtype=None` is passed,
505
- and hardware supports `f16` SIMD-instructions, this choice will be done for you.
506
- You can later double-check the used representation with `index.dtype`.
507
-
508
- :param connectivity: Connections per node in HNSW
509
- :type connectivity: Optional[int], defaults to None
510
- Hyper-parameter for the number of Graph connections
511
- per layer of HNSW. The original paper calls it "M".
512
- Optional, but can't be changed after construction.
513
-
514
- :param expansion_add: Traversal depth on insertions
515
- :type expansion_add: Optional[int], defaults to None
516
- Hyper-parameter for the search depth when inserting new
517
- vectors. The original paper calls it "efConstruction".
518
- Can be changed afterwards, as the `.expansion_add`.
519
-
520
- :param expansion_search: Traversal depth on queries
521
- :type expansion_search: Optional[int], defaults to None
522
- Hyper-parameter for the search depth when querying
523
- nearest neighbors. The original paper calls it "ef".
524
- Can be changed afterwards, as the `.expansion_search`.
525
-
526
- :param multi: Allow multiple vectors with the same key
527
- :type multi: bool, defaults to True
528
- :param path: Where to store the index
529
- :type path: Optional[os.PathLike], defaults to None
530
- :param view: Are we simply viewing an immutable index
531
- :type view: bool, defaults to False
532
- """
533
-
534
- if connectivity is None:
535
- connectivity = DEFAULT_CONNECTIVITY
536
- if expansion_add is None:
537
- expansion_add = DEFAULT_EXPANSION_ADD
538
- if expansion_search is None:
539
- expansion_search = DEFAULT_EXPANSION_SEARCH
540
-
541
- assert isinstance(connectivity, int), "Expects integer `connectivity`"
542
- assert isinstance(expansion_add, int), "Expects integer `expansion_add`"
543
- assert isinstance(expansion_search, int), "Expects integer `expansion_search`"
544
-
545
- metric = _normalize_metric(metric)
546
- if isinstance(metric, MetricKind):
547
- self._metric_kind = metric
548
- self._metric_jit = None
549
- self._metric_pointer = 0
550
- self._metric_signature = MetricSignature.ArrayArraySize
551
- elif isinstance(metric, CompiledMetric):
552
- self._metric_jit = metric
553
- self._metric_kind = metric.kind
554
- self._metric_pointer = metric.pointer
555
- self._metric_signature = metric.signature
556
- else:
557
- raise ValueError(
558
- "The `metric` must be a `CompiledMetric` or a `MetricKind`"
559
- )
560
-
561
- # Validate, that the right scalar type is defined
562
- dtype = _normalize_dtype(dtype, ndim, self._metric_kind)
563
- self._compiled = _CompiledIndex(
564
- ndim=ndim,
565
- dtype=dtype,
566
- connectivity=connectivity,
567
- expansion_add=expansion_add,
568
- expansion_search=expansion_search,
569
- multi=multi,
570
- enable_key_lookups=enable_key_lookups,
571
- metric_kind=self._metric_kind,
572
- metric_pointer=self._metric_pointer,
573
- metric_signature=self._metric_signature,
574
- )
575
-
576
- self.path = path
577
- if path is not None and os.path.exists(path):
578
- if view:
579
- self.view(path)
580
- else:
581
- self.load(path)
582
-
583
- @staticmethod
584
- def metadata(path_or_buffer: PathOrBuffer) -> Optional[dict]:
585
- try:
586
- if isinstance(path_or_buffer, bytearray):
587
- path_or_buffer = bytes(path_or_buffer)
588
- if isinstance(path_or_buffer, bytes):
589
- return _index_dense_metadata_from_buffer(path_or_buffer)
590
- else:
591
- path_or_buffer = os.fspath(path_or_buffer)
592
- if not os.path.exists(path_or_buffer):
593
- return None
594
- return _index_dense_metadata_from_path(path_or_buffer)
595
- except Exception as e:
596
- raise e
597
-
598
- @staticmethod
599
- def restore(path_or_buffer: PathOrBuffer, view: bool = False) -> Optional[Index]:
600
- meta = Index.metadata(path_or_buffer)
601
- if not meta:
602
- return None
603
-
604
- index = Index(
605
- ndim=meta["dimensions"],
606
- dtype=meta["kind_scalar"],
607
- metric=meta["kind_metric"],
608
- )
609
-
610
- if view:
611
- index.view(path_or_buffer)
612
- else:
613
- index.load(path_or_buffer)
614
- return index
615
-
616
- def __len__(self) -> int:
617
- return self._compiled.__len__()
618
-
619
- def add(
620
- self,
621
- keys: KeyOrKeysLike,
622
- vectors: VectorOrVectorsLike,
623
- *,
624
- copy: bool = True,
625
- threads: int = 0,
626
- log: Union[str, bool] = False,
627
- progress: Optional[ProgressCallback] = None,
628
- ) -> Union[int, np.ndarray]:
629
- """Inserts one or move vectors into the index.
630
-
631
- For maximal performance the `keys` and `vectors`
632
- should conform to the Python's "buffer protocol" spec.
633
-
634
- To index a single entry:
635
- keys: int, vectors: np.ndarray.
636
- To index many entries:
637
- keys: np.ndarray, vectors: np.ndarray.
638
-
639
- When working with extremely large indexes, you may want to
640
- pass `copy=False`, if you can guarantee the lifetime of the
641
- primary vectors store during the process of construction.
642
-
643
- :param keys: Unique identifier(s) for passed vectors
644
- :type keys: Optional[KeyOrKeysLike], can be `None`
645
- :param vectors: Vector or a row-major matrix
646
- :type vectors: VectorOrVectorsLike
647
- :param copy: Should the index store a copy of vectors
648
- :type copy: bool, defaults to True
649
- :param threads: Optimal number of cores to use
650
- :type threads: int, defaults to 0
651
- :param log: Whether to print the progress bar
652
- :type log: Union[str, bool], defaults to False
653
- :param progress: Callback to report stats of the progress and control it
654
- :type progress: Optional[ProgressCallback], defaults to None
655
- :return: Inserted key or keys
656
- :type: Union[int, np.ndarray]
657
- """
658
- return _add_to_compiled(
659
- self._compiled,
660
- keys=keys,
661
- vectors=vectors,
662
- copy=copy,
663
- threads=threads,
664
- log=log,
665
- progress=progress,
666
- )
667
-
668
- def search(
669
- self,
670
- vectors: VectorOrVectorsLike,
671
- count: int = 10,
672
- radius: float = math.inf,
673
- *,
674
- threads: int = 0,
675
- exact: bool = False,
676
- log: Union[str, bool] = False,
677
- progress: Optional[ProgressCallback] = None,
678
- ) -> Union[Matches, BatchMatches]:
679
- """
680
- Performs approximate nearest neighbors search for one or more queries.
681
-
682
- :param vectors: Query vector or vectors.
683
- :type vectors: VectorOrVectorsLike
684
- :param count: Upper count on the number of matches to find
685
- :type count: int, defaults to 10
686
- :param threads: Optimal number of cores to use
687
- :type threads: int, defaults to 0
688
- :param exact: Perform exhaustive linear-time exact search
689
- :type exact: bool, defaults to False
690
- :param log: Whether to print the progress bar, default to False
691
- :type log: Union[str, bool], optional
692
- :param progress: Callback to report stats of the progress and control it
693
- :type progress: Optional[ProgressCallback], defaults to None
694
- :return: Matches for one or more queries
695
- :rtype: Union[Matches, BatchMatches]
696
- """
697
-
698
- return _search_in_compiled(
699
- self._compiled.search_many,
700
- vectors,
701
- # Batch scheduling:
702
- log=log,
703
- # Search constraints:
704
- count=count,
705
- exact=exact,
706
- threads=threads,
707
- progress=progress,
708
- )
709
-
710
- def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
711
- if isinstance(keys, Iterable):
712
- return self._compiled.contains_many(np.array(keys, dtype=Key))
713
- else:
714
- return self._compiled.contains_one(int(keys))
715
-
716
- def __contains__(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
717
- return self.contains(keys)
718
-
719
- def count(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
720
- if isinstance(keys, Iterable):
721
- return self._compiled.count_many(np.array(keys, dtype=Key))
722
- else:
723
- return self._compiled.count_one(int(keys))
724
-
725
- def get(
726
- self,
727
- keys: KeyOrKeysLike,
728
- dtype: Optional[DTypeLike] = None,
729
- ) -> Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]:
730
- """Looks up one or more keys from the `Index`, retrieving corresponding vectors.
731
-
732
- Returns `None`, if one key is requested, and its not present.
733
- Returns a (row) vector, if the key maps into a single vector.
734
- Returns a (row-major) matrix, if the key maps into a multiple vectors.
735
- If multiple keys are requested, composes many such responses into a `tuple`.
736
-
737
- :param keys: One or more keys to lookup
738
- :type keys: KeyOrKeysLike
739
- :return: One or more keys lookup results
740
- :rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
741
- """
742
- if not dtype:
743
- dtype = self.dtype
744
- else:
745
- dtype = _normalize_dtype(dtype)
746
-
747
- view_dtype = _to_numpy_dtype(dtype)
748
-
749
- def cast(result):
750
- if result is not None:
751
- return result.view(view_dtype)
752
- return result
753
-
754
- is_one = not isinstance(keys, Iterable)
755
- if is_one:
756
- keys = [keys]
757
- if not isinstance(keys, np.ndarray):
758
- keys = np.array(keys, dtype=Key)
759
- else:
760
- keys = keys.astype(Key)
761
-
762
- results = self._compiled.get_many(keys, dtype)
763
- results = [cast(result) for result in results]
764
- return results[0] if is_one else results
765
-
766
- def __getitem__(
767
- self, keys: KeyOrKeysLike
768
- ) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
769
- """Looks up one or more keys from the `Index`, retrieving corresponding vectors.
770
-
771
- Returns `None`, if one key is requested, and its not present.
772
- Returns a (row) vector, if the key maps into a single vector.
773
- Returns a (row-major) matrix, if the key maps into a multiple vectors.
774
- If multiple keys are requested, composes many such responses into a `tuple`.
775
-
776
- :param keys: One or more keys to lookup
777
- :type keys: KeyOrKeysLike
778
- :return: One or more keys lookup results
779
- :rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
780
- """
781
- return self.get(keys)
782
-
783
- def remove(
784
- self,
785
- keys: KeyOrKeysLike,
786
- *,
787
- compact: bool = False,
788
- threads: int = 0,
789
- ) -> Union[int, np.ndarray]:
790
- """Removes one or move vectors from the index.
791
-
792
- When working with extremely large indexes, you may want to
793
- mark some entries deleted, instead of rebuilding a filtered index.
794
- In other cases, rebuilding - is the recommended approach.
795
-
796
- :param keys: Unique identifier for passed vectors, optional
797
- :type keys: KeyOrKeysLike
798
- :param compact: Removes links to removed nodes (expensive), defaults to False
799
- :type compact: bool, optional
800
- :param threads: Optimal number of cores to use, defaults to 0
801
- :type threads: int, optional
802
- :return: Array of integers for the number of removed vectors per key
803
- :type: Union[int, np.ndarray]
804
- """
805
- if not isinstance(keys, Iterable):
806
- return self._compiled.remove_one(keys, compact=compact, threads=threads)
807
- else:
808
- keys = np.array(keys, dtype=Key)
809
- return self._compiled.remove_many(keys, compact=compact, threads=threads)
810
-
811
- def __delitem__(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
812
- raise self.remove(keys)
813
-
814
- def rename(
815
- self,
816
- from_: KeyOrKeysLike,
817
- to: KeyOrKeysLike,
818
- ) -> Union[int, np.ndarray]:
819
- """Rename existing member vector or vectors.
820
-
821
- May be used in iterative clustering procedures, where one would iteratively
822
- relabel every vector with the name of the cluster an entry belongs to, until
823
- the system converges.
824
-
825
- :param from_: One or more keys to be renamed
826
- :type from_: KeyOrKeysLike
827
- :param to: New name or names (of identical length as `from_`)
828
- :type to: KeyOrKeysLike
829
- :return: Number of vectors that were found and renamed
830
- :rtype: int
831
- """
832
- if isinstance(from_, Iterable):
833
- from_ = np.array(from_, dtype=Key)
834
- if isinstance(to, Iterable):
835
- to = np.array(to, dtype=Key)
836
- return self._compiled.rename_many_to_many(from_, to)
837
-
838
- else:
839
- return self._compiled.rename_many_to_one(from_, int(to))
840
-
841
- else:
842
- return self._compiled.rename_one_to_one(int(from_), int(to))
843
-
844
- @property
845
- def jit(self) -> bool:
846
- """
847
- :return: True, if the provided `metric` was JIT-ed
848
- :rtype: bool
849
- """
850
- return self._metric_jit is not None
851
-
852
- @property
853
- def hardware_acceleration(self) -> str:
854
- """Describes the kind of hardware-acceleration support used in
855
- that exact instance of the `Index`, for that metric kind, and
856
- the given number of dimensions.
857
-
858
- :return: "auto", if nothing is available, ISA subset name otherwise
859
- :rtype: str
860
- """
861
- return self._compiled.hardware_acceleration
862
-
863
- @property
864
- def size(self) -> int:
865
- return self._compiled.size
866
-
867
- @property
868
- def ndim(self) -> int:
869
- return self._compiled.ndim
870
-
871
- @property
872
- def serialized_length(self) -> int:
873
- return self._compiled.serialized_length
874
-
875
- @property
876
- def metric_kind(self) -> Union[MetricKind, CompiledMetric]:
877
- return self._metric_jit.kind if self._metric_jit else self._metric_kind
878
-
879
- @property
880
- def metric(self) -> Union[MetricKind, CompiledMetric]:
881
- return self._metric_jit if self._metric_jit else self._metric_kind
882
-
883
- @metric.setter
884
- def metric(self, metric: MetricLike):
885
- metric = _normalize_metric(metric)
886
- if isinstance(metric, MetricKind):
887
- metric_kind = metric
888
- metric_pointer = 0
889
- metric_signature = MetricSignature.ArrayArraySize
890
- elif isinstance(metric, CompiledMetric):
891
- metric_kind = metric.kind
892
- metric_pointer = metric.pointer
893
- metric_signature = metric.signature
894
- else:
895
- raise ValueError(
896
- "The `metric` must be a `CompiledMetric` or a `MetricKind`"
897
- )
898
-
899
- return self._compiled.change_metric(
900
- metric_kind=metric_kind,
901
- metric_pointer=metric_pointer,
902
- metric_signature=metric_signature,
903
- )
904
-
905
- @property
906
- def dtype(self) -> ScalarKind:
907
- return self._compiled.dtype
908
-
909
- @property
910
- def connectivity(self) -> int:
911
- return self._compiled.connectivity
912
-
913
- @property
914
- def capacity(self) -> int:
915
- return self._compiled.capacity
916
-
917
- @property
918
- def memory_usage(self) -> int:
919
- return self._compiled.memory_usage
920
-
921
- @property
922
- def expansion_add(self) -> int:
923
- return self._compiled.expansion_add
924
-
925
- @property
926
- def expansion_search(self) -> int:
927
- return self._compiled.expansion_search
928
-
929
- @expansion_add.setter
930
- def expansion_add(self, v: int):
931
- self._compiled.expansion_add = v
932
-
933
- @expansion_search.setter
934
- def expansion_search(self, v: int):
935
- self._compiled.expansion_search = v
936
-
937
- def save(
938
- self,
939
- path_or_buffer: Union[str, os.PathLike, NoneType] = None,
940
- progress: Optional[ProgressCallback] = None,
941
- ) -> Optional[bytes]:
942
- assert not progress or _match_signature(
943
- progress, [int, int], bool
944
- ), "Invalid callback signature"
945
-
946
- path_or_buffer = path_or_buffer if path_or_buffer else self.path
947
- if path_or_buffer is None:
948
- return self._compiled.save_index_to_buffer(progress)
949
- else:
950
- self._compiled.save_index_to_path(os.fspath(path_or_buffer), progress)
951
-
952
- def load(
953
- self,
954
- path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None,
955
- progress: Optional[ProgressCallback] = None,
956
- ):
957
- assert not progress or _match_signature(
958
- progress, [int, int], bool
959
- ), "Invalid callback signature"
960
-
961
- path_or_buffer = path_or_buffer if path_or_buffer else self.path
962
- if path_or_buffer is None:
963
- raise Exception("Define the source")
964
- if isinstance(path_or_buffer, bytearray):
965
- path_or_buffer = bytes(path_or_buffer)
966
- if isinstance(path_or_buffer, bytes):
967
- self._compiled.load_index_from_buffer(path_or_buffer, progress)
968
- else:
969
- path_or_buffer = os.fspath(path_or_buffer)
970
- if os.path.exists(path_or_buffer):
971
- self._compiled.load_index_from_path(path_or_buffer, progress)
972
- else:
973
- raise RuntimeError("Missing file!")
974
-
975
- def view(
976
- self,
977
- path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None,
978
- progress: Optional[ProgressCallback] = None,
979
- ):
980
- assert not progress or _match_signature(
981
- progress, [int, int], bool
982
- ), "Invalid callback signature"
983
-
984
- path_or_buffer = path_or_buffer if path_or_buffer else self.path
985
- if path_or_buffer is None:
986
- raise Exception("Define the source")
987
- if isinstance(path_or_buffer, bytearray):
988
- path_or_buffer = bytes(path_or_buffer)
989
- if isinstance(path_or_buffer, bytes):
990
- self._compiled.view_index_from_buffer(path_or_buffer, progress)
991
- else:
992
- self._compiled.view_index_from_path(os.fspath(path_or_buffer), progress)
993
-
994
- def clear(self):
995
- """Erases all the vectors from the index, preserving the space for future insertions."""
996
- self._compiled.clear()
997
-
998
- def reset(self):
999
- """Erases all members from index, closing files, and returning RAM to OS."""
1000
- if not hasattr(self, "_compiled"):
1001
- return
1002
- self._compiled.reset()
1003
-
1004
- def __del__(self):
1005
- self.reset()
1006
-
1007
- def copy(self) -> Index:
1008
- result = Index(
1009
- ndim=self.ndim,
1010
- metric=self.metric,
1011
- dtype=self.dtype,
1012
- connectivity=self.connectivity,
1013
- expansion_add=self.expansion_add,
1014
- expansion_search=self.expansion_search,
1015
- path=self.path,
1016
- )
1017
- result._compiled = self._compiled.copy()
1018
- return result
1019
-
1020
- def join(
1021
- self,
1022
- other: Index,
1023
- max_proposals: int = 0,
1024
- exact: bool = False,
1025
- progress: Optional[ProgressCallback] = None,
1026
- ) -> Dict[Key, Key]:
1027
- """Performs "Semantic Join" or pairwise matching between `self` & `other` index.
1028
- Is different from `search`, as no collisions are allowed in resulting pairs.
1029
- Uses the concept of "Stable Marriages" from Combinatorics, famous for the 2012
1030
- Nobel Prize in Economics.
1031
-
1032
- :param other: Another index.
1033
- :type other: Index
1034
- :param max_proposals: Limit on candidates evaluated per vector, defaults to 0
1035
- :type max_proposals: int, optional
1036
- :param exact: Controls if underlying `search` should be exact, defaults to False
1037
- :type exact: bool, optional
1038
- :param progress: Callback to report stats of the progress and control it
1039
- :type progress: Optional[ProgressCallback], defaults to None
1040
- :return: Mapping from keys of `self` to keys of `other`
1041
- :rtype: Dict[Key, Key]
1042
- """
1043
- assert not progress or _match_signature(
1044
- progress, [int, int], bool
1045
- ), "Invalid callback signature"
1046
-
1047
- return self._compiled.join(
1048
- other=other._compiled,
1049
- max_proposals=max_proposals,
1050
- exact=exact,
1051
- progress=progress,
1052
- )
1053
-
1054
- def cluster(
1055
- self,
1056
- *,
1057
- vectors: Optional[np.ndarray] = None,
1058
- keys: Optional[np.ndarray] = None,
1059
- min_count: Optional[int] = None,
1060
- max_count: Optional[int] = None,
1061
- threads: int = 0,
1062
- log: Union[str, bool] = False,
1063
- progress: Optional[ProgressCallback] = None,
1064
- ) -> Clustering:
1065
- """
1066
- Clusters already indexed or provided `vectors`, mapping them to various centroids.
1067
-
1068
- :param vectors: .
1069
- :type vectors: Optional[VectorOrVectorsLike]
1070
- :param count: Upper bound on the number of clusters to produce
1071
- :type count: Optional[int], defaults to None
1072
-
1073
- :param threads: Optimal number of cores to use,
1074
- :type threads: int, defaults to 0
1075
- :param log: Whether to print the progress bar
1076
- :type log: Union[str, bool], defaults to False
1077
- :param progress: Callback to report stats of the progress and control it
1078
- :type progress: Optional[ProgressCallback], defaults to None
1079
- :return: Matches for one or more queries
1080
- :rtype: Union[Matches, BatchMatches]
1081
- """
1082
- assert not progress or _match_signature(
1083
- progress, [int, int], bool
1084
- ), "Invalid callback signature"
1085
-
1086
- if min_count is None:
1087
- min_count = 0
1088
- if max_count is None:
1089
- max_count = 0
1090
-
1091
- if vectors is not None:
1092
- assert keys is None, "You can either cluster vectors or member keys"
1093
- results = self._compiled.cluster_vectors(
1094
- vectors,
1095
- min_count=min_count,
1096
- max_count=max_count,
1097
- threads=threads,
1098
- progress=progress,
1099
- )
1100
- else:
1101
- if keys is None:
1102
- keys = self._compiled.get_keys_in_slice()
1103
- if not isinstance(keys, np.ndarray):
1104
- keys = np.array(keys)
1105
- keys = keys.astype(Key)
1106
- results = self._compiled.cluster_keys(
1107
- keys,
1108
- min_count=min_count,
1109
- max_count=max_count,
1110
- threads=threads,
1111
- progress=progress,
1112
- )
1113
-
1114
- batch_matches = BatchMatches(*results)
1115
- return Clustering(self, batch_matches, keys)
1116
-
1117
- def pairwise_distance(
1118
- self, left: KeyOrKeysLike, right: KeyOrKeysLike
1119
- ) -> Union[np.ndarray, float]:
1120
- assert isinstance(left, Iterable) == isinstance(right, Iterable)
1121
-
1122
- if not isinstance(left, Iterable):
1123
- return self._compiled.pairwise_distance(int(left), int(right))
1124
- else:
1125
- left = np.array(left).astype(Key)
1126
- right = np.array(right).astype(Key)
1127
- return self._compiled.pairwise_distances(left, right)
1128
-
1129
- @property
1130
- def keys(self) -> IndexedKeys:
1131
- return IndexedKeys(self)
1132
-
1133
- @property
1134
- def vectors(self) -> np.ndarray:
1135
- return self.get(self.keys, vstack=True)
1136
-
1137
- @property
1138
- def max_level(self) -> int:
1139
- return self._compiled.max_level
1140
-
1141
- @property
1142
- def nlevels(self) -> int:
1143
- return self._compiled.max_level + 1
1144
-
1145
- @property
1146
- def stats(self) -> _CompiledIndexStats:
1147
- """Get the accumulated statistics for the entire multi-level graph.
1148
-
1149
- :return: Statistics for the entire multi-level graph.
1150
- :rtype: _CompiledIndexStats
1151
-
1152
- Statistics:
1153
- - ``nodes`` (int): The number of nodes in that level.
1154
- - ``edges`` (int): The number of edges in that level.
1155
- - ``max_edges`` (int): The maximum possible number of edges in that level.
1156
- - ``allocated_bytes`` (int): The amount of allocated memory for that level.
1157
- """
1158
- return self._compiled.stats
1159
-
1160
- @property
1161
- def levels_stats(self) -> List[_CompiledIndexStats]:
1162
- """Get the accumulated statistics for every level graph.
1163
-
1164
- :return: Statistics for every level graph.
1165
- :rtype: List[_CompiledIndexStats]
1166
-
1167
- Statistics:
1168
- - ``nodes`` (int): The number of nodes in that level.
1169
- - ``edges`` (int): The number of edges in that level.
1170
- - ``max_edges`` (int): The maximum possible number of edges in that level.
1171
- - ``allocated_bytes`` (int): The amount of allocated memory for that level.
1172
- """
1173
- return self._compiled.levels_stats
1174
-
1175
- def level_stats(self, level: int) -> _CompiledIndexStats:
1176
- """Get statistics for one level of the index - one graph.
1177
-
1178
- :return: Statistics for one level of the index - one graph.
1179
- :rtype: _CompiledIndexStats
1180
-
1181
- Statistics:
1182
- - ``nodes`` (int): The number of nodes in that level.
1183
- - ``edges`` (int): The number of edges in that level.
1184
- - ``max_edges`` (int): The maximum possible number of edges in that level.
1185
- - ``allocated_bytes`` (int): The amount of allocated memory for that level.
1186
- """
1187
- return self._compiled.level_stats(level)
1188
-
1189
- @property
1190
- def specs(self) -> Dict[str, Union[str, int, bool]]:
1191
- if not hasattr(self, "_compiled"):
1192
- return "usearch.Index(failed)"
1193
- return {
1194
- "type": "usearch.Index",
1195
- "ndim": self.ndim,
1196
- "connectivity": self.connectivity,
1197
- "expansion_add": self.expansion_add,
1198
- "expansion_search": self.expansion_search,
1199
- "size": self.size,
1200
- "jit": self.jit,
1201
- "hardware_acceleration": self.hardware_acceleration,
1202
- "metric_kind": self.metric_kind,
1203
- "dtype": self.dtype,
1204
- "path": self.path,
1205
- "compiled_with_openmp": USES_OPENMP,
1206
- "compiled_with_simsimd": USES_SIMSIMD,
1207
- "compiled_with_native_f16": USES_NATIVE_F16,
1208
- }
1209
-
1210
- def __repr__(self) -> str:
1211
- if not hasattr(self, "_compiled"):
1212
- return "usearch.Index(failed)"
1213
- f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {:,} vectors in {} levels, {} hardware acceleration)"
1214
- return f.format(
1215
- self.dtype,
1216
- self.ndim,
1217
- self.metric_kind,
1218
- self.connectivity,
1219
- self.expansion_add,
1220
- self.expansion_search,
1221
- len(self),
1222
- self.nlevels,
1223
- self.hardware_acceleration,
1224
- )
1225
-
1226
- def _repr_pretty_(self, printer, cycle) -> str:
1227
- if not hasattr(self, "_compiled"):
1228
- return "usearch.Index(failed)"
1229
- level_stats = [
1230
- f"--- {i}. {self.level_stats(i).nodes:,} nodes" for i in range(self.nlevels)
1231
- ]
1232
- lines = "\n".join(
1233
- [
1234
- "usearch.Index",
1235
- "- config",
1236
- f"-- data type: {self.dtype}",
1237
- f"-- dimensions: {self.ndim}",
1238
- f"-- metric: {self.metric_kind}",
1239
- f"-- connectivity: {self.connectivity}",
1240
- f"-- expansion on addition:{self.expansion_add} candidates",
1241
- f"-- expansion on search: {self.expansion_search} candidates",
1242
- "- binary",
1243
- f"-- uses OpenMP: {USES_OPENMP}",
1244
- f"-- uses SimSIMD: {USES_SIMSIMD}",
1245
- f"-- supports half-precision: {USES_NATIVE_F16}",
1246
- f"-- uses hardware acceleration: {self.hardware_acceleration}",
1247
- "- state",
1248
- f"-- size: {self.size:,} vectors",
1249
- f"-- memory usage: {self.memory_usage:,} bytes",
1250
- f"-- max level: {self.max_level}",
1251
- *level_stats,
1252
- ]
1253
- )
1254
- printer.text(lines)
1255
-
1256
-
1257
- class Indexes:
1258
- def __init__(
1259
- self,
1260
- indexes: Iterable[Index] = [],
1261
- paths: Iterable[os.PathLike] = [],
1262
- view: bool = False,
1263
- threads: int = 0,
1264
- ) -> None:
1265
- self._compiled = _CompiledIndexes()
1266
- for index in indexes:
1267
- self._compiled.merge(index._compiled)
1268
- self._compiled.merge_paths(paths, view=view, threads=threads)
1269
-
1270
- def merge(self, index: Index):
1271
- self._compiled.merge(index._compiled)
1272
-
1273
- def merge_path(self, path: os.PathLike):
1274
- self._compiled.merge_path(os.fspath(path))
1275
-
1276
- def __len__(self) -> int:
1277
- return self._compiled.__len__()
1278
-
1279
- def search(
1280
- self,
1281
- vectors,
1282
- count: int = 10,
1283
- *,
1284
- threads: int = 0,
1285
- exact: bool = False,
1286
- progress: Optional[ProgressCallback] = None,
1287
- ):
1288
- return _search_in_compiled(
1289
- self._compiled.search_many,
1290
- vectors,
1291
- # Batch scheduling:
1292
- log=False,
1293
- # Search constraints:
1294
- count=count,
1295
- exact=exact,
1296
- threads=threads,
1297
- progress=progress,
1298
- )
1299
-
1300
-
1301
- def search(
1302
- dataset: np.ndarray,
1303
- query: np.ndarray,
1304
- count: int = 10,
1305
- metric: MetricLike = MetricKind.Cos,
1306
- *,
1307
- exact: bool = False,
1308
- threads: int = 0,
1309
- log: Union[str, bool] = False,
1310
- progress: Optional[ProgressCallback] = None,
1311
- ) -> Union[Matches, BatchMatches]:
1312
- """Shortcut for search, that can avoid index construction. Particularly useful for
1313
- tiny datasets, where brute-force exact search works fast enough.
1314
-
1315
- :param dataset: Row-major matrix.
1316
- :type dataset: np.ndarray
1317
- :param query: Query vector or vectors (also row-major), to find in `dataset`.
1318
- :type query: np.ndarray
1319
-
1320
- :param count: Upper count on the number of matches to find, defaults to 10
1321
- :type count: int, optional
1322
-
1323
- :param metric: Distance function
1324
- :type metric: MetricLike, defaults to MetricKind.Cos
1325
- Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
1326
- Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
1327
- Hamming, Tanimoto, Sorensen.
1328
-
1329
- :param threads: Optimal number of cores to use, defaults to 0
1330
- :type threads: int, optional
1331
- :param exact: Perform exhaustive linear-time exact search, defaults to False
1332
- :type exact: bool, optional
1333
- :param log: Whether to print the progress bar, default to False
1334
- :type log: Union[str, bool], optional
1335
- :param progress: Callback to report stats of the progress and control it
1336
- :type progress: Optional[ProgressCallback], defaults to None
1337
- :return: Matches for one or more queries
1338
- :rtype: Union[Matches, BatchMatches]
1339
- """
1340
- assert not progress or _match_signature(
1341
- progress, [int, int], bool
1342
- ), "Invalid callback signature"
1343
- assert dataset.ndim == 2, "Dataset must be a matrix, with a vector in each row"
1344
-
1345
- if not exact:
1346
- index = Index(
1347
- dataset.shape[1],
1348
- metric=metric,
1349
- dtype=dataset.dtype,
1350
- )
1351
- index.add(
1352
- None,
1353
- dataset,
1354
- threads=threads,
1355
- log=log,
1356
- progress=progress,
1357
- )
1358
- return index.search(
1359
- query,
1360
- count,
1361
- threads=threads,
1362
- log=log,
1363
- progress=progress,
1364
- )
1365
-
1366
- metric = _normalize_metric(metric)
1367
- if isinstance(metric, MetricKind):
1368
- metric_kind = metric
1369
- metric_pointer = 0
1370
- metric_signature = MetricSignature.ArrayArraySize
1371
- elif isinstance(metric, CompiledMetric):
1372
- metric_kind = metric.kind
1373
- metric_pointer = metric.pointer
1374
- metric_signature = metric.signature
1375
- else:
1376
- raise ValueError("The `metric` must be a `CompiledMetric` or a `MetricKind`")
1377
-
1378
- def search_batch(query, **kwargs):
1379
- assert dataset.shape[1] == query.shape[1], "Number of dimensions differs"
1380
- if dataset.dtype != query.dtype:
1381
- query = query.astype(dataset.dtype)
1382
-
1383
- return _exact_search(
1384
- dataset,
1385
- query,
1386
- metric_kind=metric_kind,
1387
- metric_signature=metric_signature,
1388
- metric_pointer=metric_pointer,
1389
- **kwargs,
1390
- )
1391
-
1392
- return _search_in_compiled(
1393
- search_batch,
1394
- query,
1395
- # Batch scheduling:
1396
- log=log,
1397
- # Search constraints:
1398
- count=count,
1399
- threads=threads,
1400
- progress=progress,
1401
- )