Introduction
Imagine having multiple financial documents—PDFs, spreadsheets, scanned receipts, and reports—and needing to answer a specific question about their contents.
You could manually open each file, search for relevant information, extract the necessary figures, and organize the results.
But what if you could upload these documents, index their contents, and ask questions directly?
That is the problem I wanted to explore with Document Intelligence, an AI-powered document-question-answering API built with Python.
The project combines document parsing, OCR, embeddings, hybrid retrieval, and retrieval-augmented generation (RAG) into a single backend workflow.
🔗 GitHub Repository:
https://github.com/yagnesh97/document-intelligence
What Problem Does It Solve?
Financial information is often distributed across different document formats.
For example:
- A financial report may be available as a PDF.
- A spreadsheet may contain revenue and expense information.
- A scanned document may contain information that cannot be extracted using normal text extraction.
- A presentation may contain financial summaries across multiple slides.
Finding relevant information across these documents can require manually opening files and searching through their contents.
Document Intelligence provides a workflow to:
- Upload documents.
- Extract their text and metadata.
- Split the extracted content into smaller chunks.
- Generate embeddings and index the chunks.
- Retrieve relevant document content using a question.
- Generate an answer using the retrieved context.
The objective is to make uploaded financial material searchable and enable question answering grounded in the retrieved document content.
What Can It Process?
Document Intelligence supports the following formats:
| Category | Supported formats |
|---|---|
| Text | .txt |
| CSV | .csv |
| Excel |
.xls, .xlsx
|
.pdf |
|
| Word | .docx |
| PowerPoint | .pptx |
| Images |
.png, .jpg, .jpeg, .tif, .tiff, .bmp, .webp, .gif
|
Different document formats require different extraction strategies.
For example, extracting text from an Excel spreadsheet is different from extracting text from a scanned PDF.
The project uses format-specific parsers and dedicated PDF analysis and OCR paths to handle these differences.
Architecture
The application is built around a FastAPI API, a Celery ingestion worker, document storage, MongoDB, and Qdrant.
The main components are:
| Component | Responsibility |
|---|---|
| FastAPI | HTTP API and request handling |
| Celery | Asynchronous document ingestion |
| Redis | Task queue and Celery backend |
| MongoDB | Document records, extracted content, metadata, and chunks |
| MinIO AIStor / S3 | Original document storage |
| Qdrant | Dense and sparse vector storage and hybrid retrieval |
| Sentence Transformers | Dense embedding generation |
| FastEmbed | Sparse embedding generation |
| OpenAI / Gemini | Question answering |
| PyMuPDF / OCRmyPDF / Tesseract | PDF extraction and OCR |
The architecture separates document ingestion from question answering.
Document processing happens asynchronously through Celery, while search and chat requests are handled synchronously by the API.
Document Processing Workflow
1. Uploading documents
The API exposes:
POST /files
The endpoint accepts multipart file uploads.
Before processing, it validates:
- Number of uploaded files
- Individual file size
- Total request size
- Supported file extensions
After validation, the API creates a document record in MongoDB.
The original file is stored in S3-compatible object storage, and the document ID is submitted to Celery for background processing.
The upload response includes the document ID and task ID.
This allows the client to track the document without waiting for the entire ingestion process to finish.
2. Extracting document content
Once the Celery worker receives the document ID, it retrieves the original file and selects a parser based on the filename extension.
For common office documents:
- Text files are processed using text extraction.
- CSV files are parsed for their content.
- Excel files include worksheet text.
- Word documents include document and table content.
- PowerPoint files include slide content.
PDF processing requires additional analysis.
3. Handling different types of PDFs
One of the interesting parts of the project is its PDF processing strategy.
Not every PDF contains extractable text.
Some PDFs contain native text, some contain scanned images, and others combine both.
Document Intelligence analyzes PDFs and classifies them into:
- Native-text PDFs
- Image-based PDFs
- Hybrid PDFs
Native-text PDFs
These are processed using PyMuPDF text extraction.
Image-based PDFs
These are processed using OCRmyPDF, followed by text extraction.
Hybrid PDFs
These are analyzed page by page.
Pages that require OCR are processed using PyMuPDF OCR, while pages with usable native text can continue through normal extraction.
This avoids treating every PDF as an image or assuming that every PDF contains extractable text.
4. OCR for scanned documents
Scanned documents require OCR because their contents are stored as images rather than selectable text.
The project uses:
- Tesseract for standalone image OCR
- OCRmyPDF for image-based PDF processing
- PyMuPDF for PDF analysis and extraction
- OpenCV and Pillow for image processing
For standalone images, the OCR pipeline applies grayscale conversion and enlargement before extracting text.
OCR details are also retained in document metadata.
The goal is to make scanned financial documents available to the same downstream indexing and retrieval workflow as other supported documents.
5. Chunking extracted content
After extraction, the document content is split into paragraph-sized chunks.
The current implementation uses blank-line paragraph boundaries.
Each chunk is stored in MongoDB with its document association and chunk information.
This makes it possible to retrieve relevant portions of a document instead of passing an entire document to the language model.
One current limitation is that chunking does not yet support configurable chunk sizes or overlap.
Building the Retrieval Pipeline
Extracting text is only the first part of the workflow.
The next challenge is finding the relevant information when a user asks a question.
Document Intelligence uses dense and sparse embeddings.
Dense embeddings
The project uses:
BAAI/bge-m3
Dense embeddings represent the semantic meaning of document chunks.
This helps retrieve content that is conceptually related to a query, even when the exact wording differs.
Sparse embeddings
The project uses:
Qdrant/bm25
Sparse retrieval helps identify content based on important terms and keyword relationships.
This is particularly useful when questions contain specific financial terminology, names, or figures.
Hybrid retrieval
The project combines dense and sparse retrieval using Qdrant reciprocal-rank fusion.
The retrieval workflow is:
- Embed the user query in both dense and sparse spaces.
- Retrieve candidate document chunks.
- Combine the results using reciprocal-rank fusion.
- Optionally rerank the retrieved candidates using a CrossEncoder.
- Return the most relevant chunks.
The optional reranker uses:
BAAI/bge-reranker-v2-m3
This additional step helps refine the candidate results before they are used for question answering.
Retrieval-Augmented Question Answering
The final stage is answering questions using retrieved document context.
Document Intelligence supports configured OpenAI and Gemini models through LangChain provider integrations.
The chat workflow is:
- Receive a question.
- Retrieve relevant document chunks.
- Use the retrieved chunks as context.
- Generate an answer using the configured language model.
- Return the answer and source chunks.
The application prompt instructs the model to use the supplied context and avoid presenting financial advice as fact.
The non-streaming chat endpoint returns the answer along with the retrieved sources.
This allows users to inspect the document content used to generate the response.
The project also supports streaming responses through server-sent events.
API Endpoints
The API currently exposes the following endpoints:
| Endpoint | Purpose |
|---|---|
GET /health |
Check API and MongoDB availability |
POST /files |
Upload documents and queue ingestion |
GET /files |
List documents |
GET /files/{document_id} |
Inspect document status, content, and metadata |
DELETE /files/{document_id} |
Delete document records, chunks, vectors, and original files |
GET /search |
Retrieve relevant document chunks |
POST /chat |
Ask questions using retrieved document context |
POST /chat/stream |
Stream generated answers |
Document processing tracks the following states:
QUEUED → PROCESSING → COMPLETED
If processing fails, the document is marked:
FAILED
The worker records the error and retries processing up to three times with backoff.
Running the Project Locally
Prerequisites
You will need:
- Python 3.14 or later
- uv
- Docker Engine and Docker Compose
- Tesseract
- OCRmyPDF and its system dependencies
- An API key for OpenAI or Gemini if you want to use chat
Clone the repository
git clone https://github.com/yagnesh97/document-intelligence.git
cd document-intelligence
Install dependencies
cp .env.example .env
uv sync --all-groups
Configure the required environment variables in .env.
At minimum, the settings class requires:
MONGODB_URIS3_ACCESS_KEYS3_SECRET_KEY
Configure the object-storage endpoint and create the bucket specified by S3_BUCKET.
Start infrastructure
docker compose up -d
The Compose configuration starts the local infrastructure, including Redis, MongoDB, MinIO AIStor, Qdrant, and Flower.
Start the API
uv run uvicorn app.api.main:app --reload
Start the Celery worker
In another terminal:
uv run celery -A app.worker.celery:celery worker --loglevel=info --pool=solo
Verify the API
curl http://127.0.0.1:8000/health
Interactive API documentation is available at:
Example: Uploading a Document
curl -X POST http://127.0.0.1:8000/files \
-F 'files=@./statement.pdf;type=application/pdf'
The response contains a document ID.
Use that ID to inspect processing status:
curl http://127.0.0.1:8000/files/<document-id>
Wait until the document reaches COMPLETED before searching its content.
Example: Searching Document Content
curl --get 'http://127.0.0.1:8000/search' \
--data-urlencode 'q=What was the operating income?' \
--data-urlencode 'limit=5'
The search response contains retrieved chunks, their scores, document identifiers, and metadata.
Search can also be scoped to a specific document using document_id.
Example: Asking Questions
curl -X POST http://127.0.0.1:8000/chat \
-H 'Content-Type: application/json' \
-d '{
"question": "Summarize the reported revenue.",
"model_id": "<llm-model-object-id>",
"document_id": "<document-id>",
"limit": 5
}'
The response contains:
- Generated answer
- Retrieved source chunks
- Document identifiers
- Chunk identifiers
- Retrieval scores
- Source metadata
The model_id must be the MongoDB ObjectId of an enabled LLM model configuration.
Testing
The project uses pytest.
Run the test suite:
./cmd.sh test
Or:
uv run pytest
Run specific tests:
uv run pytest tests/api/test_files.py
uv run pytest tests/services/parsers/pdf/test_analyzer.py
Run code checks:
./cmd.sh check
Format the code:
./cmd.sh format
Integration tests require the relevant external services and must be selected explicitly.
What I Learned
Building Document Intelligence brought several backend and AI engineering concepts together.
Some of the key areas explored include:
- Designing asynchronous document ingestion with FastAPI and Celery
- Handling multiple document formats through parser-specific workflows
- Understanding the differences between native-text and scanned PDFs
- Integrating OCR into document processing
- Generating dense and sparse embeddings
- Implementing hybrid retrieval with reciprocal-rank fusion
- Connecting retrieved document context to language models
- Managing document records, original files, chunks, and vectors across different storage systems
It also highlighted how important the document-processing stage is to the overall quality of a RAG application.
Retrieval quality depends on the content extracted from the original document, how that content is chunked, and how relevant chunks are selected.
Current Limitations
The project is still evolving.
Some current limitations include:
- Paragraph-based chunking without configurable size or overlap
- Hard-coded local Redis configuration for Celery
- No API endpoint for listing or managing LLM model configurations
- Manual creation of the configured object-storage bucket
- Parser selection based on supported filename extensions
- No application-level authentication or authorization
The service should be placed behind appropriate access controls before being exposed beyond a local development environment.
Financial documents may contain sensitive information, so storage and infrastructure access must also be configured carefully.
What's Next?
There are several areas where the project could be improved:
- More flexible chunking strategies
- Configurable chunk size and overlap
- Improved document extraction and OCR accuracy
- More configurable ingestion infrastructure
- Better model configuration management
- Additional document-processing workflows
These are potential improvements rather than features currently implemented.
Explore the Project
Document Intelligence is an open-source project for developers interested in document processing, OCR, hybrid search, and retrieval-augmented generation.
You can explore the architecture, run the application locally, experiment with different documents, and adapt the implementation to your own use cases.
🔗 GitHub Repository:
https://github.com/yagnesh97/document-intelligence
If you're building document-processing or RAG applications, feel free to explore the code, share feedback, or contribute improvements.
What would you add to a document-question-answering pipeline?
I'd be interested to hear your thoughts.

Top comments (0)