usearch 0.11.1 → 0.12.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/prerelease.yml +2 -5
- package/.github/workflows/release.yml +22 -2
- package/.gitignore +84 -0
- package/.vscode/launch.json +7 -1
- package/.vscode/settings.json +15 -1
- package/Cargo.toml +1 -1
- package/README.md +43 -8
- package/binding.gyp +2 -2
- package/c/lib.cpp +141 -0
- package/c/usearch.h +43 -0
- package/conanfile.py +1 -1
- package/docs/compilation.md +14 -3
- package/golang/go.mod +3 -0
- package/golang/lib.go +238 -0
- package/golang/usearch.h +9 -0
- package/golang/usearch_par_test.go +40 -0
- package/golang/usearch_test.go +82 -0
- package/include/usearch/usearch.hpp +595 -248
- package/java/cloud/unum/usearch/Index.java +1 -1
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +36 -44
- package/javascript/lib.cpp +10 -6
- package/javascript/usearch.js +3 -0
- package/objc/USearchObjective.mm +3 -3
- package/package.json +4 -3
- package/python/lib.cpp +354 -181
- package/python/scripts/bench.py +2 -2
- package/python/scripts/test.py +88 -29
- package/python/usearch/index.py +132 -57
- package/rust/lib.cpp +33 -24
- package/rust/lib.hpp +2 -2
- package/setup.py +4 -1
- package/src/CMakeLists.txt +4 -4
- package/src/bench.cpp +22 -19
- package/src/punned.hpp +108 -79
- package/src/test.cpp +77 -27
- package/wolfram/lib.cpp +7 -8
- package/Dockerfile +0 -16
- package/VERSION +0 -1
package/golang/lib.go
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
package usearch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"fmt"
|
|
6
|
+
"unsafe"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
#include "../c/usearch.h"
|
|
11
|
+
|
|
12
|
+
#cgo CPPFLAGS: -I.
|
|
13
|
+
#cgo LDFLAGS: -lusearch
|
|
14
|
+
*/
|
|
15
|
+
import "C"
|
|
16
|
+
|
|
17
|
+
type DistMetric int8
|
|
18
|
+
|
|
19
|
+
const (
|
|
20
|
+
L2_SQ DistMetric = iota
|
|
21
|
+
IP
|
|
22
|
+
COS
|
|
23
|
+
Haversine
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
func (m DistMetric) String() string {
|
|
27
|
+
switch m {
|
|
28
|
+
case L2_SQ:
|
|
29
|
+
return "l2sq"
|
|
30
|
+
case IP:
|
|
31
|
+
return "ip"
|
|
32
|
+
case COS:
|
|
33
|
+
return "cos"
|
|
34
|
+
case Haversine:
|
|
35
|
+
return "haversine"
|
|
36
|
+
default:
|
|
37
|
+
panic("unknown metric")
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Accuracy int8
|
|
42
|
+
|
|
43
|
+
const (
|
|
44
|
+
f32 Accuracy = iota
|
|
45
|
+
f16
|
|
46
|
+
f64
|
|
47
|
+
f8
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
func (a Accuracy) String() string {
|
|
51
|
+
switch a {
|
|
52
|
+
case f16:
|
|
53
|
+
return "f16"
|
|
54
|
+
case f32:
|
|
55
|
+
return "f32"
|
|
56
|
+
case f64:
|
|
57
|
+
return "f64"
|
|
58
|
+
case f8:
|
|
59
|
+
return "f8"
|
|
60
|
+
default:
|
|
61
|
+
panic("unknown accuracy")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type IndexConfig struct {
|
|
66
|
+
Accuracy Accuracy
|
|
67
|
+
Metric DistMetric
|
|
68
|
+
VecDimension int
|
|
69
|
+
InitCapacity int
|
|
70
|
+
Connectivity int
|
|
71
|
+
ExpansionAdd int
|
|
72
|
+
ExpansionSearch int
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func DefaultConfig(dimension int) IndexConfig {
|
|
76
|
+
c := IndexConfig{}
|
|
77
|
+
c.InitCapacity = 32
|
|
78
|
+
c.Connectivity = 16
|
|
79
|
+
c.VecDimension = dimension
|
|
80
|
+
c.ExpansionAdd = 128
|
|
81
|
+
c.ExpansionSearch = 64
|
|
82
|
+
return c
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Index struct {
|
|
86
|
+
opaque_handle *C.void
|
|
87
|
+
config IndexConfig
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func NewIndex(conf IndexConfig) *Index {
|
|
91
|
+
ind := &Index{config: conf}
|
|
92
|
+
ind.init()
|
|
93
|
+
return ind
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func (ind *Index) init() {
|
|
97
|
+
conf := ind.config
|
|
98
|
+
metric_str := C.CString(conf.Metric.String())
|
|
99
|
+
defer C.free(unsafe.Pointer(metric_str))
|
|
100
|
+
metric_len := C.int(len(conf.Metric.String()))
|
|
101
|
+
accuracy_str := C.CString(conf.Accuracy.String())
|
|
102
|
+
defer C.free(unsafe.Pointer(accuracy_str))
|
|
103
|
+
accuracy_len := C.int(len(conf.Accuracy.String()))
|
|
104
|
+
dimensions := C.int(conf.VecDimension)
|
|
105
|
+
capacity := C.int(conf.InitCapacity)
|
|
106
|
+
connectivity := C.int(conf.Connectivity)
|
|
107
|
+
expansion_add := C.int(conf.ExpansionAdd)
|
|
108
|
+
expansion_search := C.int(conf.ExpansionSearch)
|
|
109
|
+
ptr := C.usearch_new(metric_str, metric_len,
|
|
110
|
+
accuracy_str, accuracy_len,
|
|
111
|
+
dimensions, capacity, connectivity,
|
|
112
|
+
expansion_add, expansion_search)
|
|
113
|
+
|
|
114
|
+
ind.opaque_handle = (*C.void)(unsafe.Pointer(ptr))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (ind *Index) Destroy() {
|
|
118
|
+
if ind.opaque_handle == nil {
|
|
119
|
+
panic("index not initialized")
|
|
120
|
+
}
|
|
121
|
+
C.usearch_destroy(unsafe.Pointer(ind.opaque_handle))
|
|
122
|
+
ind.opaque_handle = nil
|
|
123
|
+
ind.config = IndexConfig{}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func (ind *Index) fileOp(path string, op string) error {
|
|
127
|
+
if ind.opaque_handle == nil {
|
|
128
|
+
panic("index not initialized")
|
|
129
|
+
}
|
|
130
|
+
c_path := C.CString(path)
|
|
131
|
+
defer C.free(unsafe.Pointer(c_path))
|
|
132
|
+
var errStr *C.char
|
|
133
|
+
switch op {
|
|
134
|
+
case "load":
|
|
135
|
+
errStr = C.usearch_load(unsafe.Pointer(ind.opaque_handle), c_path)
|
|
136
|
+
case "save":
|
|
137
|
+
errStr = C.usearch_save(unsafe.Pointer(ind.opaque_handle), c_path)
|
|
138
|
+
case "view":
|
|
139
|
+
errStr = C.usearch_view(unsafe.Pointer(ind.opaque_handle), c_path)
|
|
140
|
+
default:
|
|
141
|
+
panic("unknown file operation")
|
|
142
|
+
}
|
|
143
|
+
var err error
|
|
144
|
+
if errStr != nil {
|
|
145
|
+
err = errors.New(C.GoString(errStr))
|
|
146
|
+
C.free(unsafe.Pointer(errStr))
|
|
147
|
+
}
|
|
148
|
+
return err
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func (ind *Index) Save(path string) error {
|
|
152
|
+
return ind.fileOp(path, "save")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func (ind *Index) Load(path string) error {
|
|
156
|
+
return ind.fileOp(path, "load")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func (ind *Index) View(path string) error {
|
|
160
|
+
return ind.fileOp(path, "view")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func (ind *Index) Len() int {
|
|
164
|
+
return int(C.usearch_size(unsafe.Pointer(ind.opaque_handle)))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func (ind *Index) Connectivity() int {
|
|
168
|
+
return int(C.usearch_connectivity(unsafe.Pointer(ind.opaque_handle)))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func (ind *Index) VecDimension() int {
|
|
172
|
+
return int(C.usearch_dimensions(unsafe.Pointer(ind.opaque_handle)))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func (ind *Index) Capacity() int {
|
|
176
|
+
return int(C.usearch_capacity(unsafe.Pointer(ind.opaque_handle)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func (ind *Index) Reserve(capacity int) error {
|
|
180
|
+
if ind.opaque_handle == nil {
|
|
181
|
+
panic("index not initialized")
|
|
182
|
+
}
|
|
183
|
+
errStr := C.usearch_reserve(unsafe.Pointer(ind.opaque_handle), (C.int)(capacity))
|
|
184
|
+
var err error
|
|
185
|
+
if errStr != nil {
|
|
186
|
+
err = errors.New(C.GoString(errStr))
|
|
187
|
+
C.free(unsafe.Pointer(errStr))
|
|
188
|
+
}
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func (ind *Index) Add(label int, vec []float32) error {
|
|
193
|
+
if ind.opaque_handle == nil {
|
|
194
|
+
panic("index not initialized")
|
|
195
|
+
}
|
|
196
|
+
errStr := C.usearch_add(unsafe.Pointer(ind.opaque_handle), (C.int)(label), (*C.float)(&vec[0]))
|
|
197
|
+
var err error
|
|
198
|
+
if errStr != nil {
|
|
199
|
+
err = errors.New(C.GoString(errStr))
|
|
200
|
+
C.free(unsafe.Pointer(errStr))
|
|
201
|
+
}
|
|
202
|
+
return err
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// return must be int32 because int is 64bit in golang and 32bit in C
|
|
207
|
+
func (ind *Index) Search(query []float32, limit int) []int32 {
|
|
208
|
+
if ind.opaque_handle == nil {
|
|
209
|
+
panic("index not initialized")
|
|
210
|
+
}
|
|
211
|
+
if len(query) != ind.config.VecDimension {
|
|
212
|
+
panic(fmt.Sprintf("query vector dimension mismatch. expected %d, got %d",
|
|
213
|
+
ind.config.VecDimension, len(query)))
|
|
214
|
+
}
|
|
215
|
+
if limit <= 0 {
|
|
216
|
+
panic("limit must be greater than 0")
|
|
217
|
+
}
|
|
218
|
+
res := C.usearch_search(unsafe.Pointer(ind.opaque_handle),
|
|
219
|
+
(*C.float)(&query[0]), (C.int)(len(query)), (C.int)(limit))
|
|
220
|
+
|
|
221
|
+
// my understanding is that search panics in truly exceptional cases,
|
|
222
|
+
// none of which is every expected. so, not passing the error to the caller
|
|
223
|
+
if res.Error != nil {
|
|
224
|
+
defer C.free(unsafe.Pointer(res.Error))
|
|
225
|
+
panic(C.GoString(res.Error))
|
|
226
|
+
}
|
|
227
|
+
var labs []int32
|
|
228
|
+
//q:: who free's this memory? will golang do it?
|
|
229
|
+
labs = unsafe.Slice((*int32)(unsafe.Pointer(res.Labels)), res.Len)
|
|
230
|
+
return labs
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func (ind *Index) Size() int {
|
|
234
|
+
if ind.opaque_handle == nil {
|
|
235
|
+
panic("index not initialized")
|
|
236
|
+
}
|
|
237
|
+
return int(C.usearch_size(unsafe.Pointer(ind.opaque_handle)))
|
|
238
|
+
}
|
package/golang/usearch.h
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package usearch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sync"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestParallelism(t *testing.T) {
|
|
9
|
+
totalInserts := 3000
|
|
10
|
+
ind := NewIndex(DefaultConfig(128))
|
|
11
|
+
defer ind.Destroy()
|
|
12
|
+
|
|
13
|
+
var v0 = make([]float32, 128)
|
|
14
|
+
v0[0] = 4.4
|
|
15
|
+
var wg sync.WaitGroup
|
|
16
|
+
ind.Reserve(totalInserts)
|
|
17
|
+
|
|
18
|
+
// second thread
|
|
19
|
+
wg.Add(1)
|
|
20
|
+
go func() {
|
|
21
|
+
defer wg.Done()
|
|
22
|
+
var v1 = make([]float32, 128)
|
|
23
|
+
for l := 0; l < totalInserts/2; l++ {
|
|
24
|
+
ind.Add(10000+l, v1)
|
|
25
|
+
}
|
|
26
|
+
}()
|
|
27
|
+
|
|
28
|
+
for l := 0; l < totalInserts/2; l++ {
|
|
29
|
+
ind.Add(l, v0)
|
|
30
|
+
}
|
|
31
|
+
wg.Wait()
|
|
32
|
+
if ind.Len() != totalInserts {
|
|
33
|
+
t.Errorf("wrong length: expected %d, got %d", totalInserts, ind.Len())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
res := ind.Search(v0[:], 10)
|
|
37
|
+
if len(res) != 10 {
|
|
38
|
+
t.Errorf("wrong search result length: expected %d, got %d", 10, len(res))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package usearch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"runtime"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func Assure(err error) {
|
|
9
|
+
if err != nil {
|
|
10
|
+
panic(err)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
func TestUsearch(t *testing.T) {
|
|
14
|
+
runtime.LockOSThread()
|
|
15
|
+
dim := 128
|
|
16
|
+
conf := DefaultConfig(dim)
|
|
17
|
+
ind := NewIndex(conf)
|
|
18
|
+
defer ind.Destroy()
|
|
19
|
+
if ind.VecDimension() != dim {
|
|
20
|
+
t.Fatalf("expected dimension %d, got %d", dim, ind.VecDimension())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ind.Connectivity() != conf.Connectivity {
|
|
24
|
+
t.Fatalf("expected connectivity %d, got %d", conf.Connectivity, ind.Connectivity())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ind.Capacity() != conf.InitCapacity {
|
|
28
|
+
t.Fatalf("expected initial capacity %d, got %d", conf.InitCapacity, ind.Capacity())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ind.Len() != 0 {
|
|
32
|
+
t.Fatalf("expected empty initial index, got %d", ind.Len())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
vec := make([]float32, dim)
|
|
36
|
+
vec[0] = 40.0
|
|
37
|
+
vec[1] = 2.0
|
|
38
|
+
Assure(ind.Add(42, vec))
|
|
39
|
+
if ind.Len() != 1 {
|
|
40
|
+
t.Fatalf("expected index size 1, got %d", ind.Len())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
res := ind.Search(vec, 10)
|
|
44
|
+
if len(res) != 1 {
|
|
45
|
+
t.Fatalf("expected 1 result, got %d", len(res))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if res[0] != 42 {
|
|
49
|
+
t.Fatalf("expected result 42, got %d", res[0])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
vec_far := make([]float32, dim)
|
|
53
|
+
Assure(ind.Add(43, vec_far))
|
|
54
|
+
vec_close := make([]float32, dim)
|
|
55
|
+
vec_close[0] = 40.0
|
|
56
|
+
vec_close[1] = 4.0
|
|
57
|
+
res = ind.Search(vec_close, 1)
|
|
58
|
+
if len(res) != 1 {
|
|
59
|
+
t.Fatalf("expected 1 result, got %d", len(res))
|
|
60
|
+
}
|
|
61
|
+
if res[0] != 42 {
|
|
62
|
+
t.Fatalf("expected closest vector label to be 42, got %d", res[0])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
res = ind.Search(vec_close, 2)
|
|
66
|
+
if len(res) != 2 {
|
|
67
|
+
t.Fatalf("expected 2 results, got %d", len(res))
|
|
68
|
+
}
|
|
69
|
+
if !(res[0] == 42 && res[1] == 43) {
|
|
70
|
+
t.Fatalf("expected closest vector labels to be 42, 43, got %d, %d", res[0], res[1])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Assure(ind.Reserve(102))
|
|
74
|
+
for i := 0; i < 100; i++ {
|
|
75
|
+
vec[i] += 10
|
|
76
|
+
Assure(ind.Add(i, vec))
|
|
77
|
+
}
|
|
78
|
+
res = ind.Search(vec, 100)
|
|
79
|
+
if len(res) != 100 {
|
|
80
|
+
t.Fatalf("expected 100 results, got %d", len(res))
|
|
81
|
+
}
|
|
82
|
+
}
|