I started with a simple problem: I had a discovery production that was searchable in the normal ways, but not very pleasant to ask questions of.
The export had the usual pieces: load files, extracted text, metadata, natives, images, and a lot of documents that only made sense if you respected the structure they came in with. A review platform can handle that structure. A random script or chatbot will not, at least not without some help.
The question was not “can I dump this into an LLM?” I did not want that. The useful version was more specific:
Can I turn the production into a local index that a model can search, cite, and stay grounded in?
That meant building an ingestion pipeline first and treating the model as the last step, not the whole system.
Start with the export, not the model
Discovery exports are not all the same. Sometimes you get loose files. Sometimes you get a jumble of PDFs, spreadsheets, images, and folders that grew sideways over time. In this case I was working from a Relativity-style export, which at least gives you a map if you parse it correctly.
The export had four main parts:
- a load file, usually a
.dat, that acts as the map - a text folder with extracted text
- a natives folder with original files like PDFs, Word documents, spreadsheets, and email containers
- an images folder, often TIFFs, for documents that were reviewed as images
The load file is where the work really starts. It tells you which text file belongs to which document, where the native is, what the Bates range is, and which metadata fields are worth preserving. If that mapping is wrong, the rest of the system can still look impressive while quietly being useless.
The job is retrieval
Full-text search is useful, but it only gets you so far. I wanted to ask a question in plain English and get back the parts of the production that were likely to matter, even when the wording did not match exactly.
That pushed the work into a fairly ordinary retrieval pipeline:
- parse the production correctly
- find or extract usable text
- break the text into chunks that are small enough to retrieve
- embed those chunks into vectors
- store the vectors with document metadata
- give the model a search tool and make it answer from the results
None of that is especially mystical. It is mostly plumbing. But the plumbing is the difference between a useful system and a model confidently summarizing whatever happened to fit in a prompt.
Parsing is the backbone
Relativity-style exports are structured, but they are not friendly. Load files can have odd delimiters, unusual quoting, and field names that change from one production to the next.
The parser needs to answer boring questions reliably:
- what is this document’s Bates range?
- which extracted text file belongs to this record?
- is there a native file?
- what metadata should travel with it?
- if the extracted text is missing or bad, what should I try next?
This is the part I would rather get right than clever. If the system cannot keep document identity, text, and metadata tied together, semantic search just makes the mess harder to notice.
Text extraction needs fallbacks
Documents do not simply “become text.” Some exports include good extracted text. Some include text that is technically present but not worth using. Some need the native file. Some need OCR. Some will be annoying no matter what.
I treated extraction as a fallback chain.
1. Use extracted text when it is good
If the production already includes usable text files, use them. It is fast and it keeps the pipeline simple.
2. Fall back to the native file
If the extracted text is missing, empty, or obviously broken, process the original file. That might be a PDF, Word document, spreadsheet, PowerPoint, or something less cooperative.
3. Fall back to OCR
If there is no usable text and no clean native path, OCR is the fallback.
This is not exciting, but it is the work. Discovery data is uneven. A pipeline that pretends every document is the same will fail in ways that are easy to miss.
Docling helps with conversion
For the conversion layer, I have been using Docling. It handles enough common file types that I do not have to maintain a different parser for every document that shows up.
In practice, that means sending files through a local conversion service and getting back cleaner text or markdown for things like:
- PDFs
- Word documents
- Excel files
- PowerPoints
- images that need OCR
It does not remove the ugly parts. It gives them a better place to live. The ingestion script can focus on mapping documents, choosing the right fallback, and handing conversion off to a service that is better suited to that job.
That also keeps the pipeline local, which matters for this kind of material.
Chunking makes the index usable
Once the text exists, it still needs to be broken down. A 200-page document is too large and too vague as one searchable object. You want the system to retrieve the relevant passage, not wave at the whole document.
I used chunks of a few hundred words with overlap. The exact numbers can move around, but the rules are simple:
- large enough to preserve context
- small enough to return a focused hit
- always linked back to the source document and Bates information
This is where the system starts to feel practical. A result is no longer “this document had a matching word somewhere.” It is a passage, attached to a document, with enough metadata to check it.
Embeddings and the vector layer
After chunking, each chunk gets embedded. In my current setup, that uses a local embedding model through Ollama.
The vectors go into Qdrant with the metadata I want available later:
- Bates number or range
- document identifier
- custodian
- document type
- production name
- dates
- any other fields worth preserving from the load file
That metadata is not decoration. It is what lets the answer point back to something a person can inspect.
The model only gets to answer after search
The model should not be treated like it contains the production. It does not. The production lives in the index. The model gets a tool.
The pattern is:
- the documents live in the retrieval layer
- the model searches that layer
- the retrieval tool returns matching chunks and metadata
- the answer is written from those results
- citations stay attached
In this setup, the retrieval tool sits inside Open WebUI. It exposes a few functions:
- semantic search across the indexed production
- fetch by Bates number
- find similar documents
- inspect what has been indexed
When someone asks a question, the tool embeds the question, queries Qdrant, returns relevant chunks, and includes the source metadata. The model has to work from that material.
What the user experience should feel like
The person using the system should not need to know about chunk sizes, vector databases, embeddings, or load files.
They ask a normal question. The system searches the production. The answer includes citations they can follow back to the documents.
That is the entire promise. Not magic. Not “AI discovery” as a product category. Just a faster way to move through a production while keeping the documents close enough to verify.
Why I keep this local
For this kind of work, local infrastructure is part of the design. The stack I used here is local:
- Qdrant for the vector store.
- Ollama for local embeddings.
- Docling for document conversion and OCR.
- Open WebUI for the model interface and retrieval tools.
That keeps the production on controlled infrastructure. It also makes it easier to tune ingestion, retrieval, metadata, and answer rules around the actual workflow instead of bending everything around a generic hosted product.
The tradeoff is that you own more of the setup. For this use case, I think that is the right tradeoff.
The boring parts decide whether it works
A model can produce an answer quickly. That is not the hard part.
The hard parts are the pieces that determine whether the answer is worth trusting:
- parsing ugly load files correctly
- handling inconsistent export structures
- deciding when extracted text is good enough and when to fall back
- dealing with OCR failures and bad source material
- preserving metadata cleanly
- making retrieval happen before answer generation
- requiring citations
Those are not glamorous problems, but they are the real ones. If the pipeline gets them wrong, the model layer cannot fix it.
The pattern I would reuse
The reusable pattern is straightforward:
- treat the production like structured data, not a pile of files
- parse the load file carefully
- extract text with fallbacks
- chunk the text and preserve metadata
- index the chunks locally
- make the model retrieve before it answers
- require citations
That is the version I want to keep building toward. It does not replace review. It does not make the model authoritative. It turns a production into something easier to query, inspect, and move through.
For a lot of discovery work, that would already be a meaningful improvement.