usearch 0.18.8 → 0.19.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.
@@ -24,7 +24,7 @@ def random_vectors(
24
24
  # Infer default parameters from the `index`, if passed
25
25
  if index is not None:
26
26
  if not isinstance(index, Index):
27
- raise ValueError('Unsupported `index` type')
27
+ raise ValueError("Unsupported `index` type")
28
28
 
29
29
  ndim = index.ndim
30
30
  dtype = index.numpy_dtype
@@ -37,7 +37,6 @@ def random_vectors(
37
37
  return bit_vectors
38
38
 
39
39
  else:
40
-
41
40
  x = np.random.rand(count, ndim)
42
41
  if dtype == np.int8:
43
42
  x = (x * 100).astype(np.int8)
@@ -77,13 +76,65 @@ def measure_seconds(f: Callable) -> Tuple[float, Any]:
77
76
  result = f()
78
77
  b = time_ns()
79
78
  c = b - a
80
- secs = c / (10 ** 9)
79
+ secs = c / (10**9)
81
80
  return secs, result
82
81
 
83
82
 
83
+ def dcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
84
+ """Calculate DCG (Discounted Cumulative Gain) up to position k.
85
+
86
+ :param relevances: List of true relevance scores (in the order as they are ranked)
87
+ :type relevances: list
88
+ :param k: Position up to which DCG is computed
89
+ :type k: int
90
+ :return: The DCG score at position k
91
+ :rtype: float
92
+ """
93
+ if k:
94
+ relevances = np.asarray(relevances)[:k]
95
+
96
+ n_relevances = len(relevances)
97
+ if n_relevances == 0:
98
+ return 0.0
99
+
100
+ discounts = np.log2(np.arange(n_relevances) + 2)
101
+ return np.sum(relevances / discounts)
102
+
103
+
104
+ def ndcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
105
+ """Calculate NDCG (Normalized Discounted Cumulative Gain) at position k.
106
+
107
+ :param relevances: List of true relevance scores (in the order as they are ranked)
108
+ :type relevances: list
109
+ :param k: Position up to which NDCG is computed
110
+ :type k: int
111
+ :return: The NDCG score at position k
112
+ :rtype: float
113
+ """
114
+ best_dcg = dcg(sorted(relevances, reverse=True), k)
115
+ if best_dcg == 0:
116
+ return 0.0
117
+
118
+ return dcg(relevances, k) / best_dcg
119
+
120
+
121
+ def relevance(
122
+ expected: np.ndarray, predicted: np.ndarray, k: Optional[int] = None
123
+ ) -> np.ndarray:
124
+ """Calculate relevance scores. Binary relevance scores
125
+
126
+ :param expected: ground-truth labels
127
+ :type expected: np.ndarray
128
+ :param predicted: predicted labels
129
+ :type predicted: np.ndarray
130
+ """
131
+ expected = expected[:k]
132
+ predicted = predicted[:k]
133
+ return [1 if i in expected else 0 for i in predicted]
134
+
135
+
84
136
  @dataclass
85
137
  class Dataset:
86
-
87
138
  labels: np.ndarray
88
139
  vectors: np.ndarray
89
140
  queries: np.ndarray
@@ -124,8 +175,11 @@ class Dataset:
124
175
 
125
176
  d.vectors = load_matrix(vectors)
126
177
  ndim = d.vectors.shape[1]
127
- count = min(
128
- d.vectors.shape[0], count) if count is not None else d.vectors.shape[0]
178
+ count = (
179
+ min(d.vectors.shape[0], count)
180
+ if count is not None
181
+ else d.vectors.shape[0]
182
+ )
129
183
  d.vectors = d.vectors[:count, :]
130
184
  d.labels = np.arange(count, dtype=Label)
131
185
 
@@ -139,14 +193,13 @@ class Dataset:
139
193
  if k is not None:
140
194
  d.neighbors = d.neighbors[:, :k]
141
195
  else:
142
- assert k is None, 'Cant ovveride `k`, will retrieve one neighbor'
196
+ assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
143
197
  d.neighbors = np.reshape(d.labels, (count, 1))
144
198
 
145
199
  else:
146
-
147
200
  assert ndim is not None
148
201
  assert count is not None
149
- assert k is None, 'Cant ovveride `k`, will retrieve one neighbor'
202
+ assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
150
203
 
151
204
  d.vectors = random_vectors(count=count, ndim=ndim)
152
205
  d.queries = d.vectors
@@ -158,7 +211,6 @@ class Dataset:
158
211
 
159
212
  @dataclass
160
213
  class TaskResult:
161
-
162
214
  add_operations: Optional[int] = None
163
215
  add_per_second: Optional[float] = None
164
216
 
@@ -169,12 +221,12 @@ class TaskResult:
169
221
  def __repr__(self) -> str:
170
222
  parts = []
171
223
  if self.add_per_second:
172
- parts.append(f'{self.add_per_second:.2f} add/s')
224
+ parts.append(f"{self.add_per_second:.2f} add/s")
173
225
  if self.search_per_second:
174
- parts.append(f'{self.search_per_second:.2f} search/s')
226
+ parts.append(f"{self.search_per_second:.2f} search/s")
175
227
  if self.recall_at_one:
176
- parts.append(f'{self.recall_at_one * 100:.2f}% recall@1')
177
- return ', '.join(parts)
228
+ parts.append(f"{self.recall_at_one * 100:.2f}% recall@1")
229
+ return ", ".join(parts)
178
230
 
179
231
  @property
180
232
  def add_seconds(self) -> float:
@@ -188,8 +240,9 @@ class TaskResult:
188
240
  result = TaskResult()
189
241
  if self.add_operations and other.add_operations:
190
242
  result.add_operations = self.add_operations + other.add_operations
191
- result.add_per_second = result.add_operations / \
192
- (self.add_seconds + other.add_seconds)
243
+ result.add_per_second = result.add_operations / (
244
+ self.add_seconds + other.add_seconds
245
+ )
193
246
  else:
194
247
  base = self if self.add_operations else other
195
248
  result.add_operations = base.add_operations
@@ -198,10 +251,12 @@ class TaskResult:
198
251
  if self.search_operations and other.search_operations:
199
252
  result.search_operations = self.search_operations + other.search_operations
200
253
  result.recall_at_one = (
201
- self.recall_at_one * self.search_operations + other.recall_at_one *
202
- other.search_operations) / (self.search_operations + other.search_operations)
203
- result.search_per_second = result.search_operations / \
204
- (self.search_seconds + other.search_seconds)
254
+ self.recall_at_one * self.search_operations
255
+ + other.recall_at_one * other.search_operations
256
+ ) / (self.search_operations + other.search_operations)
257
+ result.search_per_second = result.search_operations / (
258
+ self.search_seconds + other.search_seconds
259
+ )
205
260
  else:
206
261
  base = self if self.search_operations else other
207
262
  result.search_operations = base.search_operations
@@ -213,21 +268,18 @@ class TaskResult:
213
268
 
214
269
  @dataclass
215
270
  class AddTask:
216
-
217
271
  labels: np.ndarray
218
272
  vectors: np.ndarray
219
273
 
220
274
  def __call__(self, index: Index) -> TaskResult:
221
-
222
275
  batch_size: int = self.vectors.shape[0]
223
276
  old_size: int = len(index)
224
- dt, _ = measure_seconds(
225
- lambda: index.add(self.labels, self.vectors))
277
+ dt, _ = measure_seconds(lambda: index.add(self.labels, self.vectors))
226
278
 
227
279
  assert len(index) == old_size + batch_size
228
280
  return TaskResult(
229
281
  add_operations=batch_size,
230
- add_per_second=batch_size/dt,
282
+ add_per_second=batch_size / dt,
231
283
  )
232
284
 
233
285
  @property
@@ -251,17 +303,21 @@ class AddTask:
251
303
 
252
304
  return [
253
305
  AddTask(
254
- labels=self.labels[start_row:start_row+batch_size],
255
- vectors=self.vectors[start_row:start_row+batch_size, :],
256
- ) for start_row in range(0, self.count, batch_size)]
306
+ labels=self.labels[start_row : start_row + batch_size],
307
+ vectors=self.vectors[start_row : start_row + batch_size, :],
308
+ )
309
+ for start_row in range(0, self.count, batch_size)
310
+ ]
257
311
 
258
312
  def clusters(self, number_of_clusters: int) -> List[AddTask]:
259
313
  """Splits this dataset into smaller chunks."""
260
314
 
261
315
  from sklearn.cluster import KMeans
316
+
262
317
  clustering = KMeans(
263
318
  n_clusters=number_of_clusters,
264
- random_state=0, n_init='auto',
319
+ random_state=0,
320
+ n_init="auto",
265
321
  ).fit(self.vectors)
266
322
 
267
323
  partitioning = defaultdict(list)
@@ -272,22 +328,23 @@ class AddTask:
272
328
  AddTask(
273
329
  labels=self.labels[rows],
274
330
  vectors=self.vectors[rows, :],
275
- ) for rows in partitioning.values()]
331
+ )
332
+ for rows in partitioning.values()
333
+ ]
276
334
 
277
335
 
278
336
  @dataclass
279
337
  class SearchTask:
280
-
281
338
  queries: np.ndarray
282
339
  neighbors: np.ndarray
283
340
 
284
341
  def __call__(self, index: Index) -> TaskResult:
285
-
286
342
  dt, results = measure_seconds(
287
- lambda: index.search(self.queries, self.neighbors.shape[1]))
343
+ lambda: index.search(self.queries, self.neighbors.shape[1])
344
+ )
288
345
 
289
346
  return TaskResult(
290
- search_per_second=self.queries.shape[0]/dt,
347
+ search_per_second=self.queries.shape[0] / dt,
291
348
  recall_at_one=results.recall_first(self.neighbors[:, 0].flatten()),
292
349
  )
293
350
 
@@ -296,31 +353,26 @@ class SearchTask:
296
353
 
297
354
  return [
298
355
  SearchTask(
299
- queries=self.queries[start_row:start_row+batch_size, :],
300
- neighbors=self.neighbors[start_row:start_row+batch_size, :],
301
- ) for start_row in range(0, self.queries.shape[0], batch_size)]
356
+ queries=self.queries[start_row : start_row + batch_size, :],
357
+ neighbors=self.neighbors[start_row : start_row + batch_size, :],
358
+ )
359
+ for start_row in range(0, self.queries.shape[0], batch_size)
360
+ ]
302
361
 
303
362
 
304
363
  @dataclass
305
364
  class Evaluation:
306
-
307
365
  tasks: List[Union[AddTask, SearchTask]]
308
366
  count: int
309
367
  ndim: int
310
368
 
311
369
  @staticmethod
312
370
  def for_dataset(
313
- dataset: Dataset,
314
- batch_size: int = 0,
315
- clusters: int = 1) -> Evaluation:
316
-
371
+ dataset: Dataset, batch_size: int = 0, clusters: int = 1
372
+ ) -> Evaluation:
317
373
  tasks = []
318
- add = AddTask(
319
- vectors=dataset.vectors,
320
- labels=dataset.labels)
321
- search = SearchTask(
322
- queries=dataset.queries,
323
- neighbors=dataset.neighbors)
374
+ add = AddTask(vectors=dataset.vectors, labels=dataset.labels)
375
+ search = SearchTask(queries=dataset.queries, neighbors=dataset.neighbors)
324
376
 
325
377
  if batch_size:
326
378
  tasks.extend(add.slices(batch_size))
@@ -340,7 +392,6 @@ class Evaluation:
340
392
  )
341
393
 
342
394
  def __call__(self, index: Index, post_clean: bool = True) -> dict:
343
-
344
395
  task_result = TaskResult()
345
396
 
346
397
  try:
@@ -1,22 +1,22 @@
1
+ from __future__ import annotations
2
+
1
3
  # The purpose of this file is to provide Pythonic wrapper on top
2
4
  # the native precompiled CPython module. It improves compatibility
3
5
  # Python tooling, linters, and static analyzers. It also embeds JIT
4
6
  # into the primary `Index` class, connecting USearch with Numba.
5
7
  import os
6
- from math import sqrt
7
- from typing import Optional, Callable, Union, NamedTuple, List, Iterable
8
+ from typing import Optional, Union, NamedTuple, List, Iterable
8
9
 
9
10
  import numpy as np
10
11
 
11
- from usearch.compiled import Index as _CompiledIndex
12
+ from usearch.compiled import Index as _CompiledIndex, IndexMetadata
12
13
  from usearch.compiled import SparseIndex as _CompiledSetsIndex
13
14
 
14
- from usearch.compiled import MetricKind, ScalarKind
15
+ from usearch.compiled import MetricKind, ScalarKind, MetricSignature
15
16
  from usearch.compiled import (
16
17
  DEFAULT_CONNECTIVITY,
17
18
  DEFAULT_EXPANSION_ADD,
18
19
  DEFAULT_EXPANSION_SEARCH,
19
-
20
20
  USES_OPENMP,
21
21
  USES_SIMSIMD,
22
22
  USES_NATIVE_F16,
@@ -41,11 +41,11 @@ def _normalize_dtype(dtype) -> ScalarKind:
41
41
 
42
42
  if isinstance(dtype, str):
43
43
  _normalize = {
44
- 'f64': ScalarKind.F64,
45
- 'f32': ScalarKind.F32,
46
- 'f16': ScalarKind.F16,
47
- 'f8': ScalarKind.F8,
48
- 'b1': ScalarKind.B1,
44
+ "f64": ScalarKind.F64,
45
+ "f32": ScalarKind.F32,
46
+ "f16": ScalarKind.F16,
47
+ "f8": ScalarKind.F8,
48
+ "b1": ScalarKind.B1,
49
49
  }
50
50
  return _normalize[dtype.lower()]
51
51
 
@@ -79,9 +79,8 @@ class Matches(NamedTuple):
79
79
  return np.sum(self.counts)
80
80
 
81
81
  def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
82
-
83
82
  if not self.is_batch:
84
- assert row is None, 'Exporting a single sequence is only for batch requests'
83
+ assert row is None, "Exporting a single sequence is only for batch requests"
85
84
  labels = self.labels
86
85
  distances = self.distances
87
86
 
@@ -94,7 +93,7 @@ class Matches(NamedTuple):
94
93
  distances = self.distances[row, :count]
95
94
 
96
95
  return [
97
- {'label': int(label), 'distance': float(distance)}
96
+ {"label": int(label), "distance": float(distance)}
98
97
  for label, distance in zip(labels, distances)
99
98
  ]
100
99
 
@@ -103,8 +102,17 @@ class Matches(NamedTuple):
103
102
  return np.sum(best_matches == expected) / len(expected)
104
103
 
105
104
  def __repr__(self) -> str:
106
- return f'usearch.Matches({self.total_matches})' if self.is_batch else \
107
- f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
105
+ return (
106
+ f"usearch.Matches({self.total_matches})"
107
+ if self.is_batch
108
+ else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
109
+ )
110
+
111
+
112
+ class CompiledMetric(NamedTuple):
113
+ pointer: int
114
+ kind: MetricKind
115
+ signature: MetricSignature
108
116
 
109
117
 
110
118
  class Index:
@@ -120,15 +128,13 @@ class Index:
120
128
  def __init__(
121
129
  self,
122
130
  ndim: int,
123
- metric: Union[MetricKind, Callable, str] = MetricKind.IP,
131
+ metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
124
132
  dtype: Optional[str] = None,
125
133
  jit: bool = False,
126
-
127
134
  connectivity: int = DEFAULT_CONNECTIVITY,
128
135
  expansion_add: int = DEFAULT_EXPANSION_ADD,
129
136
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
130
137
  tune: bool = False,
131
-
132
138
  path: Optional[os.PathLike] = None,
133
139
  view: bool = False,
134
140
  ) -> None:
@@ -171,7 +177,7 @@ class Index:
171
177
 
172
178
  :param expansion_search: Traversal depth on queries, defaults to None
173
179
  :type expansion_search: Optional[int], optional
174
- Hyper-parameter for the search depth when querying
180
+ Hyper-parameter for the search depth when querying
175
181
  nearest neighbors. The original paper calls it "ef".
176
182
  Can be changed afterwards, as the `.expansion_search`.
177
183
 
@@ -190,58 +196,58 @@ class Index:
190
196
  else:
191
197
  dtype = _normalize_dtype(dtype)
192
198
 
193
-
194
199
  if metric is None:
195
200
  metric = MetricKind.IP
196
- elif isinstance(metric, str):
201
+
202
+ if isinstance(metric, str):
197
203
  _normalize = {
198
- 'cos': MetricKind.Cos,
199
- 'ip': MetricKind.IP,
200
- 'l2_sq': MetricKind.L2sq,
201
- 'haversine': MetricKind.Haversine,
202
- 'perason': MetricKind.Pearson,
203
- 'hamming': MetricKind.Hamming,
204
- 'tanimoto': MetricKind.Tanimoto,
205
- 'sorensen': MetricKind.Sorensen,
204
+ "cos": MetricKind.Cos,
205
+ "ip": MetricKind.IP,
206
+ "l2_sq": MetricKind.L2sq,
207
+ "haversine": MetricKind.Haversine,
208
+ "perason": MetricKind.Pearson,
209
+ "hamming": MetricKind.Hamming,
210
+ "tanimoto": MetricKind.Tanimoto,
211
+ "sorensen": MetricKind.Sorensen,
206
212
  }
207
213
  metric = _normalize[metric.lower()]
208
214
 
209
- if isinstance(metric, Callable):
210
- self._metric_kind = MetricKind.Unknown
211
- self._metric_jit = metric
212
- self._metric_pointer = int(metric.address)
213
-
214
- elif isinstance(metric, MetricKind):
215
- if jit:
216
-
217
- try:
218
- from usearch.numba import jit
219
- except ImportError:
220
- raise ModuleNotFoundError(
221
- 'To use JIT install Numba with `pip install numba`.'
222
- 'Alternatively, reinstall usearch with `pip install usearch[jit]`')
223
-
224
- self._metric_kind = metric
225
- self._metric_jit = jit(
226
- ndim=ndim,
227
- metric=metric,
228
- dtype=dtype,
215
+ if isinstance(metric, MetricKind) and jit:
216
+ try:
217
+ from usearch.numba import jit
218
+ except ImportError:
219
+ raise ModuleNotFoundError(
220
+ "To use JIT install Numba with `pip install numba`."
221
+ "Alternatively, reinstall with `pip install usearch[jit]`"
229
222
  )
230
- self._metric_pointer = self._metric_jit.address if \
231
- self._metric_jit else 0
232
- else:
233
- self._metric_kind = metric
234
- self._metric_jit = None
235
- self._metric_pointer = 0
223
+
224
+ metric = jit(
225
+ ndim=ndim,
226
+ metric=metric,
227
+ dtype=dtype,
228
+ )
229
+
230
+ if isinstance(metric, MetricKind):
231
+ self._metric_kind = metric
232
+ self._metric_jit = None
233
+ self._metric_pointer = 0
234
+ self._metric_signature = MetricSignature.ArrayArraySize
235
+ elif isinstance(metric, CompiledMetric):
236
+ self._metric_jit = metric
237
+ self._metric_kind = metric.kind
238
+ self._metric_pointer = metric.pointer
239
+ self._metric_signature = metric.signature
236
240
  else:
237
241
  raise ValueError(
238
- 'The `metric` must be Numba callback or a `MetricKind`')
242
+ "The `metric` must be a `CompiledMetric` or a `MetricKind`"
243
+ )
239
244
 
240
245
  self._compiled = _CompiledIndex(
241
246
  ndim=ndim,
247
+ dtype=dtype,
242
248
  metric=self._metric_kind,
243
249
  metric_pointer=self._metric_pointer,
244
- dtype=dtype,
250
+ metric_signature=self._metric_signature,
245
251
  connectivity=connectivity,
246
252
  expansion_add=expansion_add,
247
253
  expansion_search=expansion_search,
@@ -255,17 +261,40 @@ class Index:
255
261
  else:
256
262
  self._compiled.load(path)
257
263
 
264
+ @staticmethod
265
+ def metadata(path: os.PathLike) -> IndexMetadata:
266
+ return IndexMetadata(path)
267
+
268
+ @staticmethod
269
+ def restore(path: os.PathLike, view: bool = False) -> Index:
270
+ meta = Index.metadata(path)
271
+ bits_per_scalar = {
272
+ ScalarKind.F8: 8,
273
+ ScalarKind.F16: 16,
274
+ ScalarKind.F32: 32,
275
+ ScalarKind.F64: 64,
276
+ ScalarKind.B1: 1,
277
+ }[meta.scalar_kind]
278
+ ndim = meta.bytes_for_vectors * 8 // meta.size // bits_per_scalar
279
+ return Index(
280
+ ndim=ndim,
281
+ connectivity=meta.connectivity,
282
+ metric=meta.metric,
283
+ path=path,
284
+ view=view,
285
+ )
286
+
258
287
  def add(
259
- self, labels, vectors, *,
260
- copy: bool = True, threads: int = 0) -> Union[int, np.ndarray]:
288
+ self, labels, vectors, *, copy: bool = True, threads: int = 0
289
+ ) -> Union[int, np.ndarray]:
261
290
  """Inserts one or move vectors into the index.
262
291
 
263
292
  For maximal performance the `labels` and `vectors`
264
293
  should conform to the Python's "buffer protocol" spec.
265
294
 
266
- To index a single entry:
295
+ To index a single entry:
267
296
  labels: int, vectors: np.ndarray.
268
- To index many entries:
297
+ To index many entries:
269
298
  labels: np.ndarray, vectors: np.ndarray.
270
299
 
271
300
  When working with extremely large indexes, you may want to
@@ -284,8 +313,8 @@ class Index:
284
313
  :return: Inserted label or labels
285
314
  :type: Union[int, np.ndarray]
286
315
  """
287
- assert isinstance(vectors, np.ndarray), 'Expects a NumPy array'
288
- assert vectors.ndim == 1 or vectors.ndim == 2, 'Expects a matrix or vector'
316
+ assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
317
+ assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
289
318
  is_batch = vectors.ndim == 2
290
319
  generate_labels = labels is None
291
320
 
@@ -306,8 +335,8 @@ class Index:
306
335
  return labels
307
336
 
308
337
  def search(
309
- self, vectors, k: int = 10, *,
310
- threads: int = 0, exact: bool = False) -> Matches:
338
+ self, vectors, k: int = 10, *, threads: int = 0, exact: bool = False
339
+ ) -> Matches:
311
340
  """Performs approximate nearest neighbors search for one or more queries.
312
341
 
313
342
  :param vectors: Query vector or vectors.
@@ -323,26 +352,28 @@ class Index:
323
352
  :rtype: Matches
324
353
  """
325
354
  tuple_ = self._compiled.search(
326
- vectors, k,
327
- exact=exact, threads=threads,
355
+ vectors,
356
+ k,
357
+ exact=exact,
358
+ threads=threads,
328
359
  )
329
360
  return Matches(*tuple_)
330
361
 
331
362
  @property
332
363
  def specs(self) -> dict:
333
364
  return {
334
- 'Class': 'usearch.Index',
335
- 'Connectivity': self.connectivity,
336
- 'Size': self.size,
337
- 'Dimensions': self.ndim,
338
- 'Expansion@Add': self.expansion_add,
339
- 'Expansion@Search': self.expansion_search,
340
- 'OpenMP': USES_OPENMP,
341
- 'SimSIMD': USES_SIMSIMD,
342
- 'NativeF16': USES_NATIVE_F16,
343
- 'JIT': self.jit,
344
- 'DType': self.dtype,
345
- 'Path': self.path,
365
+ "Class": "usearch.Index",
366
+ "Connectivity": self.connectivity,
367
+ "Size": self.size,
368
+ "Dimensions": self.ndim,
369
+ "Expansion@Add": self.expansion_add,
370
+ "Expansion@Search": self.expansion_search,
371
+ "OpenMP": USES_OPENMP,
372
+ "SimSIMD": USES_SIMSIMD,
373
+ "NativeF16": USES_NATIVE_F16,
374
+ "JIT": self.jit,
375
+ "DType": self.dtype,
376
+ "Path": self.path,
346
377
  }
347
378
 
348
379
  def __len__(self) -> int:
@@ -413,10 +444,8 @@ class Index:
413
444
  return self._compiled.labels
414
445
 
415
446
  def get_vectors(
416
- self,
417
- labels: np.ndarray,
418
- dtype: ScalarKind = ScalarKind.F32) -> np.ndarray:
419
-
447
+ self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
448
+ ) -> np.ndarray:
420
449
  dtype = _normalize_dtype(dtype)
421
450
  return np.vstack([self._compiled.__getitem__(l, dtype) for l in labels])
422
451