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/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
+ }
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+ #include <stdlib.h>
3
+
4
+ typedef struct {
5
+ int* Labels;
6
+ float* Distances;
7
+ int Len;
8
+ char* Error;
9
+ } SearchResults;
@@ -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
+ }