Document Stores¶
This page describes the
docculus.store package, which provides a uniform interface for
persisting LangChain Document objects, backed by
persista key-value stores.
Prerequisites: You'll need to know a bit of Python, and the persista extra
(docculus[persista]) must be installed. For a Python refresher, see the
Python tutorial.
Overview¶
The docculus.store package provides:
BaseDocumentStore: a single abstract interface that supports both synchronous methods (get,set_many, ...) and their asynchronous,a-prefixed counterparts (aget,aset_many, ...) on the same instanceDocumentStore: the concrete implementation ofBaseDocumentStore, wrapping anypersista.store.BaseStore- Ready-to-use
DocumentStoresubclasses backed by specificpersistabackends:InMemoryDocumentStore,SQLiteDocumentStore,DuckDBDocumentStore, and their "typed" variantsTypedSQLiteDocumentStore/TypedDuckDBDocumentStore
Documents are keyed by their id; the id itself is not duplicated inside the stored value.
Every store exposes:
get/aget,get_many/aget_many: read one or several documents byidset_many/aset_many: add or replace documents (upsert semantics)filter/afilter: retrieve documents whose metadata matches given keyword filters (AND-ed together)delete/adelete,delete_many/adelete_many: remove documents byidclear/aclear: remove every documentcontains/acontains,contains_many/acontains_many: check which IDs existkeys/akeys,values/avalues,iter_batches/aiter_batches: iterate over stored documentscount/acount: number of documentsopen/aopen,close/aclose,closed: connection lifecycle
Because every store implements the same BaseDocumentStore interface, application code can move
between backends -- for example using InMemoryDocumentStore in tests and SQLiteDocumentStore
or DuckDBDocumentStore in production -- without changes, and can freely mix sync and async calls
on the same store instance.
Getting Started¶
In-Memory Store¶
InMemoryDocumentStore keeps documents in a plain Python dict (via persista.store.InMemoryStore).
It requires no setup and is a good default for tests and prototyping:
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> with InMemoryDocumentStore() as store: # doctest: +SKIP
... store.set_many(
... [Document(id="1", page_content="hello", metadata={"author": "Alice"})]
... )
... print(store.count())
... print(store.get("1"))
...
1
Document(id='1', page_content='hello', metadata={'author': 'Alice'})
Constructing a store does not connect to the underlying backend -- every method (other than
open/aopen) raises RuntimeError until the store has been opened, either by calling
open()/aopen() explicitly or by using it as a context manager, as above. Every store supports
the context manager protocol, which calls open() on entry and close() automatically on exit --
prefer it over calling open()/close() manually so the underlying resources are always
released.
Setting Multiple Documents¶
set_many writes several documents in a single call, upserting any whose id already exists.
filter retrieves the documents whose metadata matches the given keyword arguments (all filters
are combined with AND):
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> with InMemoryDocumentStore() as store: # doctest: +SKIP
... store.set_many(
... [
... Document(
... id="1", page_content="Intro to Python", metadata={"author": "Alice"}
... ),
... Document(
... id="2", page_content="Advanced Python", metadata={"author": "Alice"}
... ),
... Document(
... id="3", page_content="History of Rome", metadata={"author": "Bob"}
... ),
... ]
... )
... print(len(store.filter(author="Alice")))
... print(len(store.filter()))
...
2
3
Deleting and Clearing¶
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> with InMemoryDocumentStore() as store: # doctest: +SKIP
... store.set_many(
... [Document(id="1", page_content="a"), Document(id="2", page_content="b")]
... )
... store.delete("1")
... print(store.count())
... store.clear()
... print(store.count())
...
1
0
SQL-Backed Stores¶
SQLite¶
SQLiteDocumentStore persists documents in a SQLite database, storing each document's
page_content and metadata in a single JSON metadata column. It works both with a file path
and with ":memory:":
>>> from docculus.store import SQLiteDocumentStore
>>> from langchain_core.documents import Document
>>> with SQLiteDocumentStore() as store: # doctest: +SKIP
... store.set_many(
... [Document(id="1", page_content="Intro to Python", metadata={"author": "Alice"})]
... )
... print(store.get("1"))
...
Document(id='1', page_content='Intro to Python', metadata={'author': 'Alice'})
To persist to disk, pass a file path instead of the default ":memory:":
from pathlib import Path
from docculus.store import SQLiteDocumentStore
with SQLiteDocumentStore(Path("tmp/data.sqlite")) as store:
...
DuckDB¶
DuckDBDocumentStore works the same way as SQLiteDocumentStore but is backed by
DuckDB (requires the duckdb extra):
from docculus.store import DuckDBDocumentStore
with DuckDBDocumentStore() as store:
...
Typed Stores¶
TypedSQLiteDocumentStore and TypedDuckDBDocumentStore map selected metadata fields onto native
SQL columns, via a metadata_schema that maps metadata field names to SQL types, instead of
storing metadata as a single JSON blob. Use these when metadata follows a known, fixed schema and
individual fields should be queryable as SQL columns:
>>> from docculus.store import TypedSQLiteDocumentStore
>>> from langchain_core.documents import Document
>>> schema = {"author": "TEXT", "year": "INTEGER"}
>>> with TypedSQLiteDocumentStore(metadata_schema=schema) as store: # doctest: +SKIP
... store.set_many(
... [Document(id="1", page_content="Intro to Python", metadata={"author": "Alice"})]
... )
... print(len(store.filter(author="Alice")))
...
1
Use DuckDBDocumentStore/SQLiteDocumentStore instead of the Typed... variants when document
metadata does not follow a fixed schema.
Wrapping an Arbitrary persista Store¶
DocumentStore wraps any persista.store.BaseStore instance, so you can use it with a
persista backend that has no dedicated docculus convenience class (e.g. Redis, PostgreSQL,
LMDB -- see the persista documentation for the full list):
from docculus.store import DocumentStore
from persista.store import SomeStore # any persista.store.BaseStore implementation
with DocumentStore(SomeStore(...)) as store:
...
metadata_mode controls how a document's metadata is represented in the underlying store's
values:
"flat"(the default): each metadata field is stored as its own top-level key, alongsidepage_content"single": the metadata dict is stored as a single nested JSON value under a"metadata"key
InMemoryDocumentStore always uses "flat"; SQLiteDocumentStore/DuckDBDocumentStore use
"single"; TypedSQLiteDocumentStore/TypedDuckDBDocumentStore use "flat".
Async Usage¶
Every store also exposes a-prefixed asynchronous methods (aget, aset_many, acount, ...)
that are coroutines (or async iterators), so they must be awaited and used from an async
function. The same store instance can be used from both sync and async code -- there is no
separate async class.
>>> import asyncio
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> async def main(): # doctest: +SKIP
... async with InMemoryDocumentStore() as store:
... await store.aset_many([Document(id="1", page_content="hello")])
... print(await store.acount())
... print(await store.aget("1"))
...
>>> asyncio.run(main()) # doctest: +SKIP
1
Document(id='1', page_content='hello', metadata={})
Using async with (as above) calls aopen() on entry and aclose() automatically on exit.
Checking Which IDs Exist¶
contains_many checks a batch of IDs at once, returning a list of booleans in the same order as
the input, without fetching the documents themselves:
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> with InMemoryDocumentStore() as store: # doctest: +SKIP
... store.set_many(
... [Document(id="1", page_content="a"), Document(id="2", page_content="b")]
... )
... print(store.contains_many(["1", "2", "3"]))
...
[True, True, False]
Iterating Over a Store¶
keys, values, and iter_batches iterate over a store's content without loading everything
into memory at once. iter_batches is the scalable form of values, streaming documents from the
backend in chunks of batch_size:
>>> from docculus.store import InMemoryDocumentStore
>>> from langchain_core.documents import Document
>>> with InMemoryDocumentStore() as store: # doctest: +SKIP
... store.set_many([Document(id=str(i), page_content=str(i)) for i in range(5)])
... print(sorted(store.keys()))
... for batch in store.iter_batches(batch_size=2):
... print(len(batch))
...
['0', '1', '2', '3', '4']
2
2
1
Custom Document Stores¶
To implement a store backed by something other than persista, subclass BaseDocumentStore and
implement all its abstract methods (get/aget, set_many/aset_many, filter/afilter,
delete/adelete, delete_many/adelete_many, clear/aclear, contains/acontains,
contains_many/acontains_many, keys/akeys, iter_batches/aiter_batches, count/acount,
open/aopen, close/aclose, and the closed property). values/avalues are provided by
the base class in terms of iter_batches/aiter_batches.
Document Store Factories¶
docculus.store.factory decouples document-store creation from the rest of your code via
BaseDocumentStoreFactory, whose make_document_store() returns a configured
BaseDocumentStore:
DocumentStoreFactory: wraps an already-builtBaseDocumentStoreinstance and returns it as-isConfigurableDocumentStoreFactory: accepts either aBaseDocumentStoreinstance or anobjectory-style configdict(with a"_target_"key), resolved on each callInMemoryDocumentStoreFactory,SQLiteDocumentStoreFactory,DuckDBDocumentStoreFactory,TypedSQLiteDocumentStoreFactory,TypedDuckDBDocumentStoreFactory: build a fresh instance of the corresponding ready-to-use store on each callStoreDocumentStoreFactory: builds aDocumentStorefrom apersista.store.factory.BaseStoreFactory, useful when the backing key-value store itself needs to be freshly created (e.g. a new connection) each time
>>> from docculus.store.factory import InMemoryDocumentStoreFactory
>>> factory = InMemoryDocumentStoreFactory()
>>> store = factory.make_document_store()
API Reference¶
See the reference documentation for the full API.