Auto-Categorizing Your Documents
Upload a document and the ModernBERT ONNX classifier automatically organizes it into categories. No manual labeling required.
What You Can Do
- Find documents faster by searching within a specific category
- Keep a consistent document taxonomy without manual work
- Customize the category list for your own domain
- You upload a document
- The document is parsed, chunked, and embedded
- After all embedding batches complete, the classifier selects representative chunks using the document's embeddings
- ModernBERT scores these chunks against your category list
- Categories are saved with all document chunks
In the 2026-06-11 production-stack run, /status/{job_id} reached completed first and the document-list/search metadata showed classification.status: "completed" shortly afterward.
Step 1: Upload a Document
Make sure your services are running:
just status
Now upload the Malaysia Wikipedia article from the test documents folder:
curl -X POST http://localhost:8000/embed \
-H "X-API-Key: super-secret-key" \
-F "file=@test-documents/malaysia-wikipedia.txt" \
-F "document_id=malaysia-categories-001"
You will get a response like this:
{
"job_id": "f1a49926b341496d80b13b6d706fa695",
"document_id": "malaysia-categories-001",
"status": "pending"
}
Step 2: Wait for Processing
Documents need to be parsed, chunked, and classified. This usually takes 10-30 seconds.
curl http://localhost:8000/status/f1a49926b341496d80b13b6d706fa695 \
-H "X-API-Key: super-secret-key"
When complete, you should see a response like this:
{
"job_id": "f1a49926b341496d80b13b6d706fa695",
"document_id": "malaysia-categories-001",
"status": "completed",
"chunks_created": 11,
"categories": [],
"classification_status": "pending",
"created_at": "2026-06-11T16:36:32.806578+08:00",
"completed_at": "2026-06-11T16:36:49.354379+08:00",
"entities_extracted": {
"persons": [
"Malaisia",
"Malesia",
"Borneo",
"Sarawak",
"Islam"
],
"organizations": [
"Wikipedia",
"the Malayan Union",
"the Federation of Malaya",
"the Organisation of Islamic Cooperation (OIC",
"the Association of Southeast Asian Nations"
],
"dates": [
"the 18th century",
"three years",
"1946",
"1948",
"31 August 1957"
],
"locations": [
"Malaya",
"Malaysia",
"Southeast Asia",
"South China Sea",
"Peninsular"
],
"monetary_amounts": [
"34 million",
"3 million",
"2 million"
],
"account_numbers": [],
"transaction_refs": [],
"account_types": []
}
}
Step 3: Check the Categories
List all documents to see the stored classification:
curl http://localhost:8000/documents \
-H "X-API-Key: super-secret-key"
Response:
{
"documents": [
{
"document_id": "malaysia-categories-001",
"metadata": {
"schema_version": "2",
"source": {
"filename": "malaysia-wikipedia.txt",
"mime_type": "text/plain",
"extension": "txt"
},
"document": {},
"location": {
"page_number": 1,
"page_label": "1",
"chunk_index": 2
},
"classification": {
"status": "completed",
"categories": [
"sustainability"
]
},
"quality": {
"text_extraction": "native",
"ocr_used": false,
"warnings": []
},
"custom": {}
},
"chunk_count": 11,
"created_at": "2026-06-11T16:36:32.806578+08:00"
}
],
"total": 4,
"limit": 100,
"offset": 0,
"has_more": false
}
In this run the Malaysia document eventually landed in sustainability in document-list/search metadata. The immediate /status/{job_id} response still showed classification_status: "pending", so use /documents or /search when you need to confirm final stored categories.
What the Default Categories Mean
Most deployments start from the 15-category taxonomy in .env.example:
| Category | Description |
|---|---|
financial reporting | Financial statements, annual reports, and quarterly disclosures |
risk management | Risk frameworks, controls, and exposure analysis |
corporate governance | Board governance, oversight, and policy decisions |
regulatory compliance | Compliance programs, obligations, and regulatory reporting |
business operations | Operational processes, service delivery, and internal workflows |
market analysis | Market research, forecasts, and competitive analysis |
legal | Contracts, legal review, litigation, and counsel |
human capital | Hiring, workforce planning, training, and people operations |
technology | Systems, platforms, software delivery, and cybersecurity |
sustainability | ESG, sustainability reporting, and environmental initiatives |
KYC | Know-your-customer, identity verification, and customer due diligence |
Credit | Credit reports, insolvency checks, and lending risk documents |
Collateral | Security, pledged assets, and supporting collateral documents |
Ledger | Ledger, account movement, and transaction history documents |
other | Fallback when no configured category clears the threshold |
Documents can have multiple categories when more than one label clears the threshold.
app/config.py has a smaller built-in fallback list used only when no environment value is provided. If you created .env from .env.example, your deployment uses the 15-category taxonomy above.
Change the Category List (Optional)
Edit your .env file:
ALLOWED_CATEGORIES=["engineering","product","marketing","sales","support","legal","hr","other"]
Or in Docker Compose:
environment:
- ALLOWED_CATEGORIES=["engineering","product","marketing","sales","support"]
CATEGORY_DESCRIPTIONS is optional. You can list categories without descriptions, and the InfoConnect Hybrid Search Engine will fall back to the label text itself. For example, Ledger without a description is still valid; it is simply scored as Ledger.
Descriptions are recommended when labels are terse, ambiguous, acronym-based, or client-specific. They help the classifier connect real document text to your short labels:
ALLOWED_CATEGORIES=["KYC","Credit","Collateral","Ledger"]
CATEGORY_DESCRIPTIONS={"KYC":"know your customer, identity verification, customer due diligence, IC, passport, Kad Pengenalan, Warganegara, onboarding, or beneficial ownership documents","Credit":"credit reports, bankruptcy searches, insolvency checks, lending, repayment history, or credit risk documents"}
You do not need one description per category. Add descriptions where the label alone is not enough. The returned category label stays concise, such as KYC; descriptions are used only during scoring and strong lexical evidence matching.
See Configuration for complete details on customizing categories.
Classification Backend
ModernBERT ONNX uses zero-shot NLI scoring to pick categories from your configured category list. The backend is selected with CLASSIFIER_BACKEND (default: modernbert-onnx).
How it works
- After all embedding batches complete, the system reads chunk embeddings from Qdrant
- A centroid is computed from all chunk embeddings (the "average topic" of the document)
- MMR (Maximal Marginal Relevance) selects the most representative and diverse chunks — balancing relevance to the centroid with diversity from each other
- Optionally, a Jaccard prefilter narrows the category list before scoring
- ModernBERT scores each selected chunk against each (pre-filtered) category using NLI
- Scores are aggregated across chunks (max pooling) and categories above the threshold are selected
- All document chunks in Qdrant are updated with the final categories
Config variables
| Variable | Default | Description |
|---|---|---|
MODERNBERT_ONNX_MODEL_REPO | onnx-community/ModernBERT-base-nli-ONNX | Hugging Face repo for the ONNX classifier. See Available Model Repos for options. |
MODERNBERT_ONNX_MODEL_FILE | onnx/model_quantized.onnx | Reviewed and allowlisted ONNX artifact. |
MODERNBERT_ONNX_CATEGORY_TEMPLATE | The topic of this document is {label}. | NLI hypothesis template for categories |
CATEGORY_DESCRIPTIONS | KYC/Credit/Collateral/Ledger descriptions | Optional JSON object of richer label descriptions. Missing descriptions fall back to the category label text. |
MODERNBERT_ONNX_CATEGORY_THRESHOLD | 0.35 | .env and .env.example value used in the 2026-06-11 guide run. |
MODERNBERT_ONNX_LONG_DOC_WINDOW | 1800 | Sliding window size (tokens) for documents exceeding model context |
MODERNBERT_ONNX_LONG_DOC_OVERLAP | 256 | Token overlap between consecutive windows |
MODERNBERT_ONNX_TOP_K_CHUNKS | 2 | Max additional chunks to score after the first (long-doc optimization) |
MODERNBERT_ONNX_NLI_MAX_TOKENS | 2048 | Maximum token length for NLI input sequences. Lower values reduce inference time but truncate document text |
MODERNBERT_ONNX_INTRA_OP_THREADS | 1 | ONNX Runtime intra-op CPU threads |
MODERNBERT_ONNX_INTER_OP_THREADS | 1 | ONNX Runtime inter-op CPU threads |
CLASSIFICATION_SELECT_TOP_K | 3 | Number of representative chunks to select via centroid + MMR for classification. Higher values improve coverage but increase scoring time. |
MODERNBERT_PREFILTER_ENABLED | false | Enable lexical prefilter to reduce hypothesis space before ONNX inference |
MODERNBERT_PREFILTER_TOP_K | 3 | Number of top categories to keep after lexical prefilter |
Available Model Repos
The classifier requires a 3-class NLI model outputting entailment (index 0), neutral (index 1), contradiction (index 2). Only repos with this exact label mapping are compatible.
| Repo | Size | NLI Classes | Status |
|---|---|---|---|
onnx-community/ModernBERT-base-nli-ONNX | Base (768d, 22 layers) | 3 (entailment / neutral / contradiction) | ✅ Compatible |
onnx-community/ModernBERT-large-zeroshot-v2.0-ONNX | Large (1024d, 28 layers) | 2 (entailment / not_entailment) | ❌ Incompatible — crashes at runtime |
Do not use 2-class models like ModernBERT-large-zeroshot-v2.0-ONNX. The pipeline expects exactly 3 NLI output logits and will raise a shape validation error.
Available Model Files
All files below are available inside onnx-community/ModernBERT-base-nli-ONNX:
The current release accepts only onnx/model_quantized.onnx. Other files are upstream reference
options and cannot be selected through environment overrides until a code and compliance-policy
change records their review evidence.
| File | Precision | Size | Notes |
|---|---|---|---|
onnx/model.onnx | fp32 | ~599 MB | Highest accuracy |
onnx/model_fp16.onnx | fp16 | ~300 MB | Near-lossless, good balance |
onnx/model_quantized.onnx | int8 | ~151 MB | Best accuracy/size tradeoff (recommended) |
onnx/model_int8.onnx | int8 | ~151 MB | Same as model_quantized.onnx |
onnx/model_uint8.onnx | uint8 | ~151 MB | Alternative 8-bit quantization |
onnx/model_q4.onnx | 4-bit | ~225 MB | Smaller, lower accuracy |
onnx/model_q4f16.onnx | 4-bit/fp16 | ~140 MB | Smallest file, noticeable accuracy loss |
onnx/model_bnb4.onnx | BNB 4-bit | ~218 MB | BitsAndBytes quantization |
Turn Classification Off (Optional)
CLASSIFICATION_ENABLED=false
Documents will still be processed and searchable, but they will fall back to category: ["other"].
Performance Notes
Classification runs after embedding and typically takes 2-4 seconds per document. This includes reading embeddings from Qdrant, selecting representative chunks, and scoring them.
The key performance factors are:
CLASSIFICATION_SELECT_TOP_K— more chunks means more NLI inference time (roughly +1s per chunk)MODERNBERT_PREFILTER_ENABLED— reduces hypothesis count from 7+ to 3, cutting inference by ~60%MODERNBERT_PREFILTER_TOP_K— controls how many categories survive prefiltering
For comparison, the previous inline method scored the full document text and took 15-25 seconds. The post-embedding approach scores only 3 representative chunks, achieving a 5-10x speedup.
Uploaded PPTX, XLSX, and legacy Office files (.ppt/.xls with LibreOffice) go through the same classification pipeline as other formats. Presentations emit one chunk per slide and spreadsheets emit one per visible worksheet; each chunk is scored against your category list the same way.
What's Next
- Read about customizing categories
- Try the search tuning guide
When you are done, stop all services:
just prod-down