Quick Start

Get up and running with brinicle in minutes. This guide walks you through the basic workflow using all three engines.

Shared Lifecycle

All brinicle engines follow the same lifecycle:
engine.init(mode="build")
engine.ingest(...)
engine.finalize()

engine.search(...)
The supported write modes are:
ModeMeaning
buildBuild a new index
insertAdd new records to an existing index
upsertReplace records with the same external IDs, or insert them if they do not exist
This streaming-first approach means you never need to load your entire dataset into memory. You can ingest data from files, databases, APIs, or any other source one record at a time.

Main Index and Delta Index

brinicle stores updates using a main index and a delta index. The main index stores the primary HNSW graph. The delta index stores later inserts and upserts. During search, brinicle searches both indexes, merges the results, filters deleted records, and returns the top matches. This allows brinicle to support updates without rebuilding the full index after every insert. Use VectorEngine when you already have embeddings or numeric vectors and need fast approximate nearest neighbor search:
import numpy as np
import brinicle

dim = 128

# Create an engine with 128-dimensional vectors
engine = brinicle.VectorEngine("my_index", dim=dim)

# Build the index
engine.init(mode="build")

for i in range(1000):
    vector = np.random.randn(dim).astype("float32")
    engine.ingest(str(i), vector)

engine.finalize()

# Search for the nearest neighbors
query = np.random.randn(dim).astype("float32")
results = engine.search(query, k=10)
print(results)  # ['42', '19', '705']

Search Results and Distances

Use search(...) to return external IDs only:
results = engine.search(query, k=10)
Use search_with_distance(...) to return external IDs with distances:
results = engine.search_with_distance(query, k=10)
# [('42', 0.183), ('19', 0.241)]
brinicle ranks results by ascending distance. Smaller distance means a better match. VectorEngine supports these distance functions:
Distance functionMeaning
l2Squared Euclidean distance
cosine_distance1 - cosine_similarity(a, b)
dot_product_distance-dot_product(a, b)
Batch search runs multiple queries and returns one result list per query:
queries = np.random.randn(100, dim).astype("float32")

results = engine.search_batch(
    queries,
    k=10,
    n_jobs=4,
)
n_jobs controls parallel query execution when parallel execution is available. Use ItemSearchEngine for structured catalog-like data such as products, movies, books, or any records with titles and attributes:
import brinicle

engine = brinicle.ItemSearchEngine(
    "items_index",
    dim=96,
    alpha=0.0,
)

engine.init(mode="build")

engine.ingest(
    external_id="p1",
    title="Apple iPhone 15 Pro Max 256GB",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Apple",
        "storage": "256GB",
    },
)

engine.ingest(
    external_id="p2",
    title="Samsung Galaxy S24 Ultra 512GB",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Samsung",
        "storage": "512GB",
    },
)

engine.finalize()

# Search by text query
results = engine.search("iphone 15 pro", k=10)
print(results)  # ['p1', 'p2']
For lexical-only search, use alpha=0.0. For semantic or hybrid search, provide vector_dim and pass vectors during ingest and search.

Autocomplete

Use AutocompleteEngine for query suggestions and title autocomplete:
import brinicle

ac = brinicle.AutocompleteEngine(
    "autocomplete_index",
    dim=48,
)

ac.init(mode="build")

ac.ingest("iphone 15 pro max", "iphone 15 pro max")
ac.ingest("iphone 15 case", "iphone 15 case")
ac.ingest("samsung s24 ultra", "samsung s24 ultra")

ac.finalize()

# Search for suggestions
results = ac.search("iphone", k=5)
print(results)  # ['iphone 15 pro max', 'iphone 15 case']

Updates: Insert, Upsert, and Delete

Insert new records:
engine.init(mode="insert")
engine.ingest("new_id", vector)
engine.finalize()
Upsert records:
engine.init(mode="upsert")
engine.ingest("existing_or_new_id", new_vector)
engine.finalize()
Delete records:
engine.delete_items(["id1", "id2"])
Deletes are logical until the index is compacted.

Rebuild, Compact, and Optimize

brinicle provides maintenance methods for updated indexes.
MethodMeaning
needs_rebuild()Returns whether the index has enough update or delete drift to justify rebuilding
rebuild_compact()Rebuilds the index from alive records and removes deleted records physically
optimize_graph()Rebuilds only when the index crosses the configured maintenance threshold
delta_ratio controls when brinicle considers an index ready for maintenance.

Common Configuration Parameters

ParameterMeaning
dimVector or encoded representation dimension
MHNSW graph connectivity
ef_constructionBuild-time search width
ef_searchQuery-time search width
delta_ratioMaintenance threshold for delta and deleted records
build_n_threadsNumber of build threads
seedRandom seed for graph construction
Example:
engine = brinicle.VectorEngine(
    "my_index",
    dim=384,
    M=48,
    ef_construction=1024,
    ef_search=512,
    delta_ratio=0.1,
)

Using the HTTP Server

If you prefer to deploy brinicle as a standalone service, start the HTTP server and use one of the SDK clients:
# Start the server
docker compose up -d

# Or run directly
pip install brinicle[server]
uvicorn brinicle.ref.api:app --host 0.0.0.0 --port 1984
Then connect using the Python HTTP client:
from brinicle_client import BrinicleClient

client = BrinicleClient("http://localhost:1984")

# Create an index
client.create_index("my_index", dim=128)

# Initialize and ingest
client.init("my_index", "build")
client.ingest("my_index", "v1", [0.1, 0.2, ...])
client.finalize("my_index", optimize=True)

# Search
results = client.search("my_index", [0.1, 0.2, ...], k=10)

Closing and Destroying an Index

Close loaded index resources:
engine.close()
Destroy the index files:
engine.destroy()
destroy() removes the index from disk.

Next Steps