Short answer: For a fintech customer-support knowledge base, store access and lifecycle metadata with every chunk, apply hard filters before vector scoring, and keep a small lexical fallback for ambiguous queries.
That arrangement usually gives a better quality/latency balance than making the embedding index carry tenant, locale, or product rules by itself. It also makes an answer explainable when a support lead asks why a particular policy was eligible.
The bill is not the only thing that grows. Retained chunks multiply embedding calls, index storage, and reranking work. A useful design starts by measuring those terms, then deciding what history can be discarded without making an audit or a support escalation impossible.
What does a filtered chunk actually represent?
A chunk is not merely a slice of text. In a support system it is a slice plus a contract: tenant_id, product, locale, audience, effective_from, effective_to, source_id, and a revision. If those fields are absent, a semantically similar paragraph can still be the wrong answer for a cardholder in a different country.
I once treated locale as a display concern and filtered only after retrieval. A test query returned a US fee schedule for a UK customer because both documents used the phrase “cash withdrawal.” The similarity score looked fine; the answer was not. The fix was boring: normalize the metadata at ingest and reject a chunk whose date window is missing.
The filter should be deterministic and auditable. Keep user-provided values out of an expression string; bind them as typed parameters in the index adapter. For a generic adapter, the shape can stay small:
from dataclasses import dataclass from datetime import date @dataclass(frozen=True) class Chunk: text: str tenant_id: str product: str locale: str effective_from: date effective_to: date | None source_id: str revision: int def active_for(chunk: Chunk, tenant_id: str, product: str, locale: str, on: date) -> bool: return ( chunk.tenant_id == tenant_id and chunk.product == product and chunk.locale == locale and chunk.effective_from <= on and (chunk.effective_to is None or on < chunk.effective_to) ) That predicate is intentionally plain. Your storage engine may express it differently, but the behavior should be identical in unit tests and in production.
Small detail. Big consequence.
How should retrieval architecture balance metadata filters, chunking, and latency?
Start with pre-filtering for hard boundaries: tenant isolation, legal entity, language, and document validity. Then run approximate nearest-neighbor search over the remaining candidates. If the filtered set is tiny, a lexical or exact scan can be faster and more predictable than forcing an ANN query.
Chunking controls the other side of the equation. Chunks that are too short lose the definition that makes a policy actionable; chunks that are too long dilute the embedding and increase prompt payload. For support articles, split on headings and numbered steps first, with a token ceiling as a backstop. Preserve the heading path in metadata so a citation can point back to the source section.
The ingestion worker should make this contract visible in its response and logs. Parse the document, canonicalize locale and product identifiers, calculate the source revision, and emit one record per chunk. If a required field is absent, stop that document and return a validation result such as HTTP 422; silently inventing a default tenant is an isolation bug. For a batch of 10,000 sections, the useful counters are accepted chunks, rejected chunks, vectors recomputed, and records retired. Those counters let an operator tell the difference between a genuine content update and a parser that suddenly split every page at 200 tokens. Store the parser version with the revision manifest, because changing sentence boundaries changes retrieval behavior even when the source text is byte-for-byte identical. A replay job can then rebuild only affected records and compare recall against the previous manifest before the new revision becomes active.
Use a two-stage budget: retrieve a modest candidate set, then rerank only candidates that pass the hard filter. Record p50 and p95 latency for each stage, along with the number of candidates discarded by each predicate. A sudden rise in “discarded locale” is often metadata drift, not a model regression.
The quality test should include adversarial pairs: two policies with the same wording but different regions, old and current revisions, and public versus agent-only notes. Measure recall at k and citation validity separately. A system can retrieve the right topic while citing an expired revision.
Where does retention change the operating cost?
The dominant term is usually repeated embedding and reranking of unchanged text, not the filter expression. Hash normalized source sections; when only metadata changes, update the index record without recomputing the vector. When text changes, create a new revision and mark the old one outside the active date window.
I keep raw source files for the period required by compliance, but I do not keep every intermediate chunk forever. Dropping superseded chunks lowers storage and candidate counts. The catch is forensic detail: deleting them can make it harder to reproduce an answer from a past ticket. Keep a compact revision manifest and the source URI even when the vector is retired.
Metadata drift is the quiet failure. A renamed product, mixed-case locale, or missing timezone can turn a valid filter into an empty result. Validate enums at ingest, quarantine invalid records, and alert on zero-result rates by tenant rather than globally. In one migration review, the source system changed en-GB to en_GB; every query still parsed, so dashboards looked healthy, yet the filtered candidate set fell to zero for that market until the normalizer was updated and old records were reindexed.
Another trap is filter order. Applying a soft “boost current documents” score before enforcing tenant isolation is unsafe. Hard predicates come first; freshness and authority are ranking features only after the candidate set is legal.
Finally, plan for no-match behavior. Return a clear “no eligible source” state and route it to a human workflow or a documented fallback. Do not let the generator fill the gap from an unfiltered corpus. Your mileage may vary with index size and language mix, so re-run the same evaluation set after every chunking or metadata migration.
Decision rule for a fintech support team
Choose pre-filtered ANN plus a lexical fallback when the corpus is large and boundaries are strict. Choose an exact or lexical-first path when each tenant has a small corpus and deterministic behavior matters more than throughput. Keep the interface at the adapter boundary so changing the index engine does not rewrite ticketing or policy code.
This approach is not suitable when your metadata is mostly missing or cannot be trusted; fix the ingestion contract first. It is also a poor fit for exploratory search across intentionally mixed tenants, where hard isolation would hide useful context. In that case, use an explicitly scoped cross-tenant index and label every result with its authority.
References
- https://arxiv.org/abs/2005.11401
- https://www.rfc-editor.org/rfc/rfc3339
- https://www.iso.org/standard/63555.html


