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.
- package/.github/workflows/release.yml +62 -7
- 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 -8
- 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 +178 -320
- package/python/scripts/bench.ipynb +170 -0
- package/python/scripts/bench.py +102 -185
- package/python/scripts/index_faiss.py +100 -0
- package/python/scripts/test.py +57 -34
- package/python/usearch/eval.py +353 -0
- package/python/usearch/index.py +138 -117
- 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 -161
- package/python/usearch/synthetic.py +0 -32
- package/src/punned.hpp +0 -887
- /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,198 +1,115 @@
|
|
|
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
|
|
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=
|
|
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
|
-
|
|
41
|
+
path='USearch' + ['', '+JIT'][jit] + ':' + dtype,
|
|
156
42
|
)
|
|
157
|
-
|
|
43
|
+
indexes.append(index)
|
|
158
44
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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'
|