How ChromaDB Stores and Retrieves Vector Embeddings in LifeTrace

LifeTrace uses ChromaDB as a persistent vector store that saves dense embeddings to SQLite-backed files on disk and retrieves them via cosine similarity search, supporting metadata filtering and optional cross-encoder reranking.

The freeu-group/lifetrace repository implements a complete vector storage and retrieval pipeline using ChromaDB to power semantic search over OCR text and personal documents. The core integration resides in lifetrace/llm/vector_db.py, where the VectorDatabase class orchestrates embedding generation, persistent storage, and similarity-based retrieval. Understanding how ChromaDB stores and retrieves vector embeddings reveals the mechanics behind LifeTrace’s context-aware query capabilities.

Initializing the ChromaDB Persistent Client

LifeTrace configures ChromaDB for on-disk persistence to ensure embeddings survive process restarts. The initialization flow determines storage paths, configures the client, and establishes the data schema.

Configuring the Storage Directory

The system determines the persistent directory via lifetrace/util/path_utils.py#get_vector_db_dir, reading settings.vector_db.persist_directory from the Dynaconf configuration. This path feeds directly into the Chroma client constructor.


# lifetrace/llm/vector_db.py (lines 90-104)

self.chroma_client = chromadb.PersistentClient(
    path=str(self.vector_db_path),
    settings=Settings(anonymized_telemetry=False, allow_reset=True),
)

Collection Setup

After initializing the PersistentClient, LifeTrace obtains or creates a named collection via get_or_create_collection. The collection stores three parallel data structures: documents (raw text), embeddings (dense vectors), and metadatas (JSON attributes).


# lifetrace/llm/vector_db.py (lines 90-104 continued)

self.collection = self.chroma_client.get_or_create_collection(
    name=self.collection_name,
    metadata={"description": "LifeTrace OCR text embeddings"},
)

Storing Vector Embeddings

The add_document method implements the storage logic, converting text to vectors and persisting them with searchable metadata.

Embedding Generation and Metadata Construction

LifeTrace first transforms raw text into a dense embedding vector using the configured sentence-transformers model via self.embed_text. It then constructs a metadata payload containing the timestamp, text length, and an MD5 hash for integrity verification.


# lifetrace/llm/vector_db.py (lines 81-88, 162-188)

embedding = self.embed_text(text)
doc_metadata = {
    "timestamp": get_utc_now().isoformat(),
    "text_length": len(text),
    "text_hash": hashlib.md5(text.encode(), usedforsecurity=False).hexdigest(),
}

Persisting to ChromaDB

The method calls collection.add with parallel lists for the document, its embedding, metadata, and a unique identifier. ChromaDB writes these vectors to an SQLite-backed file inside the persist directory, enabling fast nearest-neighbor lookups across sessions.


# lifetrace/llm/vector_db.py (lines 81-88, 162-188 continued)

collection.add(
    documents=[text],
    embeddings=[embedding],
    metadatas=[doc_metadata],
    ids=[doc_id],
)

Example: Storing an OCR result

from lifetrace.llm.vector_db import VectorDatabase

vec_db = VectorDatabase()
doc_id = "ocr-2024-03-02-001"
text = "Meeting notes: discuss quarterly budget and hiring plan."
vec_db.add_document(doc_id, text, metadata={"source": "screenshot_001.png"})

The search method retrieves relevant context by comparing query embeddings against the stored vector index.

LifeTrace embeds the query string using the same model (embed_text) to ensure vector space consistency. It then invokes collection.query with the query embedding and desired result count (n_results).


# lifetrace/llm/vector_db.py (lines 312-330)

query_embedding = self.embed_text(query)
results = self.collection.query(
    query_embeddings=[query_embedding],
    n_results=top_k,
    where=cleaned_where,
)

ChromaDB returns the top-k nearest vectors along with their original documents, metadata, and distance scores. LifeTrace reshapes this response into a list of dictionaries for downstream consumption.

Filtering by Metadata

The retrieval pipeline supports metadata constraints via the where parameter. LifeTrace cleans and passes filter dictionaries (e.g., source file constraints) directly to ChromaDB’s query engine.

Example: Searching with metadata filters

filters = {"source": {"$eq": "screenshot_001.png"}}
filtered = vec_db.search("budget", top_k=3, where=filters)

Reranking with Cross-Encoders

After initial retrieval, LifeTrace optionally improves relevance using a cross-encoder (self._get_cross_encoder). This secondary model reranks the candidate documents based on direct query-document interaction scores, refining the order without modifying the underlying vector storage.

Example: Reranking retrieved results

docs = [r["document"] for r in results]
reranked = vec_db.rerank(query, docs, top_k=3)

for doc, score in reranked:
    print(f"Score {score:.4f}: {doc[:100]}")

Summary

  • Persistent Storage: LifeTrace initializes chromadb.PersistentClient pointing to a configurable directory (resolved via lifetrace/util/path_utils.py), storing vectors in SQLite-backed files.
  • Three-Column Structure: Each ChromaDB collection maintains parallel columns for documents, embeddings, and metadatas, enabling rich filtering.
  • Embedding Pipeline: Text transforms into dense vectors via sentence-transformers before storage, with automatic metadata generation (timestamps, hashes).
  • Similarity Retrieval: Queries embed via the same model, then execute via collection.query for cosine-similarity nearest-neighbor search.
  • Enhanced Ranking: Optional cross-encoder reranking improves result relevance without altering the stored vector index.

Frequently Asked Questions

What storage backend does LifeTrace use for ChromaDB?

LifeTrace configures ChromaDB with PersistentClient, which stores data in SQLite-backed files on disk at the path specified by settings.vector_db.persist_directory. This ensures embeddings persist across application restarts and reboots.

How does LifeTrace generate embeddings before storing them?

The VectorDatabase class uses a sentence-transformers model (invoked via self.embed_text) to convert raw text into dense embedding vectors immediately before calling collection.add. This guarantees that both stored documents and subsequent queries occupy the same vector space.

Can I filter search results by metadata in LifeTrace?

Yes. The search method accepts a where dictionary that applies metadata constraints during the ChromaDB query. For example, you can restrict results to documents from a specific source file using filters like {"source": {"$eq": "screenshot.png"}}.

Does LifeTrace support reranking of retrieved documents?

Yes. After the initial vector similarity search, LifeTrace can invoke a cross-encoder via the rerank method to rescore the top-k candidates. This step analyzes query-document pairs directly, often improving relevance beyond pure vector cosine similarity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →