// in-house build · multimodal rag

A chart is not a picture. It is an answer nobody indexed.

Ask a question about a PDF and get an answer drawn from its text, its tables and its charts — with the page it came from attached, so you can check it rather than trust it.

Build
In-house, open source
Scope
Ingestion, retrieval, chat UI
Status
Live on Hugging Face Spaces
AsadNav architecture: an uploaded PDF is parsed into text, tables and page images, the visual elements are captioned by a vision model, the content is chunked into a parent-child hierarchy and embedded into Chroma alongside a SQLite record of documents, parents and children; a question then matches child chunks, returns their parents, builds a prompt and produces an answer citing the page it came from.
Small chunks are good at being found. Large chunks are good at being understood. The index stores both.

The challenge

Most document question-answering quietly throws away the part of the document that took the longest to produce. A PDF is parsed for text, the text is chunked and embedded, and every table, chart and diagram on the page becomes an image the pipeline steps over.

That is precisely backwards for the documents people actually want to interrogate. Reports, filings, papers and datasheets put their conclusions in the figures. The sentence next to a chart usually says "as shown below" — which is worth nothing to a retriever, because the thing it points at was never indexed.

There is a second, subtler problem underneath it. Chunk small and retrieval gets precise but the model receives a fragment with no surrounding argument. Chunk large and the model gets context but the embedding is an average of six topics and matches none of them well. Picking one chunk size means choosing which half of the system to hurt.

The approach

Turn the figures into text before indexing anything

During ingestion, page images, tables and charts are extracted and passed through a vision model that describes them in words. The caption is then embedded alongside the ordinary prose, so a question about a trend in a bar chart matches the description of that bar chart.

This happens once, at ingestion, rather than on every query. Captioning is the expensive part of the pipeline, and paying for it per question rather than per document would make the whole thing uneconomic.

Retrieve on children, answer from parents

The chunker builds a two-level hierarchy. Child chunks are small and specific, and they are what the vector search actually matches against. Parent chunks are large and carry the surrounding argument, and they are what gets sent to the model.

So a query matches a precise fragment and the system then hands the model the section that fragment lived in. The trade-off between precision and context stops being a trade-off, because the two jobs are done by different objects.

Small chunks find the answer. Big chunks explain it. Asking one chunk size to do both is where most RAG pipelines lose their accuracy.

Two stores, because they answer different questions

Chroma holds the vectors and answers "what is similar to this". SQLite holds the documents, parents and children as records and answers "what is this, where did it come from, and what is it part of". Trying to make a vector store do the second job is a familiar way to end up with provenance you cannot reconstruct.

Keeping them separate is also what makes citation cheap. The page number, the parent, the source document and the figure a caption came from are all just columns, available at answer time without a second search.

Ingestion is a job, not a request

Parsing, captioning, chunking and embedding a real PDF takes long enough that doing it inside an HTTP request is a design error. Ingestion runs as a tracked job with its own status, so the UI can report progress and a large upload cannot time out the connection that started it.

The container ships with a persistent volume for uploads, extracted images, the SQLite database and the Chroma index — without it, every restart would throw away work that cost real inference time to produce.

Where it landed

It is deployed, public and usable right now — upload a PDF with figures in it and ask something the answer to which is only in a chart. That is the demonstration worth making, and it is the one you can run yourself rather than take on trust.

  • Text, tables and charts all reachable by the same question
  • Answers carry the page, table or figure they were drawn from
  • Parent-child hierarchy separates what gets matched from what gets read
  • Ingestion tracked as a job with status, so large PDFs do not time out
  • Containerised with a persistent volume; the index survives a restart
  • Runs as a single service — the FastAPI app serves both the API and the chat UI

Stack

  • FastAPI
  • ChromaDB
  • SQLite
  • Gemini
  • Multimodal RAG
  • Docker
  • Hugging Face Spaces

// start here

Are your answers hiding in the figures?

If the documents that matter to you put their conclusions in tables and charts, a text-only pipeline will miss them silently. Tell us what your corpus looks like.

Book a call