Skip to main content

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
How it works
  1. The document is parsed and split into chunks.
  2. Dense embeddings are produced for the chunks.
  3. Centroid plus Maximal Marginal Relevance (MMR) selects up to CLASSIFICATION_SELECT_TOP_K representative chunks.
  4. An optional lexical prefilter narrows the category hypotheses.
  5. ModernBERT scores each selected chunk against the category hypotheses with NLI.
  6. Scores are max-pooled by category. Strong lexical description matches take precedence; otherwise labels at or above the threshold are used, with other as the fallback.
  7. 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:

CategoryDescription
financial reportingFinancial statements, annual reports, and quarterly disclosures
risk managementRisk frameworks, controls, and exposure analysis
corporate governanceBoard governance, oversight, and policy decisions
regulatory complianceCompliance programs, obligations, and regulatory reporting
business operationsOperational processes, service delivery, and internal workflows
market analysisMarket research, forecasts, and competitive analysis
legalContracts, legal review, litigation, and counsel
human capitalHiring, workforce planning, training, and people operations
technologySystems, platforms, software delivery, and cybersecurity
sustainabilityESG, sustainability reporting, and environmental initiatives
KYCKnow-your-customer, identity verification, and customer due diligence
CreditCredit reports, insolvency checks, and lending risk documents
CollateralSecurity, pledged assets, and supporting collateral documents
LedgerLedger, account movement, and transaction history documents
otherFallback when no configured label is selected

Documents can have multiple categories when more than one configured label qualifies.

Effective defaults

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.

Learn more

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​

VariableDefault or example valueDescription
CLASSIFIER_BACKENDmodernbert-onnxThe only supported classifier backend
CLASSIFICATION_ENABLEDtrueRuns classification when enabled
CLASSIFICATION_SELECT_TOP_K3Maximum number of representative chunks selected with centroid plus MMR
MODERNBERT_ONNX_CATEGORY_TEMPLATEThe topic of this document is {label}.NLI hypothesis template
MODERNBERT_ONNX_CATEGORY_THRESHOLD0.45Threshold used for NLI-score label selection when no strong lexical description match takes precedence
CATEGORY_DESCRIPTIONSBuilt-in descriptions; .env.example overrides KYC, Credit, Collateral, and LedgerOptional descriptions used when building hypotheses and for lexical matching
MODERNBERT_PREFILTER_ENABLEDfalseEnables the lexical prefilter before ONNX scoring
MODERNBERT_PREFILTER_TOP_K3Number of category descriptions retained when the prefilter narrows the list
MODERNBERT_ONNX_NLI_MAX_TOKENS2048Maximum tokens in each NLI input
MODERNBERT_ONNX_LONG_DOC_WINDOW1800Window size for the full-document scoring helper
MODERNBERT_ONNX_LONG_DOC_OVERLAP256Token overlap for full-document scoring windows
MODERNBERT_ONNX_TOP_K_CHUNKS2Additional chunks considered by the full-document scoring helper after its first chunk
MODERNBERT_ONNX_INTRA_OP_THREADS1ONNX Runtime intra-op thread count
MODERNBERT_ONNX_INTER_OP_THREADS1ONNX 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_K controls how many representative chunks centroid plus MMR selects.
  • MODERNBERT_PREFILTER_ENABLED and MODERNBERT_PREFILTER_TOP_K control whether lexical Jaccard ranking narrows the category hypotheses before ONNX scoring.
  • MODERNBERT_ONNX_NLI_MAX_TOKENS caps 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.

Office files

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​

When you are done, stop all services:

just prod-down