Debug RAG Metadata Filters That Return No Documents
An empty retrieval result may be caused by a type, case, or ingestion mismatch rather than missing content. Compare stored metadata and query filters directly.
Separate retrieval from filtering
A query that returns zero chunks can mean the corpus is empty, the embedding search is failing, or a metadata filter excludes every candidate. Do not start by changing the embedding model. Run the same query without the filter in a controlled test and compare the returned IDs. If results appear only without the filter, the problem is in metadata or filter semantics.
Inspect a few stored records, including field names, value types, casing, and null behavior. A date stored as a string will not always compare like a timestamp. An access group stored as an array may require an “any of” operator rather than exact equality. A field named tenant_id during ingestion but tenantId at query time is a silent exclusion.
Test the filter as data
Log a redacted filter object alongside the corpus and index version. Avoid logging private document content. Verify one known document whose metadata should satisfy the filter and one that should not. This gives the filter a positive and a negative test rather than relying on a large production query.
Check whether the database applies filters before or after approximate nearest-neighbor candidate selection. Some systems can return fewer results when a restrictive filter is pushed into the ANN search. That may require a different search strategy or a larger candidate pool, but changing the pool should be driven by a reproducible test.
Only after types, names, and operator semantics are proven should you adjust index configuration. Keep the access-control filter mandatory and test that removing it is impossible in production code. A filter bug can be a relevance issue, but a missing permission filter can expose data; test both failure directions.
Build a filter debugging fixture
Create a small fixture with one document that must match, one document that must not match, and one document whose metadata is deliberately incomplete. Give each record a stable test ID and keep the content uninteresting. The point is to test the filter contract without exposing real customer text. Run the query with no filter, with the expected filter, and with a deliberately impossible filter. The result counts should make the failure class obvious.
Compare the serialized filter sent to the search backend with the object produced by the application layer. Logging only a high-level “tenant filter enabled” message is not enough to reveal a wrong field name or string-versus-array mismatch. Redact values where necessary, but retain field names, operators, value types, and the index version. Test null and missing fields explicitly because different backends treat them differently.
Treat empty results as an observable contract
An empty result can be a legitimate answer. The application should distinguish “no authorized evidence exists” from “the search service failed” and “the filter removed all candidates.” Return a structured diagnostic internally, even if the user-facing response is intentionally concise. Metrics for zero-result rate by filter version, tenant, and index build can reveal a deployment that changed metadata schema.
When filters are pushed into approximate nearest-neighbor search, a highly selective condition may leave too few candidates for ranking. Test the same filter against a small exact-search fixture and compare candidate counts before reranking. Increasing top_k can help retrieval recall, but it must not bypass authorization or create a large context accidentally. The secure filter remains part of the query at every stage.
Finally, add regression tests for ingestion and query schema together. A renamed field should fail a contract test before it reaches production. Include a migration plan for old records and a backfill check that counts documents with the expected metadata shape. The durable fix is not a larger search window; it is a filter whose meaning remains stable from ingestion through answer generation.
Add a dashboard for zero-result rate and candidate count by filter version, tenant, and index build. A sudden change after an ingestion deploy points to schema drift faster than a generic retrieval-quality alert. Keep the fixture in automated tests and run it against every supported search backend. This protects both relevance and the authorization boundary.
Run the fixture after any metadata migration and compare counts before and after backfill. A search-quality alert should identify whether the issue is missing data, a type conversion, or a backend operator change. Keep authorization failures fail-closed even while relevance diagnostics are enabled.