Metadata Filtering#
Metadata filtering is a pre-retrieval step that narrows the document search space to a doc_ids whitelist before vector/BM25 search runs. It is applied across all retrieval entry points — the REST API, Agent Retrieval node, chat completions, and the Dify-compatible endpoint — and resolves through a shared implementation in common/metadata_utils.py .
Metadata Storage#
User-defined metadata is stored as a flexible dictionary (meta_fields) in a per-tenant ES/Infinity index named ragflow_doc_meta_{tenant_id} . Each document record has three fields:
| Field | Type | Purpose |
|---|---|---|
id | keyword | Document ID |
kb_id | keyword | Knowledge base filter |
meta_fields | dynamic object | Arbitrary user metadata |
The index is created lazily on first write and dropped when it becomes empty . On the ES backend, user metadata keys are namespaced under meta_fields.<key> via dynamic object mapping . On the Infinity backend, chunks use a per-KB table but the metadata index remains per-tenant, matching the ES layout .
API validation: meta_fields values must be str, int, float, or a list of those types. Nested dicts are rejected . When LLM extraction produces combined string values (e.g. "关羽、孙权"), DocMetadataService splits them on common delimiters before storage .
Source of truth: ES/Infinity is the sole authoritative store. MySQL meta_fields column has been removed .
Filter Modes#
apply_meta_data_filter() supports three modes :
| Mode | Behavior |
|---|---|
auto | LLM (gen_meta_filter()) generates filter conditions from the query and full metadata vocabulary |
semi_auto | Same as auto but the LLM is restricted to user-specified metadata keys (and optional per-key operator constraints) |
manual | Explicit condition list; supports {{var}} value substitution via a caller-supplied resolver |
In auto and semi_auto modes, if no documents match the generated filter the function returns None (allowing the retrieval to proceed unfiltered). In manual mode, a no-match returns the sentinel ["-999"], which guarantees zero retrieval results rather than an unfiltered fallback .
Operators#
14 filter operators are supported :
| Category | Operators |
|---|---|
| Equality | =, ≠ |
| Range | >, <, ≥, ≤ |
| Membership | in, not in |
| String | contains, not contains, start with, end with |
| Existence | empty, not empty |
String comparisons are case-insensitive. Date comparisons require strict YYYY-MM-DD format on both sides; mismatched types skip the record rather than raising . The logic key ("and" / "or") controls how multiple conditions are combined, defaulting to "and" .
The Dify integration normalizes operator names before evaluation — "is" → "=", "not is" → "≠", ">=" → "≥", etc. — via convert_conditions() .
Push-Down vs. In-Memory Evaluation#
Filtering has two execution paths :
- ES push-down (
_filter_doc_ids_by_metadata_es()) — translates conditions into an ES bool query viacommon/metadata_es_filter.py. Operators≠andnot inare routed to in-memory fallback for multi-valued fields because ESmust_not termhas different semantics than the per-bucket Python evaluation . - Infinity push-down (
_filter_doc_ids_by_metadata_infinity()) — builds a SQLWHEREclause viacommon/metadata_infinity_filter.py. - In-memory fallback (
meta_filter()) — loads all metadata for the KB set into Python viaget_flatted_meta_by_kbs()and evaluates conditions in a{field: {value: [doc_ids]}}structure . This path is used whenkb_idsis not supplied, the ES client is unavailable, or push-down returnsNone. Manual-modemanualwithkb_idsskips the expensiveget_flatted_meta_by_kbs()round-trip entirely .
If push-down returns more matches than the limit cap (default 10,000), it falls back to in-memory to avoid silent truncation .
Common Use Cases#
Location-based retrieval: Store a region or location field as meta_fields on each document. Use a manual filter with {"key": "region", "op": "=", "value": "{{user_region}}"} in the Agent Retrieval node; the manual_value_resolver substitutes the runtime value before evaluation. The resulting doc_ids whitelist is intersected with the semantic search results.
Semi-auto categorization: Configure semi_auto with ["category", "date"] in the retrieval node. The LLM generates filter conditions from those keys based on the user's query, narrowing the doc pool before vector search without hard-coding conditions.
Key Source Files#
| File | Purpose |
|---|---|
common/metadata_utils.py | apply_meta_data_filter(), meta_filter(), convert_conditions() |
common/metadata_es_filter.py | ES DSL translator; MetaFilterTranslator, build_meta_filter_query(), is_pushdown_supported() |
api/db/services/doc_metadata_service.py | DocMetadataService — CRUD, push-down routing, filter_doc_ids_by_meta_pushdown(), get_flatted_meta_by_kbs() |
agent/tools/retrieval.py | Agent Retrieval node — calls apply_meta_data_filter() with manual_value_resolver |
api/utils/validation_utils.py | validate_document_meta_fields() — validates meta_fields type constraints at API boundary |