usearch 0.14.0 → 0.15.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.
Files changed (64) hide show
  1. package/.github/workflows/release.yml +56 -4
  2. package/.github/workflows/update_version.sh +6 -1
  3. package/.vscode/launch.json +21 -22
  4. package/.vscode/settings.json +7 -1
  5. package/CITATION.cff +4 -4
  6. package/CMakeLists.txt +1 -1
  7. package/Cargo.toml +1 -1
  8. package/Package.swift +0 -1
  9. package/README.md +100 -27
  10. package/binding.gyp +0 -1
  11. package/build.gradle +1 -1
  12. package/build.rs +3 -3
  13. package/c/lib.cpp +32 -9
  14. package/c/usearch.h +67 -36
  15. package/conanfile.py +1 -1
  16. package/{src → cpp}/bench.cpp +52 -36
  17. package/{src → cpp}/test.cpp +17 -7
  18. package/docs/benchmarks.md +33 -10
  19. package/docs/c/index.rst +11 -0
  20. package/docs/c/reference.rst +7 -0
  21. package/docs/compilation.md +24 -9
  22. package/docs/conf.dox +6 -6
  23. package/docs/conf.py +3 -1
  24. package/docs/cpp/index.rst +11 -0
  25. package/docs/cpp/reference.rst +7 -0
  26. package/docs/index.rst +10 -0
  27. package/docs/java/index.rst +11 -0
  28. package/docs/java/reference.rst +7 -0
  29. package/docs/javascript/index.rst +11 -0
  30. package/docs/javascript/reference.rst +9 -0
  31. package/docs/python/index.rst +11 -0
  32. package/docs/python/reference.rst +35 -0
  33. package/golang/lib.go +1 -1
  34. package/include/usearch/{usearch.hpp → index.hpp} +152 -148
  35. package/include/usearch/index_punned_dense.hpp +633 -0
  36. package/include/usearch/index_punned_helpers.hpp +528 -0
  37. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
  38. package/javascript/lib.cpp +26 -50
  39. package/javascript/usearch.js +117 -2
  40. package/objc/USearchObjective.mm +145 -49
  41. package/objc/include/USearchObjective.h +106 -30
  42. package/package.json +2 -2
  43. package/python/lib.cpp +134 -331
  44. package/python/scripts/bench.ipynb +170 -0
  45. package/python/scripts/bench.py +87 -175
  46. package/python/scripts/index_faiss.py +100 -0
  47. package/python/scripts/test.py +41 -37
  48. package/python/usearch/eval.py +353 -0
  49. package/python/usearch/index.py +117 -113
  50. package/python/usearch/io.py +7 -2
  51. package/python/usearch/join.py +100 -0
  52. package/python/usearch/numba.py +101 -0
  53. package/rust/lib.cpp +27 -14
  54. package/rust/lib.hpp +4 -10
  55. package/rust/lib.rs +50 -17
  56. package/setup.py +12 -2
  57. package/simsimd/include/simsimd/simsimd.h +32 -3
  58. package/swift/Index+Sugar.swift +55 -29
  59. package/swift/Test.swift +1 -1
  60. package/wolfram/lib.cpp +4 -4
  61. package/python/scripts/bench_params.py +0 -201
  62. package/python/usearch/synthetic.py +0 -31
  63. package/src/punned.hpp +0 -906
  64. /package/{src → cpp}/CMakeLists.txt +0 -0
@@ -0,0 +1,170 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "!pip install pandas nbformat jinja2 kaleido tqdm sklearn scikit-learn scipy matplotlib"
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "code",
14
+ "execution_count": null,
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "from bench import *\n",
19
+ "import plotly.graph_objects as go"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": null,
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "def speed_chart(df, title):\n",
29
+ " subtitles = [\n",
30
+ " f'{n}, recall@1 ~{r*100:.2f}%'\n",
31
+ " for n, r in zip(df['names'], df['recall_at_one'])\n",
32
+ " ]\n",
33
+ " fig = go.Figure(\n",
34
+ " data=[\n",
35
+ " go.Bar(name='Add', x=subtitles, y=df['add_per_second']),\n",
36
+ " go.Bar(name='Search', x=subtitles, y=df['search_per_second']),\n",
37
+ " ])\n",
38
+ " fig.update_layout(\n",
39
+ " width=1000,\n",
40
+ " height=1000,\n",
41
+ " barmode='group',\n",
42
+ " title=dict(\n",
43
+ " text=title,\n",
44
+ " ),\n",
45
+ " legend=dict(\n",
46
+ " orientation='h',\n",
47
+ " entrywidth=70,\n",
48
+ " yanchor='bottom',\n",
49
+ " y=1.02,\n",
50
+ " xanchor='right',\n",
51
+ " x=1,\n",
52
+ " ),\n",
53
+ " )\n",
54
+ " fig.write_image(title + '.png')\n",
55
+ " return fig"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "metadata": {},
62
+ "outputs": [],
63
+ "source": [
64
+ "data = Dataset.build(\n",
65
+ " vectors='../../datasets/wiki_1M/base.1M.fbin',\n",
66
+ " queries='../../datasets/wiki_1M/query.public.100K.fbin',\n",
67
+ " neighbors='../../datasets/wiki_1M/groundtruth.public.100K.ibin',\n",
68
+ ")"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": null,
74
+ "metadata": {},
75
+ "outputs": [],
76
+ "source": [
77
+ "eval_default = Evaluation.for_dataset(data)\n",
78
+ "results_default = bench_speed(eval_default)\n",
79
+ "results_default"
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "speed_chart(results_default, 'USearch vs FAISS Speed')"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "code",
93
+ "execution_count": null,
94
+ "metadata": {},
95
+ "outputs": [],
96
+ "source": [
97
+ "eval_sliced = Evaluation.for_dataset(data, batch_size=256)\n",
98
+ "results_sliced = bench_speed(eval_sliced)\n",
99
+ "results_sliced"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": null,
105
+ "metadata": {},
106
+ "outputs": [],
107
+ "source": [
108
+ "speed_chart(results_sliced, 'USearch vs FAISS Speed with 256 Batch Size')"
109
+ ]
110
+ },
111
+ {
112
+ "cell_type": "code",
113
+ "execution_count": null,
114
+ "metadata": {},
115
+ "outputs": [],
116
+ "source": [
117
+ "eval_clustered = Evaluation.for_dataset(data, clusters=10_000)\n",
118
+ "len(eval_clustered.tasks)"
119
+ ]
120
+ },
121
+ {
122
+ "cell_type": "code",
123
+ "execution_count": null,
124
+ "metadata": {},
125
+ "outputs": [],
126
+ "source": [
127
+ "results_clustered = bench_speed(eval_clustered)\n",
128
+ "results_clustered"
129
+ ]
130
+ },
131
+ {
132
+ "cell_type": "code",
133
+ "execution_count": null,
134
+ "metadata": {},
135
+ "outputs": [],
136
+ "source": [
137
+ "speed_chart(results_clustered, 'USearch vs FAISS Speed on Clustered Data')"
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "code",
142
+ "execution_count": null,
143
+ "metadata": {},
144
+ "outputs": [],
145
+ "source": []
146
+ }
147
+ ],
148
+ "metadata": {
149
+ "kernelspec": {
150
+ "display_name": "usearch-test",
151
+ "language": "python",
152
+ "name": "python3"
153
+ },
154
+ "language_info": {
155
+ "codemirror_mode": {
156
+ "name": "ipython",
157
+ "version": 3
158
+ },
159
+ "file_extension": ".py",
160
+ "mimetype": "text/x-python",
161
+ "name": "python",
162
+ "nbconvert_exporter": "python",
163
+ "pygments_lexer": "ipython3",
164
+ "version": "3.11.3"
165
+ },
166
+ "orig_nbformat": 4
167
+ },
168
+ "nbformat": 4,
169
+ "nbformat_minor": 2
170
+ }
@@ -1,12 +1,15 @@
1
- import time
2
- import numpy as np
3
- import math
4
- from typing import Tuple, Any, Optional
1
+ import os
2
+ import logging
3
+ import itertools
4
+ from typing import List, Optional, Iterable
5
+ from dataclasses import asdict
5
6
 
6
7
  import fire
8
+ import numpy as np
9
+ import pandas as pd
7
10
 
8
- from usearch.index import Index
9
- from usearch.io import load_matrix
11
+ from usearch.index import Index, Label
12
+ from usearch.eval import Evaluation, Dataset, AddTask
10
13
  from usearch.index import (
11
14
  DEFAULT_CONNECTIVITY,
12
15
  DEFAULT_EXPANSION_ADD,
@@ -14,190 +17,99 @@ from usearch.index import (
14
17
  )
15
18
 
16
19
 
17
- def measure(f) -> Tuple[float, Any]:
18
- a = time.time_ns()
19
- result = f()
20
- b = time.time_ns()
21
- c = b - a
22
- secs = c / (10 ** 9)
23
- print(f'- Took: {secs:.2f} seconds')
24
- return secs, result
25
-
26
-
27
- def bench_faiss(
28
- index,
29
- vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
30
-
31
- dt, _ = measure(lambda: index.add(vectors))
32
- print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
33
-
34
- dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
35
- print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
36
-
37
- recall_at_one = 0.0
38
- matches = results[1]
39
- for i in range(queries.shape[0]):
40
- recall_at_one += neighbors[i, 0] in matches[i, :]
41
- recall_at_one /= queries.shape[0]
42
- print(f'- Recall@1: {recall_at_one * 100:.2f} %')
43
-
44
-
45
- def bench_usearch(
46
- index,
47
- vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
48
-
49
- labels = np.arange(vectors.shape[0], dtype=np.longlong)
50
- assert len(index) == 0
51
- dt, _ = measure(lambda: index.add(labels, vectors))
52
- assert len(index) == vectors.shape[0]
53
- print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
54
-
55
- dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
56
- print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
57
-
58
- recall_at_one = 0.0
59
- matches = results[0]
60
- for i in range(queries.shape[0]):
61
- recall_at_one += neighbors[i, 0] in matches[i, :]
62
- recall_at_one /= queries.shape[0]
63
- print(f'- Recall@1: {recall_at_one * 100:.2f} %')
64
-
65
-
66
- def main(
67
- vectors: str,
68
- queries: str,
69
- neighbors: str,
20
+ def bench_speed(
21
+ eval: Evaluation,
70
22
  connectivity: int = DEFAULT_CONNECTIVITY,
71
23
  expansion_add: int = DEFAULT_EXPANSION_ADD,
72
24
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
73
- k: Optional[int] = None,
74
- ):
75
-
76
- vectors_mat = load_matrix(vectors)
77
- queries_mat = load_matrix(queries)
78
- neighbors_mat = load_matrix(neighbors)
79
- dim = vectors_mat.shape[1]
80
- if k:
81
- neighbors_mat = neighbors_mat[:, :1]
25
+ train: bool = False,
26
+ ) -> pd.DataFrame:
82
27
 
83
- print('USearch: HNSW')
84
- index = Index(
85
- ndim=dim,
86
- expansion_add=expansion_add,
87
- expansion_search=expansion_search,
88
- connectivity=connectivity,
89
- )
90
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
91
-
92
- print('USearch: HNSW + Half Precision')
93
- index = Index(
94
- ndim=dim,
95
- dtype='f16',
96
- expansion_add=expansion_add,
97
- expansion_search=expansion_search,
98
- connectivity=connectivity,
99
- )
100
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
101
-
102
- print('USearch: HNSW + Quarter Precision')
103
- index = Index(
104
- ndim=dim,
105
- dtype='f8',
106
- expansion_add=expansion_add,
107
- expansion_search=expansion_search,
108
- connectivity=connectivity,
109
- )
110
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
111
-
112
- # Showcases how to use Numba to JIT-compile similarity measures for USearch.
113
- # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
114
- try:
115
- from numba import cfunc, types, carray
28
+ # Build various indexes:
29
+ indexes = []
30
+ jit_options = [False]
31
+ dtype_options = ['f32', 'f16', 'f8']
32
+ for jit, dtype in itertools.product(jit_options, dtype_options):
116
33
 
117
- signature_f32 = types.float32(
118
- types.CPointer(types.float32),
119
- types.CPointer(types.float32),
120
- types.uint64, types.uint64)
121
-
122
- @cfunc(signature_f32)
123
- def inner_product_f32(a, b, n, m):
124
- a_array = carray(a, dim)
125
- b_array = carray(b, dim)
126
- c = types.float32(0)
127
- for i in range(dim):
128
- c += a_array[i] * b_array[i]
129
- return types.float32(1 - c)
130
-
131
- signature_f8 = types.float32(
132
- types.CPointer(types.int8),
133
- types.CPointer(types.int8),
134
- types.uint64, types.uint64)
135
-
136
- @cfunc(signature_f8)
137
- def cos_f8(a, b, n, m):
138
- a_array = carray(a, dim)
139
- b_array = carray(b, dim)
140
- c = types.int32(0)
141
- a_sq = types.int32(0)
142
- b_sq = types.int32(0)
143
- for i in range(dim):
144
- ai = types.int16(a_array[i])
145
- bi = types.int16(b_array[i])
146
- c += ai * bi
147
- a_sq += ai * ai
148
- b_sq += bi * bi
149
- a_norm = math.sqrt(types.float32(a_sq))
150
- b_norm = math.sqrt(types.float32(b_sq))
151
- c_norm = types.float32(c) / (a_norm * b_norm)
152
- return types.float32(1 - c_norm)
153
-
154
- print('USearch: HNSW + Numba JIT')
155
34
  index = Index(
156
- ndim=dim,
35
+ ndim=eval.ndim,
36
+ jit=jit,
37
+ dtype=dtype,
157
38
  expansion_add=expansion_add,
158
39
  expansion_search=expansion_search,
159
40
  connectivity=connectivity,
160
- metric=inner_product_f32,
41
+ path='USearch' + ['', '+JIT'][jit] + ':' + dtype,
161
42
  )
162
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
43
+ indexes.append(index)
163
44
 
164
- print('USearch: HNSW + Quarter Precision + Numba JIT')
165
- index = Index(
166
- ndim=dim,
167
- dtype='f8',
45
+ # Add FAISS indexes to the mix:
46
+ try:
47
+ from index_faiss import IndexFAISS, IndexQuantizedFAISS
48
+ indexes.append(IndexFAISS(
49
+ ndim=eval.ndim,
168
50
  expansion_add=expansion_add,
169
51
  expansion_search=expansion_search,
170
52
  connectivity=connectivity,
171
- metric=cos_f8,
172
- )
173
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
174
- except ImportError:
175
- print('Skipping Numba benchmarks')
176
-
177
- # Don't depend on the FAISS installation for benchmarks
178
- try:
179
- from faiss import IndexHNSWFlat, IndexIVFPQ
53
+ path='FAISS:f32',
54
+ ))
55
+ if train:
56
+ indexes.append(IndexQuantizedFAISS(
57
+ train=eval.tasks[0].vectors,
58
+ expansion_add=expansion_add,
59
+ expansion_search=expansion_search,
60
+ connectivity=connectivity,
61
+ path='FAISS+IVFPQ:f32',
62
+ ))
63
+ except (ImportError, ModuleNotFoundError):
64
+ pass
65
+
66
+ # Time to evaluate:
67
+ results = [eval(index) for index in indexes]
68
+ return pd.DataFrame({
69
+ 'names': [i.path for i in indexes],
70
+ 'add_per_second': [x['add_per_second'] for x in results],
71
+ 'search_per_second': [x['search_per_second'] for x in results],
72
+ 'recall_at_one': [x['recall_at_one'] for x in results],
73
+ })
74
+
75
+
76
+ def bench_params(
77
+ count: int = 1_000_000,
78
+ connectivities: int = range(10, 20),
79
+ dimensions: List[int] = [
80
+ 2, 3, 4, 8, 16, 32, 96, 100,
81
+ 256, 384, 512, 768, 1024, 1536],
180
82
 
181
- print('FAISS: HNSW')
182
- index = IndexHNSWFlat(dim, connectivity)
183
- index.hnsw.efSearch = expansion_search
184
- index.hnsw.efConstruction = expansion_add
185
- bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
186
-
187
- print('FAISS: HNSW + IVFPQ')
188
- # Number of inverted lists (number of partitions or cells).
189
- nlist = 10000
190
- nsegment = 16 # Number of segments for PQ (number of subquantizers).
191
- nbit = 8 # Number of bits to encode each segment.
192
- coarse_quantizer = IndexHNSWFlat(dim, connectivity)
193
- index = IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
194
- index.train(vectors_mat)
195
- index.nprobe = 10
196
- bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
83
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
84
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
85
+ ) -> pd.DataFrame:
86
+ """Measures indexing speed for different dimensionality vectors.
197
87
 
198
- except ImportError:
199
- print('Skipping FAISS benchmarks')
88
+ :param count: Number of vectors, defaults to 1_000_000
89
+ :type count: int, optional
90
+ """
200
91
 
92
+ results = []
93
+ for connectivity, ndim in itertools.product(connectivities, dimensions):
201
94
 
202
- if __name__ == '__main__':
203
- fire.Fire(main)
95
+ task = AddTask(
96
+ labels=np.arange(count, dtype=Label),
97
+ vectors=np.random.rand(count, ndim).astype(np.float32),
98
+ )
99
+ index = Index(
100
+ ndim=ndim,
101
+ connectivity=connectivity,
102
+ expansion_add=expansion_add,
103
+ expansion_search=expansion_search,
104
+ )
105
+ result = asdict(task(index))
106
+ result['ndim'] = dimensions
107
+ result['connectivity'] = connectivity
108
+ results.append(result)
109
+
110
+ # return self._execute_tasks(
111
+ # tasks,
112
+ # title='HNSW Indexing Speed vs Vector Dimensions',
113
+ # x='ndim', y='add_per_second', log_x=True,
114
+ # )
115
+ return pd.DataFrame(results)
@@ -0,0 +1,100 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ from faiss import IndexHNSWFlat, IndexIVFPQ, read_index
6
+
7
+ from usearch.index import Matches
8
+ from usearch.index import (
9
+ DEFAULT_CONNECTIVITY,
10
+ DEFAULT_EXPANSION_ADD,
11
+ DEFAULT_EXPANSION_SEARCH,
12
+ )
13
+
14
+
15
+ class IndexFAISS:
16
+
17
+ def __init__(
18
+ self,
19
+ index: IndexHNSWFlat = None,
20
+ ndim: int = 0,
21
+ connectivity: int = DEFAULT_CONNECTIVITY,
22
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
23
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
24
+ path: Optional[os.PathLike] = None,
25
+ *args, **kwargs,
26
+ ):
27
+
28
+ if index is None:
29
+ index = IndexHNSWFlat(ndim, connectivity)
30
+ index.hnsw.efConstruction = expansion_add
31
+ index.hnsw.efSearch = expansion_search
32
+
33
+ self._faiss = index
34
+ self._specs = {
35
+ 'Class': 'usearch.IndexFAISS',
36
+ 'Dimensions': ndim,
37
+ 'Connectivity': connectivity,
38
+ 'Expansion@Add': expansion_add,
39
+ 'Expansion@Search': expansion_search,
40
+ }
41
+
42
+ self.path = path
43
+
44
+ def add(self, labels, vectors):
45
+ # Adding labels isn't supported for most index types
46
+ # self._faiss.add_with_ids(vectors, labels)
47
+ self._faiss.add(vectors)
48
+
49
+ def search(self, queries, k: int) -> Matches:
50
+ distances, labels = self._faiss.search(queries, k)
51
+ return Matches(labels, distances, np.array([k] * queries.shape[0]))
52
+
53
+ def __len__(self) -> int:
54
+ return self._faiss.ntotal
55
+
56
+ def clear(self):
57
+ self._faiss.reset()
58
+
59
+ @property
60
+ def specs(self) -> dict:
61
+ self._specs.update({
62
+ 'Size': len(self),
63
+ })
64
+ return self._specs
65
+
66
+ def load(self, path: os.PathLike):
67
+ self._faiss = read_index(path)
68
+
69
+
70
+ class IndexQuantizedFAISS(IndexFAISS):
71
+
72
+ def __init__(
73
+ self,
74
+ train: np.ndarray,
75
+ connectivity: int = DEFAULT_CONNECTIVITY,
76
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
77
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
78
+ *args, **kwargs,
79
+ ):
80
+
81
+ ndim = train.shape[1]
82
+ super().__init__(
83
+ ndim=ndim,
84
+ connectivity=connectivity,
85
+ expansion_add=expansion_add,
86
+ expansion_search=expansion_search,
87
+ *args, **kwargs,
88
+ )
89
+
90
+ nlist = 10000 # Number of inverted lists (number of partitions).
91
+ nsegment = 16 # Number of segments for PQ (number of subquantizers).
92
+ nbit = 8 # Number of bits to encode each segment.
93
+
94
+ self._original_faiss = self._faiss
95
+ self._faiss = IndexIVFPQ(
96
+ self._faiss, ndim, nlist, nsegment, nbit)
97
+
98
+ self._faiss.train(train)
99
+ self._faiss.nprobe = 10
100
+ self._specs['Class'] = 'usearch.IndexQuantizedFAISS'