Chunk Metadata Extraction#
RAGFlow's chunk metadata extraction pipeline enriches each parsed document chunk with LLM-generated signals — structured metadata fields, keywords, synthetic questions, and content tags — then aggregates those signals into a single document-level record stored in a per-tenant Elasticsearch or Infinity index. MySQL is not involved; ES/Infinity is the sole source of truth for document metadata .
Pipeline Overview#
Post-chunking enrichment runs across four async functions, all defined in chunk_post_processor.py. They are invoked on the list of chunk dicts produced by the parser:
| Function | Output fields on chunk dict | Triggered by |
|---|---|---|
extract_keywords | important_kwd, important_tks | parser_config.auto_keywords > 0 |
generate_questions | question_kwd, question_tks | parser_config.auto_questions > 0 |
generate_metadata | (merged into ES doc-meta index) | parser_config.metadata or built_in_metadata present |
apply_tags | tag_feas (TAG_FLD) | kb_parser_config.tag_kb_ids present |
Each function calls LLMBundle with the tenant's configured chat model , dispatches one async task per chunk, and await asyncio.gather()s them all. A chat_limiter semaphore bounds concurrent LLM calls.
The same logic also exists in the original task_executor.py; the chunk_post_processor.py version is the refactored extraction of those handlers.
Metadata Config Construction#
Before dispatching per-chunk tasks, build_metadata_config merges two sources from parser_config:
metadata— user-defined schema, either as a JSON schema dict or a list of{key, description, enum}items.built_in_metadata— system-defined fields (e.g.,update_time,file_name) injected fromapply_built_in_metadata.
The merged config is passed through turn2jsonschema to normalize it into a JSON schema dict before the LLM prompt is constructed .
LLM Cache (Redis)#
Every LLM call is gated by a Redis cache keyed on xxhash64(llm_name + content + task_type + config) with a 24-hour TTL . Cache hits skip the LLM entirely:
cached = get_llm_cache(chat_mdl.llm_name, chunk["content_with_weight"], "metadata", metadata_conf)
if not cached:
cached = await gen_metadata(chat_mdl, schema, content)
set_llm_cache(...)
The same get_llm_cache / set_llm_cache helpers from rag/graphrag/utils.py are shared by keyword extraction, question generation, and tagging. Cache hits for tags also use get_tags_from_cache / set_tags_to_cache to avoid re-fetching the full tag list from the KB.
Per-Chunk → Document Aggregation#
After all chunk tasks complete, generate_metadata reduces chunk-level metadata_obj dicts into a single document dict via update_metadata_to :
- Iterates chunks, calling
update_metadata_to(accumulator, chunk["metadata_obj"]). update_metadata_tomerges list values (deduplicating viadedupe_list) and overwrites scalar values.- Fetches any existing document metadata via
DocMetadataService.get_document_metadata(doc_id)and merges again, so re-runs append rather than overwrite. - Writes the final dict via
DocMetadataService.update_document_metadata(doc_id, metadata).
Before writing, DocMetadataService post-processes list values by splitting on common delimiters (、,,;;|) to fix LLM outputs that combine multi-valued fields into a single string .
DocMetadataService and the ragflow_doc_meta_{tenant_id} Index#
DocMetadataService manages a per-tenant index named ragflow_doc_meta_{tenant_id} . Each document is a record with three fields:
| Field | Type | Purpose |
|---|---|---|
id | keyword | Document ID |
kb_id | keyword | KB filter |
meta_fields | dynamic object | Arbitrary user metadata |
The index is created on first write (create_doc_meta_idx) and dropped when it becomes empty . Key operations:
update_document_metadata— ES backend uses a scriptedreplace_meta_fieldsfor full overwrite semantics (avoids deep-merge stale key leakage); Infinity backend falls back to delete+insert .filter_doc_ids_by_meta_pushdown— pushes metadata filter conditions directly into ES DSL or Infinity SQL, returning matching doc IDs without loading all metadata into Python .get_flatted_meta_by_kbs— paginates through all doc-meta records for a set of KBs and returns{field: {value: [doc_ids]}}for UI faceting .
Index Architecture#
Two separate ES/Infinity index types exist per tenant :
| Index name | Contents |
|---|---|
ragflow_{tenant_id} | All chunk vectors and text fields (shared by all KBs under a tenant) |
ragflow_doc_meta_{tenant_id} | Per-document metadata only (this pipeline's output) |
On the Infinity backend, chunks use a per-KB table (ragflow_{tenant_id}_{kb_id}), but the metadata index remains per-tenant, matching the ES layout.
Key Source Files#
| File | Role |
|---|---|
rag/svr/task_executor_refactor/chunk_post_processor.py | Pipeline entry points: extract_keywords, generate_questions, generate_metadata, apply_tags, build_metadata_config, apply_built_in_metadata |
api/db/services/doc_metadata_service.py | DocMetadataService: CRUD for ragflow_doc_meta_{tenant_id}; push-down filter; flattened metadata for UI |
common/metadata_utils.py | turn2jsonschema, update_metadata_to, dedupe_list |
rag/graphrag/utils.py | get_llm_cache, set_llm_cache (Redis, xxhash64, 24h TTL) |
rag/prompts/generator.py | gen_metadata, keyword_extraction, question_proposal, content_tagging — LLM prompt functions |
rag/svr/task_executor.py | Original (non-refactored) implementation of the same per-chunk enrichment logic |