azure-search-openai-demo 数据摄取实战指南:从本地 prepdocs 到 Azure Functions 云摄取流水线
【免费下载链接】azure-search-openai-demoA sample app for the Retrieval-Augmented Generation pattern running in Azure, using Azure AI Search for retrieval and Azure OpenAI large language models to power ChatGPT-style and Q&A experiences.项目地址: https://gitcode.com/GitHub_Trending/az/azure-search-openai-demo
本文聚焦 azure-search-openai-demo 项目的数据摄取(Data Ingestion)组件,系统讲解两种摄取方式——本地摄取(
prepdocs.py脚本)与云摄取(Azure Functions 作为 Azure AI Search 自定义技能)——以及贯穿两者的三阶段处理流水线:文档提取、图片处理、文本处理与索引写入。读完本文,你将掌握支持的文档格式与解析器选择逻辑、分块与向量化的底层实现、分类摄取与增量去重、文档删除,以及云摄取索引器的架构与启用步骤,并学会用索引查询快速验证摄取结果。
一、数据摄取概览:同一套处理逻辑,两种运行载体
azure-search-openai-demo的目标是在 Azure AI Search 与 Azure OpenAI 之上搭建一套完整的 RAG 聊天应用,让你能够基于自定义数据(企业内部文档、领域知识集等)进行对话。整个项目从部署到使用的完整指引见 主 README,而数据摄取是其中最关键的环节之一——没有它,检索与对话都无从谈起。
项目提供了两种摄取数据的方式:
- 本地摄取(Manual / Local ingestion):在你自己的机器上运行,适用于一次性初始化索引或小批量数据;
- 云摄取(Cloud ingestion):在 Azure Functions 中运行,以 Azure AI Search 索引器自定义技能的形式执行,适合大规模、持续更新的数据集。
关键设计:这两种方式复用完全相同的文档处理代码(app/backend/prepdocslib/下的解析器、切分器、图注处理器等),只是运行载体不同——本地跑在 Python 进程里,云端跑在函数应用里。这意味着两种方式的处理结果(分块、嵌入、元数据)是一致的,切换方式不会改变索引的数据质量。
从app/backend/prepdocs.py的入口逻辑可以印证这一点:当环境变量USE_CLOUD_INGESTION为true时,prepdocs.py会直接提示改用setup_cloud_ingestion.py并退出(见 prepdocs.py),而scripts/prepdocs.sh也会在检测到云摄取开启时跳过本地流程(见 prepdocs.sh)。
二、支持的文档格式与解析器选择
要摄取某种格式,首先需要有一个能把它转成文本的工具。项目默认使用Azure Document Intelligence(简称 DI)做手动索引,同时为多种格式提供了本地解析器。本地解析器不如 Azure Document Intelligence 精细,但可以显著降低成本。
| 格式 | 手动索引(Manual indexing) | 集成向量化(Integrated Vectorization) |
|---|---|---|
| 支持(DI 或本地 PyPDF) | 支持 | |
| HTML | 支持(DI 或本地 BeautifulSoup) | 支持 |
| DOCX、PPTX、XLSX | 支持(DI) | 支持 |
| 图片(JPG、PNG、BMP、TIFF、HEIFF) | 支持(DI) | 支持 |
| TXT | 支持(本地) | 支持 |
| JSON | 支持(本地) | 支持 |
| CSV | 支持(本地) | 支持 |
从源码看,解析器的选择逻辑集中在app/backend/prepdocslib/servicesetup.py的build_file_processors()函数中,它按文件扩展名构建FileProcessor字典,FileStrategy.parse_file()再根据扩展名取出对应处理器(见 filestrategy.py)。两个开关环境变量控制本地解析器的启用:
USE_LOCAL_PDF_PARSER=true:使用基于 PyPDF 的本地 PDF 解析器(app/backend/prepdocslib/pdfparser.py);USE_LOCAL_HTML_PARSER=true:使用基于 BeautifulSoup 的本地 HTML 解析器(app/backend/prepdocslib/htmlparser.py)。
此外,对超大的 CSV 文件还可以通过CSV_MAX_PAGE_CHARS环境变量设置按字符数将多行 CSV 聚合成一页,避免一次性加载导致内存溢出(见 prepdocs.py)。
三、摄取流水线的三个核心阶段
无论是本地摄取还是云摄取,原始文档都要经过三个主要阶段才能变成 Azure AI Search 中可检索的内容。这三个阶段在本地由prepdocslib各模块依次执行,在云端则映射为四个 Azure Functions 自定义技能(详见第五节)。
3.1 文档提取(Document Extraction)
第一阶段使用与每种文件格式匹配的解析器,从源文档中提取文本和结构化内容:
- PDF、HTML、DOCX、PPTX、XLSX 与图片:默认调用 Azure Document Intelligence 提取带版式信息的文本、表格和图元。也可用 PyPDF、BeautifulSoup 等本地解析器降低成本;
- TXT、JSON、CSV:直接用轻量级本地解析器提取内容,对应 textparser.py、jsonparser.py、csvparser.py。
提取过程中有两个值得注意的细节:
- 表格被转换为 HTML 标记,以保留其行列结构;
- 图元(开启多模态时)被识别并用边界框(bounding box)与占位符标注,例如
<figure id="fig1"></figure>。
本阶段的输出是一组页面(Page),每个页面包含嵌入表格 HTML 和图元占位符的文本。在云端,document_extractor函数输出的每个页面结构为{"page_num": 0, "text": "...", "figure_ids": ["fig1"]},图元则携带figure_id、page_num、filename、mime_type、bytes_base64、bbox、title、placeholder等字段(见 document_extractor/function_app.py)。
3.2 图片处理(Figure Processing)
该阶段是可选的,仅当同时满足两个条件时才会执行:多模态功能已开启(USE_MULTIMODAL=true)且文档本身包含图元。完整的多模态功能说明见 多模态文档。
开启多模态后,上一阶段提取的图元会依次经历四个子步骤:
- 裁剪并保存:根据边界框坐标把图从 PDF 中裁剪出来,存为 PNG 文件;
- 生成描述:根据配置使用 Azure OpenAI 的 GPT-4 Vision 模型或 Azure AI Content Understanding 生成文本描述;
- 上传:把图上传到 Azure Blob Storage 并取得 URL;
- 向量化(可选):若开启了图片嵌入,使用 Azure AI Vision 计算图元的向量嵌入。
源码层面,app/backend/prepdocslib/figureprocessor.py中的FigureProcessor通过MediaDescriptionStrategy枚举(NONE/OPENAI/CONTENTUNDERSTANDING)选择描述生成机制,process_page_image()函数依次完成"生成描述 → 上传图片 → 计算图片嵌入"的完整流程,并且图片嵌入失败只会记录告警而不会中断整个流程(见 figureprocessor.py 与 figureprocessor.py)。
本阶段的输出是增强后的图元元数据:描述文本、存储 URL、以及可选的嵌入向量。
3.3 文本处理(Text Processing)
最后一个阶段把提取的文本与图元描述合并、切分成可检索的块,并计算嵌入向量。它包含两个子步骤。
图注合并(Figure Merging)
首先,页面文本中的图元占位符会被替换成包含图注和生成描述的完整 HTML 标记,形成一条将视觉内容融入其中的连贯文本叙事。对应的实现是 textprocessor.py 中的combine_text_with_figures(),它调用build_figure_markup()生成<figure><figcaption>...</figcaption></figure>这样的标记(见 figureprocessor.py)。
分块(Chunking)
合并后的文本会通过一个语义感知的切分器按语义边界切块。关键参数(来自 textsplitter.py):
- 默认块大小约 1000 字符(英文约 400–500 token),对应
DEFAULT_SECTION_LENGTH = 1000; - 相邻块之间约 10% 重叠,对应
DEFAULT_OVERLAP_PERCENT = 10; - 单块 token 上限默认 500(
max_tokens_per_section = 500)。
切分器采用滑动窗口思路:一个块结尾处的句子会同时出现在下一个块的开头附近,从而降低块边界处丢失关键上下文的风险。实现上,SentenceTextSplitter.split_pages()会:
- 先把
<figure>...</figure>块当作不可分割的原子单元提取出来,绝不把一个图元切碎或重复(含图元的块可突破 token 上限); - 文本部分按句子结束符(标准标点加 CJK 标点,见
STANDARD_SENTENCE_ENDINGS与CJK_SENTENCE_ENDINGS)拆分成 span,再按字符数与 token 数双阈值累积成块; - 支持跨页合并与语义重叠:仅当上一块未以句子结束符收尾、下一块以小写字母开头且不是标题(
_is_heading_like()可识别 Markdown 标题、全大写/标题化短行、编号列表等)时,才把下一块的前缀追加到上一块末尾(见 textsplitter.py)。
为什么要分块?虽然 Azure AI Search 能索引完整文档,但对 RAG 模式而言分块是必需的:OpenAI 上下文窗口有 token 上限,分块能限制发送给 OpenAI 的信息量。把内容切成聚焦的块后,系统可以只检索并把最相关的文本片段注入 LLM 提示词,既提升回答质量又降低成本。
如需调整切分算法,可修改 textsplitter.py;关于切分器如何工作(图元、递归、合并启发式、保证与示例)的更深入、图文并茂的解释,参见 文本切分器文档。
嵌入计算(Embedding)
最后,若启用了向量搜索(USE_VECTORS默认开启),为每个块用 Azure OpenAI 的嵌入模型(text-embedding-ada-002、text-embedding-3-small或text-embedding-3-large)计算文本嵌入。嵌入按批量生成以提高效率,并带重试逻辑以应对限流;prepdocs.py的--disablebatchvectors参数可以关闭批量计算(见 prepdocs.py)。嵌入维度默认 1536,可通过AZURE_OPENAI_EMB_DIMENSIONS覆盖。
3.4 索引写入(Indexing)
最后一步是把块写入 Azure AI Search。每个块作为索引中的独立文档存储,其元数据记录了来源文件与页码。若开启了向量搜索,嵌入向量与文本一起存储,供查询时做相似度检索。从 searchmanager.py 可以看到,向量索引使用HNSW 算法、余弦相似度(HnswAlgorithmConfiguration(metric="cosine")),并默认启用Binary Quantization 压缩(truncation_dimension=1024)来降低存储成本。
一个最终索引块文档的示例:
{ "id": "file-Northwind_Health_Plus_Benefits_Details_pdf-4E6F72746877696E645F4865616C74685F506C75735F42656E65666974735F44657461696C732E706466-page-0", "content": "# Zava\n\nNorthwind Health Plus Plan\n...", "category": null, "sourcepage": "Northwind_Health_Plus_Benefits_Details.pdf#page=1", "sourcefile": "Northwind_Health_Plus_Benefits_Details.pdf", "storageUrl": "https://std4gfbajn3e3yu.blob.core.windows.net/content/Northwind_Health_Plus_Benefits_Details.pdf", "embedding": [0.0123, -0.0456, ...] }若开启了多模态,该文档还会包含images字段,content字段中也会带有图元描述。从 listfilestrategy.py 可以看到id的生成规则:file-前缀 + 文件名 ASCII 化 + 文件名十六进制哈希,因此同一文件重新摄取不会产生重复文档。
四、本地摄取:prepdocs.py的完整用法
prepdocs.py脚本同时负责上传与索引两类工作。典型用法是通过scripts/prepdocs.sh(Mac/Linux)或scripts/prepdocs.ps1(Windows)调用——这些脚本会自动创建 Python 虚拟环境,并根据当前azd环境传入所需参数。你也可以直接给脚本追加额外参数,例如scripts/prepdocs.ps1 --removeall。每当运行azd up或azd provision时,该脚本都会被自动调用。
从 prepdocs.sh 可以看到,脚本实际执行的是./.venv/bin/python ./app/backend/prepdocs.py './data/*' --verbose $additionalArgs,即以data/目录下的所有文件为输入。prepdocs.py索引文档的步骤如下:
- 若索引尚不存在,在 Azure AI Search 中创建新索引;
- 把文档上传到 Azure Blob Storage;
- 将文档切分为文本块;
- 把块上传到 Azure AI Search;若使用向量(默认启用),同时计算嵌入并随文本一起上传。
这些步骤在FileStrategy.run()中对应为upload_blob()→parse_file()→update_content()(见 filestrategy.py)。prepdocs.py还支持**集成向量化(Integrated Vectorization)**模式:当USE_FEATURE_INT_VECTORIZATION=true时改用IntegratedVectorizerStrategy,把向量化交给 Azure AI Search 内置技能在云端完成(见 prepdocs.py)。
4.1 数据分类:用--category增强检索
为了增强检索功能,可以在摄取时用--category参数对数据分类,例如:
scripts/prepdocs.ps1 --category ExampleCategoryName该参数会写入索引文档的category字段(对应前文示例 JSON 中的"category": null),从而允许你基于这些分类过滤搜索结果。
运行脚本写入期望的分类后,还需要把这些分类添加到"Include Category"(包含分类)下拉列表中——该下拉框位于开发者设置里,对应前端组件 Settings.tsx。下拉框默认选项为 "All"(全部)。把具体分类纳入其中后,你就可以更精准地缩小搜索范围。
4.2 增量摄取更多文档与 MD5 去重
要上传更多 PDF,把它们放进data/文件夹,然后运行:
./scripts/prepdocs.sh或:
./scripts/prepdocs.ps1prepdocs脚本会为每个上传的文件写一个.md5 文件,保存该文件的 MD5 哈希。每次重新运行时,脚本都会把当前哈希与已存哈希比对,若文件未发生变化则跳过上传。这一去重逻辑实现在LocalListFileStrategy.check_md5()中:命中缓存则跳过并记录Skipping '...', no changes detected.,否则写入新哈希(见 listfilestrategy.py)。这意味着重复执行脚本是安全的,不会重复索引未变更的文件。
4.3 删除文档
你可能需要从索引中移除文档——例如在使用示例数据时,先删掉索引中已有的文档,再加入自己的数据。
- 删除全部文档:
./scripts/prepdocs.sh --removeall或:
./scripts/prepdocs.ps1 --removeall- 删除单个文档:使用
--remove标志。打开scripts/prepdocs.sh或scripts/prepdocs.ps1,把data/*替换为data/YOUR-DOCUMENT-FILENAME-GOES-HERE.pdf,然后运行:
scripts/prepdocs.sh --remove或:
scripts/prepdocs.ps1 --removeprepdocs.py会把--removeall、--remove分别映射为DocumentAction.RemoveAll/DocumentAction.Remove(见 prepdocs.py),最终由FileStrategy.run()执行 blob 与索引的双重删除(见 filestrategy.py)。另外该脚本还支持--searchkey、--storagekey、--documentintelligencekey等可选参数,用于以服务密钥代替当前用户身份登录各 Azure 服务。
五、云摄取:Azure Functions 自定义技能架构
项目包含一个可选的云摄取功能:用 Azure Functions 作为 Azure AI Search 索引器的自定义技能,在云端完成数据摄取。这种方式把摄取负载从本地机器卸载到云端,可以更弹性、高效地处理大数据集。
5.1 启用云摄取
前提:如果你之前已部署过,需要删除现有搜索索引或新建一个索引——该功能无法用于已有索引。因为新建索引的 schema 中会新增一个parent_id字段,索引器用它内部管理块的生命周期。
按以下步骤启用:
- 设置新的索引名:
azd env set AZURE_SEARCH_INDEX cloudindex- 开启云摄取:
azd env set USE_CLOUD_INGESTION true- (推荐)把嵌入模型的容量提升到所在区域/订阅允许的最大配额,避免 Azure Functions 生成嵌入时触发限流:
azd env set AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY 400- 预置新的 Azure Functions 资源、部署函数应用并更新搜索索引器:
azd up- 该命令会把
data/文件夹中的文档上传到 Blob 存储容器,创建索引器(indexer)与技能集(skillset),并运行索引器摄取数据。之后可在门户中监控索引器状态。 - 当有新文档需要摄取时,把文档上传到 Blob 存储容器,然后从 Azure 门户运行索引器即可。
整个配置过程由 setup_cloud_ingestion.py 驱动:它从环境变量读取三个技能函数的端点(DOCUMENT_EXTRACTOR_SKILL_ENDPOINT、FIGURE_PROCESSOR_SKILL_ENDPOINT、TEXT_PROCESSOR_SKILL_ENDPOINT)及各自的托管标识资源 ID,构造CloudIngestionStrategy完成索引器、技能集和数据源的创建(见 setup_cloud_ingestion.py)。同时它也会检查AZURE_ENFORCE_ACCESS_CONTROL与USE_CLOUD_INGESTION_ACLS的配套关系,避免在未开启 ACL 提取的情况下索引无 ACL 文档导致访问控制失效(见 setup_cloud_ingestion.py)。
5.2 索引器架构:四个自定义技能
云摄取流水线在 Azure AI Search 索引器中使用4 个 Azure Functions 作为自定义技能,每个函数对应摄取过程的一个阶段:
- 用户上传文档到 Azure Blob Storage(content 容器);
- Azure AI Search 索引器监控 blob 容器并编排处理流程;
- 自定义技能分三阶段处理文档:
- Document Extractor(技能 #1):从源文档提取文本与图元元数据;
- Figure Processor(技能 #2):为图元补充描述与嵌入;
- Shaper Skill(技能 #3):Azure AI Search 内置技能,整合富化数据;
- Text Processor(技能 #4):把文本与富化图元合并、切块并生成嵌入;
- Azure AI Search 索引接收最终带嵌入的处理后块。
函数定义在app/functions/目录,自定义技能集在 setup_cloud_ingestion.py 中配置。
Document Extractor 函数(app/functions/document_extractor/)
- 实现 文档提取 阶段;
- 输出带
<figure id="...">占位符的 Markdown 文本及图元元数据; - 其 HTTP 路由为
extract,要求索引器把 batchSize 设为 1(每次请求一个 record,见 document_extractor/function_app.py);若存储启用了 ADLS Gen2 ACL,还会同步提取文件的 oids/groups 访问控制信息(get_file_acls())。
Figure Processor 函数(app/functions/figure_processor/)
- 实现 图片处理 阶段;
- 输出带描述、URL 和嵌入的富化图元元数据;
- 其 HTTP 路由为
process,通过ImageOnPage.from_skill_payload()还原图元,再复用本地相同的process_page_image()完成描述、上传与嵌入(见 figure_processor/function_app.py)。
Shaper Skill(Azure AI Search 内置技能)
- 把 figure processor 产生的富化结果合并回主文档上下文;
- 这是必需的,因为 Azure AI Search 的富化树按上下文隔离数据;
- Shaper 显式合并三部分:
document_extractor输出的原始pages数组、带描述/URL/嵌入的富化figures数组、文件元数据(file_name、storageUrl); - 生成
consolidated_document对象供 text processor 消费。
Text Processor 函数(app/functions/text_processor/)
- 实现 文本处理 阶段(图注合并、切块、嵌入);
- 接收 Shaper 技能传来的带富化图元的合并文档;
- 输出带图元引用与嵌入的可检索块;
- 其配置中
USE_VECTORS默认true、嵌入维度默认 3072、默认嵌入模型text-embedding-3-large(见 text_processor/function_app.py)。
5.3 增加与删除文档
增加文档:先把新文档上传到数据源(默认是 Blob 存储),然后到 Azure 门户运行索引器。索引器会识别新文档并摄取进索引。
删除文档:先从数据源(默认 Blob 存储)删除对应文档,再到 Azure 门户运行索引器。索引器会负责把这些文档从索引中移除。这正是parent_id字段发挥作用的场景——索引器通过它管理每个源文件对应的全部块的生命周期。
5.4 定时索引
如果希望索引器自动运行,可以按照 Azure AI Search 的索引器计划功能配置定时执行,实现周期性的数据同步。
六、排障技巧:用索引查询验证摄取结果
如果你不确定某个文件是否成功上传,可以从 Azure 门户或通过 REST API 查询索引。打开索引,把下面的查询粘贴到搜索栏。
查看索引中已上传的全部文件名:
{ "search": "*", "count": true, "top": 1, "facets": ["sourcefile"] }搜索特定文件名:
{ "search": "*", "count": true, "top": 1, "filter": "sourcefile eq 'employee_handbook.pdf'", "facets": ["sourcefile"] }第一个查询利用sourcefile字段的 facet 聚合列出所有已索引的文件名,第二个查询用filter精确匹配某个文件。如果期望的文件没有出现在 facet 列表或过滤结果中,说明它未被成功摄取,可以结合 blob 容器内容与索引器执行历史进一步排查。仓库中还有完整的端到端与单元测试(如 tests/test_app.py、tests/test_prepdocs.py、tests/test_prepdocslib_textsplitter.py)可以帮你验证摄取相关代码的正确性。
七、小结
数据摄取是 RAG 应用的地基。通过本文可以看到,azure-search-openai-demo将"文档提取 → 图片处理 → 文本处理 → 索引写入"这一套逻辑同时落地在本地脚本与云端函数两种载体上:
- 本地摄取适合初始化与小批量场景,
prepdocs.py配合--category、MD5 增量去重、--remove/--removeall提供了完整的数据生命周期管理; - 云摄取把同样的处理逻辑搬到 Azure Functions 自定义技能中,配合 Azure AI Search 索引器实现自动化、可扩展、可持续更新的大规模摄取,并支持定时索引;
- 无论哪种方式,最终产出的都是带
sourcepage、sourcefile、category与可选embedding的块文档,供对话检索直接使用。
掌握这些环节后,你就可以根据自己的数据规模、更新频率与成本预算,选择最合适的摄取方式,并利用索引查询快速验证结果、持续维护索引数据。
【免费下载链接】azure-search-openai-demoA sample app for the Retrieval-Augmented Generation pattern running in Azure, using Azure AI Search for retrieval and Azure OpenAI large language models to power ChatGPT-style and Q&A experiences.项目地址: https://gitcode.com/GitHub_Trending/az/azure-search-openai-demo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考