RagLeap
Blog

ragleap-memory 0.3.0: vector and hybrid search for LLM memory in one SQLite file, with your own embedder

ragleap-memory 0.3.0 adds optional vector and hybrid search to a small Python library that keeps long-term memory for LLM apps in one SQLite file. The vectors come from an embedder function you supply: the library has no provider code, no API keys and no network access. It is alpha and not independently audited. Source: its README and changelog at the 0.3.0 tag.

What is ragleap-memory?

Durable, tenant-scoped memory for LLM applications. Every call takes a Scope from ragleap-security and is filtered by it inside the SQL. Entries are typed (episode, fact, summary) with a trust label (untrusted, model, owner), and new entries default to untrusted. Install it with pip install ragleap-memory.

How does vector and hybrid search work?

Keyword search cannot find “car” in a note that says “automobile”. Version 0.3.0 can also search by meaning. This is the README example:

def embed(texts):                      # your function: a list of strings in, one list of numbers per string out
    return my_model.embed(texts)       # any embedding model; it must always return the same number of values

store = SQLiteStore("memory.db", embedder=embed, embedding_model="my-model-v1", embedding_dim=384,
                    embed_quota_tokens=200_000)        # per tenant per UTC day; off by default

store.add(alice, "The red automobile is parked outside")
print(store.embed_pending(alice, dry_run=True))        # what WOULD be sent; nothing is sent or changed
store.embed_pending(alice)                             # give a vector to entries that have none yet

store.search(alice, "car", mode="vector")              # by meaning
store.search(alice, "car", mode="hybrid", k=3)         # keyword and vector results fused

Keyword search stays the default. Hybrid mode fuses the keyword and vector ranks by reciprocal rank fusion (k=60), and a vector scan covers at most max_vector_candidates entries (default 5,000, newest first, after the filters).

What leaves the process?

The texts you embed leave the process, wherever your function sends them. add never calls the embedder; only embed_pending and a vector or hybrid search do (one call for the query). A per-tenant daily cap, embed_quota_tokens, is off by default: if a batch would pass it, only the entries that fit are embedded, and if none fit an error is raised before your function is called. Vectors are stored unencrypted next to the text, are deleted with their entry, and are not part of export files.

How do I move a memory somewhere else?

Since 0.2.0 a memory can be downloaded and uploaded as a JSON Lines file. This is the README example:

store.export_file(alice, "alice-memory.jsonl")                  # download: one JSON Lines file
result = other_store.import_file(alice, "alice-memory.jsonl")   # upload: continue from it somewhere else
print(result)   # ImportResult(imported=..., skipped_duplicates=..., dropped_links=...)

An uploaded file is untrusted input: an import is strict and all-or-nothing, and by default every imported entry becomes untrusted. Run embed_pending again after an import, because export files carry no vectors.

What should I know before upgrading?

A 0.1.0 or 0.2.0 file is migrated in place to schema 3 on first open, and a migrated file cannot be opened by 0.2.0 or older. Back it up first. Since 0.2.0, search is no longer read-only, because it records the tokens it served.

How was it tested, and what is not verified?

Different measures, kept apart. Our Tests page counts 15 test files and 222 test functions on the main branch (a count, not a pass count). The changelog records 472 tests passing and 177 of 177 mutations killed on Python 3.12.3 and 3.10.20 (the author’s sandbox). Not verified: SQLite 3.37.2 (the VPS), Windows and macOS, big-endian machines, any real embedding provider (the tests use a fake embedder, so retrieval quality is not measured and depends on your embedder), and several processes writing one file or network file systems. The README’s scan time of about 128 ms for 10,000 vectors of 384 numbers is one sandbox measurement.

More: the library page, the release notes and the source.

Back to the blog