usearch 0.21.0 → 0.22.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/prerelease.yml +19 -0
- package/.github/workflows/release.yml +28 -0
- package/.github/workflows/update_version.sh +2 -1
- package/CMakeLists.txt +4 -0
- package/Cargo.toml +1 -1
- package/README.md +94 -605
- package/c/lib.cpp +19 -15
- package/c/usearch.h +26 -23
- package/cmake/emscripten.cmake +8 -0
- package/conanfile.py +1 -1
- package/cpp/README.md +88 -0
- package/cpp/bench.cpp +32 -41
- package/cpp/test.cpp +13 -7
- package/docs/golang/index.rst +6 -0
- package/docs/index.rst +5 -2
- package/docs/python/reference.rst +9 -2
- package/docs/rust/index.rst +6 -0
- package/docs/swift/index.rst +6 -0
- package/golang/README.md +28 -0
- package/include/usearch/index.hpp +204 -78
- package/include/usearch/index_punned_dense.hpp +239 -85
- package/java/README.md +22 -0
- package/javascript/README.md +27 -0
- package/javascript/lib.cpp +1 -1
- package/package.json +1 -1
- package/python/README.md +283 -0
- package/python/lib.cpp +61 -11
- package/python/scripts/join.py +7 -3
- package/python/scripts/test.py +28 -243
- package/python/scripts/test_jit.py +244 -0
- package/python/usearch/index.py +97 -25
- package/rust/README.md +67 -0
- package/swift/README.md +20 -0
- package/wasm/CMakeLists.txt +15 -0
- package/wasm/lib.cpp +5 -0
- package/wasmer.toml +8 -0
package/python/usearch/index.py
CHANGED
|
@@ -319,15 +319,18 @@ class Index:
|
|
|
319
319
|
pass `copy=False`, if you can guarantee the lifetime of the
|
|
320
320
|
primary vectors store during the process of construction.
|
|
321
321
|
|
|
322
|
-
:param labels: Unique identifier for passed vectors, optional
|
|
322
|
+
:param labels: Unique identifier(s) for passed vectors, optional
|
|
323
323
|
:type labels: Buffer
|
|
324
|
-
:param vectors:
|
|
324
|
+
:param vectors: Vector or a row-major matrix
|
|
325
325
|
:type vectors: Buffer
|
|
326
326
|
:param copy: Should the index store a copy of vectors, defaults to True
|
|
327
327
|
:type copy: bool, optional
|
|
328
328
|
:param threads: Optimal number of cores to use, defaults to 0
|
|
329
329
|
:type threads: int, optional
|
|
330
|
-
|
|
330
|
+
:param log: Whether to print the progress bar, default to False
|
|
331
|
+
:type log: Union[str, bool], optional
|
|
332
|
+
:param batch_size: Number of vectors to process at once, defaults to 0
|
|
333
|
+
:type batch_size: int, optional
|
|
331
334
|
:return: Inserted label or labels
|
|
332
335
|
:type: Union[int, np.ndarray]
|
|
333
336
|
"""
|
|
@@ -399,17 +402,21 @@ class Index:
|
|
|
399
402
|
log: Union[str, bool] = False,
|
|
400
403
|
batch_size: int = 0,
|
|
401
404
|
) -> Matches:
|
|
402
|
-
"""
|
|
405
|
+
"""
|
|
406
|
+
Performs approximate nearest neighbors search for one or more queries.
|
|
403
407
|
|
|
404
408
|
:param vectors: Query vector or vectors.
|
|
405
409
|
:type vectors: Buffer
|
|
406
410
|
:param k: Upper limit on the number of matches to find, defaults to 10
|
|
407
411
|
:type k: int, optional
|
|
408
|
-
|
|
409
412
|
:param threads: Optimal number of cores to use, defaults to 0
|
|
410
413
|
:type threads: int, optional
|
|
411
414
|
:param exact: Perform exhaustive linear-time exact search, defaults to False
|
|
412
415
|
:type exact: bool, optional
|
|
416
|
+
:param log: Whether to print the progress bar, default to False
|
|
417
|
+
:type log: Union[str, bool], optional
|
|
418
|
+
:param batch_size: Number of vectors to process at once, defaults to 0
|
|
419
|
+
:type batch_size: int, optional
|
|
413
420
|
:return: Approximate matches for one or more queries
|
|
414
421
|
:rtype: Matches
|
|
415
422
|
"""
|
|
@@ -461,6 +468,34 @@ class Index:
|
|
|
461
468
|
)
|
|
462
469
|
return Matches(*tuple_)
|
|
463
470
|
|
|
471
|
+
def remove(
|
|
472
|
+
self,
|
|
473
|
+
labels: Union[int, Iterable[int]],
|
|
474
|
+
*,
|
|
475
|
+
compact: bool = False,
|
|
476
|
+
threads: int = 0,
|
|
477
|
+
) -> Union[bool, int]:
|
|
478
|
+
"""Removes one or move vectors from the index.
|
|
479
|
+
|
|
480
|
+
When working with extremely large indexes, you may want to
|
|
481
|
+
mark some entries deleted, instead of rebuilding a filtered index.
|
|
482
|
+
In other cases, rebuilding - is the recommended approach.
|
|
483
|
+
|
|
484
|
+
:param labels: Unique identifier for passed vectors, optional
|
|
485
|
+
:type labels: Buffer
|
|
486
|
+
:param compact: Removes links to removed nodes (expensive), defaults to False
|
|
487
|
+
:type compact: bool, optional
|
|
488
|
+
:param threads: Optimal number of cores to use, defaults to 0
|
|
489
|
+
:type threads: int, optional
|
|
490
|
+
:return: Number of removed entries
|
|
491
|
+
:type: Union[bool, int]
|
|
492
|
+
"""
|
|
493
|
+
return self._compiled.remove(labels, compact=compact, threads=threads)
|
|
494
|
+
|
|
495
|
+
def rename(self, label_from: int, label_to: int) -> bool:
|
|
496
|
+
"""Relabel existing entry"""
|
|
497
|
+
return self._compiled.rename(label_from, label_to)
|
|
498
|
+
|
|
464
499
|
@property
|
|
465
500
|
def specs(self) -> dict:
|
|
466
501
|
return {
|
|
@@ -481,6 +516,19 @@ class Index:
|
|
|
481
516
|
def __len__(self) -> int:
|
|
482
517
|
return self._compiled.__len__()
|
|
483
518
|
|
|
519
|
+
def __delitem__(self, label: int) -> bool:
|
|
520
|
+
raise self.remove(label)
|
|
521
|
+
|
|
522
|
+
def __contains__(self, label: int) -> bool:
|
|
523
|
+
return self._compiled.__contains__(label)
|
|
524
|
+
|
|
525
|
+
def __getitem__(self, label: int) -> np.ndarray:
|
|
526
|
+
dtype = self.dtype
|
|
527
|
+
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
528
|
+
vector = self._compiled.__getitem__(label, get_dtype)
|
|
529
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
530
|
+
return None if vector is None else vector.view(view_dtype)
|
|
531
|
+
|
|
484
532
|
@property
|
|
485
533
|
def jit(self) -> bool:
|
|
486
534
|
return self._metric_jit is not None
|
|
@@ -567,19 +615,50 @@ class Index:
|
|
|
567
615
|
return result
|
|
568
616
|
|
|
569
617
|
def join(self, other: Index, max_proposals: int = 0, exact: bool = False) -> dict:
|
|
618
|
+
"""Performs "Semantic Join" or pairwise matching between `self` & `other` index.
|
|
619
|
+
Is different from `search`, as no collisions are allowed in resulting pairs.
|
|
620
|
+
Uses the concept of "Stable Marriages" from Combinatorics, famous for the 2012
|
|
621
|
+
Nobel Prize in Economics.
|
|
622
|
+
|
|
623
|
+
:param other: Another index.
|
|
624
|
+
:type other: Index
|
|
625
|
+
:param max_proposals: Limit on candidates evaluated per vector, defaults to 0
|
|
626
|
+
:type max_proposals: int, optional
|
|
627
|
+
:param exact: Controls if underlying `search` should be exact, defaults to False
|
|
628
|
+
:type exact: bool, optional
|
|
629
|
+
:return: Mapping from labels of `self` to labels of `other`
|
|
630
|
+
:rtype: dict
|
|
631
|
+
"""
|
|
570
632
|
return self._compiled.join(
|
|
571
633
|
other=other._compiled,
|
|
572
634
|
max_proposals=max_proposals,
|
|
573
635
|
exact=exact,
|
|
574
636
|
)
|
|
575
637
|
|
|
638
|
+
def get_labels(self, offset: int = 0, limit: int = 0) -> np.ndarray:
|
|
639
|
+
if limit == 0:
|
|
640
|
+
limit = 2**63 - 1
|
|
641
|
+
return self._compiled.get_labels(offset, limit)
|
|
642
|
+
|
|
576
643
|
@property
|
|
577
644
|
def labels(self) -> np.ndarray:
|
|
645
|
+
"""Retrieves the labels of all vectors present in `self`
|
|
646
|
+
|
|
647
|
+
:return: Array of labels
|
|
648
|
+
:rtype: np.ndarray
|
|
649
|
+
"""
|
|
578
650
|
return self._compiled.labels
|
|
579
651
|
|
|
580
652
|
def get_vectors(
|
|
581
|
-
self,
|
|
653
|
+
self,
|
|
654
|
+
labels: np.ndarray,
|
|
655
|
+
dtype: ScalarKind = ScalarKind.F32,
|
|
582
656
|
) -> np.ndarray:
|
|
657
|
+
"""Retrieves vectors associated with given `labels`
|
|
658
|
+
|
|
659
|
+
:return: Matrix of vectors (row-major)
|
|
660
|
+
:rtype: np.ndarray
|
|
661
|
+
"""
|
|
583
662
|
dtype = _normalize_dtype(dtype, self._metric_kind)
|
|
584
663
|
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
585
664
|
vectors = np.vstack([self._compiled.__getitem__(l, get_dtype) for l in labels])
|
|
@@ -590,22 +669,6 @@ class Index:
|
|
|
590
669
|
def vectors(self) -> np.ndarray:
|
|
591
670
|
return self.get_vectors(self.labels, self.dtype)
|
|
592
671
|
|
|
593
|
-
def __delitem__(self, label: int):
|
|
594
|
-
raise NotImplementedError()
|
|
595
|
-
|
|
596
|
-
def __contains__(self, label: int) -> bool:
|
|
597
|
-
return self._compiled.__contains__(label)
|
|
598
|
-
|
|
599
|
-
def __getitem__(self, label: int) -> np.ndarray:
|
|
600
|
-
dtype = self.dtype
|
|
601
|
-
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
602
|
-
vector = self._compiled.__getitem__(label, get_dtype)
|
|
603
|
-
view_dtype = _to_numpy_dtype(dtype)
|
|
604
|
-
return None if vector is None else vector.view(view_dtype)
|
|
605
|
-
|
|
606
|
-
def remove(self, label: int):
|
|
607
|
-
pass
|
|
608
|
-
|
|
609
672
|
@property
|
|
610
673
|
def max_level(self) -> int:
|
|
611
674
|
return self._compiled.max_level
|
|
@@ -618,15 +681,24 @@ class Index:
|
|
|
618
681
|
return self._compiled.level_stats(level)
|
|
619
682
|
|
|
620
683
|
def __repr__(self) -> str:
|
|
621
|
-
|
|
684
|
+
f = "usearch.Index({} x {}, {}, expansion: {} & {}, {} vectors in {} levels)"
|
|
685
|
+
return f.format(
|
|
686
|
+
self.dtype,
|
|
687
|
+
self.ndim,
|
|
688
|
+
self.metric,
|
|
689
|
+
self.expansion_add,
|
|
690
|
+
self.expansion_search,
|
|
691
|
+
len(self),
|
|
692
|
+
self.max_level + 1,
|
|
693
|
+
)
|
|
622
694
|
|
|
623
695
|
def _repr_pretty_(self) -> str:
|
|
624
696
|
level_stats = [
|
|
625
|
-
f"--- {i}. {self.level_stats(i)} nodes" for i in range(self.max_level)
|
|
697
|
+
f"--- {i}. {self.level_stats(i).nodes} nodes" for i in range(self.max_level)
|
|
626
698
|
]
|
|
627
699
|
return "\n".join(
|
|
628
700
|
[
|
|
629
|
-
"usearch.
|
|
701
|
+
"usearch.Index",
|
|
630
702
|
"- config" f"-- data type: {self.dtype}",
|
|
631
703
|
f"-- dimensions: {self.ndim}",
|
|
632
704
|
f"-- metric: {self.metric}",
|
package/rust/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# USearch for Rust
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
cargo add usearch
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```rust
|
|
12
|
+
|
|
13
|
+
let options = IndexOptions {
|
|
14
|
+
dimensions: 5,
|
|
15
|
+
metric: MetricKind::IP,
|
|
16
|
+
quantization: ScalarKind::F16,
|
|
17
|
+
connectivity: 0,
|
|
18
|
+
expansion_add: 0,
|
|
19
|
+
expansion_search: 0
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let index = new_index(&options).unwrap();
|
|
23
|
+
|
|
24
|
+
assert!(index.reserve(10).is_ok());
|
|
25
|
+
assert!(index.capacity() >= 10);
|
|
26
|
+
assert!(index.connectivity() != 0);
|
|
27
|
+
assert_eq!(index.dimensions(), 3);
|
|
28
|
+
assert_eq!(index.size(), 0);
|
|
29
|
+
|
|
30
|
+
let first: [f32; 3] = [0.2, 0.1, 0.2];
|
|
31
|
+
let second: [f32; 3] = [0.2, 0.1, 0.2];
|
|
32
|
+
|
|
33
|
+
assert!(index.add(42, &first).is_ok());
|
|
34
|
+
assert!(index.add(43, &second).is_ok());
|
|
35
|
+
assert_eq!(index.size(), 2);
|
|
36
|
+
|
|
37
|
+
// Read back the tags
|
|
38
|
+
let results = index.search(&first, 10).unwrap();
|
|
39
|
+
assert_eq!(results.count, 2);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Multi-Threading
|
|
43
|
+
|
|
44
|
+
```rust
|
|
45
|
+
assert!(index.add_in_thread(42, &first, 0).is_ok());
|
|
46
|
+
assert!(index.add_in_thread(43, &second, 0).is_ok());
|
|
47
|
+
let results = index.search_in_thread(&first, 10, 0).unwrap();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
|
|
51
|
+
Aside from the `add` and `search`, USearch Rust binding also provides `add_in_thread` and `search_in_thread`, which let users identify the calling thread to use underlying temporary memory more efficiently.
|
|
52
|
+
|
|
53
|
+
## Serialization
|
|
54
|
+
|
|
55
|
+
```rust
|
|
56
|
+
assert!(index.save("index.usearch").is_ok());
|
|
57
|
+
assert!(index.load("index.usearch").is_ok());
|
|
58
|
+
assert!(index.view("index.usearch").is_ok());
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Metrics
|
|
62
|
+
|
|
63
|
+
```rust
|
|
64
|
+
assert!(new_l2sq(3, &quant, 0, 0, 0).is_ok());
|
|
65
|
+
assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
|
|
66
|
+
assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
67
|
+
```
|
package/swift/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# USearch for Swift
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```txt
|
|
6
|
+
https://github.com/unum-cloud/usearch
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```swift
|
|
12
|
+
let index = Index.l2sq(dimensions: 3, connectivity: 8)
|
|
13
|
+
let vectorA: [Float32] = [0.3, 0.5, 1.2]
|
|
14
|
+
let vectorB: [Float32] = [0.4, 0.2, 1.2]
|
|
15
|
+
index.add(label: 42, vector: vectorA[...])
|
|
16
|
+
index.add(label: 43, vector: vectorB[...])
|
|
17
|
+
|
|
18
|
+
let results = index.search(vector: vectorA[...], count: 10)
|
|
19
|
+
assert(results.0[0] == 42)
|
|
20
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
include(emscripten)
|
|
2
|
+
|
|
3
|
+
set(CMAKE_C_COMPILER emcc)
|
|
4
|
+
set(CMAKE_CXX_COMPILER em++)
|
|
5
|
+
|
|
6
|
+
set(USEARCH_PUNNED_INCLUDE_DIRS
|
|
7
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../c"
|
|
8
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
|
9
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../fp16/include"
|
|
10
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../robin-map/include"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
add_executable(index lib.cpp)
|
|
14
|
+
target_include_directories(index PRIVATE ${USEARCH_PUNNED_INCLUDE_DIRS})
|
|
15
|
+
set_target_properties(index PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/wasm)
|
package/wasm/lib.cpp
ADDED