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.
- package/.github/workflows/release.yml +56 -4
- package/.github/workflows/update_version.sh +6 -1
- package/.vscode/launch.json +21 -22
- package/.vscode/settings.json +7 -1
- package/CITATION.cff +4 -4
- package/CMakeLists.txt +1 -1
- package/Cargo.toml +1 -1
- package/Package.swift +0 -1
- package/README.md +100 -27
- package/binding.gyp +0 -1
- package/build.gradle +1 -1
- package/build.rs +3 -3
- package/c/lib.cpp +32 -9
- package/c/usearch.h +67 -36
- package/conanfile.py +1 -1
- package/{src → cpp}/bench.cpp +52 -36
- package/{src → cpp}/test.cpp +17 -7
- package/docs/benchmarks.md +33 -10
- package/docs/c/index.rst +11 -0
- package/docs/c/reference.rst +7 -0
- package/docs/compilation.md +24 -9
- package/docs/conf.dox +6 -6
- package/docs/conf.py +3 -1
- package/docs/cpp/index.rst +11 -0
- package/docs/cpp/reference.rst +7 -0
- package/docs/index.rst +10 -0
- package/docs/java/index.rst +11 -0
- package/docs/java/reference.rst +7 -0
- package/docs/javascript/index.rst +11 -0
- package/docs/javascript/reference.rst +9 -0
- package/docs/python/index.rst +11 -0
- package/docs/python/reference.rst +35 -0
- package/golang/lib.go +1 -1
- package/include/usearch/{usearch.hpp → index.hpp} +152 -148
- package/include/usearch/index_punned_dense.hpp +633 -0
- package/include/usearch/index_punned_helpers.hpp +528 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
- package/javascript/lib.cpp +26 -50
- package/javascript/usearch.js +117 -2
- package/objc/USearchObjective.mm +145 -49
- package/objc/include/USearchObjective.h +106 -30
- package/package.json +2 -2
- package/python/lib.cpp +134 -331
- package/python/scripts/bench.ipynb +170 -0
- package/python/scripts/bench.py +87 -175
- package/python/scripts/index_faiss.py +100 -0
- package/python/scripts/test.py +41 -37
- package/python/usearch/eval.py +353 -0
- package/python/usearch/index.py +117 -113
- package/python/usearch/io.py +7 -2
- package/python/usearch/join.py +100 -0
- package/python/usearch/numba.py +101 -0
- package/rust/lib.cpp +27 -14
- package/rust/lib.hpp +4 -10
- package/rust/lib.rs +50 -17
- package/setup.py +12 -2
- package/simsimd/include/simsimd/simsimd.h +32 -3
- package/swift/Index+Sugar.swift +55 -29
- package/swift/Test.swift +1 -1
- package/wolfram/lib.cpp +4 -4
- package/python/scripts/bench_params.py +0 -201
- package/python/usearch/synthetic.py +0 -31
- package/src/punned.hpp +0 -906
- /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
|
+
}
|
package/python/scripts/bench.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
from typing import
|
|
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.
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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=
|
|
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
|
-
|
|
41
|
+
path='USearch' + ['', '+JIT'][jit] + ':' + dtype,
|
|
161
42
|
)
|
|
162
|
-
|
|
43
|
+
indexes.append(index)
|
|
163
44
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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'
|