usearch 0.13.0 → 0.15.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.
Files changed (64) hide show
  1. package/.github/workflows/release.yml +62 -7
  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 -8
  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 +178 -320
  44. package/python/scripts/bench.ipynb +170 -0
  45. package/python/scripts/bench.py +102 -185
  46. package/python/scripts/index_faiss.py +100 -0
  47. package/python/scripts/test.py +57 -34
  48. package/python/usearch/eval.py +353 -0
  49. package/python/usearch/index.py +138 -117
  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 -161
  62. package/python/usearch/synthetic.py +0 -32
  63. package/src/punned.hpp +0 -887
  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,198 +1,115 @@
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
10
+
11
+ from usearch.index import Index, Label
12
+ from usearch.eval import Evaluation, Dataset, AddTask
13
+ from usearch.index import (
14
+ DEFAULT_CONNECTIVITY,
15
+ DEFAULT_EXPANSION_ADD,
16
+ DEFAULT_EXPANSION_SEARCH,
17
+ )
18
+
19
+
20
+ def bench_speed(
21
+ eval: Evaluation,
22
+ connectivity: int = DEFAULT_CONNECTIVITY,
23
+ expansion_add: int = DEFAULT_EXPANSION_ADD,
24
+ expansion_search: int = DEFAULT_EXPANSION_SEARCH,
25
+ train: bool = False,
26
+ ) -> pd.DataFrame:
27
+
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):
7
33
 
8
- from usearch.index import Index
9
- from usearch.io import load_matrix
10
-
11
-
12
- def measure(f) -> Tuple[float, Any]:
13
- a = time.time_ns()
14
- result = f()
15
- b = time.time_ns()
16
- c = b - a
17
- secs = c / (10 ** 9)
18
- print(f'- Took: {secs:.2f} seconds')
19
- return secs, result
20
-
21
-
22
- def bench_faiss(
23
- index,
24
- vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
25
-
26
- dt, _ = measure(lambda: index.add(vectors))
27
- print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
28
-
29
- dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
30
- print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
31
-
32
- recall_at_one = 0.0
33
- matches = results[1]
34
- for i in range(queries.shape[0]):
35
- recall_at_one += neighbors[i, 0] in matches[i, :]
36
- recall_at_one /= queries.shape[0]
37
- print(f'- Recall@1: {recall_at_one * 100:.2f} %')
38
-
39
-
40
- def bench_usearch(
41
- index,
42
- vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
43
-
44
- labels = np.arange(vectors.shape[0], dtype=np.longlong)
45
- assert len(index) == 0
46
- dt, _ = measure(lambda: index.add(labels, vectors))
47
- assert len(index) == vectors.shape[0]
48
- print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
49
-
50
- dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
51
- print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
52
-
53
- recall_at_one = 0.0
54
- matches = results[0]
55
- for i in range(queries.shape[0]):
56
- recall_at_one += neighbors[i, 0] in matches[i, :]
57
- recall_at_one /= queries.shape[0]
58
- print(f'- Recall@1: {recall_at_one * 100:.2f} %')
59
-
60
-
61
- def main(
62
- vectors: str,
63
- queries: str,
64
- neighbors: str,
65
- connectivity: int = 16,
66
- expansion_add: int = 128,
67
- expansion_search: int = 64,
68
- k: Optional[int] = None,
69
- ):
70
-
71
- vectors_mat = load_matrix(vectors)
72
- queries_mat = load_matrix(queries)
73
- neighbors_mat = load_matrix(neighbors)
74
- dim = vectors_mat.shape[1]
75
- if k:
76
- neighbors_mat = neighbors_mat[:, :1]
77
-
78
- print('USearch: HNSW')
79
- index = Index(
80
- ndim=dim,
81
- expansion_add=expansion_add,
82
- expansion_search=expansion_search,
83
- connectivity=connectivity,
84
- )
85
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
86
-
87
- print('USearch: HNSW + Half Precision')
88
- index = Index(
89
- ndim=dim,
90
- dtype='f16',
91
- expansion_add=expansion_add,
92
- expansion_search=expansion_search,
93
- connectivity=connectivity,
94
- )
95
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
96
-
97
- print('USearch: HNSW + Quarter Precision')
98
- index = Index(
99
- ndim=dim,
100
- dtype='f8',
101
- expansion_add=expansion_add,
102
- expansion_search=expansion_search,
103
- connectivity=connectivity,
104
- )
105
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
106
-
107
- # Showcases how to use Numba to JIT-compile similarity measures for USearch.
108
- # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
109
- try:
110
- from numba import cfunc, types, carray
111
-
112
- signature_f32 = types.float32(
113
- types.CPointer(types.float32),
114
- types.CPointer(types.float32),
115
- types.uint64, types.uint64)
116
-
117
- @cfunc(signature_f32)
118
- def inner_product_f32(a, b, n, m):
119
- a_array = carray(a, dim)
120
- b_array = carray(b, dim)
121
- c = types.float32(0)
122
- for i in range(dim):
123
- c += a_array[i] * b_array[i]
124
- return types.float32(1 - c)
125
-
126
- signature_f8 = types.float32(
127
- types.CPointer(types.int8),
128
- types.CPointer(types.int8),
129
- types.uint64, types.uint64)
130
-
131
- @cfunc(signature_f8)
132
- def cos_f8(a, b, n, m):
133
- a_array = carray(a, dim)
134
- b_array = carray(b, dim)
135
- c = types.int32(0)
136
- a_sq = types.int32(0)
137
- b_sq = types.int32(0)
138
- for i in range(dim):
139
- ai = types.int16(a_array[i])
140
- bi = types.int16(b_array[i])
141
- c += ai * bi
142
- a_sq += ai * ai
143
- b_sq += bi * bi
144
- a_norm = math.sqrt(types.float32(a_sq))
145
- b_norm = math.sqrt(types.float32(b_sq))
146
- c_norm = types.float32(c) / (a_norm * b_norm)
147
- return types.float32(1 - c_norm)
148
-
149
- print('USearch: HNSW + Numba JIT')
150
34
  index = Index(
151
- ndim=dim,
35
+ ndim=eval.ndim,
36
+ jit=jit,
37
+ dtype=dtype,
152
38
  expansion_add=expansion_add,
153
39
  expansion_search=expansion_search,
154
40
  connectivity=connectivity,
155
- metric=inner_product_f32,
41
+ path='USearch' + ['', '+JIT'][jit] + ':' + dtype,
156
42
  )
157
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
43
+ indexes.append(index)
158
44
 
159
- print('USearch: HNSW + Quarter Precision + Numba JIT')
160
- index = Index(
161
- ndim=dim,
162
- 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,
163
50
  expansion_add=expansion_add,
164
51
  expansion_search=expansion_search,
165
52
  connectivity=connectivity,
166
- metric=cos_f8,
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],
82
+
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.
87
+
88
+ :param count: Number of vectors, defaults to 1_000_000
89
+ :type count: int, optional
90
+ """
91
+
92
+ results = []
93
+ for connectivity, ndim in itertools.product(connectivities, dimensions):
94
+
95
+ task = AddTask(
96
+ labels=np.arange(count, dtype=Label),
97
+ vectors=np.random.rand(count, ndim).astype(np.float32),
167
98
  )
168
- bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
169
- except ImportError:
170
- print('Skipping Numba benchmarks')
171
-
172
- # Don't depend on the FAISS installation for benchmarks
173
- try:
174
- from faiss import IndexHNSWFlat, IndexIVFPQ
175
-
176
- print('FAISS: HNSW')
177
- index = IndexHNSWFlat(dim, connectivity)
178
- index.hnsw.efSearch = expansion_search
179
- index.hnsw.efConstruction = expansion_add
180
- bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
181
-
182
- print('FAISS: HNSW + IVFPQ')
183
- # Number of inverted lists (number of partitions or cells).
184
- nlist = 10000
185
- nsegment = 16 # Number of segments for PQ (number of subquantizers).
186
- nbit = 8 # Number of bits to encode each segment.
187
- coarse_quantizer = IndexHNSWFlat(dim, connectivity)
188
- index = IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
189
- index.train(vectors_mat)
190
- index.nprobe = 10
191
- bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
192
-
193
- except ImportError:
194
- print('Skipping FAISS benchmarks')
195
-
196
-
197
- if __name__ == '__main__':
198
- fire.Fire(main)
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'