Legal
Soft-launch readyTextTimeline
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.