Vector stores¶
Configuration¶
Retriever store configuration models
File: klea_utils/stores/config.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.stores.config.BM25StoreInfo(*, name: str, path: str, default_k: int | None = None, k_max: int | None = None, k_inc: int | None = None, loaded_object: Any | None = None)[source]¶
Bases:
StoreInfoInformation about a single BM25 store.
pathpoints to the pickled document corpus that theBM25RetrieverManagerloads to build its keyword index.- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.stores.config.PerDomainConfig(*, vector_stores: list[VectorStoreInfo] = [], bm25_stores: list[BM25StoreInfo] = [])[source]¶
Bases:
BaseModelConfiguration for a single domain.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.stores.config.RetrieverConfig(*, domains: dict[str, PerDomainConfig])[source]¶
Bases:
BaseModelTop-level retriever configuration.
Holds the per-domain store configuration for all retriever managers (vector stores and BM25 stores).
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.stores.config.StoreInfo(*, name: str, path: str, default_k: int | None = None, k_max: int | None = None, k_inc: int | None = None, loaded_object: Any | None = None)[source]¶
Bases:
BaseModelInformation about a single store used by a retriever manager.
default_k,k_max, andk_incconfigure retrieval depth per store. When leftNonethey fall back to the global values set on the retriever manager, so stores that do not need tuning inherit the graph-wide defaults.loaded_objectholds the lazily-instantiated retriever object for the store (e.g. a LangChain VectorStore or BM25Retriever).- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.stores.config.VectorStoreInfo(*, name: str, path: str, default_k: int | None = None, k_max: int | None = None, k_inc: int | None = None, loaded_object: Any | None = None)[source]¶
Bases:
StoreInfoInformation about a single vector store.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Ingestion¶
- class klea_utils.stores.ingestion.StoresBuilder(embedding_model: str, logger: Logger, max_tokens: int = 450, merge_peers: bool = True, tokenizer_model: str = 'BAAI/bge-m3', do_ocr: bool = True, embed_batch_size: int = 256)[source]¶
Bases:
objectBuild stores from a directory of source documents.
Uses Docling for document conversion and token-aware chunking, then embeds chunks and writes them to a vector store backend. Optionally also writes the combined chunked corpus for BM25 retrieval.
- DEFAULT_EMBED_BATCH_SIZE = 256¶
Number of chunks embedded per
add_documentscall instore_all(). Batching gives the embedding phase (which can take minutes for large corpora) a progress signal between calls; embedding backends like Ollama send all texts in a single request otherwise. The value is mostly a progress-granularity knob, not a throughput one.
- build(source_dir: str, store_uri: str, collection_name: str, force: bool = False, metadata_map_path: str | None = None, bm25_path: str | None = None) None[source]¶
Full pipeline: chunk documents and write them to a vector store.
Convenience wrapper around
chunk_all()+store_all().- Parameters:
source_dir – Path to a directory containing source documents
store_uri – Vector store URI (e.g.
chroma:/path)collection_name – Collection name for the store
force – Re-process all files even if unchanged
metadata_map_path – Optional path to a metadata map JSON file
bm25_path – Optional path to write the combined BM25 corpus to
- chunk_all(source_path: Path, metadata_map: dict[str, dict[str, Any]] | None = None, force: bool = False) tuple[list[tuple[str, list[langchain_core.documents.Document], Path]], dict[str, dict[str, Any]]][source]¶
Convert, chunk, cache, and enrich metadata for all files.
Skips converting files whose cache entry exists (unless
forceisTrue). Always caches newly-converted chunks. Heading chains are collected per file for template generation. The per-fileDEFAULTtemplate entry is pre-filled with the automatically-extracted bibliographic metadata (seeextract_metadata()).- Parameters:
source_path – Resolved source directory path
metadata_map – Metadata map for heading-based enrichment, or
Noneforce – Re-process all files even if cached
- Returns:
(results, file_headings)where results is a list of(file_hash, docs, file_path)tuples and file_headings is a{file_name: {"DEFAULT": {extracted metadata}, "heading > heading": {}, ...}}dict
- store_all(results: list[tuple[str, list[langchain_core.documents.Document], Path]], store_uri: str, collection_name: str, force: bool = False, bm25_path: str | None = None) None[source]¶
Write chunked documents to a vector store.
Initialises the embedding model on first call if not already done. Skips files whose hash is already present in the store (unless
forceisTrue). Optionally also writes the combined document corpus for BM25 retrieval.- Parameters:
results – List of
(file_hash, docs, file_path)tuples fromchunk_all()store_uri – Vector store URI
collection_name – Collection name for the store
force – Re-store all files even if already indexed
bm25_path – Optional path to write the combined BM25 corpus to
- write_bm25_store(results: list[tuple[str, list[langchain_core.documents.Document], Path]], bm25_path: str) None[source]¶
Write the combined chunked documents to a BM25 corpus.
Flattens the per-file chunked documents from
chunk_all()into a single list and pickles it to bm25_path. This file is the BM25 store: aBM25RetrieverManagerloads it at runtime to build its keyword index. It is independent of the per-file.klea-cache, so the cache can be removed once the corpus has been written.The corpus holds the same chunk units (and metadata) that are stored in the vector store, so BM25 and vector retrieval return consistent results.
- Parameters:
results – List of
(file_hash, docs, file_path)tuples fromchunk_all()bm25_path – Path to write the combined corpus pickle to
- write_heading_template(file_headings: dict[str, dict[str, Any]], source_dir: Path) None[source]¶
Write a metadata-map template JSON file organised per source file.
Each file gets a
"DEFAULT"placeholder and one entry per unique heading chain found in that file. The user fills in the{}with their metadata key-value pairs.Refuses to write when file_headings is empty (no files were chunked): an existing template is preserved rather than clobbered with an empty one.
- Parameters:
file_headings –
{file_name: {"DEFAULT": {}, "heading > heading": {}, ...}, ...}fromchunk_all()source_dir – Resolved source directory path (template is written alongside it)
Retrieval¶
- class klea_utils.stores.retrieval.base.BaseKleaRetriever(config: RetrieverConfig, logger: Logger, default_k: int = 5, k_max: int = 10, k_inc: int = 1)[source]¶
Bases:
ABCBase class for domain-aware retriever managers.
Holds the machinery common to all retrievers: lazy per-domain store loading, per-store retrieval depth (
k) tracking with graph-wide fallbacks, and the retrieval contractretrieve(domain, query) -> list[tuple[Document, float]].Subclasses must implement:
_stores_of(): the list of stores configured for a domain_instantiate_store(): build the underlying retriever object for a store_retrieve_from_store(): run a single store against a query
Subclasses should set
source_labelto a human-readable name for the retriever type (e.g."vector store","BM25"), used to label the original per-source scores preserved during fusion.- inc_k() bool[source]¶
Increase k for all loaded stores by their per-store increment.
Each store’s k is capped by its own
k_max, so stores with a smaller cap stop being incremented sooner. Stores that are not yet loaded keep their default k until they are loaded.- Returns:
True if at least one store’s k was increased
- load(domain_name: str) None[source]¶
Load stores for a domain (lazy loading).
- Parameters:
domain_name – Name of the domain to load stores for
- retrieve(domain_name: str, query: str) list[tuple[langchain_core.documents.Document, float]][source]¶
Retrieve documents from all stores for a domain.
- Parameters:
domain_name – Name of the domain to search in
query – User query string
- Returns:
List of (document, relevance_score) tuples
- class klea_utils.stores.retrieval.vs.VSRetriever(config: RetrieverConfig, logger: Logger, embedding_model: str, default_k: int = 5, k_max: int = 10, k_inc: int = 1)[source]¶
Bases:
BaseKleaRetrieverManages domain-specific vector stores.
Loads vector stores on demand per domain and provides similarity search retrieval across multiple stores within a domain.
Store paths use a URI-style scheme prefix to identify the backend:
chroma:/path/to/dir— ChromaDB (persistent, local disk)qdrant:http://host:port— Qdrant (remote HTTP)pgvector:postgresql://host/db— PGVector (PostgreSQL)
- class klea_utils.stores.retrieval.bm25.BM25RetrieverManager(config: RetrieverConfig, logger: Logger, default_k: int = 5, k_max: int = 10, k_inc: int = 1)[source]¶
Bases:
BaseKleaRetrieverManages domain-specific BM25 keyword stores.
Each BM25 store is a pickled corpus of chunked documents written by
write_bm25_store. Stores are loaded lazily per domain: the corpus is unpickled and used to build alangchain_community.retrievers.BM25Retriever, which is queried with BM25 keyword scoring.A store whose corpus file is missing is skipped with a warning, so a misconfigured domain degrades gracefully instead of failing retrieval.
Scalability note: this is a pure-Python in-memory index (
rank_bm25). Building and querying stay fast to well over ~100k chunks, but memory grows roughly with the total number of unique terms (one Python dict entry per term per chunk, ~100-150 bytes each), so a single collection becomes heavy in the ~50-100k chunk range. That is far beyond current corpora, but if large platformed deployments are ever planned, consider a proper keyword backend (e.g. Elasticsearch, Qdrant sparse vectors, or Postgres full-text search) instead.
Utilities¶
Vector store utilities
File: klea_utils/stores/utils.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.stores.utils.CHROMA_HNSW_SPACE = 'cosine'¶
HNSW distance space used for Chroma collections created by Klea. Cosine makes the vector-store relevance scores true cosine similarities (
1 - cosine_distance), so ascore_thresholdreads as a minimum cosine similarity.
- klea_utils.stores.utils.RRF_K = 60¶
Rank offset for Reciprocal Rank Fusion. A document at rank r within a source’s result list contributes
1 / (RRF_K + r)to its fused score.
- klea_utils.stores.utils.SOURCE_SCORES_KEY = '_source_scores'¶
Metadata key holding each document’s original per-source scores (e.g.
{"vector store": 0.87, "BM25": 3.21}), set byrrf_merge().
- klea_utils.stores.utils.format_source_scores(doc: langchain_core.documents.Document, precision: int = 2) str | None[source]¶
Format a document’s original per-source scores for display.
- Parameters:
doc – Document with
SOURCE_SCORES_KEYmetadataprecision – Number of decimal places per score
- Returns:
Joined string like
"vector store 0.87, BM25 3.21", orNoneif the document has no per-source scores
- klea_utils.stores.utils.instantiate_vector_store(path: str, name: str, embeddings, logger: Logger, create: bool = False)[source]¶
Instantiate a vector store based on the URI scheme in path.
Expected format:
"scheme:location".If
createisTrue, the store is created if it does not exist (relevant for ChromaDB which requires a local directory). For Qdrant and PGVector the flag is a no-op — collections are created on first write.For ChromaDB,
locationmust point at the store folder. Chroma always stores its database as<folder>/chroma.sqlite3and the filename is not configurable, so a path pointing at an existing file (even thechroma.sqlite3itself) is rejected. The collectionnameselects which collection within the store file is addressed: a single ChromaDB store file can hold multiple collections, so reusing an existing folder with a new collection name creates a new collection in it.New Chroma collections are created with the
CHROMA_HNSW_SPACEHNSW distance space (cosine). The configuration is only applied at collection creation; loading an existing collection keeps its own distance space.- Parameters:
path – URI-style string with scheme prefix (e.g.
"chroma:/path/to/dir","qdrant:http://localhost:6333","pgvector:postgresql://localhost/db")name – Collection name for the vector store
embeddings – Embedding function to use
logger – Logger instance
create – If
True, allow creating a new store
- Returns:
Instantiated LangChain VectorStore
- Raises:
ValueError – If the scheme is missing or unknown
FileNotFoundError – If
createisFalseand a local ChromaDB store does not exist
- klea_utils.stores.utils.normalize_text(text: str) str[source]¶
Normalise free text for consistent indexing and retrieval.
Document conversion (e.g. Docling’s PDF extraction) embeds typographic artifacts that hurt search: soft hyphens (
\u00ad) split words mid-token, no-break / zero-width characters distort embeddings and BM25 keyword matching, and ligatures / full-width forms / superscripts / typographic spaces tokenise differently from their plain equivalents. This strips or maps them so that indexed chunks and retrieval queries share the same plain-text form.The final pass uses NFKC compatibility composition, which (unlike NFC) also folds ligatures (
\ufb01-> “fi”), full-width forms (\uff21-> “A”), superscripts (\u00b2-> “2”), typographic spaces (en/em/thin/ideographic), and the non-breaking hyphen (\u2011->\u2010). Typographic em/en dashes are kept unchanged.- Parameters:
text – Raw text, possibly containing typographic artifacts
- Returns:
Normalised plain text
- klea_utils.stores.utils.rrf_merge(result_sets: list[tuple[str, list[tuple[langchain_core.documents.Document, float]]]], num_refs_max: int) list[tuple[langchain_core.documents.Document, float]][source]¶
Fuse per-source retrieval results with Reciprocal Rank Fusion.
Scores from different retrievers (e.g. cosine similarity vs BM25) are not comparable, so each document is scored purely by its rank within each source’s result list. The original per-source scores are preserved in each document’s
SOURCE_SCORES_KEYmetadata for display.- Parameters:
result_sets – List of
(source_label, results)pairs, where each results is a list of(document, score)tuples already ranked by its sourcenum_refs_max – Maximum number of documents to return
- Returns:
Documents ordered by RRF score, deduplicated by content, capped at num_refs_max
- klea_utils.stores.utils.serialize_vs_retrieval(reference_material: dict[str, list[tuple[langchain_core.documents.Document, float]]]) str[source]¶
Serialize vector store retrieval results into text for use in prompt context.
Documents are sorted by relevance score within each group. Uses Docling
HybridChunkermetadata format:headings: list of heading hierarchy (most specific last)file_name: source filename_source_scores: optional per-retriever scores (from the RRF merge)Optional custom keys from the
--metadata-map(e.g.,url)
- Parameters:
reference_material – Dict mapping query/domain to list of (doc, score) tuples
- Returns:
Formatted string representation of references