usearch 0.4.0 → 0.8.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 (45) hide show
  1. package/.clang-tidy +60 -0
  2. package/.github/workflows/release.yml +42 -16
  3. package/.github/workflows/update_version.sh +1 -0
  4. package/.vscode/launch.json +22 -0
  5. package/.vscode/settings.json +5 -0
  6. package/.vscode/tasks.json +4 -4
  7. package/CITATION.cff +11 -0
  8. package/CMakeLists.txt +108 -95
  9. package/Cargo.toml +1 -1
  10. package/Dockerfile +14 -0
  11. package/README.md +8 -7
  12. package/VERSION +1 -1
  13. package/binding.gyp +2 -2
  14. package/build.rs +2 -2
  15. package/cmake/config.cmake.in +15 -0
  16. package/cmake/pkg-config.pc.in +4 -0
  17. package/cmake/usearchConfigVersion.cmake.in +20 -0
  18. package/conanfile.py +1 -1
  19. package/docs/benchmarks.md +11 -2
  20. package/docs/{Compilation.md → compilation.md} +54 -27
  21. package/include/usearch/usearch.hpp +664 -390
  22. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +17 -16
  23. package/javascript/lib.cpp +13 -13
  24. package/javascript/test.js +1 -1
  25. package/objc/USearchObjective.mm +46 -29
  26. package/objc/include/USearchObjective.h +18 -26
  27. package/package.json +1 -1
  28. package/pyproject.toml +3 -2
  29. package/python/lib.cpp +179 -120
  30. package/python/scripts/bench.py +188 -0
  31. package/python/scripts/bench_params.py +113 -0
  32. package/python/{test.py → scripts/test.py} +32 -13
  33. package/python/usearch/client.py +100 -14
  34. package/python/usearch/server.py +104 -23
  35. package/rust/lib.cpp +15 -14
  36. package/rust/lib.hpp +5 -5
  37. package/rust/lib.rs +3 -3
  38. package/setup.py +7 -0
  39. package/src/CMakeLists.txt +107 -0
  40. package/src/bench.cpp +45 -15
  41. package/src/{advanced.hpp → punned.hpp} +227 -199
  42. package/src/test.cpp +62 -0
  43. package/swift/Test.swift +1 -1
  44. package/wolfram/lib.cpp +14 -14
  45. package/python/bench.py +0 -146
@@ -0,0 +1,188 @@
1
+ import time
2
+ import numpy as np
3
+ import math
4
+
5
+ import fire
6
+
7
+ from usearch.index import Index
8
+ from usearch.io import load_matrix
9
+
10
+
11
+ def measure(f) -> float:
12
+ a = time.time_ns()
13
+ result = f()
14
+ b = time.time_ns()
15
+ c = b - a
16
+ secs = c / (10 ** 9)
17
+ print(f'- Took: {secs:.2f} seconds')
18
+ return secs, result
19
+
20
+
21
+ def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
22
+ dt, _ = measure(lambda: index.add(vectors))
23
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
24
+
25
+ dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
26
+ print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
27
+
28
+ recall_at_one = 0.0
29
+ matches = results[1]
30
+ for i in range(queries.shape[0]):
31
+ recall_at_one += neighbors[i, 0] in matches[i, :]
32
+ recall_at_one /= queries.shape[0]
33
+ print(f'- Recall@1: {recall_at_one * 100:.2f} %')
34
+
35
+
36
+ def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
37
+ labels = np.arange(vectors.shape[0], dtype=np.longlong)
38
+ assert len(index) == 0
39
+ dt, _ = measure(lambda: index.add(labels, vectors))
40
+ assert len(index) == vectors.shape[0]
41
+ print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
42
+
43
+ dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
44
+ print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
45
+
46
+ recall_at_one = 0.0
47
+ matches = results[0]
48
+ for i in range(queries.shape[0]):
49
+ recall_at_one += neighbors[i, 0] in matches[i, :]
50
+ recall_at_one /= queries.shape[0]
51
+ print(f'- Recall@1: {recall_at_one * 100:.2f} %')
52
+
53
+
54
+ def main(
55
+ vectors: str,
56
+ queries: str,
57
+ neighbors: str,
58
+ connectivity: int = 16,
59
+ expansion_add: int = 128,
60
+ expansion_search: int = 64,
61
+ ):
62
+
63
+ vectors_mat = load_matrix(vectors)
64
+ queries_mat = load_matrix(queries)
65
+ neighbors_mat = load_matrix(neighbors)
66
+ dim = vectors_mat.shape[1]
67
+
68
+ print('USearch: HNSW')
69
+ index = Index(
70
+ ndim=dim,
71
+ expansion_add=expansion_add,
72
+ expansion_search=expansion_search,
73
+ connectivity=connectivity,
74
+ )
75
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
76
+
77
+ print('USearch: HNSW + Half Precision')
78
+ index = Index(
79
+ ndim=dim,
80
+ dtype='f16',
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 + Quarter Precision')
88
+ index = Index(
89
+ ndim=dim,
90
+ dtype='f8',
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
+ # Showcases how to use Numba to JIT-compile similarity measures for USearch.
98
+ # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
99
+ try:
100
+ from numba import cfunc, types, carray
101
+
102
+ signature_f32 = types.float32(
103
+ types.CPointer(types.float32),
104
+ types.CPointer(types.float32),
105
+ types.size_t, types.size_t)
106
+
107
+ @cfunc(signature_f32)
108
+ def inner_product_f32(a, b, n, m):
109
+ a_array = carray(a, n)
110
+ b_array = carray(b, n)
111
+ c = types.float32(0)
112
+ for i in range(n):
113
+ c += a_array[i] * b_array[i]
114
+ return types.float32(1 - c)
115
+
116
+ signature_f8 = types.float32(
117
+ types.CPointer(types.int8),
118
+ types.CPointer(types.int8),
119
+ types.size_t, types.size_t)
120
+
121
+ @cfunc(signature_f8)
122
+ def cos_f8(a, b, n, m):
123
+ a_array = carray(a, n)
124
+ b_array = carray(b, n)
125
+ c = types.int32(0)
126
+ a_sq = types.int32(0)
127
+ b_sq = types.int32(0)
128
+ for i in range(n):
129
+ ai = types.int16(a_array[i])
130
+ bi = types.int16(b_array[i])
131
+ c += ai * bi
132
+ a_sq += ai * ai
133
+ b_sq += bi * bi
134
+ a_norm = math.sqrt(types.float32(a_sq))
135
+ b_norm = math.sqrt(types.float32(b_sq))
136
+ c_norm = types.float32(c) / (a_norm * b_norm)
137
+ return types.float32(1 - c_norm)
138
+
139
+ print('USearch: HNSW + Numba JIT')
140
+ index = Index(
141
+ ndim=dim,
142
+ expansion_add=expansion_add,
143
+ expansion_search=expansion_search,
144
+ connectivity=connectivity,
145
+ metric_pointer=inner_product_f32.address,
146
+ )
147
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
148
+
149
+ print('USearch: HNSW + Quarter Precision + Numba JIT')
150
+ index = Index(
151
+ ndim=dim,
152
+ dtype='f8',
153
+ expansion_add=expansion_add,
154
+ expansion_search=expansion_search,
155
+ connectivity=connectivity,
156
+ metric_pointer=cos_f8.address,
157
+ )
158
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
159
+ except ImportError:
160
+ print('Skipping Numba benchmarks')
161
+
162
+ # Don't depend on the FAISS installation for benchmarks
163
+ try:
164
+ from faiss import IndexHNSWFlat, IndexIVFPQ
165
+
166
+ print('FAISS: HNSW')
167
+ index = IndexHNSWFlat(dim, connectivity)
168
+ index.hnsw.efSearch = expansion_search
169
+ index.hnsw.efConstruction = expansion_add
170
+ bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
171
+
172
+ print('FAISS: HNSW + IVFPQ')
173
+ # Number of inverted lists (number of partitions or cells).
174
+ nlist = 10000
175
+ nsegment = 16 # Number of segments for PQ (number of subquantizers).
176
+ nbit = 8 # Number of bits to encode each segment.
177
+ coarse_quantizer = IndexHNSWFlat(dim, connectivity)
178
+ index = IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
179
+ index.train(vectors_mat)
180
+ index.nprobe = 10
181
+ bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
182
+
183
+ except ImportError:
184
+ print('Skipping FAISS benchmarks')
185
+
186
+
187
+ if __name__ == '__main__':
188
+ fire.Fire(main)
@@ -0,0 +1,113 @@
1
+ import os
2
+ import time
3
+ import numpy as np
4
+
5
+ import fire
6
+ import plotly.express as px
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+
11
+ from usearch.index import Index
12
+ from usearch.io import load_matrix
13
+
14
+
15
+ def measure(f) -> float:
16
+ a = time.time_ns()
17
+ result = f()
18
+ b = time.time_ns()
19
+ c = b - a
20
+ secs = c / (10 ** 9)
21
+ print(f'- Took: {secs:.2f} seconds')
22
+ return secs, result
23
+
24
+
25
+ def bench_usearch(index, vectors: np.ndarray) -> float:
26
+ labels = np.arange(vectors.shape[0], dtype=np.longlong)
27
+ assert len(index) == 0
28
+ dt, _ = measure(lambda: index.add(labels, vectors))
29
+ assert len(index) == vectors.shape[0]
30
+ insertions_per_second = vectors.shape[0]/dt
31
+ print(f'- Performance: {insertions_per_second:.2f} insertions/s')
32
+ return insertions_per_second
33
+
34
+
35
+ class Main:
36
+
37
+ def dimensions(
38
+ self,
39
+ count: int = 1_000_000,
40
+ connectivity: int = 16,
41
+ expansion_add: int = 128,
42
+ expansion_search: int = 64,
43
+ ):
44
+
45
+ dimensions = [
46
+ 2, 3, 4, 8, 16, 32, 96, 100,
47
+ 256, 384, 512, 768, 1024, 1536]
48
+ results = []
49
+
50
+ try:
51
+ for ndim in dimensions:
52
+ vectors_mat = np.random.rand(count, ndim).astype(np.float32)
53
+
54
+ print(f'USearch for {ndim}-dimensional f32 vectors')
55
+ index = Index(
56
+ ndim=ndim,
57
+ expansion_add=expansion_add,
58
+ expansion_search=expansion_search,
59
+ connectivity=connectivity,
60
+ )
61
+ speed = bench_usearch(index, vectors_mat)
62
+ results.append({'ndim': ndim, 'speed': speed})
63
+ del index
64
+ except KeyboardInterrupt:
65
+ pass
66
+
67
+ df = pd.DataFrame(results)
68
+ fig = px.line(
69
+ df, x='ndim', y='speed', log_x=True,
70
+ title='HNSW Indexing Speed vs Vector Dimensions')
71
+ fig.write_image(os.path.join(os.path.dirname(
72
+ __file__), 'bench_dimensions.png'))
73
+ fig.show()
74
+
75
+ def connectivity(
76
+ self,
77
+ count: int = 1_000_000,
78
+ ndim: int = 256,
79
+ expansion_add: int = 128,
80
+ expansion_search: int = 64,
81
+ ):
82
+
83
+ connectivities = range(10, 20)
84
+ results = []
85
+
86
+ try:
87
+ for connectivity in connectivities:
88
+ vectors_mat = np.random.rand(count, ndim).astype(np.float32)
89
+
90
+ print(f'USearch for {connectivity} connectivity')
91
+ index = Index(
92
+ ndim=ndim,
93
+ expansion_add=expansion_add,
94
+ expansion_search=expansion_search,
95
+ connectivity=connectivity,
96
+ )
97
+ speed = bench_usearch(index, vectors_mat)
98
+ results.append({'connectivity': connectivity, 'speed': speed})
99
+ del index
100
+ except KeyboardInterrupt:
101
+ pass
102
+
103
+ df = pd.DataFrame(results)
104
+ fig = px.line(
105
+ df, x='connectivity', y='speed', log_x=True,
106
+ title='HNSW Indexing Speed vs Connectivity')
107
+ fig.write_image(os.path.join(os.path.dirname(
108
+ __file__), 'bench_connectivity.png'))
109
+ fig.show()
110
+
111
+
112
+ if __name__ == '__main__':
113
+ fire.Fire(Main)
@@ -1,37 +1,57 @@
1
1
  import pytest
2
2
  import numpy as np
3
3
 
4
-
4
+ from usearch.io import load_matrix
5
5
  from usearch.index import Index, SetsIndex
6
6
 
7
7
  dimensions = [13, 99, 100, 256]
8
8
  sizes = [1, 2, 100]
9
- types = ['f32', 'f16', 'f8', 'f64']
9
+ index_types = ['f64', 'f32', 'f16', 'f8']
10
+ numpy_types = [np.float64, np.float32, np.float16, np.byte]
11
+ connectivities = [3, 13, 50]
10
12
 
11
13
 
12
14
  @pytest.mark.parametrize('ndim', dimensions)
13
- @pytest.mark.parametrize('dtype', types)
14
- def test_cos(ndim: int, dtype: str):
15
- index = Index(ndim=ndim, metric='cos', dtype=dtype)
16
- vector = np.random.uniform(0, 0.7, (index.ndim)).astype(np.float32)
15
+ @pytest.mark.parametrize('index_type', index_types)
16
+ @pytest.mark.parametrize('numpy_type', numpy_types)
17
+ @pytest.mark.parametrize('connectivity', connectivities)
18
+ def test_l2sq(ndim: int, index_type: str, numpy_type: str, connectivity: int):
19
+ index = Index(
20
+ metric='l2sq',
21
+ ndim=ndim,
22
+ dtype=index_type,
23
+ connectivity=connectivity,
24
+ )
25
+ assert index.ndim == ndim
26
+ assert index.connectivity == connectivity
27
+
28
+ vector = np.random.uniform(0, 0.7, (index.ndim)).astype(numpy_type)
17
29
  index.add(42, vector)
18
30
  matches, distances, count = index.search(vector, 10)
19
31
 
32
+ assert len(index) == 1
20
33
  assert len(matches) == count
21
34
  assert len(distances) == count
22
35
  assert count == 1
23
36
  assert matches[0] == 42
24
37
  assert distances[0] == pytest.approx(0, abs=1e-3)
25
38
 
39
+ index.save('tmp.usearch')
40
+ index.clear()
41
+ assert len(index) == 0
42
+
43
+ index.load('tmp.usearch')
44
+ assert len(index) == 1
45
+
26
46
 
27
47
  @pytest.mark.parametrize('ndim', dimensions)
28
48
  @pytest.mark.parametrize('size', sizes)
29
- @pytest.mark.parametrize('dtype', types)
30
- def test_cos_batch(ndim: int, size: int, dtype: str):
31
- index = Index(ndim=ndim, metric='cos', dtype=dtype)
49
+ @pytest.mark.parametrize('index_type', index_types)
50
+ @pytest.mark.parametrize('numpy_type', numpy_types)
51
+ def test_l2sq_batch(ndim: int, size: int, index_type: str, numpy_type: str):
52
+ index = Index(ndim=ndim, metric='l2sq', dtype=index_type)
32
53
 
33
- vectors = np.random.uniform(
34
- 0, 0.3, (size, index.ndim)).astype(np.float32)
54
+ vectors = np.random.uniform(0, 0.7, (size, index.ndim)).astype(numpy_type)
35
55
  labels = np.array(range(size), dtype=np.longlong)
36
56
 
37
57
  index.add(labels, vectors)
@@ -84,8 +104,7 @@ def test_sets():
84
104
  index = SetsIndex()
85
105
  index.add(10, np.array([10, 12, 15], dtype=np.uint32))
86
106
  index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
87
- results = index.search(np.array([12, 15], dtype=np.uint32), 20)
88
-
107
+ # results = index.search(np.array([12, 15], dtype=np.uint32), 10)
89
108
  # assert list(results) == [10, 11]
90
109
 
91
110
 
@@ -1,34 +1,120 @@
1
+ from typing import Union, Optional
2
+
1
3
  import numpy as np
2
4
  from ucall.client import Client
3
5
 
4
6
 
7
+ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
8
+ if vector.dtype != np.int8 and vector.dtype != np.uint8 and vector.dtype != np.byte:
9
+ return None
10
+ if not np.all((vector >= 0) | (vector <= 100)):
11
+ return None
12
+
13
+ # Let's map [0, 100] to the range from [23, 123],
14
+ # poking 60 and replacing with the 124.
15
+ vector += 23
16
+ vector[vector == 60] = 124
17
+ ascii = str(vector)
18
+ return ascii
19
+
20
+
5
21
  class IndexClient:
6
22
 
7
23
  def __init__(self, uri: str = '127.0.0.1', port: int = 8545, use_http: bool = True) -> None:
8
24
  self.client = Client(uri=uri, port=port, use_http=use_http)
9
25
 
10
- def add(self, labels: np.array, vectors: np.array):
11
- if isinstance(labels, int):
12
- return self.client.add_one(label=labels, vectors=vectors)
26
+ def add_one(self, label: int, vector: np.ndarray):
27
+ assert isinstance(label, int)
28
+ assert isinstance(vector, np.ndarray)
29
+ vector = vector.flatten()
30
+ ascii = _vector_to_ascii(vector)
31
+ if ascii:
32
+ self.client.add_ascii(label=label, string=ascii)
13
33
  else:
14
- return self.client.add_many(labels=labels, vectors=vectors)
34
+ self.client.add_one(label=label, vectors=vector)
35
+
36
+ def add_many(self, labels: np.ndarray, vectors: np.ndarray):
37
+ assert isinstance(labels, int)
38
+ assert isinstance(vectors, np.ndarray)
39
+ assert labels.ndim == 1 and vectors.ndim == 2
40
+ assert labels.shape[0] == vectors.shape[0]
41
+ self.client.add_many(labels=labels, vectors=vectors)
42
+
43
+ def add(self, labels: Union[np.ndarray, int], vectors: np.ndarray):
44
+ if isinstance(labels, int) or len(labels) == 1:
45
+ return self.add_one(labels, vectors)
46
+ else:
47
+ return self.add_many(labels, vectors)
48
+
49
+ def search_one(self, vector: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
50
+ matches: list[dict] = []
51
+ vector = vector.flatten()
52
+ ascii = _vector_to_ascii(vector)
53
+ if ascii:
54
+ matches = self.client.search_ascii(string=ascii, count=count)
55
+ else:
56
+ matches = self.client.search_one(vector=vector, count=count)
57
+
58
+ print(matches.data)
59
+ matches = matches.json
60
+
61
+ labels = np.array((1, count), dtype=np.uint32)
62
+ distances = np.array((1, count), dtype=np.float32)
63
+ counts = np.array((1), dtype=np.uint32)
64
+ for col, result in enumerate(matches):
65
+ labels[0, col] = result['label']
66
+ distances[0, col] = result['distance']
67
+ counts[0] = len(matches)
15
68
 
16
- def search(self, vectors: np.array, count: int) -> tuple[np.array, np.array, np.array]:
17
- matches = []
18
- distances = []
19
- counts = []
20
- # return self.client.search_one(vectors=vectors, count=count)
21
- return matches, distances, counts
69
+ return labels, distances, counts
70
+
71
+ def search_many(self, vectors: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
72
+ batch_size: int = vectors.shape[0]
73
+ list_of_matches: list[list[dict]] = self.client.search_many(
74
+ vectors=vectors, count=count)
75
+
76
+ labels = np.array((batch_size, count), dtype=np.uint32)
77
+ distances = np.array((batch_size, count), dtype=np.float32)
78
+ counts = np.array((batch_size), dtype=np.uint32)
79
+ for row, matches in enumerate(list_of_matches):
80
+ for col, result in enumerate(matches):
81
+ labels[row, col] = result['label']
82
+ distances[row, col] = result['distance']
83
+ counts[row] = len(results)
84
+
85
+ return labels, distances, counts
86
+
87
+ def search(self, vectors: np.ndarray, count: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
88
+ if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
89
+ return self.search_one(vectors, count)
90
+ else:
91
+ return self.search_many(vectors, count)
22
92
 
23
93
  def __len__(self):
24
- return self.client.size()
94
+ return self.client.size().json()
25
95
 
26
96
  @property
27
97
  def ndim(self):
28
- return self.client.ndim()
98
+ return self.client.ndim().json()
29
99
 
30
100
  def capacity(self):
31
- return self.client.capacity()
101
+ return self.client.capacity().json()
32
102
 
33
103
  def connectivity(self):
34
- return self.client.connectivity()
104
+ return self.client.connectivity().json()
105
+
106
+ def load(self, path: str):
107
+ raise NotImplementedError()
108
+
109
+ def view(self, path: str):
110
+ raise NotImplementedError()
111
+
112
+ def save(self, path: str):
113
+ raise NotImplementedError()
114
+
115
+
116
+ if __name__ == '__main__':
117
+ index = IndexClient()
118
+ index.add(42, np.array([0.4] * 256, dtype=np.float32))
119
+ results = index.search(np.array([0.4] * 256, dtype=np.float32), 10)
120
+ print(results)
@@ -1,32 +1,49 @@
1
- import ucall.rich_posix as ucall
2
- import usearch
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
3
 
4
- import fire
4
+ import os
5
+ import argparse
5
6
  import numpy as np
6
- from PIL import Image
7
7
 
8
+ from ucall.rich_posix import Server
9
+ from usearch.index import Index
8
10
 
9
- def serve(ndim: int, metric: str = 'ip'):
10
11
 
11
- server = ucall.Server()
12
- index = usearch.Index(ndim=ndim, metric=metric)
12
+ def _results_to_json(results: tuple[np.ndarray, np.ndarray, np.ndarray], row: int) -> list[dict]:
13
+ count = results[2][row]
14
+ labels = results[0][row, :count]
15
+ distances = results[1][row, :count]
16
+ return [{'label': int(l), 'distance': float(d)} for l, d in zip(labels, distances)]
13
17
 
14
- @server
15
- def add_one(label: int, vector: np.array):
16
- labels = np.array([label], dtype=np.longlong)
17
- vectors = vector.reshape(vector.shape[0], 1)
18
- index.add(labels, vectors, copy=True)
19
18
 
20
- @server
21
- def add_many(labels: np.array, vectors: np.array):
22
- labels = labels.astype(np.longlong)
23
- index.add(labels, vectors, copy=True)
19
+ def _ascii_to_vector(string: str) -> np.ndarray:
20
+ """
21
+ WARNING: A dirty performance hack!
22
+ Assuming the `f8` vectors in our implementations are just integers,
23
+ and generally contain scalars in the [0, 100] range, we can transmit
24
+ them as JSON-embedded strings. The only symbols we must avoid are
25
+ the double-quote '"' (code 22) and backslash '\' (code 60).
26
+ Printable ASCII characters are in [20, 126].
27
+ """
28
+ vector = np.array(string, dtype=np.int8)
29
+ vector[vector == 124] = 60
30
+ vector -= 23
31
+ return vector
24
32
 
25
- @server
26
- def search_one(vector: np.array, count: int) -> np.ndarray:
27
- vectors = vector.reshape(vector.shape[0], 1)
28
- results = index.search(vectors, 3)
29
- return results[0][:results[2][0]]
33
+
34
+ def serve(
35
+ ndim_: int, metric: str = 'ip',
36
+ port: int = 8545, threads: int = 1,
37
+ path: str = 'index.usearch', immutable: bool = False):
38
+
39
+ server = Server(port=port)
40
+ index = Index(ndim=ndim_, metric=metric)
41
+
42
+ if os.path.exists(path):
43
+ if immutable:
44
+ index.view(path)
45
+ else:
46
+ index.load(path)
30
47
 
31
48
  @server
32
49
  def size() -> int:
@@ -44,8 +61,72 @@ def serve(ndim: int, metric: str = 'ip'):
44
61
  def connectivity() -> int:
45
62
  return index.connectivity()
46
63
 
47
- server.run()
64
+ @server
65
+ def add_one(label: int, vector: np.ndarray):
66
+ print('adding', label, vector)
67
+ labels = np.array([label], dtype=np.longlong)
68
+ vectors = vector.flatten().reshape(vector.shape[0], 1)
69
+ index.add(labels, vectors)
70
+
71
+ @server
72
+ def add_many(labels: np.ndarray, vectors: np.ndarray):
73
+ labels = labels.astype(np.longlong)
74
+ index.add(labels, vectors, threads=threads)
75
+
76
+ @server
77
+ def search_one(vector: np.ndarray, count: int) -> list[dict]:
78
+ print('search', vector, count)
79
+ vectors = vector.reshape(vector.shape[0], 1)
80
+ results = index.search(vectors, count)
81
+ return _results_to_json(results, 0)
82
+
83
+ @server
84
+ def search_many(vectors: np.ndarray, count: int) -> list[list[dict]]:
85
+ results = index.search(vectors, count)
86
+ return [_results_to_json(results, i) for i in range(vectors.shape[0])]
87
+
88
+ @server
89
+ def add_ascii(label: int, string: str):
90
+ return add_one(label, _ascii_to_vector(string))
91
+
92
+ @server
93
+ def search_ascii(string: str, count: int):
94
+ return search_one(_ascii_to_vector(string), count)
95
+
96
+ try:
97
+ server.run()
98
+ except KeyboardInterrupt:
99
+ if not immutable:
100
+ index.save(path)
48
101
 
49
102
 
50
103
  if __name__ == '__main__':
51
- fire(serve)
104
+
105
+ parser = argparse.ArgumentParser()
106
+ parser.add_argument('-v', '--verbose', help='log server activity')
107
+ parser.add_argument(
108
+ '--ndim', type=int,
109
+ help='dimensionality of the vectors')
110
+ parser.add_argument(
111
+ '--immutable', type=bool, default=False,
112
+ help='the index can not be updated')
113
+
114
+ parser.add_argument(
115
+ '--metric', type=str, default='ip', choices=['ip', 'cos', 'l2sq', 'haversine'],
116
+ help='distance function to compare vectors')
117
+ parser.add_argument(
118
+ '-p', '--port', type=int, default=8545,
119
+ help='port to open for client connections')
120
+ parser.add_argument(
121
+ '-j', '--threads', type=int, default=1,
122
+ help='number of CPU threads to use')
123
+ parser.add_argument(
124
+ '--path', type=str, default='index.usearch',
125
+ help='where to store the index')
126
+
127
+ args = parser.parse_args()
128
+ assert args.ndim is not None, 'Define the number of dimensions!'
129
+ serve(
130
+ ndim_=args.ndim, metric=args.metric,
131
+ threads=args.threads, port=args.port,
132
+ path=args.path, immutable=args.immutable)