Brinicle

Autocomplete

Low-RAM autocomplete and query suggestion search

Autocomplete Engine

AutocompleteEngine is brinicle's high-level engine for autocomplete and suggestion search.

Use it for:

  • query suggestions
  • title suggestions
  • product-name suggestions
  • category suggestions
  • curated search phrases

AutocompleteEngine uses the same disk-first HNSW infrastructure as the other brinicle engines, but it encodes suggestion text internally before indexing and searching.

It supports:

  • build
  • insert
  • upsert
  • delete
  • single-query search
  • batch search
  • search with distances
  • local sharding for large indexes
  • compact rebuild
  • graph optimization

Constructor

ac = brinicle.AutocompleteEngine(
    index_path,
    dim=48,
    tokenizer_path=None,
    text_prep=None,
    delta_ratio=0.10,
    M=16,
    ef_construction=200,
    ef_search=64,
    build_n_threads=1,
    seed=0,
    autocomplete_config=None,
    n_shards=1,
)

Parameters

ParameterMeaning
index_pathBase path for the index files
dimDimension used for encoded autocomplete tokens
tokenizer_pathOptional custom tokenizer path
text_prepOptional text preprocessing
delta_ratioMaintenance threshold for delta and deleted records
MHNSW graph connectivity
ef_constructionBuild-time search width
ef_searchDefault query-time search width
build_n_threadsNumber of build threads
seedRandom seed for graph construction
autocomplete_configOptional autocomplete scoring configuration
n_shardsNumber of local shards used when building the index

Example:

ac = brinicle.AutocompleteEngine(
    "autocomplete_index",
    dim=48,
    M=32,
    ef_construction=512,
    ef_search=128,
)

Local Sharding

n_shards controls how many local shards brinicle creates for the autocomplete index during build.

The current sharding implementation is local sharding. It is not distributed sharding across multiple machines.

During insertion, brinicle uses hash-based routing to decide which shard receives each suggestion. During search, brinicle searches all shards, then merges the partial results into a final ranked suggestion list.

For multi-shard indexes, search methods accept n_jobs, which controls how many shards are searched in parallel.

Higher n_jobs can improve search speed, but it also increases CPU and I/O usage.

Example:

ac = brinicle.AutocompleteEngine(
    "large_autocomplete_index",
    dim=48,
    n_shards=50,
)

results = ac.search(
    "iphone",
    k=5,
    n_jobs=8,
)

As a practical starting point, keep n_shards=1 for smaller autocomplete indexes. Sharding becomes more useful when the index contains more than about 2 million suggestions. For example, for an index with around 100 million suggestions, n_shards=50 can be a reasonable starting point.

Benchmark n_shards and n_jobs with your real data and hardware, because the best values depend on suggestion count, encoded dimension, storage speed, and available CPU cores.


Suggestion Format

Each suggestion has an external_id and a text value.

ac.ingest(
    "iphone 15 pro max",
    "iphone 15 pro max",
)

The text value is encoded and indexed.

The external_id is what search returns. It can be the suggestion text itself, an item ID, a query ID, or any caller-defined identifier.

Example with separate IDs:

ac.ingest(
    "suggestion_001",
    "iphone 15 pro max",
)

How Autocomplete Scoring Works

Autocomplete search is prefix-oriented.

brinicle compares query tokens with suggestion tokens from the beginning of the text. Earlier token matches matter more than later token matches.

Example:

query: "iphone 15"

good:  "iphone 15 pro max"
weaker: "case for iphone 15"

Both suggestions contain related terms, but the first one is prefix-aligned with the query.

Smaller distance means a better match.


Query Length and Thresholds

AutocompleteEngine works cautiously. It usually gives clearer results when the query contains at least one complete term.

Very short partial inputs such as:

"iph"

may not produce strong or obvious matches.

Queries such as:

"iphone"
"iphone 15"
"samsung"

usually provide clearer signals.

Use threshold to filter weak or irrelevant suggestions:

results = ac.search(
    "iphone",
    k=5,
    threshold=0.8,
)

Lower thresholds make autocomplete stricter. Higher thresholds allow more distant suggestions.


AutocompleteConfig

Use AutocompleteConfig when you want direct control over autocomplete scoring.

cfg = brinicle.AutocompleteConfig()

cfg.search_position_decay = 0.5
cfg.search_length_penalty = 0.2

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

AutocompleteConfig has separate build-time and search-time parameters.

Build-time parameters affect graph construction.

Search-time parameters affect query ranking.


AutocompleteConfig Parameters

ParameterEffect
build_position_decayControls how much later token positions matter during graph construction
search_position_decayControls how much later token positions matter during search
build_length_penaltyLength penalty used during graph construction
search_length_penaltyPenalizes suggestions that are much longer than the query during search

position_decay controls how quickly later token positions lose importance.

Lower values make early tokens much more important.

Higher values make later tokens matter more.

length_penalty controls how strongly longer suggestions are penalized.

Higher values make autocomplete stricter toward long suggestions.

Lower values allow longer completions more easily.


Building an Autocomplete Index

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()

Use search(...) to return suggestion IDs.

results = ac.search("iphone", k=5)

print(results)

Example output:

["iphone 15 pro max", "iphone 15 case"]

Search Parameters

ac.search(
    query,
    k=10,
    efs=None,
    threshold=float("inf"),
    normalize=True,
    n_jobs=1,
)
ParameterMeaning
queryText query
kMaximum number of suggestions
efsQuery-time search width
thresholdMaximum accepted distance
normalizeWhether to normalize encoded query values
n_jobsNumber of shards to search in parallel on multi-shard indexes

Increasing efs usually improves recall, but increases query latency.

n_jobs controls how many shards are searched in parallel. Higher values can reduce latency on multi-shard indexes, but they also consume more CPU and I/O. For n_shards=1, you can ignore this parameter.


Search with Distance

Use search_with_distance(...) to return suggestion IDs and distances.

On multi-shard indexes, n_jobs controls how many shards are searched in parallel.

results = ac.search_with_distance(
    "iphone",
    k=5,
)

print(results)

Example output:

[("iphone 15 pro max", 0.0), ("iphone 15 case", 0.12)]

The result format is:

[(external_id, distance), ...]

Use search_batch(...) to search multiple autocomplete queries.

results = ac.search_batch(
    ["iphone", "samsung", "laptop"],
    k=5,
    n_jobs=4,
)

The return value contains one suggestion list per query:

[
    ["iphone 15 pro max", "iphone 15 case"],
    ["samsung s24 ultra"],
    ["laptop stand", "laptop sleeve"],
]

For single-shard indexes, n_jobs controls parallel query execution when parallel execution is available. For multi-shard indexes, it is also used by sharded search execution, so higher values can reduce latency but increase CPU and I/O usage.


Insert

Use insert mode to add new suggestions to an existing index.

ac.init(mode="insert")

ac.ingest("iphone 16 pro", "iphone 16 pro")
ac.ingest("iphone 16 pro case", "iphone 16 pro case")

ac.finalize()

Inserted suggestions are added through the delta index.


Upsert

Use upsert mode to replace existing suggestions or insert new ones.

ac.init(mode="upsert")

ac.ingest("iphone 15 pro max", "iphone 15 pro max 256gb")

ac.finalize()

If the external ID already exists, brinicle marks the old record as deleted and inserts the new version.

If the external ID does not exist, the suggestion is inserted as a new record.


Delete

Use delete_items(...) to delete suggestions by external ID.

deleted_count, not_found = ac.delete_items(
    ["iphone 15 case", "missing"],
    return_not_found=True,
)

print(deleted_count)
print(not_found)

If return_not_found=False, the second returned value is None.

Deletes are logical until compact rebuild.


Rebuild and Optimize

Autocomplete indexes use the same maintenance model as VectorEngine.

ac.needs_rebuild()

Returns whether the index has enough update or delete drift to justify rebuilding.

ac.rebuild_compact()

Rebuilds the index from alive records, removes deleted records physically, and clears the delta index.

ac.optimize_graph()

Runs conditional maintenance. If the index crosses the configured maintenance threshold, brinicle rebuilds the graph.


Close and Destroy

Close loaded index resources:

ac.close()

Destroy the index files:

ac.destroy()

destroy() removes the index from disk.


Complete API Reference

init

ac.init(mode="build")

Starts a write session.

Supported modes:

build
insert
upsert

ingest

ac.ingest(
    external_id,
    text,
    normalize=True,
)

Adds one suggestion to the current write session.


finalize

ac.finalize(
    optimize=False,
    M=0,
    ef_construction=0,
    ef_search=0,
    build_n_threads=0,
    seed=0,
)

Completes the pending write session.


search

ac.search(
    query,
    k=10,
    efs=None,
    threshold=float("inf"),
    normalize=True,
    n_jobs=1,
)

Returns suggestion IDs.


search_with_distance

ac.search_with_distance(
    query,
    k=10,
    efs=None,
    threshold=float("inf"),
    normalize=True,
    n_jobs=1,
)

Returns (external_id, distance) pairs.


search_batch

ac.search_batch(
    queries,
    k=10,
    efs=None,
    threshold=float("inf"),
    normalize=True,
    n_jobs=1,
)

Runs batch autocomplete search.


delete_items

ac.delete_items(
    external_ids,
    return_not_found=False,
)

Deletes suggestions by external ID.


needs_rebuild

ac.needs_rebuild()

Returns whether the index has crossed its maintenance threshold.


rebuild_compact

ac.rebuild_compact(
    M=16,
    ef_construction=200,
    ef_search=64,
    build_n_threads=1,
    seed=0,
)

Rebuilds the index from alive records.


optimize_graph

ac.optimize_graph()

Runs conditional graph maintenance.


close

ac.close()

Closes loaded index resources.


destroy

ac.destroy()

Removes index files from disk.

On this page