Legal

Soft-launch ready

TextTimeline

Retrieval-first evidence search for family-law text exports. Every result cites the source message.

A search engine for SMS evidence. An attorney asks a question in plain English; every finding traces back to a specific timestamped message.

Problem

The problem.

Family-law attorneys and pro-se clients receive SMS exports with thousands or tens of thousands of messages (threats, custody-handoff disputes, financial misconduct, substance-abuse references) buried in conversational noise. Hand-assembling a defensible chronology costs hours of paralegal time per matter. LLM-only "summarize the corpus" pipelines are slow, expensive, and unverifiable. Judges reject narrative without per-message provenance. If an attorney can't trace a finding to a specific timestamped message, it can't go in a brief.

Approach

How I built it.

  • Build search an attorney can defend in court. Each message is enriched at upload with sentiment, named entities, and hand-curated lexicons for threats, financial misconduct, and coparenting friction.
  • Each matter gets its own pair of indexes: a semantic index (FAISS over Gemini Enterprise embeddings) and a lexical index (BM25). Both live in isolated cloud storage scoped to that matter.
  • At query time, both indexes search in parallel and fuse via Reciprocal Rank Fusion. Filter metadata (threats, financial signals) is folded in as a soft RRF boost by default rather than a hard WHERE clause. Eval data showed hard filters hurt 6 of 9 categories (ADR 0008).
  • Gemini 2.5 Flash is on the hot path for two narrow jobs only: turning the user's plain-English question into structured search terms, and synthesizing the final ≤50-message answer with mandatory message-ID citations.
  • Citation IDs that don't exist in the underlying corpus are stripped after the model returns. A hallucinated citation can never reach an attorney's brief. Synthesis raises a SynthesisError before shipping.
  • A 90-query eval harness with an LLM-as-judge gates every retrieval-shape change. Same eval drove the soft-boost decision in ADR 0008.

Architecture

What's running, and why.

Ingestion
SMS XML parsed into normalized message objects, then enriched with spaCy named-entity recognition, VADER sentiment, and curated lexicons (threats, financial, coparenting, substance use). 1 GB cap per matter to fit the Cloud Run memory budget.
Embedding & indexing
Gemini Enterprise text-embedding-005 (768d) over each message, then FAISS flat-IP index plus a rank_bm25 lexical index. Both serialized to a per-matter prefix in GCS.
Retrieval
FAISS + BM25 fused via Reciprocal Rank Fusion (k=60). Metadata filters default to a third soft-boost RRF channel; hard filtering is opt-in only for threats and financial categories per ADR 0008.
Storage
Firestore for matter and report state. GCS bucket tt-evidence-texttimeline-prod for index artifacts and rendered reports with a 30-day TTL. Raw uploads are never persisted.
Inference
Gemini 2.5 Flash via Gemini Enterprise for query expansion (turning prose into exemplar messages + BM25 keywords) and final synthesis. Hallucinated citation IDs are stripped post-LLM.
Web surface
FastAPI backend (6 routers, 27 routes) plus a React 19 + Vite SPA. Free preview tier with a 3-finding cap; paid Evidence Report exports to PDF and CSV with per-finding message_id columns and footnotes.
Eval harness
90 curated queries × 4 retrieval variants (semantic / lexical / hybrid / hybrid_filtered) judged by Gemini Flash with confidence. Every retrieval-shape change runs through the harness before merging.

Outcome

What shipped.

P@10 0.70

Hybrid retrieval, freeform queries (vs. 0.42 single-mode)

100%

Citation coverage. Synthesis fails closed if no IDs survive.

−0.18

Hard-filter penalty on coercive_control (drove ADR 0008 reversion)

~60 s

Ingest a 5,828-message / 1.8 MB matter on Cloud Run 8 GB / 4 vCPU

185/185

Backend tests green; tsc --noEmit clean

Pending

Latency, cost-per-report, time-to-timeline. Not yet instrumented under load.

Stack

  • Python 3.12
  • FastAPI
  • Pydantic v2
  • React 19
  • TypeScript
  • Vite
  • FAISS
  • rank_bm25
  • spaCy
  • VADER
  • reportlab
  • Gemini Enterprise
  • text-embedding-005
  • Gemini 2.5 Flash
  • Cloud Run
  • Firestore
  • GCS
  • Cloud Build

What this proves

Where this ports.

Hybrid retrieval at small per-matter scale beats both single-mode retrieval and naive RAG when defensibility matters. Citations become a property of the index, not a property of the prompt. One engineer shipped this to production on Cloud Run in a 15-day sprint, no vector database. The pattern ports to discovery, compliance review, and internal investigations: anywhere a finding has to trace back to a source.

Want the same pattern in your business?

Thirty minutes. Describe what you're working on and I'll tell you if I can ship it.