USearch Documentation

repository·main·Indexed 26 days ago

https://github.com/unum-cloud/usearch

A high-performance, single-file similarity search and clustering engine for vectors and texts. USearch provides SIMD-accelerated distance kernels, support for user-defined metrics, and memory-mapped disk access. It offers interfaces for C, C++, and C# (via Cloud.Unum.USearch NuGet package), featuring capabilities for exact search, filtered search with predicates, and parallel indexing using OpenMP or custom executors.

Tokens
44.9K
Snippets
108
Records
199
Agent score
85%

What's inside USearch

  1. Quickstart USearch in C#

    main

    Initialize a USearchIndex by specifying the metric, quantization type, and dimensions. You can then add vectors using Add and perform similarity searches using Search.

    Key Parameters for USearchIndex constructor:

    • metricKind: The distance metric (e.g., MetricKind.Cos).
    • quantization: The scalar type (e.g., ScalarKind.Float32, Float64, BFloat16, Float16, E5M2, E4M3, E3M2, E2M3, Int8, UInt8).
    • dimensions: Number of dimensions in the input vectors.
    • connectivity (optional): Frequency of connections in the graph.
    • expansionAdd (optional): Controls indexing recall.
    • expansionSearch (optional): Controls search quality.
    using System.Diagnostics;
    using Cloud.Unum.USearch;
    
    using var index = new USearchIndex(
        metricKind: MetricKind.Cos, // Choose cosine metric
        quantization: ScalarKind.Float32, // or Float64, BFloat16, Float16, E5M2, E4M3, E3M2, E2M3, Int8, UInt8
        dimensions: 3,  // Define the number of dimensions in input vectors
        connectivity: 16, // How frequent should the connections in the graph be, optional
        expansionAdd: 128, // Control the recall of indexing, optional
        expansionSearch: 64 // Control the quality of search, optional
    );
    
    var vector = new float[] { 0.2f, 0.6f, 0.4f };
    index.Add(42, vector);
    int matches = index.Search(vector, 10, out ulong[] keys, out float[] distances);
    
    Trace.Assert(index.Size() == 1);
    Trace.Assert(matches == 1);
    Trace.Assert(keys[0] == 42);
    Trace.Assert(distances[0] <= 0.001f);
  2. Quickstart: Create and Search an Index

    main

    Use Index.Config to initialize a new index. You must specify the metric, dimensions, and optionally quantization and capacity. The Index implements AutoCloseable, so it should be used within a try-with-resources block.

    Common metrics include Index.Metric.COSINE (or "cos").

    import cloud.unum.usearch.Index;
    
    public class Main {
        public static void main(String[] args) {
            try (Index index = new Index.Config()
                    .metric(Index.Metric.COSINE)              // Or "cos"
                    .quantization(Index.Quantization.FLOAT32) // Or "f32"
                    .dimensions(3)
                    .capacity(100)
                    .build()) {
                
                // Add to Index
                float[] vector = {0.1f, 0.2f, 0.3f};
                index.add(42L, vector);
    
                // Search
                long[] keys = index.search(new float[]{0.1f, 0.2f, 0.3f}, 10);
                for (long key : keys) {
                    System.out.println("Found key: " + key);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  3. Quickstart USearch Indexing and Searching

    main

    Create an index using USearchIndex.make, add vectors using add(key:vector:), and perform similarity searches with search(vector:count:).

    Supported vector types:

    • Float32 and Float64 are always supported.
    • Float16 support depends on your OS and hardware.

    Note: When using USearch within a SwiftUI App struct, ensure you handle or ignore return values from methods like reserve and add to comply with SwiftUI's view builder requirements.

    let index = USearchIndex.make(metric: .Cos, dimensions: 3, connectivity: 8)
    let vectorA: [Float32] = [0.3, 0.5, 1.2]
    let vectorB: [Float32] = [0.4, 0.2, 1.2]
    
    index.add(key: 42, vector: vectorA)
    index.add(key: 43, vector: vectorB)
    
    let results = index.search(vector: vectorA, count: 10)
    // results is a tuple where results.0 contains the keys
    
    let retrieved: [[Float32]]? = index.get(key: 42)
  4. Quickstart USearch in C

    main

    To use USearch, initialize an index with usearch_init_options_t, reserve capacity with usearch_reserve, add vectors with usearch_add, and perform searches with usearch_search. Always check the usearch_error_t pointer for errors and use usearch_free to clean up.

    #include <stdio.h>
    #include <assert.h>
    #include <usearch/usearch.h>
    
    int main() {
    
        // Construct:
        size_t dimensions = 128;
        usearch_error_t error = NULL;
        usearch_init_options_t opts = {
            .metric_kind = usearch_metric_cos_k,
            .scalar_kind = usearch_scalar_f16_k, // or f32_k, bf16_k, e5m2_k, e4m3_k, e3m2_k, e2m3_k, i8_k, u8_k
            .dimensions = dimensions,
            .expansion_add = 0, // for defaults
            .expansion_search = 0 // for defaults
        };
        usearch_index_t index = usearch_init(&opts, &error);
    
        size_t vectors_count = 1000;
        usearch_reserve(index, vectors_count, &error);
        if (error) goto cleanup;
    
        // Populate:
        float vector[dimensions]; // don't forget to fill the vector with data
        usearch_add(index, 42, &vector[0], usearch_scalar_f32_k, &error);
        if (error) goto cleanup;
        
        // Check up:
        assert(usearch_size(index, &error) == 1);
        assert(usearch_capacity(index, &error) == vectors_count);
        assert(usearch_contains(index, 42, &error));
    
        // Search:
        usearch_key_t found_keys[10];
        usearch_distance_t found_distances[10];
        size_t found_count = usearch_search(
            index, &vector[0], usearch_scalar_f32_k, 10, 
            &found_keys[0], &found_distances[0], &error);
    
      cleanup:
        if (error) fprintf(stderr, "Error: %s\n", error);
        if (index) usearch_free(index, &error);
        return error ? 1 : 0;
    }
  5. Quickstart: Create and use a USearchIndex

    main

    To set up a vector search index in Objective-C, use [USearchIndex make:...] to initialize the index with a metric, dimensions, connectivity, and quantization type. You must call [index reserve:] to allocate space for vectors before adding data.

    Supported vector addition methods include addDouble:vector:, addHalf:vector:, and others depending on your data type. Searching is performed via searchSingle:count:keys:distances:.

    #import "USearchIndex.h"
    
    // Creating an index with specific parameters
    USearchIndex *index = [USearchIndex make:USearchMetricIP 
                                  dimensions:3 
                                connectivity:10 
                                quantization:USearchScalarF32];
    
    // Reserving space for vectors
    [index reserve:10];
    
    // Adding a double-precision vector (will be cast to float32)
    double doubleVector[3] = {0.1, 0.2, 0.3};
    [index addDouble:44 vector:doubleVector];
    
    // Searching with an integer vector (will be cast to float32)
    int intQueryVector[3] = {1, 2, 3};
    UInt32 count = 5;
    USearchKey keys[count];
    float distances[count];
    [index searchSingle:(Float32 const *)intQueryVector count:count keys:keys distances:distances];
    
    // Adding a half-precision vector (requires casting to specified quantization type)
    void *halfVector = ...; // Assume half-precision data
    [index addHalf:45 vector:halfVector];
  6. Quickstart USearch for Go

    main

    To get started, initialize a Go module, install the package via go get, and use usearch.NewIndex to create an index.

    Important: Always call index.Reserve(capacity) before performing your first write operation to ensure efficient memory allocation.

    package main
    
    import (
    	"fmt"
    	"runtime"
    	usearch "github.com/unum-cloud/usearch/golang"
    )
    
    func main() {
    	// Create Index
    	vectorSize := 3
    	vectorsCount := 100
    	conf := usearch.DefaultConfig(uint(vectorSize))
    	conf.Quantization = usearch.F32 // or BF16, F16, E5M2, E4M3, E3M2, E2M3, I8, U8
    	index, err := usearch.NewIndex(conf)
    	if err != nil {
    		panic("Failed to create Index")
    	}
    	defer index.Destroy()
    
    	// Reserve capacity and configure internal threading
    	err = index.Reserve(uint(vectorsCount))
    	_ = index.ChangeThreadsAdd(uint(runtime.NumCPU()))
    	_ = index.ChangeThreadsSearch(uint(runtime.NumCPU()))
    	for i := 0; i < vectorsCount; i++ {
    		err = index.Add(usearch.Key(i), []float32{float32(i), float32(i + 1), float32(i + 2)})
    		if err != nil {
    			panic("Failed to add")
    		}
    	}
    
    	// Search
    	keys, distances, err := index.Search([]float32{0.0, 1.0, 2.0}, 3)
    	if err != nil {
    		panic("Failed to search")
    	}
    	fmt.Println(keys, distances)
    }
  7. Quickstart: Basic Index Operations

    main

    The high-level C++11 interface provides essential methods for vector search: reserve(), add(), search(), size(), capacity(), save(), load(), and view().

    For most use cases, use index_dense_t. If you need to store more than 4 billion entries, use index_dense_big_t or instantiate the template variant index_dense_gt<vector_key_t, internal_id_t> directly.

    #include <usearch/index.hpp>
    #include <usearch/index_dense.hpp>
    
    using namespace unum::usearch;
    
    int main(int argc, char **argv) {
        metric_punned_t metric(3, metric_kind_t::l2sq_k, scalar_kind_t::f32_k);
        
        index_dense_t index = index_dense_t::make(metric);
        float vec[3] = {0.1, 0.3, 0.2};
        
        index.reserve(10); // Pre-allocate memory for 10 vectors
        index.add(42, &vec[0]); // Pass a key and a vector
        auto results = index.search(&vec[0], 5); // Pass a query and limit number of results
        
        for (std::size_t i = 0; i != results.size(); ++i)
            std::printf("Found matching key: %zu", results[i].member.key);
        return 0;
    }
  8. Quickstart: Create and use a USearch Index

    main

    Initialize an index using new_index with an IndexOptions struct. You can then reserve capacity, add vectors with associated keys, and perform searches.

    use usearch::{Index, IndexOptions, MetricKind, ScalarKind, new_index};
    
    let options = IndexOptions {
        dimensions: 3,
        metric: MetricKind::IP,
        quantization: ScalarKind::BF16,
        connectivity: 0,
        expansion_add: 0,
        expansion_search: 0,
    };
    
    let index: Index = new_index(&options).unwrap();
    
    assert!(index.reserve(10).is_ok());
    
    let first: [f32; 3] = [0.2, 0.1, 0.2];
    let second: [f32; 3] = [0.2, 0.1, 0.2];
    
    assert!(index.add(42, &first).is_ok());
    assert!(index.add(43, &second).is_ok());
    
    // Read back the tags
    let results = index.search(&first, 10).unwrap();
    assert_eq!(results.keys.len(), 2);
  9. Quickstart USearch Index

    main

    Initialize a USearch Index with specific dimensions and a distance metric. You can add vectors using unique keys and perform similarity searches to retrieve the nearest neighbors.

    import numpy as np
    from usearch.index import Index, Matches
    
    index = Index(
        ndim=3, # Define the number of dimensions in input vectors
        metric='cos', # Choose 'l2sq', 'ip', 'haversine' or other metric, default = 'cos'
        dtype='bf16', # Quantize to 'f16', 'e5m2', 'e4m3', 'e3m2', 'e2m3', 'u8', 'i8', 'b1'..., default = None
        connectivity=16, # How frequent should the connections in the graph be, optional
        expansion_add=128, # Control the recall of indexing, optional
        expansion_search=64, # Control the quality of search, optional
    )
    
    vector = np.array([0.2, 0.6, 0.4])
    index.add(42, vector)
    matches: Matches = index.search(vector, 10)
    
    assert len(index) == 1
    assert len(matches) == 1
    assert matches[0].key == 42
    assert matches[0].distance <= 0.001
    assert np.allclose(index[42], vector)
  10. Quickstart with USearch Index

    main

    To perform vector search, create a new usearch.Index instance, add vectors using BigInt keys, and execute searches. Note that keys must be 64-bit integers represented as JavaScript BigInt (e.g., 42n).

    const assert = require('node:assert');
    const usearch = require('usearch');
    
    // Initialize index with metric, connectivity, and dimensions
    const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
    
    // Add a vector with a BigInt key
    index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
    
    // Search for the nearest neighbors
    const results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);
    
    assert(index.size() === 1);
    assert.deepEqual(results.keys, new BigUint64Array([42n]));
    assert.deepEqual(results.distances, new Float32Array([0]));
    
    // Remove the vector
    index.remove(42n);
  11. Define User-Defined Metrics with Numba JIT

    main

    To avoid the overhead of Python callables, use Numba to JIT-compile a function with a matching signature and pass it to the engine via CompiledMetric.

    from numba import cfunc, types, carray
    
    dim = 256
    signature = types.float32(
        types.CPointer(types.float32),
        types.CPointer(types.float32))
    
    @cfunc(signature)
    def inner_product(a, b):
        a_array = carray(a, ndim)
        b_array = carray(b, ndim)
        c = 0.0
        for i in range(ndim):
            c += a_array[i] * b_array[i]
        return 1 - c
    
    index = Index(ndim=ndim, metric=CompiledMetric(
        pointer=inner_product.address,
        kind=MetricKind.IP,
        signature=MetricSignature.ArrayArray,
    ))
  12. Clustering with USearch

    main

    USearch supports two types of clustering:

    1. Identify cluster for a vector: Find which cluster an external vector belongs to by specifying a 'clustering level' (HNSW graph layer). Passing 0 searches all levels except the bottom one.
    2. Split index into clusters: Automatically split the entire index into a set of clusters with centroids. This requires providing an iterator over a range of vectors and uses an auto-tuning algorithm to pick the best level and merge small clusters.

    Example of identifying a cluster for a single vector:

    some_scalar_t vector[3] = {0.1, 0.3, 0.2};
    cluster_result_t result = index.cluster(&vector, index.max_level() / 2);
    match_t cluster = result.cluster;
    member_cref_t member = cluster.member;
    distance_t distance = cluster.distance;