How Klassio classifies
What happens between hitting Search and Klassio surfacing a CN code. Every step is documented and logged — so you, an auditor, or a customs officer can follow the chain from query to answer.
- Auditable end-to-end
- CN 2026 · CELEX 02025R1926
- GDPR · self-hosted embedding
The flow, from query to answer
Seven steps from query to answer. Step four is conditional — it runs only when the head noun in your query resolves to a specific chapter.
Hover or tab to a node to follow the flow. Click or press Enter for details.
Select a node in the graph to see details.
-
Query
You can type a product description in Swedish or English ("oak chair with leather upholstery"), or enter a CN code directly ("94016100"). Klassio handles both — free text goes through the full pipeline, CN codes go straight to lookup. For free text, Klassio first normalises your description to its customs "nature" (head noun + material + function, stripping brand and marketing words) to drive retrieval — the judge still sees your original wording.
-
Embedding
The query is encoded into a 1024-dimensional vector by a model trained in parallel on Swedish and English. That means "ekstol" and "oak chair" map to similar vectors — semantic similarity, not just keyword matching.
-
Hybrid retrieval
Two retrievers run in parallel. Vector search finds semantic matches via cosine distance in pgvector; BM25 (Postgres FTS) finds lexical token matches. Results are fused via RRF — robust to mismatched ranking scales and catches what either retriever alone would miss.
-
Chapter anchor
ConditionalA small head-noun lexicon of stems ("chair" → 94 furniture, "beans" → 09/12, "boot" → 64, "drawer" → 94) provides a chapter hint. When the hint fires the top-10 in-chapter candidates are pulled via vector search and prepended — so the judge always sees the right chapter even when raw embedding pulls the wrong way.
-
GRI judge
The reranked shortlist (a cross-encoder trims the top-50 retrieved candidates to ~30) goes to an LLM judge along with each candidate's chapter description, classification rules, residual flag, and EUDR scope. The model applies the EU General Rules of Interpretation (GRI) in order and returns a structured verdict: winner + runner-up + rules applied + a bilingual rationale. When the description signals an incomplete article (e.g. "sold separately", "not included"), parts subheadings (xx 91 / xx 99) are also injected into the candidate list so GRI 2(a) can be applied correctly — a partial-component kit lands on the right parts subheading instead of the complete-article heading.
-
Calibration
Status is decided by seven rules — is the top-1 a residual heading, does the judge's chapter sit outside top-2, are top-1 and top-2 in different chapters, did the judge fail, is the top-1/top-2 distance margin under 5 pp, did the judge apply GRI 2(a) (incomplete article — always Review recommended), and (added 2026-05-19) when the judge could not fire AND the embedding fallback's top-1 lands outside the resolved chapter hint, the status is downgraded to Manual review rather than surfaced as a confident answer. Klassio is conservative by default — only when every gate clears does the status escalate to Confident.
-
Answer
Every answer carries the GRI rule that was applied, a short bilingual rationale, the chapter description, EUDR scope (in/out + is_ex), and the MFN duty headline from Tulltaxan. The result is auditable without clicking through.
The seven steps land on an 8-digit CN code — the General Interpretative Rules govern to CN-8. The 10th digit is TARIC’s measures layer, not an interpretation question, so it is resolved separately: a single declarable child is chosen automatically; an objective product property (voltage, %ABV, weight/m²…) yields a constrained pick flagged for review; an end-use, origin, or manufacturer question is never answered with a guess — we ask instead.
The MFN (erga omnes) duty is inheritance-aware: when the 10-digit code has no own rate it inherits the nearest parent level’s — e.g. a laptop (8471300000) inherits 0 % from heading 8471. When the rate is unknown 0 % is never shown; the page shows “Tullsats okänd — verifiera i Taric”. The 10th digit is surfaced as a soft review signal and never raises the CN-level confidence.
An LLM alone hallucinates CN codes — invents codes that don't exist. A pure embedding picks the wrong chapter when material outweighs product ("oak chair with leather" → leather, not chair). The tool combines embedding for breadth, BM25 for precision, and lets the judge pick from a shortlist already anchored to the CN nomenclature. The judge cannot invent — it must pick from the list, and its pick is strictly validated against the shortlist before it ships.
Every search writes a judge_decisions row: what the embedding picked, what the judge picked, which GRI rule applied, a bilingual rationale, and latency. That log is the foundation for improving Klassio over time — synonym suggestions are mined from rationales, hard cases (where embedding and judge disagree) feed regression tests, and with enough validated cases the embedding can be fine-tuned.
The hashed query (sha256, not PII) and a normalised form are always logged — for operation, security, and product improvement. In the operational log the raw cleartext query is kept at most 30 days (a security buffer against abuse), then deleted. Queries are also used to improve Klassio — by default, unless you have turned it off — and such queries (including the query text) are retained as training data. Exclude any individual search as you make it, or manage sharing in Settings → Privacy. Signed-in users are covered by the right to erasure (GDPR Art. 17), which cascades to judge_decisions.
Engineering notes Model names, parameters and libraries for each step — for developers and auditors who want depth.
- 01 · Query
- /api/mcp via STDIO or HTTP. Input: free text (≤4000 chars) or CN code (2/4/6/8/10 digits). Free text is normalised for retrieval (normalize-query.ts, nq-v2); the judge sees the original.
- 02 · Embedding
- Model: multilingual-e5-large-instruct (1024 dim). Sidecar in Coolify, no external API call.
- 03 · Hybrid retrieval
- pgvector cosine distance + Postgres FTS (websearch_to_tsquery, swedish + english). RRF k=60. Top-50 → cross-encoder rerank → top-30 to the judge.
- 04 · Chapter anchor
- src/lib/server/head-noun-lexicon.ts — ~190 stems + ~30 multi-word phrases. On match: top-10 in-chapter via vector search, deduped against the main list.
- 05 · GRI judge
- claude-sonnet-4-6 (default; claude-haiku-4-5 selectable) with tool_use forced output (submit_verdict). Schema: VerdictSchema (winner + runner_up + gri_applied + bilingual rationale). 10s timeout, 2048 max output tokens, 24h judge_cache.
- 06 · Calibration
- src/lib/server/calibrate.ts — seven rules (incl. off-shortlist rule #7). Cosine-distance margin threshold of 5 pp for "confident". GRI 2(a) → always review_recommended.
- 07 · Answer
- Output: 3 candidates with eudr, duty, classification_rules, judge_winner, gri_applied, bilingual rationale, judge_debug. Logged to judge_decisions for audit.
Search your own product
Describe the product in your own words and Klassio suggests a CN code with reasoning, duty and EUDR status.