Auto-Categorizing Your Documents
The ModernBERT ONNX classifier attempts to assign labels from the configured category list.
When classification completes without a qualifying configured label, it stores the other
fallback. Categories are stored in metadata.classification.categories on the document's
indexed chunks.
What You Can Do
- Find documents faster by filtering on a stored category
- Keep a consistent document taxonomy without manual labeling
- Customize the category list and, when needed, its scoring descriptions
- The document is parsed and split into chunks.
- Dense embeddings are produced for the chunks.
- Centroid plus Maximal Marginal Relevance (MMR) selects up to
CLASSIFICATION_SELECT_TOP_Krepresentative chunks. - An optional lexical prefilter narrows the category hypotheses.
- ModernBERT scores each selected chunk against the category hypotheses with NLI.
- Scores are max-pooled by category. Strong lexical description matches take precedence;
otherwise labels at or above the threshold are used, with
otheras the fallback. - The resulting categories and classification status are written to every document chunk.
In the normal processing path, classification follows completion of the embedding batches. Publication-enabled processing selects from staged dense embeddings before dispatching the immutable generation's embedding tasks, then uses the same representative-chunk scoring.
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": "<job-id>",
"document_id": "malaysia-categories-001",
"status": "pending"
}
Step 2: Wait for Processing
Poll until the embedding job reaches a terminal status:
curl http://localhost:8000/status/<job-id> \
-H "X-API-Key: super-secret-key"
When the embedding work is complete, the response can still show classification as pending:
{
"job_id": "<job-id>",
"document_id": "malaysia-categories-001",
"status": "completed",
"chunks_created": 11,
"categories": [],
"classification_status": "pending",
"entities_extracted": {
"persons": [],
"organizations": [],
"dates": [],
"locations": [],
"monetary_amounts": [],
"account_numbers": [],
"transaction_refs": [],
"account_types": []
},
"error": null
}
Use the documents endpoint or a search response to confirm the final classification metadata after the classifier finishes.
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"
The documents response shows stored classification metadata for each listed document. In
publication-backed mode, entries also carry the selected version_id. Relevant response
fields (other metadata fields omitted) look like this:
{
"documents": [
{
"document_id": "malaysia-categories-001",
"version_id": null,
"metadata": {
"schema_version": "2",
"classification": {
"status": "completed",
"categories": ["other"]
}
}
}
],
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false
}
After classification finishes, the documents response shows the stored
metadata.classification.categories and metadata.classification.status. The sample above
illustrates a completed classification; the immediate status response may still report
classification_status: "pending" while the post-embedding task is running.
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 label is selected |
Documents can have multiple categories when more than one configured label qualifies.
The built-in defaults in app/config.py and the 15-category list in .env.example use the
same taxonomy. Setting ALLOWED_CATEGORIES in .env replaces that list.
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. For a known built-in label, an omitted description uses
the classifier's built-in description; for a custom label, the label text is used. A
description can include synonyms or examples that should be considered during NLI scoring
and the optional lexical prefilter.
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. The returned category label stays concise,
such as KYC; descriptions are used during scoring and strong lexical description matching.
See Configuration for complete details on customizing categories.
Classification Backend
CLASSIFIER_BACKEND currently supports modernbert-onnx. The ModernBERT service runs
zero-shot NLI scoring against category hypotheses built from each label or its configured
description.
Execution details
In the normal processing path, the classifier reads the dense vectors written by the
embedding batches. In publication-enabled processing, it selects representative text from
staged dense embeddings before the immutable generation's embedding tasks are dispatched.
Both paths use centroid plus MMR selection and score only the selected representative chunks.
The selected categories and metadata.classification.status are then applied to all chunks in
the Document Version.
If MODERNBERT_PREFILTER_ENABLED=true, the lexical prefilter uses Jaccard overlap to rank
category descriptions and narrows the hypotheses when it can safely do so. When it cannot
narrow the list, scoring falls back to all configured categories.
Model identifiers
The commercial-use model allowlist requires these exact ModernBERT settings:
MODERNBERT_ONNX_MODEL_REPO=onnx-community/ModernBERT-base-nli-ONNX
MODERNBERT_ONNX_MODEL_FILE=onnx/model_quantized.onnx
Any other repository or file value fails settings validation when the API or a worker imports the configuration.
Config variables
| Variable | Default or example value | Description |
|---|---|---|
CLASSIFIER_BACKEND | modernbert-onnx | The only supported classifier backend |
CLASSIFICATION_ENABLED | true | Runs classification when enabled |
CLASSIFICATION_SELECT_TOP_K | 3 | Maximum number of representative chunks selected with centroid plus MMR |
MODERNBERT_ONNX_CATEGORY_TEMPLATE | The topic of this document is {label}. | NLI hypothesis template |
MODERNBERT_ONNX_CATEGORY_THRESHOLD | 0.45 | Threshold used for NLI-score label selection when no strong lexical description match takes precedence |
CATEGORY_DESCRIPTIONS | Built-in descriptions; .env.example overrides KYC, Credit, Collateral, and Ledger | Optional descriptions used when building hypotheses and for lexical matching |
MODERNBERT_PREFILTER_ENABLED | false | Enables the lexical prefilter before ONNX scoring |
MODERNBERT_PREFILTER_TOP_K | 3 | Number of category descriptions retained when the prefilter narrows the list |
MODERNBERT_ONNX_NLI_MAX_TOKENS | 2048 | Maximum tokens in each NLI input |
MODERNBERT_ONNX_LONG_DOC_WINDOW | 1800 | Window size for the full-document scoring helper |
MODERNBERT_ONNX_LONG_DOC_OVERLAP | 256 | Token overlap for full-document scoring windows |
MODERNBERT_ONNX_TOP_K_CHUNKS | 2 | Additional chunks considered by the full-document scoring helper after its first chunk |
MODERNBERT_ONNX_INTRA_OP_THREADS | 1 | ONNX Runtime intra-op thread count |
MODERNBERT_ONNX_INTER_OP_THREADS | 1 | ONNX Runtime inter-op thread count |
Turn Classification Off (Optional)
CLASSIFICATION_ENABLED=false
With CLASSIFICATION_ENABLED=false, documents are still parsed, embedded, and searchable.
Classification is marked disabled and the metadata fallback is
metadata.classification.categories: ["other"].
Classification work
The current pipeline scores only the selected representative chunks. The main settings that change the amount of classification work are:
CLASSIFICATION_SELECT_TOP_Kcontrols how many representative chunks centroid plus MMR selects.MODERNBERT_PREFILTER_ENABLEDandMODERNBERT_PREFILTER_TOP_Kcontrol whether lexical Jaccard ranking narrows the category hypotheses before ONNX scoring.MODERNBERT_ONNX_NLI_MAX_TOKENScaps each tokenized NLI input.
The normal path selects from vectors already written by embedding batches. Publication-enabled processing selects from staged dense embeddings before dispatching the immutable generation's embedding tasks. In both cases, the final classification metadata is applied to every chunk.
Uploaded PPTX, XLSX, and legacy Office files (.ppt/.xls with LibreOffice) use the same
classification pipeline as other formats. Native text from slides and visible, non-empty
worksheets feeds chunking; representative chunks are scored, and the selected categories are
written to every chunk in the Document Version.
What's Next
- Read about customizing categories
- Try the search tuning guide
When you are done, stop all services:
just prod-down