LlamaIndex 系列【23】检索增强策略:元数据过滤(Metadata Filtering)

发布时间:2026/9/11 18:08:12

LlamaIndex 系列【23】检索增强策略:元数据过滤(Metadata Filtering) 文章目录1. 前言2. 核心概念2.1 元数据2.2 元数据过滤2.3 元数据过滤 vs 相似度排序3. 案例演示3.1 加载文档3.2 默认元数据3.3 添加元数据3.4 元数据过滤器3.5 混合检索 元数据过滤3.6 执行检索 结果分析3.7 完整代码1. 前言在之前我们学习了核心检索技术Keyword Search关键词检索 /BM25根据关键词出现频次、精确匹配度打分排序。Semantic Search语义检索 / 稠密向量检索根据文本语义含义打分排序匹配方式更加灵活不依赖字面相同。Hybrid Search混合检索组合上面多种检索方式把多路召回的文档合并、重新排序输出一份统一的结果列表。接下来我们继续学习元数据过滤Metadata Filtering。2. 核心概念2.1 元数据元数据是挂在文档或节点上的键值对描述它是什么、来自哪、属于谁等。常见的有标题、作者、创建日期、访问权限等。在LlamaIndex 系列【9】RAG 核心对象Document文档中已经详细介绍过文档元数据。2.2 元数据过滤元数据过滤Metadata Filtering是在检索时先用节点上附加的结构化属性做硬性筛选只保留符合条件的那部分节点然后再在剩下的节点里做向量/BM25打分排序。工作流程元数据过滤本身不执行检索打分它是基于用户属性而非提问文本内容对文档做范围裁剪、过滤筛选是RAG检索体系里非常常用的前置过滤手段最常见是权限和范围控制防止LLM看到不该看的内容租户隔离只检索tenant_id A 公司的文档时间范围只检索publish_date 2024-01-01文档类型只检索doc_type 合同语言、版本、是否已审核等2.3 元数据过滤 vs 相似度排序这是两种完全不同的机制别混淆相似度打分BM25/ 向量给每个节点算一个分按分数软排序谁相关谁靠前是排名。元数据过滤用一个布尔条件直接决定要不要这个节点是筛除和相似度无关。典型流程是先过滤缩小候选集→ 再打分在候选集里排序。比如只检索category 部署的节点那售后政策、价格这些节点即使文本再相似也直接被排除。3. 案例演示目标在混合检索BM25稀疏 向量稠密基础上叠加元数据过滤先按结构化条件硬性筛除不符合的节点再在剩余节点里按相似度排序实现「先过滤、后排序」的召回控制。在权限隔离、时间范围、文档类型过滤等场景这是标准做法。3.1 加载文档这里的Document是文档的原始载体还没切块后面会进一步切成检索单元Node。fromllama_index.coreimportSimpleDirectoryReader documentsSimpleDirectoryReader(./data).load_data()print(f读取文档数:{len(documents)})3.2 默认元数据SimpleDirectoryReader读文件时会自动写入一批默认元数据无需手动指定可直接用来做来源、时间、类型过滤。default_metadocuments[0].metadatafork,vindefault_meta.items():print(f{k}{v})实际输出file_path D:\...\data\公司介绍.md # 文件绝对路径 file_name 公司介绍.md # 文件名 file_type text/markdown # 文件类型 file_size 757 # 文件大小字节 creation_date 2026-08-28 # 创建日期 last_modified_date 2026-08-28 # 最后修改日期注意这些默认字段只有文件层面的信息不含业务语义比如这份文档属于什么类别、来自哪个系统、优先级多少。所以下一步要手动补充。3.3 添加元数据业务上通常需要category类别、source来源、priority优先级等自定义字段。关键做法是在分块之前把元数据写到Document.metadata这样SentenceSplitter切出来的每个Node会自动继承这些元数据无需逐个节点设置。fromllama_index.core.node_parserimportSentenceSplitter category_map{公司介绍.md:公司,售后政策与常见问题.md:售后,星云知识库产品说明.md:产品,}priority_map{公司:1,售后:2,产品:3}fordocindocuments:file_namedoc.metadata.get(file_name,)doc.metadata[category]category_map.get(file_name,其他)doc.metadata[source]官网doc.metadata[priority]priority_map.get(doc.metadata[category],0)nodesSentenceSplitter(chunk_size512).get_nodes_from_documents(documents)分块后每个节点的metadata都带上了category / source / priority这是过滤的基础。3.4 元数据过滤器过滤条件用MetadataFilters表达它由「一组MetadataFilter 一个FilterCondition」组成MetadataFilter(key, value, operator)对某个元数据字段做比较默认FilterCondition.AND / OR / NOT多条条件之间的逻辑关系operator支持EQ()、NE(!)、GT、GTE、LT、LTE、IN、NIN、CONTAINS、TEXT_MATCH、IS_EMPTY等。fromllama_index.core.vector_stores.typesimport(MetadataFilters,MetadataFilter,FilterCondition,FilterOperator,)# 单条件category 产品filter_productMetadataFilters(filters[MetadataFilter(keycategory,value产品)],conditionFilterCondition.AND,)# 数值比较priority 2filter_high_priorityMetadataFilters(filters[MetadataFilter(keypriority,value2,operatorFilterOperator.GTE)],conditionFilterCondition.AND,)# 列表category IN {售后, 产品}filter_in_categoriesMetadataFilters(filters[MetadataFilter(keycategory,value[售后,产品],operatorFilterOperator.IN),],conditionFilterCondition.AND,)3.5 混合检索 元数据过滤BM25和向量两路检索器都通过各自的filters参数接收过滤器先各自过滤、再交给QueryFusionRetriever做结果融合。向量那路用as_retriever(filters...)BM25那路用from_defaults(filters...)。defbuild_fusion_retriever(filtersNone):bm25ChineseBM25Retriever.from_defaults(nodesnodes,similarity_top_k3,filtersfilters)vectorvector_index.as_retriever(similarity_top_k3,filtersfilters)returnQueryFusionRetriever(retrievers[vector,bm25],llmllm,num_queries1,# 不做 LLM 查询扩展仅结果融合modereciprocal_rerank,# RRF 融合similarity_top_k3,use_asyncFalse,)fusion_productbuild_fusion_retriever(filtersfilter_product)实现细节重要新版BM25的filters只把不匹配节点的分数置0通过corpus_weight_mask但仍会返回这些节点和向量那路的真剔除不一致。因此要在BM25检索器的_retrieve里补一句「剔除0分结果」否则融合结果尾部会残留被过滤的节点。def_retrieve(self,query_bundle):query_bundle.query_strzh_seg(query_bundle.query_str)resultssuper()._retrieve(query_bundle)ifself.corpus_weight_maskisnotNone:# 有过滤器时剔除被压成 0 分的节点results[rforrinresultsifr.scoreandr.score0]returnresults3.6 执行检索 结果分析说明用同一个查询词对比「不过滤」和「各种过滤」的召回差异验证过滤是否生效。query星云defshow(title,retriever):print(f---{title}---)fori,iteminenumerate(retriever.retrieve(query),1):catitem.node.metadata.get(category)print(f [{i}] 融合分{item.score:.4f}category{cat})show(不过滤,fusion_no_filter)show(过滤 category产品,fusion_product)show(过滤 priority2,fusion_high_priority)show(过滤 category IN [售后,产品],fusion_in_categories)结果不过滤 → 产品 / 公司 / 售后3 个 过滤 category产品 → 产品1 个 过滤 priority2 → 产品 / 售后2 个 过滤 category IN [售后,产品] → 产品 / 售后2 个结果分析查询词「星云」在三份文档正文里都出现所以不过滤时三份都会命中加category产品后其余两份被硬性剔除只剩1条——过滤生效。priority2时「售后」仍被召回但融合分更低。原因是「售后」这份文档语义上和「星云」相关同属星云科技但正文里未必包含「星云」这个关键词——于是向量那路能命中它而BM25关键词那路命中不了。这正体现了混合检索关键词 语义互补的价值。融合分是RRF分数对每个节点在各路检索器的排名rank上累加1/(rank 60)所以同时被两路命中的节点分数更高、排序更靠前。3.7 完整代码 3. 案例演示元数据过滤 在混合检索BM25 稀疏 向量稠密的基础上叠加「元数据过滤」 先按结构化条件category / source / priority 等硬性筛除不符合条件的节点 再在剩余节点里做相似度打分排序实现先过滤、后排序的召回控制。 流程 3.1 准备数据 —— 读取本地文档并分块 3.2 默认元数据 —— 观察 SimpleDirectoryReader 自动写入的元数据 3.3 添加元数据 —— 给文档/节点补充业务元数据 3.4 元数据过滤器 —— 构造 MetadataFilters 3.5 混合检索 元数据过滤 —— BM25/向量两路都带上 filters 后融合 3.6 执行检索 结果分析 —— 对比不过滤与过滤后的召回差异 importcopyimportosimportjiebafromdotenvimportload_dotenvfromllama_index.coreimportSettings,SimpleDirectoryReader,VectorStoreIndexfromllama_index.core.node_parserimportSentenceSplitterfromllama_index.core.retrieversimportQueryFusionRetrieverfromllama_index.core.vector_stores.typesimport(FilterCondition,FilterOperator,MetadataFilter,MetadataFilters,)fromllama_index.core.vector_stores.utilsimportnode_to_metadata_dictfromllama_index.embeddings.openai_likeimportOpenAILikeEmbeddingfromllama_index.llms.openai_likeimportOpenAILikefromllama_index.retrievers.bm25importBM25Retriever# 通用配置 load_dotenv()api_keyos.environ[DASHSCOPE_API_KEY]API_BASEhttps://ws-jfb8j8mx0n7e2k6a.cn-beijing.maas.aliyuncs.com/compatible-mode/v1embed_modelOpenAILikeEmbedding(model_nametext-embedding-v3,api_keyapi_key,api_baseAPI_BASE,)llmOpenAILike(modelqwen3.8-max,api_keyapi_key,api_baseAPI_BASE,is_chat_modelTrue,)Settings.llmllm# 中文 BM25 检索器 defzh_seg(text:str)-str:return .join(jieba.cut(text))classChineseBM25Retriever(BM25Retriever):支持中文分词的 BM25 检索器同时支持 metadata filters。classmethoddeffrom_defaults(cls,nodes,similarity_top_k2,**kwargs):original_nodesnodes segmented_nodes[copy.deepcopy(n)forninnodes]forninsegmented_nodes:n.set_content(zh_seg(n.get_content()))retrieversuper().from_defaults(nodessegmented_nodes,similarity_top_ksimilarity_top_k,token_patternr(?u)\b\w\b,skip_stemmingTrue,**kwargs,)retriever.corpus[node_to_metadata_dict(n)|{node_id:n.node_id}forninoriginal_nodes]returnretrieverdef_retrieve(self,query_bundle):query_bundle.query_strzh_seg(query_bundle.query_str)resultssuper()._retrieve(query_bundle)# BM25 的 filters 只把不匹配节点的分数置 0weight_mask但仍会返回这些节点# 这里剔除 0 分结果使元数据过滤成为硬性筛除而非压分。ifself.corpus_weight_maskisnotNone:results[rforrinresultsifr.scoreandr.score0]returnresults# 3.1 准备数据 documentsSimpleDirectoryReader(./data).load_data()print(f3.1 读取文档数:{len(documents)})fordocindocuments:print(f -{doc.metadata.get(file_name)}长度{len(doc.text)}字)# 3.2 默认元数据 print(\n3.2 SimpleDirectoryReader 自动写入的默认元数据以第一个文档为例:)default_metadocuments[0].metadatafork,vindefault_meta.items():print(f{k}{v})# 3.3 添加元数据 # 按文件名给每个文档补充业务元数据类别 / 来源 / 优先级category_map{公司介绍.md:公司,售后政策与常见问题.md:售后,星云知识库产品说明.md:产品,}priority_map{公司:1,售后:2,产品:3}fordocindocuments:file_namedoc.metadata.get(file_name,)doc.metadata[category]category_map.get(file_name,其他)doc.metadata[source]官网doc.metadata[priority]priority_map.get(doc.metadata[category],0)# 分块节点会继承所属文档的 metadatasplitterSentenceSplitter(chunk_size512)nodessplitter.get_nodes_from_documents(documents)print(\n3.3 补充元数据并分块后各节点的 category / priority:)fornodeinnodes:print(f category{node.metadata.get(category):4}fpriority{node.metadata.get(priority)}f{node.text[:20]!r})# 3.4 元数据过滤器 # 例 1单条件 —— 只要 category 产品filter_productMetadataFilters(filters[MetadataFilter(keycategory,value产品)],conditionFilterCondition.AND,)# 例 2数值比较 —— priority 2filter_high_priorityMetadataFilters(filters[MetadataFilter(keypriority,value2,operatorFilterOperator.GTE),],conditionFilterCondition.AND,)# 例 3IN 列表 —— category 属于 {售后, 产品}filter_in_categoriesMetadataFilters(filters[MetadataFilter(keycategory,value[售后,产品],operatorFilterOperator.IN),],conditionFilterCondition.AND,)print(\n3.4 已构造 3 组过滤器)print( filter_product : category 产品)print( filter_high_priority : priority 2)print( filter_in_categories : category IN [售后, 产品])# 3.5 混合检索 元数据过滤 # 向量索引只建一次两路检索器各自携带 filters 后交给 QueryFusionRetriever 融合vector_indexVectorStoreIndex(nodesnodes,embed_modelembed_model)defbuild_fusion_retriever(filtersNone):构造BM25 向量的融合检索器可选传入 metadata filters。bm25ChineseBM25Retriever.from_defaults(nodesnodes,similarity_top_k3,filtersfilters)vectorvector_index.as_retriever(similarity_top_k3,filtersfilters)returnQueryFusionRetriever(retrievers[vector,bm25],llmllm,num_queries1,# 不做 LLM 查询扩展modereciprocal_rerank,# RRF 融合similarity_top_k3,use_asyncFalse,)fusion_no_filterbuild_fusion_retriever(filtersNone)fusion_productbuild_fusion_retriever(filtersfilter_product)fusion_high_prioritybuild_fusion_retriever(filtersfilter_high_priority)fusion_in_categoriesbuild_fusion_retriever(filtersfilter_in_categories)# 3.6 执行检索 结果分析 query星云defshow(title,retriever):print(f\n---{title}---)resultsretriever.retrieve(query)ifnotresults:print( 无结果)returnfori,iteminenumerate(results,1):catitem.node.metadata.get(category)priitem.node.metadata.get(priority)print(f [{i}] 融合分{item.score:.4f}category{cat}fpriority{pri}{item.node.text[:24]!r})print(f\n3.6 查询词:{query!r}三份文档正文都含「星云」)show(不过滤应返回全部 3 个节点,fusion_no_filter)show(过滤 category产品应只剩 1 个节点,fusion_product)show(过滤 priority2应剩 售后产品 2 个节点,fusion_high_priority)show(过滤 category IN [售后,产品],fusion_in_categories)
延伸阅读

更多相关文章

2026/9/11 18:08:12

GB 46859 儿童手表跨品牌加好友:BLE 抓包协议逐帧分析

孩子手里的小米手表,怎么和同桌的华为手表加上好友?放在三年前,答案基本是"不能"。小天才封闭社交、各家碰一碰只认自家。直到 2025-12-02,GB 46859-2025《儿童手表安全技术要求》正式发布,2027-01-01 强制实…

2026/9/11 18:03:11

与技术人员以及技术人员对外的沟通指南

与技术人员以及技术人员对外的沟通指南 一、写在前面 技术人员往往性格直白、表达直接,习惯用技术可行性和自身能力边界来判断问题。这不是缺点,而是职业训练带来的思维习惯。 沟通卡在「看起来很简单,对方却觉得做不了」或「技术上对了&…

2026/9/11 18:03:11

MATLAB中CEC2005基准函数的正确实现与集成方法

简介:本资源是CEC2005多峰优化测试函数的MATLAB完整实现包,面向智能优化算法研究者、进化计算方向研究生及算法工程师,用于系统评估与对比各类全局优化算法(如粒子群、差分进化、遗传算法等)在复杂多模态问题上的收敛性…

2026/9/11 19:53:27

AI 写的代码出 bug 算谁的?——生成日志、责任链与网关熔断

AI 写的代码出 bug 算谁的?——生成日志、责任链与网关熔断 一个所有团队都在回避的问题 你的 AI 写了一行有 bug 的代码,导致生产事故。谁负责?写提示词的工程师?生成代码的模型?合并它的 reviewer?绝大多…

2026/9/11 19:53:27

网络赌博治安治理难点解析

一部手机、一条私信、一个小程序,就可能把普通人拖进网络赌博的泥潭。2026年,网络赌博跨境化、伪装化、私域化特征愈发突出,呈现出 “反复性强、治后反弹” 的局面。从网安行业技术观测视角来看,当下网络赌博之所以难以有效治理&a…

2026/9/11 19:53:27

STM32项目开源:图书馆环境监测系统(代码+原理图+仿真)

文章目录一、前言二、项目功能简介三、PCB设计四、原理图设计五、程序设计六、包含内容七、资料分享一、前言 实物项目成品: 仿真项目成品: 哔哩哔哩开源视频链接(复制浏览器打开): https://www.bilibili.com/vid…

2026/9/11 19:53:27

网络毕设项目|网络毕设|车载通信 V2X 的传播模型和拥塞控制性能分析

第一章 绪论1.1 研究背景与意义城市化的不断推进让机动车的保有量有了持续的上涨,交通拥堵、事故频发还有环境被污染的问题也变得越来越突出,全球每年因为道路交通事故失去生命的人数能达到 135 万左右,相关的经济损失被带到了数千亿美元的规…

2026/9/10 16:39:38

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/10 11:16:38

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 12:32:02

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/10 15:19:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/10 15:49:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码