Skip to main content

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
How it works
  1. You upload a document
  2. The document is parsed, chunked, and embedded
  3. After all embedding batches complete, the classifier selects representative chunks using the document's embeddings
  4. ModernBERT scores these chunks against your category list
  5. 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:

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 category clears the threshold

Documents can have multiple categories when more than one label clears the threshold.

Effective defaults

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.

Learn more

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

  1. After all embedding batches complete, the system reads chunk embeddings from Qdrant
  2. A centroid is computed from all chunk embeddings (the "average topic" of the document)
  3. MMR (Maximal Marginal Relevance) selects the most representative and diverse chunks — balancing relevance to the centroid with diversity from each other
  4. Optionally, a Jaccard prefilter narrows the category list before scoring
  5. ModernBERT scores each selected chunk against each (pre-filtered) category using NLI
  6. Scores are aggregated across chunks (max pooling) and categories above the threshold are selected
  7. All document chunks in Qdrant are updated with the final categories

Config variables

VariableDefaultDescription
MODERNBERT_ONNX_MODEL_REPOonnx-community/ModernBERT-base-nli-ONNXHugging Face repo for the ONNX classifier. See Available Model Repos for options.
MODERNBERT_ONNX_MODEL_FILEonnx/model_quantized.onnxReviewed and allowlisted ONNX artifact.
MODERNBERT_ONNX_CATEGORY_TEMPLATEThe topic of this document is {label}.NLI hypothesis template for categories
CATEGORY_DESCRIPTIONSKYC/Credit/Collateral/Ledger descriptionsOptional JSON object of richer label descriptions. Missing descriptions fall back to the category label text.
MODERNBERT_ONNX_CATEGORY_THRESHOLD0.35.env and .env.example value used in the 2026-06-11 guide run.
MODERNBERT_ONNX_LONG_DOC_WINDOW1800Sliding window size (tokens) for documents exceeding model context
MODERNBERT_ONNX_LONG_DOC_OVERLAP256Token overlap between consecutive windows
MODERNBERT_ONNX_TOP_K_CHUNKS2Max additional chunks to score after the first (long-doc optimization)
MODERNBERT_ONNX_NLI_MAX_TOKENS2048Maximum token length for NLI input sequences. Lower values reduce inference time but truncate document text
MODERNBERT_ONNX_INTRA_OP_THREADS1ONNX Runtime intra-op CPU threads
MODERNBERT_ONNX_INTER_OP_THREADS1ONNX Runtime inter-op CPU threads
CLASSIFICATION_SELECT_TOP_K3Number of representative chunks to select via centroid + MMR for classification. Higher values improve coverage but increase scoring time.
MODERNBERT_PREFILTER_ENABLEDfalseEnable lexical prefilter to reduce hypothesis space before ONNX inference
MODERNBERT_PREFILTER_TOP_K3Number 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.

RepoSizeNLI ClassesStatus
onnx-community/ModernBERT-base-nli-ONNXBase (768d, 22 layers)3 (entailment / neutral / contradiction)✅ Compatible
onnx-community/ModernBERT-large-zeroshot-v2.0-ONNXLarge (1024d, 28 layers)2 (entailment / not_entailment)❌ Incompatible — crashes at runtime
warning

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:

Commercial-use allowlist

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.

FilePrecisionSizeNotes
onnx/model.onnxfp32~599 MBHighest accuracy
onnx/model_fp16.onnxfp16~300 MBNear-lossless, good balance
onnx/model_quantized.onnxint8~151 MBBest accuracy/size tradeoff (recommended)
onnx/model_int8.onnxint8~151 MBSame as model_quantized.onnx
onnx/model_uint8.onnxuint8~151 MBAlternative 8-bit quantization
onnx/model_q4.onnx4-bit~225 MBSmaller, lower accuracy
onnx/model_q4f16.onnx4-bit/fp16~140 MBSmallest file, noticeable accuracy loss
onnx/model_bnb4.onnxBNB 4-bit~218 MBBitsAndBytes 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.

Office files

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

When you are done, stop all services:

just prod-down