usearch 2.2.0 → 2.3.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.
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from inspect import signature
2
3
 
3
4
  # The purpose of this file is to provide Pythonic wrapper on top
4
5
  # the native precompiled CPython module. It improves compatibility
@@ -8,6 +9,7 @@ import os
8
9
  import math
9
10
  from dataclasses import dataclass
10
11
  from typing import (
12
+ Any,
11
13
  Optional,
12
14
  Union,
13
15
  NamedTuple,
@@ -72,6 +74,17 @@ NoneType = type(None)
72
74
 
73
75
  PathOrBuffer = Union[str, os.PathLike, bytes]
74
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
+
75
88
 
76
89
  def _normalize_dtype(
77
90
  dtype,
@@ -153,12 +166,16 @@ def _search_in_compiled(
153
166
  vectors: np.ndarray,
154
167
  *,
155
168
  log: Union[str, bool],
156
- batch_size: int,
169
+ progress: Optional[ProgressCallback],
157
170
  **kwargs,
158
171
  ) -> Union[Matches, BatchMatches]:
159
172
  #
160
173
  assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
161
174
  assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
175
+ assert not progress or _match_signature(
176
+ progress, [int, int], bool
177
+ ), "Invalid callback"
178
+
162
179
  if vectors.ndim == 1:
163
180
  vectors = vectors.reshape(1, len(vectors))
164
181
  count_vectors = vectors.shape[0]
@@ -168,42 +185,25 @@ def _search_in_compiled(
168
185
  ) -> Union[BatchMatches, Matches]:
169
186
  return batch_matches[0] if count_vectors == 1 else batch_matches
170
187
 
171
- if log and batch_size == 0:
172
- batch_size = int(math.ceil(count_vectors / 100))
173
-
174
- if batch_size:
175
- tasks = [
176
- vectors[start_row : start_row + batch_size, :]
177
- for start_row in range(0, count_vectors, batch_size)
178
- ]
179
- tasks_matches = []
188
+ # Create progress bar if needed
189
+ if log:
180
190
  name = log if isinstance(log, str) else "Search"
181
191
  pbar = tqdm(
182
- tasks,
183
192
  desc=name,
184
193
  total=count_vectors,
185
194
  unit="vector",
186
- disable=log is False,
187
195
  )
188
- for vectors in tasks:
189
- tuple_ = compiled_callable(vectors, **kwargs)
190
- tasks_matches.append(BatchMatches(*tuple_))
191
- pbar.update(vectors.shape[0])
192
196
 
193
- pbar.close()
194
- return distil_batch(
195
- BatchMatches(
196
- keys=np.vstack([m.keys for m in tasks_matches]),
197
- distances=np.vstack([m.distances for m in tasks_matches]),
198
- counts=np.concatenate([m.counts for m in tasks_matches], axis=None),
199
- visited_members=sum([m.visited_members for m in tasks_matches]),
200
- computed_distances=sum([m.computed_distances for m in tasks_matches]),
201
- )
202
- )
197
+ def update_progress_bar(processed: int, total: int) -> bool:
198
+ pbar.update(processed - pbar.n)
199
+ return progress if progress else True
203
200
 
201
+ tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
202
+ pbar.close()
204
203
  else:
205
204
  tuple_ = compiled_callable(vectors, **kwargs)
206
- return distil_batch(BatchMatches(*tuple_))
205
+
206
+ return distil_batch(BatchMatches(*tuple_))
207
207
 
208
208
 
209
209
  def _add_to_compiled(
@@ -214,9 +214,13 @@ def _add_to_compiled(
214
214
  copy: bool,
215
215
  threads: int,
216
216
  log: Union[str, bool],
217
- batch_size: int,
217
+ progress: Optional[ProgressCallback],
218
218
  ) -> Union[int, np.ndarray]:
219
+ #
219
220
  assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
221
+ assert not progress or _match_signature(
222
+ progress, [int, int], bool
223
+ ), "Invalid callback"
220
224
  assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
221
225
  if vectors.ndim == 1:
222
226
  vectors = vectors.reshape(1, len(vectors))
@@ -235,37 +239,29 @@ def _add_to_compiled(
235
239
 
236
240
  assert len(keys) == count_vectors
237
241
 
238
- # If logging is requested, and batch size is undefined, set it to grow 1% at a time:
239
- if log and batch_size == 0:
240
- batch_size = int(math.ceil(count_vectors / 100))
241
-
242
- # Split into batches and log progress, if needed
243
- if batch_size:
244
- keys = [
245
- keys[start_row : start_row + batch_size]
246
- for start_row in range(0, count_vectors, batch_size)
247
- ]
248
- vectors = [
249
- vectors[start_row : start_row + batch_size, :]
250
- for start_row in range(0, count_vectors, batch_size)
251
- ]
252
- tasks = zip(keys, vectors)
242
+ # Create progress bar if needed
243
+ if log:
253
244
  name = log if isinstance(log, str) else "Add"
254
245
  pbar = tqdm(
255
- tasks,
256
246
  desc=name,
257
247
  total=count_vectors,
258
248
  unit="vector",
259
- disable=log is False,
260
249
  )
261
- for keys, vectors in tasks:
262
- compiled.add_many(keys, vectors, copy=copy, threads=threads)
263
- pbar.update(len(keys))
264
250
 
265
- pbar.close()
251
+ def update_progress_bar(processed: int, total: int) -> bool:
252
+ pbar.update(processed - pbar.n)
253
+ return progress(processed, total) if progress else True
266
254
 
255
+ compiled.add_many(
256
+ keys,
257
+ vectors,
258
+ copy=copy,
259
+ threads=threads,
260
+ progress=update_progress_bar,
261
+ )
262
+ pbar.close()
267
263
  else:
268
- compiled.add_many(keys, vectors, copy=copy, threads=threads)
264
+ compiled.add_many(keys, vectors, copy=copy, threads=threads, progress=progress)
269
265
 
270
266
  return keys
271
267
 
@@ -625,7 +621,7 @@ class Index:
625
621
  copy: bool = True,
626
622
  threads: int = 0,
627
623
  log: Union[str, bool] = False,
628
- batch_size: int = 0,
624
+ progress: Optional[ProgressCallback] = None,
629
625
  ) -> Union[int, np.ndarray]:
630
626
  """Inserts one or move vectors into the index.
631
627
 
@@ -651,8 +647,8 @@ class Index:
651
647
  :type threads: int, defaults to 0
652
648
  :param log: Whether to print the progress bar
653
649
  :type log: Union[str, bool], defaults to False
654
- :param batch_size: Number of vectors to process at once
655
- :type batch_size: int, defaults to 0
650
+ :param progress: Callback to report stats of the progress and control it
651
+ :type progress: Optional[ProgressCallback], defaults to None
656
652
  :return: Inserted key or keys
657
653
  :type: Union[int, np.ndarray]
658
654
  """
@@ -663,7 +659,7 @@ class Index:
663
659
  copy=copy,
664
660
  threads=threads,
665
661
  log=log,
666
- batch_size=batch_size,
662
+ progress=progress,
667
663
  )
668
664
 
669
665
  def search(
@@ -675,7 +671,7 @@ class Index:
675
671
  threads: int = 0,
676
672
  exact: bool = False,
677
673
  log: Union[str, bool] = False,
678
- batch_size: int = 0,
674
+ progress: Optional[ProgressCallback] = None,
679
675
  ) -> Union[Matches, BatchMatches]:
680
676
  """
681
677
  Performs approximate nearest neighbors search for one or more queries.
@@ -690,8 +686,8 @@ class Index:
690
686
  :type exact: bool, defaults to False
691
687
  :param log: Whether to print the progress bar, default to False
692
688
  :type log: Union[str, bool], optional
693
- :param batch_size: Number of vectors to process at once
694
- :type batch_size: int, defaults to 0
689
+ :param progress: Callback to report stats of the progress and control it
690
+ :type progress: Optional[ProgressCallback], defaults to None
695
691
  :return: Matches for one or more queries
696
692
  :rtype: Union[Matches, BatchMatches]
697
693
  """
@@ -701,11 +697,11 @@ class Index:
701
697
  vectors,
702
698
  # Batch scheduling:
703
699
  log=log,
704
- batch_size=batch_size,
705
700
  # Search constraints:
706
701
  count=count,
707
702
  exact=exact,
708
703
  threads=threads,
704
+ progress=progress,
709
705
  )
710
706
 
711
707
  def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
@@ -932,37 +928,57 @@ class Index:
932
928
  self._compiled.expansion_search = v
933
929
 
934
930
  def save(
935
- self, path_or_buffer: Union[str, os.PathLike, NoneType] = None
931
+ self,
932
+ path_or_buffer: Union[str, os.PathLike, NoneType] = None,
933
+ progress: Optional[ProgressCallback] = None,
936
934
  ) -> Optional[bytes]:
935
+ assert not progress or _match_signature(
936
+ progress, [int, int], bool
937
+ ), "Invalid callback signature"
938
+
937
939
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
938
940
  if path_or_buffer is None:
939
- return self._compiled.save_index_to_buffer()
941
+ return self._compiled.save_index_to_buffer(progress)
940
942
  else:
941
- self._compiled.save_index_to_path(os.fspath(path_or_buffer))
943
+ self._compiled.save_index_to_path(os.fspath(path_or_buffer), progress)
944
+
945
+ def load(
946
+ self,
947
+ path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None,
948
+ progress: Optional[ProgressCallback] = None,
949
+ ):
950
+ assert not progress or _match_signature(
951
+ progress, [int, int], bool
952
+ ), "Invalid callback signature"
942
953
 
943
- def load(self, path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None):
944
954
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
945
955
  if path_or_buffer is None:
946
956
  raise Exception("Define the source")
947
957
  if isinstance(path_or_buffer, bytearray):
948
958
  path_or_buffer = bytes(path_or_buffer)
949
959
  if isinstance(path_or_buffer, bytes):
950
- self._compiled.load_index_from_buffer(path_or_buffer)
960
+ self._compiled.load_index_from_buffer(path_or_buffer, progress)
951
961
  else:
952
- self._compiled.load_index_from_path(os.fspath(path_or_buffer))
962
+ self._compiled.load_index_from_path(os.fspath(path_or_buffer), progress)
953
963
 
954
964
  def view(
955
- self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None
965
+ self,
966
+ path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None,
967
+ progress: Optional[ProgressCallback] = None,
956
968
  ):
969
+ assert not progress or _match_signature(
970
+ progress, [int, int], bool
971
+ ), "Invalid callback signature"
972
+
957
973
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
958
974
  if path_or_buffer is None:
959
975
  raise Exception("Define the source")
960
976
  if isinstance(path_or_buffer, bytearray):
961
977
  path_or_buffer = bytes(path_or_buffer)
962
978
  if isinstance(path_or_buffer, bytes):
963
- self._compiled.view_index_from_buffer(path_or_buffer)
979
+ self._compiled.view_index_from_buffer(path_or_buffer, progress)
964
980
  else:
965
- self._compiled.view_index_from_path(os.fspath(path_or_buffer))
981
+ self._compiled.view_index_from_path(os.fspath(path_or_buffer), progress)
966
982
 
967
983
  def clear(self):
968
984
  """Erases all the vectors from the index, preserving the space for future insertions."""
@@ -993,6 +1009,7 @@ class Index:
993
1009
  other: Index,
994
1010
  max_proposals: int = 0,
995
1011
  exact: bool = False,
1012
+ progress: Optional[ProgressCallback] = None,
996
1013
  ) -> Dict[Key, Key]:
997
1014
  """Performs "Semantic Join" or pairwise matching between `self` & `other` index.
998
1015
  Is different from `search`, as no collisions are allowed in resulting pairs.
@@ -1005,13 +1022,20 @@ class Index:
1005
1022
  :type max_proposals: int, optional
1006
1023
  :param exact: Controls if underlying `search` should be exact, defaults to False
1007
1024
  :type exact: bool, optional
1025
+ :param progress: Callback to report stats of the progress and control it
1026
+ :type progress: Optional[ProgressCallback], defaults to None
1008
1027
  :return: Mapping from keys of `self` to keys of `other`
1009
1028
  :rtype: Dict[Key, Key]
1010
1029
  """
1030
+ assert not progress or _match_signature(
1031
+ progress, [int, int], bool
1032
+ ), "Invalid callback signature"
1033
+
1011
1034
  return self._compiled.join(
1012
1035
  other=other._compiled,
1013
1036
  max_proposals=max_proposals,
1014
1037
  exact=exact,
1038
+ progress=progress,
1015
1039
  )
1016
1040
 
1017
1041
  def cluster(
@@ -1023,7 +1047,7 @@ class Index:
1023
1047
  max_count: Optional[int] = None,
1024
1048
  threads: int = 0,
1025
1049
  log: Union[str, bool] = False,
1026
- batch_size: int = 0,
1050
+ progress: Optional[ProgressCallback] = None,
1027
1051
  ) -> Clustering:
1028
1052
  """
1029
1053
  Clusters already indexed or provided `vectors`, mapping them to various centroids.
@@ -1037,11 +1061,15 @@ class Index:
1037
1061
  :type threads: int, defaults to 0
1038
1062
  :param log: Whether to print the progress bar
1039
1063
  :type log: Union[str, bool], defaults to False
1040
- :param batch_size: Number of vectors to process at once, defaults to 0
1041
- :type batch_size: int, defaults to 0
1064
+ :param progress: Callback to report stats of the progress and control it
1065
+ :type progress: Optional[ProgressCallback], defaults to None
1042
1066
  :return: Matches for one or more queries
1043
1067
  :rtype: Union[Matches, BatchMatches]
1044
1068
  """
1069
+ assert not progress or _match_signature(
1070
+ progress, [int, int], bool
1071
+ ), "Invalid callback signature"
1072
+
1045
1073
  if min_count is None:
1046
1074
  min_count = 0
1047
1075
  if max_count is None:
@@ -1054,6 +1082,7 @@ class Index:
1054
1082
  min_count=min_count,
1055
1083
  max_count=max_count,
1056
1084
  threads=threads,
1085
+ progress=progress,
1057
1086
  )
1058
1087
  else:
1059
1088
  if keys is None:
@@ -1066,6 +1095,7 @@ class Index:
1066
1095
  min_count=min_count,
1067
1096
  max_count=max_count,
1068
1097
  threads=threads,
1098
+ progress=progress,
1069
1099
  )
1070
1100
 
1071
1101
  batch_matches = BatchMatches(*results)
@@ -1231,17 +1261,18 @@ class Indexes:
1231
1261
  *,
1232
1262
  threads: int = 0,
1233
1263
  exact: bool = False,
1264
+ progress: Optional[ProgressCallback] = None,
1234
1265
  ):
1235
1266
  return _search_in_compiled(
1236
1267
  self._compiled.search_many,
1237
1268
  vectors,
1238
1269
  # Batch scheduling:
1239
1270
  log=False,
1240
- batch_size=None,
1241
1271
  # Search constraints:
1242
1272
  count=count,
1243
1273
  exact=exact,
1244
1274
  threads=threads,
1275
+ progress=progress,
1245
1276
  )
1246
1277
 
1247
1278
 
@@ -1254,7 +1285,7 @@ def search(
1254
1285
  exact: bool = False,
1255
1286
  threads: int = 0,
1256
1287
  log: Union[str, bool] = False,
1257
- batch_size: int = 0,
1288
+ progress: Optional[ProgressCallback] = None,
1258
1289
  ) -> Union[Matches, BatchMatches]:
1259
1290
  """Shortcut for search, that can avoid index construction. Particularly useful for
1260
1291
  tiny datasets, where brute-force exact search works fast enough.
@@ -1279,12 +1310,14 @@ def search(
1279
1310
  :type exact: bool, optional
1280
1311
  :param log: Whether to print the progress bar, default to False
1281
1312
  :type log: Union[str, bool], optional
1282
- :param batch_size: Number of vectors to process at once, defaults to 0
1283
- :type batch_size: int, optional
1284
-
1313
+ :param progress: Callback to report stats of the progress and control it
1314
+ :type progress: Optional[ProgressCallback], defaults to None
1285
1315
  :return: Matches for one or more queries
1286
1316
  :rtype: Union[Matches, BatchMatches]
1287
1317
  """
1318
+ assert not progress or _match_signature(
1319
+ progress, [int, int], bool
1320
+ ), "Invalid callback signature"
1288
1321
  assert dataset.ndim == 2, "Dataset must be a matrix, with a vector in each row"
1289
1322
 
1290
1323
  if not exact:
@@ -1298,14 +1331,14 @@ def search(
1298
1331
  dataset,
1299
1332
  threads=threads,
1300
1333
  log=log,
1301
- batch_size=batch_size,
1334
+ progress=progress,
1302
1335
  )
1303
1336
  return index.search(
1304
1337
  query,
1305
1338
  count,
1306
1339
  threads=threads,
1307
1340
  log=log,
1308
- batch_size=batch_size,
1341
+ progress=progress,
1309
1342
  )
1310
1343
 
1311
1344
  metric = _normalize_metric(metric)
@@ -1329,8 +1362,8 @@ def search(
1329
1362
  dataset,
1330
1363
  query,
1331
1364
  metric_kind=metric_kind,
1332
- metric_pointer=metric_pointer,
1333
1365
  metric_signature=metric_signature,
1366
+ metric_pointer=metric_pointer,
1334
1367
  **kwargs,
1335
1368
  )
1336
1369
 
@@ -1339,8 +1372,8 @@ def search(
1339
1372
  query,
1340
1373
  # Batch scheduling:
1341
1374
  log=log,
1342
- batch_size=batch_size,
1343
1375
  # Search constraints:
1344
1376
  count=count,
1345
1377
  threads=threads,
1378
+ progress=progress,
1346
1379
  )