usearch 0.17.1 → 0.18.1

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.
@@ -42,13 +42,13 @@ jobs:
42
42
  run: pytest python/scripts/test.py
43
43
 
44
44
 
45
- test_python_36:
46
- name: Test Python 3.6
45
+ test_python_37:
46
+ name: Test Python 3.7
47
47
  runs-on: ${{ matrix.os }}
48
48
  strategy:
49
49
  matrix:
50
50
  os: [ubuntu-20.04]
51
- python-version: ["3.6"]
51
+ python-version: ["3.7"]
52
52
 
53
53
  steps:
54
54
  - uses: actions/checkout@v3
@@ -62,7 +62,7 @@ jobs:
62
62
  - name: Install dependencies
63
63
  run: |
64
64
  python -m pip install --upgrade pip
65
- pip install pytest numpy dataclasses
65
+ pip install pytest numpy
66
66
 
67
67
  - name: Build locally
68
68
  run: python -m pip install .
@@ -125,6 +125,8 @@ jobs:
125
125
 
126
126
  - name: Build wheels
127
127
  run: python -m cibuildwheel
128
+ env:
129
+ CIBW_SKIP: cp36-*
128
130
 
129
131
  - uses: actions/upload-artifact@v3
130
132
  with:
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.17.1"
3
+ version = "0.18.1"
4
4
  authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -276,7 +276,7 @@ The difference would be in the shape of the tensors.
276
276
 
277
277
  ```py
278
278
  n = 100
279
- labels = np.array(range(n), dtype=np.longlong)
279
+ labels = np.arange(n)
280
280
  vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
281
281
 
282
282
  index.add(labels, vectors, threads=..., copy=...)
@@ -584,7 +584,7 @@ fingerprints = np.vstack([encoder.GetFingerprint(x) for x in molecules])
584
584
  fingerprints = np.packbits(fingerprints, axis=1)
585
585
 
586
586
  index = Index(ndim=2048, metric=MetricKind.BitwiseTanimoto)
587
- labels = np.array(len(molecules), dtype=np.longlong)
587
+ labels = np.arange(len(molecules))
588
588
 
589
589
  index.add(labels, fingerprints)
590
590
  matches = index.search(fingerprints, 10)
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '0.17.1'
7
+ version = '0.18.1'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
package/python/lib.cpp CHANGED
@@ -26,7 +26,7 @@ using namespace unum;
26
26
  namespace py = pybind11;
27
27
  using py_shape_t = py::array::ShapeContainer;
28
28
 
29
- using label_t = Py_ssize_t;
29
+ using label_t = std::uint32_t;
30
30
  using distance_t = punned_distance_t;
31
31
  using metric_t = punned_metric_t;
32
32
  using id_t = std::uint32_t;
@@ -132,11 +132,11 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
132
132
  config.store_vector = copy;
133
133
 
134
134
  // https://docs.python.org/3/library/struct.html#format-characters
135
- if (vector_info.format == "B")
135
+ if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1")
136
136
  index.add(label, reinterpret_cast<b1x8_t const*>(vector_data), config).error.raise();
137
- else if (vector_info.format == "c" || vector_info.format == "b")
137
+ else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1")
138
138
  index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
139
- else if (vector_info.format == "e")
139
+ else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2")
140
140
  index.add(label, reinterpret_cast<f16_t const*>(vector_data), config).error.raise();
141
141
  else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
142
142
  index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
@@ -156,7 +156,7 @@ static void add_many_to_index( //
156
156
  py::buffer_info labels_info = labels.request();
157
157
  py::buffer_info vectors_info = vectors.request();
158
158
 
159
- if (labels_info.format != py::format_descriptor<label_t>::format())
159
+ if (labels_info.itemsize != sizeof(label_t))
160
160
  throw std::invalid_argument("Incompatible label type!");
161
161
 
162
162
  if (labels_info.ndim != 1)
@@ -181,7 +181,7 @@ static void add_many_to_index( //
181
181
  char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
182
182
 
183
183
  // https://docs.python.org/3/library/struct.html#format-characters
184
- if (vectors_info.format == "B")
184
+ if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
185
185
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
186
186
  add_config_t config;
187
187
  config.store_vector = copy;
@@ -190,7 +190,7 @@ static void add_many_to_index( //
190
190
  b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
191
191
  index.add(label, vector, config).error.raise();
192
192
  });
193
- else if (vectors_info.format == "c" || vectors_info.format == "b")
193
+ else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
194
194
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
195
195
  add_config_t config;
196
196
  config.store_vector = copy;
@@ -200,7 +200,7 @@ static void add_many_to_index( //
200
200
  reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
201
201
  index.add(label, vector, config).error.raise();
202
202
  });
203
- else if (vectors_info.format == "e")
203
+ else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
204
204
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
205
205
  add_config_t config;
206
206
  config.store_vector = copy;
@@ -249,15 +249,15 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
249
249
  config.exact = exact;
250
250
 
251
251
  // https://docs.python.org/3/library/struct.html#format-characters
252
- if (vector_info.format == "B") {
252
+ if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1") {
253
253
  punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
254
254
  result.error.raise();
255
255
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
256
- } else if (vector_info.format == "c" || vector_info.format == "b") {
256
+ } else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1") {
257
257
  punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
258
258
  result.error.raise();
259
259
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
260
- } else if (vector_info.format == "e") {
260
+ } else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2") {
261
261
  punned_search_result_t result = index.search(reinterpret_cast<f16_t const*>(vector_data), wanted, config);
262
262
  result.error.raise();
263
263
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
@@ -320,7 +320,7 @@ static py::tuple search_many_in_index( //
320
320
  auto counts_py1d = counts_py.template mutable_unchecked<1>();
321
321
 
322
322
  // https://docs.python.org/3/library/struct.html#format-characters
323
- if (vectors_info.format == "B")
323
+ if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
324
324
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
325
325
  search_config_t config;
326
326
  config.thread = thread_idx;
@@ -331,7 +331,7 @@ static py::tuple search_many_in_index( //
331
331
  counts_py1d(task_idx) =
332
332
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
333
333
  });
334
- else if (vectors_info.format == "c" || vectors_info.format == "b")
334
+ else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
335
335
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
336
336
  search_config_t config;
337
337
  config.thread = thread_idx;
@@ -342,7 +342,7 @@ static py::tuple search_many_in_index( //
342
342
  counts_py1d(task_idx) =
343
343
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
344
344
  });
345
- else if (vectors_info.format == "e")
345
+ else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
346
346
  executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
347
347
  search_config_t config;
348
348
  config.thread = thread_idx;
@@ -1,21 +1,13 @@
1
- from typing import Tuple, Any, Callable, Union, Optional, List, NamedTuple
1
+ from __future__ import annotations
2
+ from time import time_ns
3
+ from typing import Tuple, Any, Callable, Union, Optional, List
4
+ from dataclasses import dataclass, asdict
2
5
  from collections import defaultdict
3
6
 
4
7
  import numpy as np
5
8
 
6
9
  from usearch.io import load_matrix
7
- from usearch.index import Index, Matches, MetricKind, MetricKindBitwise
8
-
9
-
10
- # For compatibility with Python 3.6
11
- try:
12
- from time import time_ns
13
- except ImportError:
14
- from datetime import datetime
15
-
16
- def time_ns():
17
- now = datetime.now()
18
- return int(now.timestamp() * 1e9)
10
+ from usearch.index import Index, Matches, MetricKind, MetricKindBitwise, Label
19
11
 
20
12
 
21
13
  def random_vectors(
@@ -89,7 +81,8 @@ def measure_seconds(f: Callable) -> Tuple[float, Any]:
89
81
  return secs, result
90
82
 
91
83
 
92
- class Dataset(NamedTuple):
84
+ @dataclass
85
+ class Dataset:
93
86
 
94
87
  labels: np.ndarray
95
88
  vectors: np.ndarray
@@ -134,7 +127,7 @@ class Dataset(NamedTuple):
134
127
  count = min(
135
128
  d.vectors.shape[0], count) if count is not None else d.vectors.shape[0]
136
129
  d.vectors = d.vectors[:count, :]
137
- d.labels = np.arange(count, dtype=np.longlong)
130
+ d.labels = np.arange(count, dtype=Label)
138
131
 
139
132
  if queries is not None:
140
133
  d.queries = load_matrix(queries)
@@ -157,13 +150,14 @@ class Dataset(NamedTuple):
157
150
 
158
151
  d.vectors = random_vectors(count=count, ndim=ndim)
159
152
  d.queries = d.vectors
160
- d.labels = np.arange(count, dtype=np.longlong)
153
+ d.labels = np.arange(count, dtype=Label)
161
154
  d.neighbors = np.reshape(d.labels, (count, 1))
162
155
 
163
156
  return d
164
157
 
165
158
 
166
- class TaskResult(NamedTuple):
159
+ @dataclass
160
+ class TaskResult:
167
161
 
168
162
  add_operations: Optional[int] = None
169
163
  add_per_second: Optional[float] = None
@@ -190,7 +184,7 @@ class TaskResult(NamedTuple):
190
184
  def search_seconds(self) -> float:
191
185
  return self.search_operations / self.search_per_second
192
186
 
193
- def __add__(self, other):
187
+ def __add__(self, other: TaskResult):
194
188
  result = TaskResult()
195
189
  if self.add_operations and other.add_operations:
196
190
  result.add_operations = self.add_operations + other.add_operations
@@ -217,7 +211,8 @@ class TaskResult(NamedTuple):
217
211
  return result
218
212
 
219
213
 
220
- class AddTask(NamedTuple):
214
+ @dataclass
215
+ class AddTask:
221
216
 
222
217
  labels: np.ndarray
223
218
  vectors: np.ndarray
@@ -251,7 +246,7 @@ class AddTask(NamedTuple):
251
246
  self.labels = self.labels[new_order]
252
247
  self.vectors = self.vectors[new_order, :]
253
248
 
254
- def slices(self, batch_size: int) -> list:
249
+ def slices(self, batch_size: int) -> List[AddTask]:
255
250
  """Splits this dataset into smaller chunks."""
256
251
 
257
252
  return [
@@ -260,7 +255,7 @@ class AddTask(NamedTuple):
260
255
  vectors=self.vectors[start_row:start_row+batch_size, :],
261
256
  ) for start_row in range(0, self.count, batch_size)]
262
257
 
263
- def clusters(self, number_of_clusters: int) -> list:
258
+ def clusters(self, number_of_clusters: int) -> List[AddTask]:
264
259
  """Splits this dataset into smaller chunks."""
265
260
 
266
261
  from sklearn.cluster import KMeans
@@ -280,7 +275,8 @@ class AddTask(NamedTuple):
280
275
  ) for rows in partitioning.values()]
281
276
 
282
277
 
283
- class SearchTask(NamedTuple):
278
+ @dataclass
279
+ class SearchTask:
284
280
 
285
281
  queries: np.ndarray
286
282
  neighbors: np.ndarray
@@ -295,7 +291,7 @@ class SearchTask(NamedTuple):
295
291
  recall_at_one=results.recall_first(self.neighbors[:, 0].flatten()),
296
292
  )
297
293
 
298
- def slices(self, batch_size: int) -> list:
294
+ def slices(self, batch_size: int) -> List[SearchTask]:
299
295
  """Splits this dataset into smaller chunks."""
300
296
 
301
297
  return [
@@ -305,7 +301,8 @@ class SearchTask(NamedTuple):
305
301
  ) for start_row in range(0, self.queries.shape[0], batch_size)]
306
302
 
307
303
 
308
- class Evaluation(NamedTuple):
304
+ @dataclass
305
+ class Evaluation:
309
306
 
310
307
  tasks: List[Union[AddTask, SearchTask]]
311
308
  count: int
@@ -315,7 +312,7 @@ class Evaluation(NamedTuple):
315
312
  def for_dataset(
316
313
  dataset: Dataset,
317
314
  batch_size: int = 0,
318
- clusters: int = 1):
315
+ clusters: int = 1) -> Evaluation:
319
316
 
320
317
  tasks = []
321
318
  add = AddTask(
@@ -356,5 +353,5 @@ class Evaluation(NamedTuple):
356
353
  index.clear()
357
354
  return {
358
355
  **index.specs,
359
- **task_result._asdict(),
356
+ **asdict(task_result),
360
357
  }
@@ -29,7 +29,7 @@ MetricKindBitwise = (
29
29
 
30
30
  SparseIndex = _CompiledSetsIndex
31
31
 
32
- Label = np.longlong
32
+ Label = np.uint32
33
33
 
34
34
 
35
35
  def _normalize_dtype(dtype) -> ScalarKind:
@@ -295,9 +295,9 @@ class Index:
295
295
  else:
296
296
  labels = start_id
297
297
  if isinstance(labels, np.ndarray):
298
- labels = labels.astype(np.longlong)
298
+ labels = labels.astype(Label)
299
299
  elif isinstance(labels, Iterable):
300
- labels = np.array(labels, dtype=np.longlong)
300
+ labels = np.array(labels, dtype=Label)
301
301
 
302
302
  self._compiled.add(labels, vectors, copy=copy, threads=threads)
303
303
 
@@ -7,7 +7,7 @@ import numpy as np
7
7
  from typing import List
8
8
 
9
9
  from ucall.rich_posix import Server
10
- from usearch.index import Index, list_matches
10
+ from usearch.index import Index, Matches, Label
11
11
 
12
12
 
13
13
  def _ascii_to_vector(string: str) -> np.ndarray:
@@ -58,26 +58,25 @@ def serve(
58
58
  @server
59
59
  def add_one(label: int, vector: np.ndarray):
60
60
  print('adding', label, vector)
61
- labels = np.array([label], dtype=np.longlong)
61
+ labels = np.array([label], dtype=Label)
62
62
  vectors = vector.flatten().reshape(vector.shape[0], 1)
63
63
  index.add(labels, vectors)
64
64
 
65
65
  @server
66
66
  def add_many(labels: np.ndarray, vectors: np.ndarray):
67
- labels = labels.astype(np.longlong)
68
67
  index.add(labels, vectors, threads=threads)
69
68
 
70
69
  @server
71
70
  def search_one(vector: np.ndarray, count: int) -> List[dict]:
72
71
  print('search', vector, count)
73
72
  vectors = vector.reshape(vector.shape[0], 1)
74
- results = index.search(vectors, count)
75
- return list_matches(results, 0)
73
+ results: Matches = index.search(vectors, count)
74
+ return results.to_list()
76
75
 
77
76
  @server
78
77
  def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
79
- results = index.search(vectors, count)
80
- return [list_matches(results, i) for i in range(vectors.shape[0])]
78
+ results: Matches = index.search(vectors, count)
79
+ return results.to_list()
81
80
 
82
81
  @server
83
82
  def add_ascii(label: int, string: str):