storage (+ docstore) — Key-value byte stores: in-memory, file, Redis, encoder layers

Key-value byte stores: in-memory, file, Redis, encoder layers

Module langchain_classic/storage/ — Key-value byte stores: in-memory, file, Redis, encoder layers. Part of the State & Tools group.

🎯 Why This Module Exists

This module is documented from the storage_family_deep.md Phase 1B research artifact. See sections below for design decisions, algorithms, error philosophy, performance, and evolution clues.

14
Source files
0
Sub-directories
State & Tools
Category
Python
Language

ByteStore Encoder layers

Architecture

flowchart LR
  subgraph Core["langchain_classic/storage/"]
    BASE["Base abstraction"]
    IMPLS["Concrete implementations"]
    BASE --> IMPLS
  end
  subgraph Up["Upstream callers"]
    U1["chains/"]
    U2["agents/"]
  end
  subgraph Down["Downstream"]
    D1["langchain_core"]
    D2["External providers"]
  end
  Up --> Core
  Core --> Down

Data Flow

sequenceDiagram
  participant Caller as Caller
  participant Storage as storage (+ docstore)
  participant Core as langchain_core
  participant Ext as External
  Caller->>Storage: invoke(input)
  Note over Storage: Validate & route
  Storage->>Core: delegate primitive
  Core->>Ext: provider call
  Ext-->>Core: response
  Core-->>Storage: typed output
  Storage-->>Caller: result

Existence rationale

This family handles document substrates: how raw content gets in (document_loaders), how it's chunked / filtered (document_transformers, text_splitter), where it's indexed (vectorstores), where its raw bytes live (storage, docstore), and how the substrate's interface looks to the rest of the library. Most concrete classes (PyPDFLoader, BeautifulSoupTransformer, Chroma, FAISS, Pinecone) live in langchain_community or provider packages; what langchain_classic keeps are the abstract contracts (BaseLoader, VectorStore, BaseDocumentTransformer, ByteStore, Docstore) plus a few non-trivial in-tree implementations (InMemoryDocstore, LocalFileStore, EmbeddingsRedundantFilter, LongContextReorder).

Design decisions

DecisionChoiceAlternativeRationale
Document = (page_content, metadata)Tuple-like Pydantic classUntyped dictStrong typing across the pipeline; metadata is canonical
Loaders return iterableslazy_load() -> Iterator[Document]ListSupports streaming huge corpora without loading all into RAM
Vectorstore as RetrieverVectorStore.as_retriever()Caller wraps manuallyOne-line adapter; ensures the retriever uses the store's native MMR/filter capabilities
Splitters live on Documentsplit_documents(docs) -> list[Document]String-only splittersCarries metadata through (page numbers, source URIs) — critical for citations
ByteStore separate from BaseStoreByteStore is BaseStore[str, bytes]One typed storeEncoder-backed pattern wraps a ByteStore to give typed views
LocalFileStore over flat directoryOne key-per-fileSQLiteEasy to inspect / rsync; trivial cross-process
EncoderBackedStore adapterWraps a ByteStore with key_encoder/value_encoder/value_decoderBake encoding into storesLets users use the same backend for raw bytes and typed values

Algorithm deep-dives

1. RecursiveCharacterTextSplitter

Problem. Naïve fixed-size chunking cuts mid-sentence and breaks code blocks. We want chunks that respect structure — paragraphs > sentences > words > characters — falling back as needed.

Trace. With separators ["\n\n", "\n", " ", ""]:

  1. Try splitting on "\n\n". If a piece is still too big, recurse with the next separator.
  2. Continue down the list until each piece ≤ chunk_size.
  3. Re-merge adjacent pieces back up to chunk_size to maximise context.
  4. Add chunk_overlap characters from the previous chunk to the next.

Why this works. Most documents have hierarchy — preserving it means summaries of chunks make sense individually. Overlap prevents losing context at boundaries.

2. EmbeddingsRedundantFilter

Problem. Retrievers often return near-duplicates from over-aggressive chunking. Drop them before they waste prompt tokens.

Trace.

  1. Embed all returned docs once.
  2. Greedy: for each doc in order, drop if its embedding's max cosine similarity to any already-kept doc exceeds similarity_threshold (default 0.95).
  3. Return survivors.

Complexity. O(K²) similarity comparisons for K retrieved docs — K is usually ≤ 20.

3. LongContextReorder (Lost-in-the-Middle)

Problem. Empirical: LLMs attend best to the start and end of long contexts; the middle gets ignored. Reorder retrieved docs to put the most relevant at the edges.

Trace. Given docs sorted by relevance (descending):

  1. Even-ranked docs go to the start (in order).
  2. Odd-ranked docs go to the end (in reverse).
  3. Result: [1st, 3rd, 5th, …, 4th, 2nd] — most relevant at extremes.

Why. Mitigates the U-shaped attention curve documented in the LiM paper.

Error philosophy

Loaders raise on fatal I/O (file not found) and emit warnings on partial failures (a corrupted page in a PDF skips that page rather than killing the load). Vectorstores propagate vendor errors. Splitters never error — at worst they emit a chunk larger than chunk_size if no separator can split it.

Performance characteristics

Evolution clues

Step-by-Step Walkthrough

Follow a typical operation through the storage/ module:

  1. Caller invokes the public surface — usually .invoke(input) on a Runnable, or a legacy __call__(inputs_dict) on a Chain subclass.
  2. Pydantic validation — every entry point is a BaseModel; constructor args, input dicts, and tool args_schema are validated before any expensive work runs. Fail-fast on type errors.
  3. CallbackManager.configure — a fresh manager is built per call, fans on_*_start events to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4 run_id for tree reconstruction.
  4. storage (+ docstore)-specific logic — the module's core method runs: prompt rendering, retrieval, tool execution, memory load, document split, etc. See the deep-dive sections above.
  5. Provider call — typically a single HTTP request to OpenAI/Anthropic/etc., or a local SDK call. Retries are handled by tenacity with exponential backoff and a configurable max-attempts knob.
  6. Streaming fan-out — if the provider streams tokens, each chunk fires on_llm_new_token through the callback manager so downstream observers (UI, log, tracer) see partial output.
  7. OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object; OutputParserException may trigger a retry chain (RetryWithErrorOutputParser).
  8. Memory write-back — for chains/agents that have a BaseMemory attached, save_context(inputs, outputs) persists the turn before returning to the caller.
  9. on_*_end fan-out — the callback manager closes the run span; tracers serialise the Run tree and ship it to LangSmith asynchronously.
  10. Result returned — caller receives a typed object (AIMessage, dict, BaseModel, list[Document], ...) matching the module's declared output schema.

What Could Go Wrong

Failure Mode Trigger Detection Recovery
Provider 5xx / rate-limit OpenAI/Anthropic/etc. transient error HTTP status from SDK tenacity retries with exponential backoff up to max_retries; then surface to caller
Output parser failure LLM returned malformed JSON / wrong schema OutputParserException in parser RetryWithErrorOutputParser re-prompts with the parser error included; or surface
Pydantic validation Caller passes wrong-shaped dict ValidationError at __init__ / invoke Surface immediately — no fallback is correct here
Callback handler exception User-supplied tracer raises Try/except around each fan-out Logged and swallowed by default; raise_error=True to opt in to propagation
Provider auth missing OPENAI_API_KEY unset SDK AuthenticationError Surface with a helpful message; no retry (would just fail again)

Performance Profile

Hot Paths

Resource Usage

Directory Map

Source tree for storage (+ docstore)
storage/
├── __init__.py
├── _lc_store.py
├── encoder_backed.py
├── exceptions.py
├── file_system.py
├── in_memory.py
├── redis.py
└── upstash_redis.py