Skip to content

Cross-lingual corpus

The failure this prevents: an employee at the Hyderabad office asks how much unused leave they may carry forward. The answer comes back “a maximum of 10 days”, quoted verbatim from the English handbook, with a real document ID and a passing faithfulness gate. The binding Telugu annexure for that office caps it at 5 — and it was never retrieved, because an English query shares zero BM25 tokens with Telugu prose.

Nothing in the pipeline is broken. Groundedness is 100%, the citation is real, the quote is exact. The answer is simply from a document that does not govern the person asking — and at the point of use that is indistinguishable from a correct answer.

This is the sibling failure to Regulated audit and Authority: there, the wrong document was reachable and shouldn’t have been; here, the right document was unreachable, and no ordering over what you retrieved can rescue a passage you never retrieved.

  1. Configure a reformulator. The fan-out rewrites the question into each requested language with a small model, then RRF-fuses every retrieval. Without an endpoint there are no extra queries, so it is required rather than silently skipped.

    from citenexus.config.schema import ReformulationConfig
    ReformulationConfig(
    enabled=True, # default: False
    model="gemini-2.5-flash-lite", # a SMALL model is enough
    endpoint=gemini, # required — no endpoint, no fan-out
    )
  2. Name the languages the corpus is written in. Same keyword on ask() and retrieve(); the default is ("en",), which is the pre-fan-out behaviour.

    response = rag.ask(
    "How many days of earned leave may a Hyderabad employee carry forward?",
    search_languages=("en", "ta", "te"),
    )

    The original question is always issued too, so the fan-out is strictly additive: it can surface candidates the original missed, never displace them.

  3. Read the two language fields separately. They answer different questions.

    response.answer_language # "en" — resolved for the ANSWER
    response.sources[0].passage_language # "te" — the SOURCE's own language
    response.sources[0].passage # verbatim Telugu, untranslated

Searching a language is a capability claim, so it is checked first

Section titled “Searching a language is a capability claim, so it is checked first”

An unsupported language raises before any model call is spent — it does not return an empty list, and it does not abstain.

rag.ask(q, search_languages=("en", "kn"))
# UnsupportedSearchLanguageError: search language 'kn' (Kannada) is written in
# 'kannada', which this tokenizer does not claim (ADR-0011).

That is deliberate. Returning [] for a language the tokenizer cannot segment is indistinguishable from “the corpus does not contain this”, and routing a capability gap through the abstention channel is exactly how an ASCII-only tokenizer once hid inside a library advertised as multilingual.

Telugu is now claimedU+0C00–U+0C7F was a hole in the script range table and is fixed, with a golden fixture behind it. Kannada, Malayalam, Gujarati, Gurmukhi, Oriya and Sinhala are named in the table and deliberately not claimed: naming them is what lets the refusal say “Kannada is not supported” instead of “unknown language code”. See Languages & multilingual for the full script matrix.

From the live run in examples/multilingual/ on 2026-08-16 — 12 authored documents (4 English / 4 Tamil / 4 Telugu, two of the Tamil ones as real PDFs), 22 questions all asked in English, real Jina embeddings + jina-reranker-v2-base-multilingual, real gemini-2.5-flash generation at temperature 0, nothing mocked. One constant changed between runs: ("en",)("en", "ta", "te").

bucket n answered before answered after
Tamil-only 6 1 4
Telugu-only 7 2 7
English control 5 5 5
Ungroundable (must abstain) 4 0 0
metric before after
answered / abstained 8 / 14 16 / 6
groundedness_rate 100% 100%
citation_rate 100% 100%
cited_right_document_rate 75% 100%
answer_when_groundable 44% 89%
abstain_when_ungroundable 100% 100%

The Hyderabad question is now answered 5 రోజులు, cited to 09-te-hyderabad-leave-annexure. The two rows that must never move — abstention on ungroundable questions, and groundedness — did not move. The recall was not bought out of the abstention guarantee.

Known limitation: verbatim wins over answer language

Section titled “Known limitation: verbatim wins over answer language”

The Hyderabad answer comes back as 5 రోజులు — Telugu script — while answer_language reads "en". That is a real inconsistency, and it is not solved. It falls out of two individually correct rules that genuinely conflict:

  • The strict flow is extractive — the answer must survive the faithfulness gate against the passage it cites, which is what makes “no ungrounded claim” checkable at all.
  • Citations stay verbatim in their source language, because a translated quote is no longer the evidence.

When the only support for an English question is a Telugu clause, “answer in the query’s language” and “quote verbatim” cannot both hold — and verbatim wins. answer_language then reports what the resolution chain decided, not what script the returned text is in.

Two candidate fixes, neither implemented: make answer_language descriptive of the returned text (cheap, but it changes the meaning of a field with conformance vectors on it), or generate an answer alongside the verbatim citation (a real design change to the strict flow, not a patch). Until one lands: read sources[*].passage_language, not answer_language, when you need to know what script is in front of you.

One reformulation call per requested language per question, cached per (question, language) and shared across ask() and retrieve(). Three languages is three calls — one per requested language, including the one the question is already in — yielding up to three extra queries. A reformulation that fails, comes back empty, or duplicates the original contributes nothing — retrieval proceeds with fewer queries rather than erroring.

The benchmark above is Python, because the fan-out it measures is Python. See Languages & multilingual for the script matrix all three share.