usearch 0.19.0 → 0.19.2

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.
@@ -8,6 +8,7 @@ import os
8
8
  from typing import Optional, Union, NamedTuple, List, Iterable
9
9
 
10
10
  import numpy as np
11
+ from tqdm import tqdm
11
12
 
12
13
  from usearch.compiled import Index as _CompiledIndex, IndexMetadata
13
14
  from usearch.compiled import SparseIndex as _CompiledSetsIndex
@@ -33,32 +34,71 @@ SparseIndex = _CompiledSetsIndex
33
34
  Label = np.uint32
34
35
 
35
36
 
36
- def _normalize_dtype(dtype) -> ScalarKind:
37
- if dtype is None:
38
- return ScalarKind.F32
37
+ def _normalize_dtype(dtype, metric: MetricKind = MetricKind.IP) -> ScalarKind:
38
+ if dtype is None or dtype == "":
39
+ return ScalarKind.B1 if metric in MetricKindBitwise else ScalarKind.F32
40
+
39
41
  if isinstance(dtype, ScalarKind):
40
42
  return dtype
41
43
 
42
44
  if isinstance(dtype, str):
45
+ dtype = dtype.lower()
46
+
47
+ _normalize = {
48
+ "f64": ScalarKind.F64,
49
+ "f32": ScalarKind.F32,
50
+ "f16": ScalarKind.F16,
51
+ "f8": ScalarKind.F8,
52
+ "b1": ScalarKind.B1,
53
+ "b1x8": ScalarKind.B1,
54
+ np.float64: ScalarKind.F64,
55
+ np.float32: ScalarKind.F32,
56
+ np.float16: ScalarKind.F16,
57
+ np.int8: ScalarKind.F8,
58
+ }
59
+ return _normalize[dtype]
60
+
61
+
62
+ def _to_numpy_compatible_dtype(dtype: ScalarKind) -> ScalarKind:
63
+ _normalize = {
64
+ ScalarKind.F64: ScalarKind.F64,
65
+ ScalarKind.F32: ScalarKind.F32,
66
+ ScalarKind.F16: ScalarKind.F16,
67
+ ScalarKind.F8: ScalarKind.F16,
68
+ ScalarKind.B1: ScalarKind.B1,
69
+ }
70
+ return _normalize[dtype]
71
+
72
+
73
+ def _to_numpy_dtype(dtype: ScalarKind):
74
+ _normalize = {
75
+ ScalarKind.F64: np.float64,
76
+ ScalarKind.F32: np.float32,
77
+ ScalarKind.F16: np.float16,
78
+ ScalarKind.F8: np.float16,
79
+ ScalarKind.B1: np.uint8,
80
+ }
81
+ return _normalize[dtype]
82
+
83
+
84
+ def _normalize_metric(metric):
85
+ if metric is None:
86
+ return MetricKind.IP
87
+
88
+ if isinstance(metric, str):
43
89
  _normalize = {
44
- "f64": ScalarKind.F64,
45
- "f32": ScalarKind.F32,
46
- "f16": ScalarKind.F16,
47
- "f8": ScalarKind.F8,
48
- "b1": ScalarKind.B1,
90
+ "cos": MetricKind.Cos,
91
+ "ip": MetricKind.IP,
92
+ "l2_sq": MetricKind.L2sq,
93
+ "haversine": MetricKind.Haversine,
94
+ "perason": MetricKind.Pearson,
95
+ "hamming": MetricKind.Hamming,
96
+ "tanimoto": MetricKind.Tanimoto,
97
+ "sorensen": MetricKind.Sorensen,
49
98
  }
50
- return _normalize[dtype.lower()]
99
+ return _normalize[metric.lower()]
51
100
 
52
- if not isinstance(dtype, ScalarKind):
53
- _normalize = {
54
- np.float64: ScalarKind.F64,
55
- np.float32: ScalarKind.F32,
56
- np.float16: ScalarKind.F16,
57
- np.int8: ScalarKind.F8,
58
- }
59
- return _normalize[dtype]
60
-
61
- return dtype
101
+ return metric
62
102
 
63
103
 
64
104
  class Matches(NamedTuple):
@@ -67,7 +107,7 @@ class Matches(NamedTuple):
67
107
  counts: Union[np.ndarray, int]
68
108
 
69
109
  @property
70
- def is_batch(self) -> bool:
110
+ def is_multiple(self) -> bool:
71
111
  return isinstance(self.counts, np.ndarray)
72
112
 
73
113
  @property
@@ -79,7 +119,7 @@ class Matches(NamedTuple):
79
119
  return np.sum(self.counts)
80
120
 
81
121
  def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
82
- if not self.is_batch:
122
+ if not self.is_multiple:
83
123
  assert row is None, "Exporting a single sequence is only for batch requests"
84
124
  labels = self.labels
85
125
  distances = self.distances
@@ -98,13 +138,13 @@ class Matches(NamedTuple):
98
138
  ]
99
139
 
100
140
  def recall_first(self, expected: np.ndarray) -> float:
101
- best_matches = self.labels if not self.is_batch else self.labels[:, 0]
141
+ best_matches = self.labels if not self.is_multiple else self.labels[:, 0]
102
142
  return np.sum(best_matches == expected) / len(expected)
103
143
 
104
144
  def __repr__(self) -> str:
105
145
  return (
106
146
  f"usearch.Matches({self.total_matches})"
107
- if self.is_batch
147
+ if self.is_multiple
108
148
  else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
109
149
  )
110
150
 
@@ -127,10 +167,9 @@ class Index:
127
167
 
128
168
  def __init__(
129
169
  self,
130
- ndim: int,
170
+ ndim: int = 0,
131
171
  metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
132
- dtype: Optional[str] = None,
133
- jit: bool = False,
172
+ dtype: Optional[Union[str, ScalarKind]] = None,
134
173
  connectivity: int = DEFAULT_CONNECTIVITY,
135
174
  expansion_add: int = DEFAULT_EXPANSION_ADD,
136
175
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
@@ -142,6 +181,10 @@ class Index:
142
181
 
143
182
  :param ndim: Number of vector dimensions
144
183
  :type ndim: int
184
+ Required for some metrics, optional for others.
185
+ Haversine, for example, only applies to 2-dimensional latitude/longitude
186
+ coordinates. Angular (Cos) and Euclidean (L2sq), obviously, apply to
187
+ vectors with arbitrary number of dimensions.
145
188
 
146
189
  :param metric: Distance function, defaults to MetricKind.IP
147
190
  :type metric: Union[MetricKind, Callable, str], optional
@@ -151,18 +194,12 @@ class Index:
151
194
  Not every kind is JIT-able. For Jaccard distance, use `SparseIndex`.
152
195
 
153
196
  :param dtype: Scalar type for internal vector storage, defaults to None
154
- :type dtype: str, optional
197
+ :type dtype: Optional[Union[str, ScalarKind]], optional
155
198
  For continuous metrics can be: f16, f32, f64, or f8.
156
199
  For bitwise metrics it's implementation-defined, and can't change.
157
200
  Example: you can use the `f16` index with `f32` vectors in Euclidean space,
158
201
  which will be automatically downcasted.
159
202
 
160
- :param jit: Enable Numba to JIT compile the metric, defaults to False
161
- :type jit: bool, optional
162
- This can result in up-to 3x performance difference on very large vectors
163
- and very recent hardware, as the Python module is compiled with high
164
- compatibility in mind and avoids very fancy assembly instructions.
165
-
166
203
  :param connectivity: Connections per node in HNSW, defaults to None
167
204
  :type connectivity: Optional[int], optional
168
205
  Hyper-parameter for the number of Graph connections
@@ -190,43 +227,7 @@ class Index:
190
227
  :type view: bool, optional
191
228
  """
192
229
 
193
- if metric in MetricKindBitwise:
194
- assert dtype is None or dtype == ScalarKind.B1
195
- dtype = ScalarKind.B1
196
- else:
197
- dtype = _normalize_dtype(dtype)
198
-
199
- if metric is None:
200
- metric = MetricKind.IP
201
-
202
- if isinstance(metric, str):
203
- _normalize = {
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,
212
- }
213
- metric = _normalize[metric.lower()]
214
-
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]`"
222
- )
223
-
224
- metric = jit(
225
- ndim=ndim,
226
- metric=metric,
227
- dtype=dtype,
228
- )
229
-
230
+ metric = _normalize_metric(metric)
230
231
  if isinstance(metric, MetricKind):
231
232
  self._metric_kind = metric
232
233
  self._metric_jit = None
@@ -242,6 +243,8 @@ class Index:
242
243
  "The `metric` must be a `CompiledMetric` or a `MetricKind`"
243
244
  )
244
245
 
246
+ # Validate, that the right scalar type is defined
247
+ dtype = _normalize_dtype(dtype, self._metric_kind)
245
248
  self._compiled = _CompiledIndex(
246
249
  ndim=ndim,
247
250
  dtype=dtype,
@@ -285,7 +288,14 @@ class Index:
285
288
  )
286
289
 
287
290
  def add(
288
- self, labels, vectors, *, copy: bool = True, threads: int = 0
291
+ self,
292
+ labels,
293
+ vectors,
294
+ *,
295
+ copy: bool = True,
296
+ threads: int = 0,
297
+ log: Union[str, bool] = False,
298
+ batch_size: int = 0,
289
299
  ) -> Union[int, np.ndarray]:
290
300
  """Inserts one or move vectors into the index.
291
301
 
@@ -315,27 +325,67 @@ class Index:
315
325
  """
316
326
  assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
317
327
  assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
318
- is_batch = vectors.ndim == 2
319
- generate_labels = labels is None
328
+ count_vectors = vectors.shape[0] if vectors.ndim == 2 else 1
329
+ is_multiple = count_vectors > 1
330
+ if not is_multiple:
331
+ vectors = vectors.flatten()
320
332
 
321
- # If no `labels` were provided, generate some
333
+ # Validate of generate teh labels
334
+ generate_labels = labels is None
322
335
  if generate_labels:
323
336
  start_id = len(self._compiled)
324
- if is_batch:
325
- labels = np.arange(start_id, start_id + vectors.shape[0])
337
+ if is_multiple:
338
+ labels = np.arange(start_id, start_id + count_vectors, dtype=Label)
326
339
  else:
327
340
  labels = start_id
328
- if isinstance(labels, np.ndarray):
329
- labels = labels.astype(Label)
330
- elif isinstance(labels, Iterable):
331
- labels = np.array(labels, dtype=Label)
341
+ else:
342
+ if isinstance(labels, np.ndarray):
343
+ if not is_multiple:
344
+ labels = int(labels[0])
345
+ else:
346
+ labels = labels.astype(Label)
347
+ count_labels = len(labels) if isinstance(labels, Iterable) else 1
348
+ assert count_labels == count_vectors
349
+
350
+ # Split into batches and log progress, if needed
351
+ if batch_size:
352
+ labels = [
353
+ labels[start_row : start_row + batch_size]
354
+ for start_row in range(0, count_vectors, batch_size)
355
+ ]
356
+ vectors = [
357
+ vectors[start_row : start_row + batch_size, :]
358
+ for start_row in range(0, count_vectors, batch_size)
359
+ ]
360
+ tasks = zip(labels, vectors)
361
+ name = log if isinstance(log, str) else "Add"
362
+ pbar = tqdm(
363
+ tasks,
364
+ desc=name,
365
+ total=count_vectors,
366
+ unit="Vector",
367
+ disable=log is False,
368
+ )
369
+ for labels, vectors in tasks:
370
+ self._compiled.add(labels, vectors, copy=copy, threads=threads)
371
+ pbar.update(len(labels))
332
372
 
333
- self._compiled.add(labels, vectors, copy=copy, threads=threads)
373
+ pbar.close()
374
+
375
+ else:
376
+ self._compiled.add(labels, vectors, copy=copy, threads=threads)
334
377
 
335
378
  return labels
336
379
 
337
380
  def search(
338
- self, vectors, k: int = 10, *, threads: int = 0, exact: bool = False
381
+ self,
382
+ vectors,
383
+ k: int = 10,
384
+ *,
385
+ threads: int = 0,
386
+ exact: bool = False,
387
+ log: Union[str, bool] = False,
388
+ batch_size: int = 0,
339
389
  ) -> Matches:
340
390
  """Performs approximate nearest neighbors search for one or more queries.
341
391
 
@@ -427,18 +477,30 @@ class Index:
427
477
  def expansion_search(self, v: int):
428
478
  self._compiled.expansion_search = v
429
479
 
430
- def save(self, path: os.PathLike):
480
+ def save(self, path: Optional[os.PathLike] = None):
481
+ path = path if path else self.path
482
+ if path is None:
483
+ raise Exception("Define `path` argument")
431
484
  self._compiled.save(path)
432
485
 
433
- def load(self, path: os.PathLike):
486
+ def load(self, path: Optional[os.PathLike] = None):
487
+ path = path if path else self.path
488
+ if path is None:
489
+ raise Exception("Define `path` argument")
434
490
  self._compiled.load(path)
435
491
 
436
- def view(self, path: os.PathLike):
492
+ def view(self, path: Optional[os.PathLike] = None):
493
+ path = path if path else self.path
494
+ if path is None:
495
+ raise Exception("Define `path` argument")
437
496
  self._compiled.view(path)
438
497
 
439
498
  def clear(self):
440
499
  self._compiled.clear()
441
500
 
501
+ def close(self):
502
+ self._compiled.close()
503
+
442
504
  @property
443
505
  def labels(self) -> np.ndarray:
444
506
  return self._compiled.labels
@@ -446,12 +508,15 @@ class Index:
446
508
  def get_vectors(
447
509
  self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
448
510
  ) -> np.ndarray:
449
- dtype = _normalize_dtype(dtype)
450
- return np.vstack([self._compiled.__getitem__(l, dtype) for l in labels])
511
+ dtype = _normalize_dtype(dtype, self._metric_kind)
512
+ get_dtype = _to_numpy_compatible_dtype(dtype)
513
+ vectors = np.vstack([self._compiled.__getitem__(l, get_dtype) for l in labels])
514
+ view_dtype = _to_numpy_dtype(dtype)
515
+ return vectors.view(view_dtype)
451
516
 
452
517
  @property
453
518
  def vectors(self) -> np.ndarray:
454
- return self.get_vectors(self.labels)
519
+ return self.get_vectors(self.labels, self.dtype)
455
520
 
456
521
  def __delitem__(self, label: int):
457
522
  raise NotImplementedError()
@@ -460,4 +525,8 @@ class Index:
460
525
  return self._compiled.__contains__(label)
461
526
 
462
527
  def __getitem__(self, label: int) -> np.ndarray:
463
- return self._compiled.__getitem__(label)
528
+ dtype = self.dtype
529
+ get_dtype = _to_numpy_compatible_dtype(dtype)
530
+ vector = self._compiled.__getitem__(label, get_dtype)
531
+ view_dtype = _to_numpy_dtype(dtype)
532
+ return None if vector is None else vector.view(view_dtype)
@@ -31,7 +31,7 @@ def load_matrix(
31
31
  dtype = np.int32
32
32
  scalar_size = 4
33
33
  elif filename.endswith(".bbin"):
34
- dtype = np.int8
34
+ dtype = np.uint8
35
35
  scalar_size = 1
36
36
  else:
37
37
  raise Exception("Unknown file type")
@@ -69,7 +69,7 @@ def save_matrix(vectors: np.ndarray, filename: str):
69
69
  elif filename.endswith(".ibin"):
70
70
  dtype = np.int32
71
71
  elif filename.endswith(".bbin"):
72
- dtype = np.int8
72
+ dtype = np.uint8
73
73
  else:
74
74
  raise Exception("Unknown file type")
75
75
 
@@ -4,31 +4,19 @@
4
4
  # into the primary `Index` class, connecting USearch with Numba.
5
5
  from math import sqrt
6
6
 
7
- from numba import cfunc, types, carray
8
-
9
7
  from usearch.index import MetricKind, ScalarKind, MetricSignature, CompiledMetric
10
8
 
11
9
 
12
- signature_i8args = types.float32(types.CPointer(types.int8), types.CPointer(types.int8))
13
-
14
- signature_f16args = types.float32(
15
- types.CPointer(types.float16), types.CPointer(types.float16)
16
- )
17
-
18
- signature_f32args = types.float32(
19
- types.CPointer(types.float32), types.CPointer(types.float32)
20
- )
21
-
22
- signature_f64args = types.float32(
23
- types.CPointer(types.float64), types.CPointer(types.float64)
24
- )
25
-
26
-
27
10
  def jit(
28
- ndim: int, metric: MetricKind = MetricKind.Cos, dtype: ScalarKind = ScalarKind.F32
11
+ ndim: int,
12
+ metric: MetricKind = MetricKind.Cos,
13
+ dtype: ScalarKind = ScalarKind.F32,
29
14
  ) -> CompiledMetric:
30
- """JIT-compiles a distance metric specifically tuned for the target hardware
31
- and number of dimensions.
15
+ """JIT-compiles the metric for target hardware and number of dimensions.
16
+
17
+ This can result in up-to 3x performance difference on very large vectors
18
+ and very recent hardware, as the Python module is compiled with high
19
+ compatibility in mind and avoids very fancy assembly instructions.
32
20
 
33
21
  Uses Numba `cfunc` functionality, annotating it with Numba `types` instead
34
22
  of `ctypes` to support half-precision.
@@ -37,6 +25,24 @@ def jit(
37
25
  assert isinstance(metric, MetricKind)
38
26
  assert isinstance(dtype, ScalarKind)
39
27
 
28
+ from numba import cfunc, types, carray
29
+
30
+ signature_i8args = types.float32(
31
+ types.CPointer(types.int8), types.CPointer(types.int8)
32
+ )
33
+
34
+ signature_f16args = types.float32(
35
+ types.CPointer(types.float16), types.CPointer(types.float16)
36
+ )
37
+
38
+ signature_f32args = types.float32(
39
+ types.CPointer(types.float32), types.CPointer(types.float32)
40
+ )
41
+
42
+ signature_f64args = types.float32(
43
+ types.CPointer(types.float64), types.CPointer(types.float64)
44
+ )
45
+
40
46
  numba_supported_types = (
41
47
  ScalarKind.F8,
42
48
  # Half-precision is still unsupported
@@ -46,7 +52,7 @@ def jit(
46
52
  ScalarKind.F64,
47
53
  )
48
54
  if dtype not in numba_supported_types:
49
- return None
55
+ return metric
50
56
 
51
57
  scalar_kind_to_accumulator_type = {
52
58
  ScalarKind.F8: types.int32,
@@ -109,7 +115,7 @@ def jit(
109
115
 
110
116
  pointer = cfunc(scalar_kind_to_signature[dtype])(metric_kind_to_function[metric])
111
117
  return CompiledMetric(
112
- pointer=pointer,
118
+ pointer=pointer.address,
113
119
  kind=metric,
114
120
  signature=MetricSignature.ArrayArray,
115
121
  )
package/setup.py CHANGED
@@ -5,97 +5,94 @@ from setuptools import setup
5
5
  from pybind11.setup_helpers import Pybind11Extension
6
6
 
7
7
 
8
- compile_args = ['-std=c++11', '-O3']
8
+ compile_args = []
9
9
  link_args = []
10
10
  macros_args = [
11
- ('USEARCH_USE_NATIVE_F16', '0'),
12
- ('USEARCH_USE_SIMSIMD', '0'),
11
+ ("USEARCH_USE_NATIVE_F16", "0"),
12
+ ("USEARCH_USE_SIMSIMD", "0"),
13
13
  ]
14
14
 
15
- if sys.platform == 'linux':
16
- compile_args.append('-fopenmp')
17
- link_args.append('-lgomp')
18
- compile_args.append('-Wno-unknown-pragmas')
19
- macros_args.append(('USEARCH_USE_OPENMP', '1'))
15
+ if sys.platform == "linux":
16
+ compile_args.append("-std=c++11")
17
+ compile_args.append("-O3")
18
+ compile_args.append("-Wno-unknown-pragmas")
20
19
 
21
- if sys.platform == 'darwin':
20
+ macros_args.append(("USEARCH_USE_OPENMP", "1"))
21
+ compile_args.append("-fopenmp")
22
+ link_args.append("-lgomp")
23
+
24
+ if sys.platform == "darwin":
22
25
  # MacOS 10.15 or higher is needed for `aligned_alloc` support.
23
26
  # https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
24
- compile_args.append('-mmacosx-version-min=10.15')
25
- compile_args.append('-Wno-unknown-pragmas')
27
+ compile_args.append("-mmacosx-version-min=10.15")
28
+ compile_args.append("-std=c++11")
29
+ compile_args.append("-O3")
30
+ compile_args.append("-Wno-unknown-pragmas")
26
31
 
27
- if sys.platform == 'win32':
28
- compile_args = ['/std:c++14', '/O2']
32
+ # Linking OpenMP requires additional preparion in CIBuildWheel
33
+ # macros_args.append(("USEARCH_USE_OPENMP", "1"))
34
+ # compile_args.append("-Xpreprocessor -fopenmp")
35
+ # link_args.append("-Xpreprocessor -lomp")
29
36
 
37
+ if sys.platform == "win32":
38
+ compile_args.append("/std:c++14")
39
+ compile_args.append("/O2")
30
40
 
31
41
  ext_modules = [
32
42
  Pybind11Extension(
33
- 'usearch.compiled',
34
- ['python/lib.cpp'],
43
+ "usearch.compiled",
44
+ ["python/lib.cpp"],
35
45
  extra_compile_args=compile_args,
36
46
  extra_link_args=link_args,
37
47
  define_macros=macros_args,
38
48
  ),
39
49
  ]
40
50
 
41
- __version__ = open('VERSION', 'r').read().strip()
42
- __lib_name__ = 'usearch'
51
+ __version__ = open("VERSION", "r").read().strip()
52
+ __lib_name__ = "usearch"
43
53
 
44
54
 
45
55
  this_directory = os.path.abspath(os.path.dirname(__file__))
46
- with open(os.path.join(this_directory, 'README.md')) as f:
56
+ with open(os.path.join(this_directory, "README.md")) as f:
47
57
  long_description = f.read()
48
58
 
49
59
 
50
60
  setup(
51
-
52
61
  name=__lib_name__,
53
62
  version=__version__,
54
- packages=['usearch'],
55
- package_dir={'usearch': 'python/usearch'},
56
-
57
- description='Smaller & Faster Single-File Vector Search Engine from Unum',
63
+ packages=["usearch"],
64
+ package_dir={"usearch": "python/usearch"},
65
+ description="Smaller & Faster Single-File Vector Search Engine from Unum",
58
66
  long_description=long_description,
59
- long_description_content_type='text/markdown',
60
- license='Apache-2.0',
61
-
67
+ long_description_content_type="text/markdown",
68
+ license="Apache-2.0",
62
69
  classifiers=[
63
- 'Development Status :: 5 - Production/Stable',
64
-
65
- 'Natural Language :: English',
66
- 'Intended Audience :: Developers',
67
- 'Intended Audience :: Information Technology',
68
- 'License :: OSI Approved :: Apache Software License',
69
-
70
- 'Programming Language :: C++',
71
- 'Programming Language :: Python :: 3 :: Only',
72
- 'Programming Language :: Python :: Implementation :: CPython',
73
- 'Programming Language :: Java',
74
- 'Programming Language :: JavaScript',
75
- 'Programming Language :: Objective C',
76
- 'Programming Language :: Rust',
77
- 'Programming Language :: Other',
78
-
79
- 'Operating System :: MacOS',
80
- 'Operating System :: Unix',
81
- 'Operating System :: Microsoft :: Windows',
82
-
83
- 'Topic :: System :: Clustering',
84
- 'Topic :: Database :: Database Engines/Servers',
85
- 'Topic :: Scientific/Engineering :: Artificial Intelligence',
70
+ "Development Status :: 5 - Production/Stable",
71
+ "Natural Language :: English",
72
+ "Intended Audience :: Developers",
73
+ "Intended Audience :: Information Technology",
74
+ "License :: OSI Approved :: Apache Software License",
75
+ "Programming Language :: C++",
76
+ "Programming Language :: Python :: 3 :: Only",
77
+ "Programming Language :: Python :: Implementation :: CPython",
78
+ "Programming Language :: Java",
79
+ "Programming Language :: JavaScript",
80
+ "Programming Language :: Objective C",
81
+ "Programming Language :: Rust",
82
+ "Programming Language :: Other",
83
+ "Operating System :: MacOS",
84
+ "Operating System :: Unix",
85
+ "Operating System :: Microsoft :: Windows",
86
+ "Topic :: System :: Clustering",
87
+ "Topic :: Database :: Database Engines/Servers",
88
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
86
89
  ],
87
-
88
- include_dirs=[
89
- 'include',
90
- 'fp16/include', 'robin-map/include', 'simsimd/include'],
91
-
90
+ include_dirs=["include", "fp16/include", "robin-map/include", "simsimd/include"],
92
91
  ext_modules=ext_modules,
93
92
  install_requires=[
94
- 'numpy',
95
- 'pandas',
93
+ "numpy",
94
+ "pandas",
95
+ "tqdm",
96
96
  'ucall; python_version >= "3.9"',
97
97
  ],
98
- extras_require={
99
- 'jit': ['numba', 'cppyy'],
100
- },
101
98
  )
package/swift/Test.swift CHANGED
@@ -6,16 +6,20 @@
6
6
  //
7
7
 
8
8
  import Foundation
9
+ import XCTest
9
10
  import USearch
10
11
 
11
12
  @available(iOS 13, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
12
- func testUnit() throws {
13
- let index = USearchIndex.make(metric:USearchMetric.l2sq, dimensions: 4, connectivity: 8, quantization:USearchScalar.F32)
14
- let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
15
- let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
16
- index.add(label: 42, vector: vectorA[...])
17
- index.add(label: 43, vector: vectorB[...])
18
-
19
- let results = index.search(vector: vectorA[...], count: 10)
20
- assert(results.0[0] == 42)
13
+ class Test: XCTestCase {
14
+ func testUnit() throws {
15
+ let index = USearchIndex.make(metric:USearchMetric.l2sq, dimensions: 4, connectivity: 8, quantization:USearchScalar.F32)
16
+ let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
17
+ let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
18
+ index.reserve(2)
19
+ index.add(label: 42, vector: vectorA[...])
20
+ index.add(label: 43, vector: vectorB[...])
21
+
22
+ let results = index.search(vector: vectorA[...], count: 10)
23
+ assert(results.0[0] == 42)
24
+ }
21
25
  }