利用ai_extract和ai_prep_search为Databricks RAG实现元数据过滤。
将ai_parse_document、ai_extract v2.1和ai_prep_search整合在一起,使每个数据块都包含结构化元数据,从而让向量搜索在按相似度排序之前进行过滤。
来源: Adding ai_extract to the mix: building a unified RAG pipeline with three Databricks SQL AI functions
基于 SQL AI 函数 ai_prep_search 构建的 Databricks 流水线已经能完成许多工作。输入的是解析后的文档内容,输出的是经过语义和上下文增强的数据块,这些数据块可直接用于向量搜索。整个流程十分清晰:解析文档、准备数据块、将其整理为 Delta 表并建立索引。
然而,当该流程在文档语料库上运行时——此处为一系列Orion Robotics手册——仍存在不足。这些分块本身质量不错。chunk_to_embed字段包含了恰当的文档上下文,chunk_to_retrieve也足够干净,可直接传递给大语言模型。问题出现在查询阶段,因为没有可靠的过滤方法。像“帮我找到Orion Motion Controller的固件指南”这样的请求只能依靠语义相似度来满足,因为该索引没有document_type、product_name或target_audience这类结构化字段。
在基于智能体的检索方式中,这确实是一个缺陷。纯语义搜索在演示环境中表现尚可,但在实际生产环境中,智能体必须在执行向量相似度计算之前缩小搜索范围,而元数据过滤器正是承担这一功能的工具。解决方法是将在流程中加入ai_extract v2.1。
为何元数据过滤器如此重要
RAG中的混合搜索通常指的是向量搜索与BM25之类的关键词匹配技术的结合。虽然有用,但对于结构化文档集合而言,另一种混合搜索方式更为重要:即向量搜索与元数据过滤的结合。
想象这样一个智能体:它已经知道用户需要《猎户座臂》的手册。对各类文档中的10,000个片段进行排序显然是浪费资源。更好的方法是先筛选出相关文档,再利用向量相似度在这批较少的文档中找出最佳匹配项。Databricks Vector Search通过在创建索引时指定过滤列来实现这一功能。
问题在于元数据必须干净、一致且类型正确。如果某行的document_type值为“用户手册”,另一行为“manual”,而第三行则没有该字段,过滤功能就会失效。这样一来检索质量会很差,人们会花费数小时去调试问题,而真正的根源其实是早先创建的不一致元数据。
ai_extract v2.1 可解决这一问题。它接收由 ai_parse_document 生成的结构化 VARIANT,并依据模式验证后返回带类型的元数据。枚举字段用于限定固定值,字符串字段负责提取自由文本,而数组则用于处理多值字段。由于提取过程遵循既定模式,因此即使不同文档的布局各异,其输出结果仍保持一致。
三个函数,三种功能
这三个函数的功能彼此不重叠,因此在查看代码笔记本之前将它们分开会更有帮助。
- ai_parse_document v2.0 能将原始二进制文件(PDF、DOCX、图片等)转换为结构化 VARIANT,识别出文本块、标题、表格、图表、边界框以及文档的层级结构。它仅能从结构层面了解文档的内容,却无法知晓文档的具体主题。
每个函数都有其特定的职责,整个处理流程就是这些功能的组合。
笔记本中的逐步操作
该笔记本基于Orion Robotics语料库运行,该语料库存储在Unity Catalog卷中,包含PDF、DOCX文件以及图片。首先进行解析:
SELECT
path,
ai_parse_document(
content,
MAP('version', '2.0')
) AS parsed
FROM READ_FILES(
'/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/',
format => 'binaryFile'
)
卷中的每个文件都会以二进制形式被读取并转换为结构化的VARIANT类型。明确选择2.0版本是明智的,因为该架构包含了ai_prep_search在决定数据分块时所依赖的边界框坐标与图形描述。若使用默认版本,Databricks可能会在流程背后改变其行为。
示例运行产生了以下结果:
下一个查询会从解析后的输出中提取元数据:
WITH parsed_documents AS (
SELECT path, ai_parse_document(content, MAP('version', '2.0')) AS parsed
FROM READ_FILES('/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/', format => 'binaryFile')
)
SELECT
path,
ai_extract(
parsed,
'{
"document_title": {"type": "string", "description": "Main title or heading of the document"},
"document_type": {"type": "enum", "labels": ["manual", "datasheet", "guide", "report", "specification", "policy", "other"]},
"product_name": {"type": "string", "description": "Primary product or system referenced"},
"version": {"type": "string", "description": "Document or product version if mentioned"},
"key_topics": {"type": "array", "items": {"type": "string"}},
"target_audience": {"type": "enum", "labels": ["engineer", "operator", "manager", "customer", "general"]}
}',
MAP(
'version', '2.1',
'instructions', 'These are technical documents from Orion Robotics. Extract metadata that would help users filter and find relevant documents.'
)
) AS metadata
FROM parsed_documents
有些选项需要解释。由于过滤器需要一组固定的值,document_type和target_audience都是枚举类型。如果以自由文本形式输入,它们可能会返回“manual”、“User Manual”或“technical manual”之类的内容,从而导致匹配失败。instructions选项用于提供背景信息,在此情况下即为这些文件属于Orion Robotics的技术文档,这样有助于提升提取效果。此外,ai_extract会直接接受ai_parse_document输出的VARIANT格式数据,而不会进行任何纯文本转换。
示例运行后的输出:
接下来是分块处理:
WITH parsed_documents AS ( ... )
SELECT
path,
ai_prep_search(parsed) AS chunked
FROM parsed_documents
ai_prep_search 使用相同格式解析后的VARIANT数据。文档会被分割成多个块并存储在document.contents数组中,每个块都包含chunk_id、chunk_position、chunk_to_retrieve和chunk_to_embed这些字段。后两者所对应的文本并非完全相同。
chunk_to_embed包含文档标题、章节标题及页面详情等额外上下文信息,以便嵌入模型确定该文本应放置的位置。chunk_to_retrieve则是经过整理的纯文本,通常为展示给用户的版本或传递给大语言模型的内容。
这种分离设计十分合理。虽然许多RAG流程都是从同一文本进行嵌入和回答,但富含上下文的文本更适合用于嵌入模型,而纯文本则更适合作为阅读内容,因此保留两者会更好。
示例输出:
将所有部分整合在一起
综合查询语句就是各部分交汇的地方:
WITH parsed_documents AS (
SELECT path, ai_parse_document(content, MAP('version', '2.0')) AS parsed
FROM READ_FILES('/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/', format => 'binaryFile')
),
doc_metadata AS (
SELECT
path,
parsed,
ai_extract(
parsed,
'{ ... }',
MAP('version', '2.1', 'instructions', 'These are technical documents from Orion Robotics. Extract metadata for search filtering.')
) AS metadata
FROM parsed_documents
),
doc_chunks AS (
SELECT path, ai_prep_search(parsed) AS chunked
FROM doc_metadata
)
SELECT
chunk.value:chunk_id::STRING AS chunk_id,
chunk.value:chunk_position::INT AS chunk_position,
chunk.value:chunk_to_retrieve::STRING AS chunk_to_retrieve,
chunk.value:chunk_to_embed::STRING AS chunk_to_embed,
doc_chunks.chunked:document.source_uri::STRING AS source_uri,
dm.metadata:response:document_title::STRING AS document_title,
dm.metadata:response:document_type::STRING AS document_type,
dm.metadata:response:product_name::STRING AS product_name,
dm.metadata:response:version::STRING AS doc_version,
dm.metadata:response:target_audience::STRING AS target_audience
FROM
doc_chunks
JOIN doc_metadata dm ON doc_chunks.path = dm.path,
LATERAL variant_explode(doc_chunks.chunked:document.contents) AS chunk
LATERAL variant_explode 步骤会将嵌套的块数组展开为多行。通过路径进行的 JOIN 操作会将文档级别的元数据附加到该文档的每个块上。最终结果为每个块对应一行,其中既包含检索字段也包含可过滤的元数据列。这就是存储在 Delta 表中的结构,也会被用于为向量搜索索引提供数据。
示例运行结果:
表格的呈现形式如下:
CREATE OR REPLACE TABLE <YOUR_CATALOG>.<YOUR_SCHEMA>.orion_docs_rag_chunks AS
-- [full CTE chain from Step 4]
设置好表格后,即可使用以下参数创建 Mosaic AI 向量搜索 Delta Sync 索引:
- 嵌入源列:chunk_to_embed
- 主键:chunk_id
- 可过滤字段:doc_version 以及 document_type、product_name 和 target_audience
chunk_id在每次运行时都保持不变,由文档 ID 和该块在整体中的位置组成。正因如此,对 Delta 表进行增量 MERGE 更新时只需保留未被修改的块,而无需重新索引,这对于定期更新语料库而言能节省大量时间。
查询时的优势
其好处体现在用户提出查询时,而非加载数据时。假设请求为“查找 Orion Motion Controller 的固件指南”,此时检索可通过两步完成:
- 确定匹配的元数据筛选条件,例如 document_type = “guide” 和 product_name = “Orion Motion Controller”。
- 在通过这些筛选条件的文档中执行查询和搜索。
由于缺乏 ai_extract,智能体只能依赖语义相似度。对于少量相似文档来说这或许可行,但当涉及成千上万个涵盖多种产品、类型和受众群体的文件时,这种方法就不可靠了。基于枚举的元数据机制可以解决这个问题:智能体无需在手册和指南之间猜测,只需通过 document_type = “manual”这一条件即可精确筛选出目标文档。
局限性及成本
ai_prep_search 无法控制分块方式。分块大小、重叠度以及算法均不可设置。对于许多文档 RAG 应用而言,这已是可接受的,因为加入上下文的语义分块通常比简单的固定大小分割更有效。若需要父子分块或精细的重叠控制,则仍需自定义逻辑。
ai_extract 会增加成本与延迟,因为每份文档都需要多一次大语言模型调用。Delta Sync 可以跳过未发生变化的文档,但首次完整导入仍可能耗费较多资源,因此在处理整个语料库之前先进行样本测试是明智之举。
当 ai_parse_document 的 VARIANT 输出会被传递给 ai_prep_search 或 ai_extract 时,应使用其 2.0 版本。该版本可提供边界框、元素类型以及图表描述。原始文本虽可直接传递给 ai_extract,但会失去解析带来的结构化优势。
设计总结
加入ai_extract v2.1虽然只需少量额外代码,但却显著改变了架构,因为现在每个数据块都包含结构清晰、类型明确且可过滤的元数据,混合检索方式也变得更加可靠。整个处理流程仍分为三个明确的阶段:
- ai_parse_document用于获取文档结构。
- ai_extract用于提取有用的元数据。
- ai_prep_search用于构建语义化数据块。
这种分离方式在处理来自不同产品、版本、受众群体的各类文档时都能发挥优势。在没有元数据过滤的情况下,检索主要依赖相似度搜索;而有了元数据过滤后,系统会先进行筛选,再进行排序。
这种方法并非适用于所有情况。分块级元数据、父子节点检索或自定义分块仍需编写专用代码。不过,对于许多 Databricks 文档 RAG 流水线而言,这仍是一个更简洁明了的起点。而且分块仍需经过测试和评估,绝不能盲目信任。