Getting started¶
This page builds an index in memory and completes queries with it. It then stores the same documents in a database on disk, serves them from an engine that keeps itself up to date, and moves the database to object storage.
Install¶
The wheel is abi3 for CPython 3.11 and newer, and includes every storage backend. It releases the GIL while it searches and builds.
cargo add completype --features store # databases on local disk and in memory
cargo add completype --features aws-credentials,gcp,azure # plus S3, GCS and Azure
cargo add tokio --features macros,rt-multi-thread # the database API is async
| Cargo feature | Adds |
|---|---|
| none | The engine: Segment, SegmentWriter, Index, Engine |
store |
Database, Transaction, Replica, Lease, ChangeSet, Ingestor, local and in-memory stores, and the collections layer: connect, Client, Collection |
aws / gcp / azure |
S3, Google Cloud Storage, Azure Blob Storage |
aws-credentials |
S3 credentials through the AWS SDK chain |
An index in memory¶
A document has an id, a text and a popularity weight, and optionally synonyms, abbreviations, contexts and
an embedding. Index.from_documents builds an index from documents in memory, with no database.
from completype import Index
docs = [
{"id": "bohemian", "text": "Bohemian Rhapsody – Queen", "popularity": 0.95, "synonyms": ["is this the real life"]},
{"id": "dancing", "text": "Dancing Queen – ABBA", "popularity": 0.85},
{"id": "dark", "text": "Dancing in the Dark – Bruce Springsteen", "popularity": 0.7},
{"id": "bridge", "text": "Under the Bridge – Red Hot Chili Peppers", "popularity": 0.75, "abbreviations": ["RHCP"]},
]
index = Index.from_documents(docs)
for query in ["danc", "RHCP", "bohemain rapsody", "queen"]:
print(query, [(s.text, s.kind, round(s.score, 3)) for s in index.complete(query, limit=3)])
use completype::{Document, Index};
let index = Index::from_documents([
Document::keyed("bohemian", "Bohemian Rhapsody – Queen", 0.95)
.with_synonym("is this the real life"),
Document::keyed("dancing", "Dancing Queen – ABBA", 0.85),
Document::keyed("dark", "Dancing in the Dark – Bruce Springsteen", 0.7),
Document::keyed("bridge", "Under the Bridge – Red Hot Chili Peppers", 0.75)
.with_abbreviation("RHCP"),
])?;
for query in ["danc", "RHCP", "bohemain rapsody", "queen"] {
for s in index.complete(query, 3) {
println!("{query}: {} {} {:.3}", s.text, s.kind.as_str(), s.score);
}
}
danc [('Dancing Queen – ABBA', MatchKind.PREFIX, 0.547), ('Dancing in the Dark – Bruce Springsteen', MatchKind.PREFIX, 0.462)]
RHCP [('Under the Bridge – Red Hot Chili Peppers', MatchKind.ABBREVIATION, 0.486)]
bohemain rapsody [('Bohemian Rhapsody – Queen', MatchKind.FUZZY, 0.186)]
queen [('Bohemian Rhapsody – Queen', MatchKind.INFIX, 0.356), ('Dancing Queen – ABBA', MatchKind.INFIX, 0.329)]
Reading suggestions¶
Each suggestion carries everything a completion box needs to render it.
| Field | Meaning |
|---|---|
id |
The id you gave: an int, or the string key. In Rust, id is the 64-bit id and key the string. |
text |
The document's text, to display. |
kind |
How it matched: exact, prefix, abbreviation, infix, fuzzy, synonym or semantic. |
score |
The ranking score, normalised so the strongest matches score around 1. |
highlights |
Ranges of text that matched the query. Python gives (start, end) character offsets; Rust gives byte ranges. |
layer |
The index the suggestion came from, in a search over layers; None for a single index. |
Synonyms are searched separately, with complete_aliases. Its suggestions carry the document's text, not
the synonym:
print(index.complete("is this the real life"))
print(index.complete_aliases("is this the real life"))
A database¶
A database keeps named indexes in a directory or a bucket. completype.connect(url) opens one, and creates a
local one if it does not exist. A transaction changes one or more indexes and commits a new version of
the database. An engine from the database serves its indexes.
use std::{sync::Arc, time::Duration};
use completype::{Database, Engine, IndexOptions, Replica};
let database = Database::open("./data", Vec::<(String, String)>::new()).await?;
let mut txn = database.begin().await?;
txn.append_documents("songs", docs, [])?;
println!("{}", txn.commit().await?.version);
let engine = Arc::new(Engine::new());
let replica = Arc::new(Replica::new(database.clone(), IndexOptions::default()));
replica.sync(&engine).await?;
let follower = replica.follow(&engine, Duration::from_secs(5)); // syncs until dropped
for layered in engine.complete(&["songs"], "danc", 10) {
println!("{} {}", layered.suggestion.text, layered.suggestion.kind.as_str());
}
1
[('Dancing Queen – ABBA', MatchKind.PREFIX), ('Dancing in the Dark – Bruce Springsteen', MatchKind.PREFIX)]
engine.complete takes the query and a list of index names to search. With more than one name, later
indexes override earlier ones per document id; see Layers.
Staying up to date¶
Any number of processes can open the same database, and any of them can commit. An engine from
db.engine() loads every index when it is created, and then keeps itself on the latest version: its
first query starts a background thread that syncs every sync_every seconds (5 by default). A sync
downloads only segments the engine does not hold, and queries never wait for it.
import time
engine = completype.connect("./data").engine(sync_every=1.0) # in a serving process
print(engine.complete("blind", ["songs"])) # starts the background sync
txn = db.begin() # in another process
txn.append("songs", [{"id": "blinding", "text": "Blinding Lights – The Weeknd", "popularity": 0.9}])
txn.commit()
time.sleep(1.5)
print(engine.complete("blind", ["songs"]), engine.version)
print(engine.sync_status["error"])
[]
[Suggestion(id='blinding', text='Blinding Lights – The Weeknd', score=0.5666, kind=MatchKind.PREFIX, layer='songs')] 2
None
engine.versionis the version the engine serves.engine.sync_statusisNoneuntil the first background sync, and then holdssynced_at(seconds since the Unix epoch) anderror. A failed sync keeps serving the version the engine has.engine.sync()syncs now, and returns the new version orNone. Withsync_every=None, nothing syncs in the background, andsync()is the only way to load new versions.
In Rust, Replica::follow starts the same thread, and stops it when the returned Follower is dropped.
Commits are create-only writes to storage, so concurrent writers do not corrupt each other: a commit that loses a race retries on top of the newer version.
A database in object storage¶
The same code works on S3, Google Cloud Storage and Azure Blob Storage: only the URL changes. Pass
cache_dir to keep downloaded segments on local disk, so a restarted process does not download them again.
db = completype.connect("s3://my-bucket/completions", cache_dir="/var/cache/completype")
# or "gs://bucket/prefix", "az://container/prefix"
S3 credentials resolve like boto3's. Serverless deployment covers credentials,
compaction, cleanup and what each process does.
Or use collections¶
completype.Client is a convenience layer over a database: named collections that keep their settings,
compact themselves after writes, report stats(), and complete synonyms in the same call. A collection is
an index of the same name, so both work on the same database.
client = completype.Client("./data")
print(client.collections(), [s.text for s in client["songs"].complete("blind")])
See Collections.
Next steps¶
- Concepts: indexes, segments, engines, databases and versions.
- Documents: every field, string ids, table input, segments and offline builds.
- Completion: match kinds, highlights, contexts, synonyms and ranking.
- Serverless deployment: storage, credentials, transactions, engines, compaction and cleanup.
- Many writers: change sets and a lease-elected ingestor.
- asyncio: the same API in FastAPI and other asyncio services.