首页 / 文章

利用ai_extract和ai_prep_search为Databricks RAG实现元数据过滤。

将ai_parse_document、ai_extract v2.1和ai_prep_search整合在一起,使每个数据块都包含结构化元数据,从而让向量搜索在按相似度排序之前进行过滤。

发布者:REACTAPP.TOP阅读约 7 分钟1713 词

来源: 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,并依据模式验证后返回带类型的元数据。枚举字段用于限定固定值,字符串字段负责提取自由文本,而数组则用于处理多值字段。由于提取过程遵循既定模式,因此即使不同文档的布局各异,其输出结果仍保持一致。

三个函数,三种功能

这三个函数的功能彼此不重叠,因此在查看代码笔记本之前将它们分开会更有帮助。

  1. ai_parse_document v2.0 能将原始二进制文件(PDF、DOCX、图片等)转换为结构化 VARIANT,识别出文本块、标题、表格、图表、边界框以及文档的层级结构。它仅能从结构层面了解文档的内容,却无法知晓文档的具体主题。
  • ai_extract v2.1会获取已解析的VARIANT数据,然后根据用户提供的架构提取文档级别的元数据。它之所以能理解内容主题,只是因为架构指明了需要查找的信息。其输出是一个新的VARIANT,其中包含诸如document_title、document_type、product_name和target_audience之类的带类型标注的字段,以及架构中定义的其他字段。2.1版本加入了置信度评分和引用功能,是目前应使用的版本。
  • ai_prep_search则利用解析过程中得到的结构信息(边界框、元素类型、章节层次结构)将已解析的VARIANT拆分成适合搜索的语义单元。它会保留文档级别的元数据,因为这部分工作由ai_extract负责。
  • 每个函数都有其特定的职责,整个处理流程就是这些功能的组合。

    笔记本中的逐步操作

    该笔记本基于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 索引:

    1. 嵌入源列:chunk_to_embed
    2. 主键:chunk_id
    3. 可过滤字段:doc_version 以及 document_type、product_name 和 target_audience

    chunk_id在每次运行时都保持不变,由文档 ID 和该块在整体中的位置组成。正因如此,对 Delta 表进行增量 MERGE 更新时只需保留未被修改的块,而无需重新索引,这对于定期更新语料库而言能节省大量时间。

    查询时的优势

    其好处体现在用户提出查询时,而非加载数据时。假设请求为“查找 Orion Motion Controller 的固件指南”,此时检索可通过两步完成:

    1. 确定匹配的元数据筛选条件,例如 document_type = “guide” 和 product_name = “Orion Motion Controller”。
    2. 在通过这些筛选条件的文档中执行查询和搜索。

    由于缺乏 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虽然只需少量额外代码,但却显著改变了架构,因为现在每个数据块都包含结构清晰、类型明确且可过滤的元数据,混合检索方式也变得更加可靠。整个处理流程仍分为三个明确的阶段:

    1. ai_parse_document用于获取文档结构。
    2. ai_extract用于提取有用的元数据。
    3. ai_prep_search用于构建语义化数据块。

    这种分离方式在处理来自不同产品、版本、受众群体的各类文档时都能发挥优势。在没有元数据过滤的情况下,检索主要依赖相似度搜索;而有了元数据过滤后,系统会先进行筛选,再进行排序。

    这种方法并非适用于所有情况。分块级元数据、父子节点检索或自定义分块仍需编写专用代码。不过,对于许多 Databricks 文档 RAG 流水线而言,这仍是一个更简洁明了的起点。而且分块仍需经过测试和评估,绝不能盲目信任。