Retrieval Filtering and Scoring#
Dify's knowledge retrieval pipeline applies quality gates in three distinct phases: pre-retrieval SQL metadata filtering, concurrent vector/keyword search with deferred threshold application, and post-fusion scoring via reranking. The result is a quality-first model where top_k is a ceiling, not a floor.
Pipeline Overview#
Query
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Stage 1 β SQL Metadata Filter (pre-search) β
β get_metadata_filter_condition() β
β β SELECT doc_id WHERE doc_metadata... β
β β metadata_filter_document_ids (allowlist) β
β Short-circuit: 0 docs matched β return [] β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β document_ids_filter
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Stage 2 β Concurrent Search β
β embedding_search() β score_threshold=0.0 β
β full_text_index_search() (hybrid only) β
β keyword_search() β
β β top_k caps candidate count β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β raw candidates
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Stage 3 β Fusion / Reranking + Threshold β
β WeightRerankRunner (weighted score fusion) β
β OR RerankModelRunner (external model) β
β score_threshold applied here β
β top_n = top_k (max, not guaranteed) β
βββββββββββββββββββββββββββββββββββββββββββββββ
Stage 1: Pre-Retrieval Metadata Filtering#
DatasetRetrieval.get_metadata_filter_condition() runs before any vector or keyword search. It evaluates metadata conditions against DatasetDocument.doc_metadata (a JSON column) via SQLAlchemy and returns a dict[str, list[str]] allowlist keyed by dataset ID.
Three modes :
| Mode | Behavior |
|---|---|
disabled | Returns (None, None) immediately β zero overhead |
manual | Static conditions from node config; {{variable}} placeholders are resolved at runtime |
automatic | An LLM extracts filter conditions from the query using a few-shot prompt; defaults to OR logic |
process_metadata_filter_func() translates each Condition into a SQLAlchemy expression against DatasetDocument.doc_metadata. It supports string ops (LIKE/NOT LIKE), equality, numeric comparisons, in/not in, empty/not empty, and time operators (before/after). An empty in [] short-circuits to literal(False) .
Short-circuit: If a metadata filter is active but produces zero matching documents, retrieval returns [] immediately β no vector query is issued .
The document_ids_filter list is threaded through all search methods: embedding_search(), full_text_index_search(), and keyword_search().
Stage 2: Concurrent Search with Deferred Threshold#
RetrievalService._retrieve() fans out to up to three search paths in parallel using a ThreadPoolExecutor .
Critical design: score threshold deferral. For HYBRID_SEARCH, embedding_search forces score_threshold=0.0 at vector retrieval time . The code comment explains the reason directly:
Applying the user score threshold at vector retrieval time uses embedding similarity, which is not comparable to reranked or fused scores and incorrectly drops high-quality chunks.
This ensures that chunks which score poorly on raw embedding similarity but rank highly after fusion are not prematurely discarded.
Stage 3: Fusion, Reranking, and Final Threshold#
After all parallel futures complete, hybrid results are deduplicated via _deduplicate_documents() (highest-score-wins by doc_id, first-seen by content key), then passed to DataPostProcessor .
Two fusion strategies:
WEIGHTED_SCORE β WeightRerankRunner
Computes vector_weight Γ cosine_similarity + keyword_weight Γ TF-IDF_cosine . Vector scores reuse the stored retrieval score if available; otherwise cosine similarity is recomputed from the raw document vector . Score threshold is applied inline: any document below the threshold is dropped before top_n truncation .
RERANKING_MODEL β RerankModelRunner
Deduplicates by doc_id, then calls an external rerank model via invoke_rerank / invoke_multimodal_rerank . Score threshold and top_n are applied after receiving the model's scores .
Fallback threshold: If no rerank runner is active (e.g., single-dataset hybrid with no reranker), a final _filter_documents_by_vector_score_threshold() pass is applied to the fused list .
top_k as a Maximum#
top_k is passed as top_n into every rerank runner and governs the final [:top_n] slice . Because the score threshold filter runs before the truncation, the actual returned count can be anywhere from 0 to top_k. The default is top_k=4 . Callers should never assume len(results) == top_k.
For single-dataset retrieval (calculate_vector_score), the same pattern holds: threshold filtering runs first, then [:top_k] truncation .
Key Files#
| File | Role |
|---|---|
api/core/rag/retrieval/dataset_retrieval.py | Orchestration: get_metadata_filter_condition(), process_metadata_filter_func(), multiple_retrieve(), calculate_vector_score() |
api/core/rag/datasource/retrieval_service.py | RetrievalService: concurrent search, deduplication, threshold deferral, _filter_documents_by_vector_score_threshold() |
api/core/rag/rerank/weight_rerank.py | WeightRerankRunner: weighted TF-IDF + cosine fusion with inline threshold |
api/core/rag/rerank/rerank_model.py | RerankModelRunner: external rerank model invocation with threshold + top_n |
api/core/rag/entities/metadata_entities.py | Condition, MetadataFilteringCondition, SupportedComparisonOperator |