Repository: unum-cloud/usearch (language: python)

Issue:
Bug: MultiIndex returns strange search results
### Describe the bug

MultiIndex (even from one index) gives wrong nearest neighbours.

### Steps to reproduce

```python
import numpy as np
from usearch.index import Indexes, Index

np.random.seed(42)
data = np.random.random((10_000, 256))

index = Index(ndim=256)
index.add(np.arange(data.shape[0]), data)

gt = np.arange(1000)[:, np.newaxis]
print('Single index recall@1:', (index.search(data[:1000], 1).keys == gt).sum(axis=1).mean())

multi = Indexes(
    indexes=[index]
)

print('Multi index recall@1:', (multi.search(data[:1000], 1).keys == gt).sum(axis=1).mean())
```

Output:

```
Single index recall@1: 0.927
Multi index recall@1: 0.001
```

### Expected behavior

Similar performance

### USearch version

2.5.1

### Operating System

Ubuntu 23.04

### Hardware architecture

x86

### Which interface are you using?

Python bindings

### Contact Details

_No response_

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/Dockerfile.libs b/.github/workflows/Dockerfile.libs
index 55072e0a..00576223 100644
--- a/.github/workflows/Dockerfile.libs
+++ b/.github/workflows/Dockerfile.libs
@@ -7,7 +7,7 @@ ARG docker_ip
 ARG user_pass
 
 RUN apt-get update && \
-    apt-get install -y --no-install-recommends cmake g++-12 build-essential libjemalloc-dev golang openssh-client sshpass && \
+    apt-get install -y --no-install-recommends cmake g++-12 libc++-dev build-essential libjemalloc-dev golang openssh-client sshpass && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 22c571ec..ba5243d6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -95,7 +95,7 @@ jobs:
         --build-arg docker_ip=${{ steps.dockerip.outputs.docker_ip }} \
         --build-arg user_pass=${{ steps.userpass.outputs.user_pass }} \
         --file .github/workflows/Dockerfile.libs \
-        --tag unum/ustore-deps:latest \
+        --tag unum/usearch-library:latest \
         --load .
     - name: Clear Docker buildx cache
       run: docker buildx prune --all --force
diff --git a/python/lib.cpp b/python/lib.cpp
index c598c756..6f37db9f 100644
--- a/python/lib.cpp
+++ b/python/lib.cpp
@@ -373,8 +373,8 @@ static void search_typed(                                       //
                     atomic_error.store("Operation has been terminated");
                     return false;
                 }
-            return true;
         }
+        return true;
     });
 
     // At the end report the latest numbers, because the reporter thread may be finished earlier
diff --git a/rust/lib.cpp b/rust/lib.cpp
index 7bb27b99..11217e20 100644
--- a/rust/lib.cpp
+++ b/rust/lib.cpp
@@ -42,6 +42,11 @@ size_t NativeIndex::get_f32(key_t key, rust::Slice<float> vec) const { if (vec.s
 size_t NativeIndex::get_f64(key_t key, rust::Slice<double> vec) const { if (vec.size() % dimensions()) throw std::invalid_argument("Vector length must be a multiple of index dimensionality"); return index_->get(key, vec.data(), vec.size() / dimensions()); }
 // clang-format on
 
+size_t NativeIndex::expansion_add() const { return index_->expansion_add(); }
+size_t NativeIndex::expansion_search() const { return index_->expansion_search(); }
+void NativeIndex::change_expansion_add(size_t n) const { index_->change_expansion_add(n); }
+void NativeIndex::change_expansion_search(size_t n) const { index_->change_expansion_search(n); }
+
 size_t NativeIndex::remove(key_t key) const {
     labeling_result_t result = index_->remove(key);
     result.error.raise();
diff --git a/rust/lib.hpp b/rust/lib.hpp
index e21cb568..f0262ac7 100644
--- a/rust/lib.hpp
+++ b/rust/lib.hpp
@@ -36,6 +36,11 @@ class NativeIndex {
     size_t get_f32(key_t key, rust::Slice<float> vector) const;
     size_t get_f64(key_t key, rust::Slice<double> vector) const;
 
+    size_t expansion_add() const;
+    size_t expansion_search() const;
+    void change_expansion_add(size_t n) const;
+    void change_expansion_search(size_t n) const;
+
     size_t count(key_t key) const;
     size_t remove(key_t key) const;
     size_t rename(key_t from, key_t to) const;
diff --git a/rust/lib.rs b/rust/lib.rs
index 090c3759..4d72686a 100644
--- a/rust/lib.rs
+++ b/rust/lib.rs
@@ -46,6 +46,11 @@ pub mod ffi {
         /// Low-level C++ interface that is further wrapped into the high-level `Index`
         type NativeIndex;
 
+        pub fn expansion_add(self: &NativeIndex) -> usize;
+        pub fn expansion_search(self: &NativeIndex) -> usize;
+        pub fn change_expansion_add(self: &NativeIndex, n: usize) -> Result<()>;
+        pub fn change_expansion_search(self: &NativeIndex, n: usize) -> Result<()>;
+
         pub fn new_native_index(options: &IndexOptions) -> Result<UniquePtr<NativeIndex>>;
         pub fn reserve(self: &NativeIndex, capacity: usize) -> Result<()>;
         pub fn dimensions(self: &NativeIndex) -> usize;
@@ -176,6 +181,26 @@ impl Index {
         }
     }
 
+    /// Retrieves the expansion value used during index creation.
+    pub fn expansion_add(self: &Index) -> usize {
+        self.inner.expansion_add()
+    }
+
+    /// Retrieves the expansion value used during search.
+    pub fn expansion_search(self: &Index) -> usize {
+        self.inner.expansion_search()
+    }
+
+    /// Updates the expansion value used during index creation. Rarely used.
+    pub fn change_expansion_add(self: &Index, n: usize) -> Result<(), cxx::Exception> {
+        self.inner.change_expansion_add(n)
+    }
+
+    /// Updates the expansion value used during search operations.
+    pub fn change_expansion_search(self: &Index, n: usize) -> Result<(), cxx::Exception> {
+        self.inner.change_expansion_search(n)
+    }
+
     /// Performs k-Approximate Nearest Neighbors (kANN) Search for closest vectors to the provided query.
     ///
     /// # Arguments
@@ -204,10 +229,10 @@ impl Index {
         T::add(self, key, vector)
     }
 
-    /// Extracts one or more vectors matching specified key.
-    /// The `vector` slice must be a multiple number of dimensions in the index.
-    /// After the execution return the number `X` of vectors found.
-    /// The first `X * dimensions` elements of the vector slice will be filled.
+    /// Extracts one or more vectors matching the specified key.
+    /// The `vector` slice must be a multiple of the number of dimensions in the index.
+    /// After the execution, return the number `X` of vectors found.
+    /// The vector slice's first `X * dimensions` elements will be filled.
     ///
     /// If you are a novice user, consider `export`.
     ///
@@ -224,7 +249,7 @@ impl Index {
     }
 
     /// Extracts one or more vectors matching specified key into supplied resizable vector.
-    /// The `vector` is resized to a multiple number of dimensions in the index.
+    /// The `vector` is resized to a multiple of the number of dimensions in the index.
     ///
     /// # Arguments
     ///
@@ -448,6 +473,9 @@ mod tests {
 
         let index = Index::new(&options).unwrap();
 
+        assert!(index.expansion_add() > 0);
+        assert!(index.expansion_search() > 0);
+
         assert!(index.reserve(10).is_ok());
         assert!(index.capacity() >= 10);
         assert!(index.connectivity() != 0);
@@ -464,7 +492,11 @@ mod tests {
             index.memory_usage(),
             index.capacity(),
         );
+        assert!(index.change_expansion_add(10).is_ok());
+        assert_eq!(index.expansion_add(), 10);
         assert!(index.add(42, &first).is_ok());
+        assert!(index.change_expansion_add(12).is_ok());
+        assert_eq!(index.expansion_add(), 12);
         assert!(index.add(43, &second).is_ok());
         assert_eq!(index.size(), 2);
         println!(
@@ -475,11 +507,19 @@ mod tests {
             index.capacity(),
         );
 
+        assert!(index.change_expansion_search(10).is_ok());
+        assert_eq!(index.expansion_search(), 10);
         // Read back the tags
         let results = index.search(&first, 10).unwrap();
         println!("{:?}", results);
         assert_eq!(results.keys.len(), 2);
 
+        assert!(index.change_expansion_search(12).is_ok());
+        assert_eq!(index.expansion_search(), 12);
+        let results = index.search(&first, 10).unwrap();
+        println!("{:?}", results);
+        assert_eq!(results.keys.len(), 2);
+
         // Validate serialization
         assert!(index.save("index.rust.usearch").is_ok());
         assert!(index.load("index.rust.usearch").is_ok());


Test patch:
diff --git a/python/scripts/test_tooling.py b/python/scripts/test_tooling.py
index 81f4c7cf..aef7367c 100644
--- a/python/scripts/test_tooling.py
+++ b/python/scripts/test_tooling.py
@@ -118,5 +118,16 @@ def test_multi_index():
     index_b.add(43, vectors[1])
 
     indexes = Indexes([index_a, index_b])
+
+    # Search top 10 for 1
     matches = indexes.search(vectors[2], 10)
     assert len(matches) == 2
+
+    # Search top 1 for 1
+    matches = indexes.search(vectors[2], 1)
+    assert len(matches) == 1
+
+    # Search top 10 for 3
+    matches = indexes.search(vectors, 10)
+    assert len(matches) == 3
+    assert len(matches[0].keys) == 2