序言
最近在学习Azure AI Search(其实我好几年前接触过它,我记得2021年它还叫Azure Search,但那个时候还是传统search的路数,现在真的变化很大),想归纳整理一下我自己的理解。抛砖引玉,欢迎大家指教!
RAG
首先说一下RAG吧。
RAG(Retrieval Augmented Generation,检索增强生成)是现在非常火的AI技术,让LLM模型依据咱自己的知识库进行回答。RAG有两个核心步骤
- 检索(Retrieval):首先它从数据库、文档或 Web 等外部源检索相关信息。生成(Generation):然后收集到相关信息后,就会将其用于指导和增强生成的回复
截止2025年的微软的技术体系里,其实有好几种方法来实现RAG(可能不止我说的这几种)
- Copilot Studio 这个我玩过,可以用public web site/sharepoint pages and documents/dataverse table/Azure AI Search来作为知识库,dataverse的数据它还能进行简单的聚合计算SharePoint Agent 用SharePoint站点内容创建的应答机器人,要是有Copilot 365license那就是免费的。去年出来的,我没有试过。。。Power BI Copilot这个好像要那个5000美刀一个月的PowerBI Premium Capacity license才行,实在没实力接触。。。Power BI的data model现在都叫“Semantic model"了(语义模型),这个功能应该很牛逼。最后就是本文要说的Azure AI Search+Azure Open AI了. 检索(Retrieval) 交给Azure AI Search,生成(Generation) 交给Azure Open AI. 这个方案应该是最支持扩展的。
基于Azure AI Search的RAG Solution
以下是我自己的理解,可能有偏颇,欢迎大家指正。(这个是微软官方的资料: learn.microsoft.com/en-us/azure… ,术语有点多,建议中文英文切换着看)
总的来说,Azure AI Search的RAG solution的后台部分主要由三大部分组成
- 数据源, 如果是文件的话一般是 Azure Blob Storage(大家可以理解成网盘),图中以下service都能成为Azure AI Search的数据源
- Azure AI Search,用来创建索引以及检索Azure Open AI,大语言模型,用以生成回复,在对源数据进行矢量化(vectorization)的时候也需要Azure Open AI参与
创建Azure AI Search Index(索引)
Index
创建Azure AI Search Index第一步就是连接数据源,然后创建index。这个章节讲的“index”其实是一个专门的术语,如果你把Azure AI Search想象成一个数据库,search这个行为想象成对数据库进行查询,那么“index”就类似数据库里的表,“index field”是表里的字段,具体的索引文件类似表里的每个具体的record。"index"就好比数据库里表的定义(类似SharePoint的Search Schema),如何填充这张表是“Indexer”的事情,index只关心这张表本身。
选择数据源后,Azure AI Search的向导会根据数据源的schema自动生成一些字段,你也可以自己做一些调整。
Azure AI Search Index里的字段其实可以分成两类,一种是支持传统full text search那种字段,字段有一堆是否searchable/facetable/retriveable/sortable的属性,和SharePoint Search非常相似。
你可以用这些属性来进一步缩小搜索范围,比如learn.microsoft.com/zh-cn/azure… 这个例子里就拿价格做过滤,然后按照地理位置远近排序
POST /indexes/hotels/docs/search?api-version=2024-07-01 { "search": "Spacious, air-condition* +\"Ocean view\"", "searchFields": "description, title", "searchMode": "any", "filter": "price ge 60 and price lt 300", "orderby": "geo.distance(location, geography'POINT(-159.476235 22.227659)')","queryType": "full" }
还有一种矢量索引字段 (Vector Index)。
Vector这个词我感觉我们国内通常翻译成“向量”,高等数学,线性代数好像都用“向量”,但是微软官网翻译成“矢量”,咱们这里就沿用官方的御用翻译吧。
体验过传统search(现在各大主流搜索引擎好像都加了语义识别来精炼出关键词,现在体验又不一样了)与基于大语言模型的AI的话,会发现他们的查询输入方式完全不一样。传统search实际上是关键词的search,把最主要的关键词放到查询里即可。你要是多加了一些次要的词的话很有可能什么都匹配不到了。传统search一般也不会让你打很多字上去。
大语言模型AI则是你说得越多,AI越是能理解你到底要什么,给出的回复质量越是高。它会判断你的具体意图到底是什么,如果你言简意赅的话还会帮你脑补补完你的意图(这点DeepSeek上体现得很明显,特别是你要选了“深度思考”的话,它会脑补出好多好多东西)
然后把分析出来的你的具体意图转化成高维空间向量(这个过程叫做“Embedding(嵌入)”)
比如我输入“什么是人工智能”,LLM的嵌入模型(例如text-embedding-3-large)会把它转换为一个高维向量(例如:[0.23, -0.45, 0.89, ..., 0.12]
)。这个“Embedding”可能有点抽象,我(一个没怎么系统学过机器学习的学渣 )个人理解是这个向量体现对应事物的特征(想起很久很久以前学校学的主成分分析)。特征相似(语义接近)的事物在这个向量空间的位置也比较接近,这样就可以做比对查找了。这种搜索就是矢量搜索(Vector Search).
万事万物皆可“Embedding”,传统search只能根据图片的标签来搜索,矢量索引就可以直接对比图片本身。
下图"text_vector"这个field就是一个矢量字段,它有3072个维度。某个index document里具体的vector数据(可以看到右边缩略图里有很长的数据)
Vector Index有很多设置(比如哪些字段和vector index相关,embedding模型选哪种,向量化用的是哪种算法,如何压缩索引),需要配置相应的Vector Profile,概念还蛮多的。
创建包含矢量索引的Search Indexer的大致步骤
这是微软的原文:learn.microsoft.com/en-us/azure… ,我讲讲自己的理解。
有了数据库的表(index)之后,我们还得导入数据进去,这个活是由Indexer来干的。
一个比较典型的含有Vector的Indexer一般会包含以下步骤
- 连接数据源Document Trunk(把文件分成小块,不然模型一口吞不下)Embedding(嵌入)Content Enrichment(不知道怎么翻译成中文最好,”内容扩充“?这一步会产生一些额外的metadata)最后一步叫“index mapping”,把上述步骤产生的output给组装成json文件,最后塞到index(塞到数据库)里去。“index mapping"本身还分index projection/field mapping/output field mapping等好几种。
(这个是一个我项目里debug索引的截图,我感觉很适合拿来讲解创建索引的流程)
下面我们分别来介绍一下。
对文件进行分块(Chunk)
我们需要用嵌入模型(embedding model)来对文件向量化。大家知道,模型是有最大token的输入限制的(比如text-embedding-ada-002 model的最大token是8191,一个token对应4个character,8191个token大致是五六千英文单词),太大的文件不拆分肯定没法塞进去。所以第一步要对文件进行分块(Chunk)。
Chunk Document里详细介绍了文件分块的策略以及注意事项。Chunk通常是整个RAG解决方案的第一步,Chunk策略选择得当与否对整个RAG的效果有关键影响。要是Chunk做得不理想搞不好就“断章取义”了。
微软现在比较推荐用一种叫“Document Layout”的“Skill(技能)”来对文件进行Chunk。这种技能可以
根据段落或句子表示的语义上连贯的片段对内容进行分块。 然后,这些片段可以独立处理,并重新组合为语义表征,而不会丢失信息、解释或语义相关性。 文本的固有含义会用作分块过程的指导。
现在这个Skill还处于preview阶段,大家可以自己试试。
Chunk把一个大文件拆分成好几个小块,这样就会产生一对多索引(one to many index)以及父子层级关系.一般要求子级的索引也能追溯父节点的相关属性,例如文件名,作者,修改时间等,这就涉及了索引投影(index projection)以及创建父子索引对(简单地说就是Parend Index有个parent id字段,Childindex也有parent id字段,这样把他们联系起来。
详情可以看learn.microsoft.com/zh-cn/azure…
提到Embedding首先要选Embedding Model,Azure AI Search自己不提供,你得去Azure Open AI上创建embedding model
注意:Azure Open AI Service和Azure AI Search必须在同一个region(区域),不然他们之间没法互相调用
Content Enrichment
简单的说,Content Enrichment就是对原始数据进行处理,扩充索引字段,比如说用Entity Extract把内容里的邮箱地址给抓取出来,或者用OCR把图片里的文字给识别出来。从广义上来说,文件向量化这件事本身也是一种Content Enrichment。
每一种Content Enrichment的处理方式都是一个"Skill".微软有很多现成的Skill可以直接用。
Azure AI Search Index Pipeline的每一步操作都要用skill来实现,比如上文说的“Chunk",你要选具体的skill来实现(前面说的"Document Layout"就是一个用于Chunk的skill)。skill设置里会有context(上下文),input(输入),output(输出)
一群skill组成一个Skill Set(技能集合),某些skill的output会是另一些skill的input,由此形成它们之间的依赖关系,成为整个一个pipeline。
一个skill set的实例
{ "@odata.etag": "\"0x8DD65BCE8953FA9\"", "name": "vector-index-nasa-skillset", "description": "Skillset to chunk documents and generate embeddings", "skills": [ { "@odata.type": "#Microsoft.Skills.Text.SplitSkill", "name": "#1", "description": "Split skill to chunk documents", "context": "/document", "defaultLanguageCode": "en", "textSplitMode": "pages", "maximumPageLength": 2000, "pageOverlapLength": 500, "maximumPagesToTake": 0, "unit": "characters", "inputs": [ { "name": "text", "source": "/document/content", "inputs": [] } ], "outputs": [ { "name": "textItems", "targetName": "pages" } ] }, { "@odata.type": "#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill", "name": "#2", "context": "/document/pages/*", "resourceUri": "https://xxx-ai-service-east-us.openai.azure.com", "apiKey": "<redacted>", "deploymentId": "text-embedding-3-large-3", "dimensions": 3072, "modelName": "text-embedding-3-large", "inputs": [ { "name": "text", "source": "/document/pages/*", "inputs": [] } ], "outputs": [ { "name": "embedding", "targetName": "text_vector" } ] }, { "@odata.type": "#Microsoft.Skills.Text.V3.EntityRecognitionSkill", "name": "#3", "context": "/document/pages/*", "categories": [ "Location" ], "defaultLanguageCode": "en", "inputs": [ { "name": "text", "source": "/document/pages/*", "inputs": [] } ], "outputs": [ { "name": "locations", "targetName": "locations" } ] } ], "cognitiveServices": { "@odata.type": "#Microsoft.Azure.Search.AIServicesByKey", "subdomainUrl": "https://xxx-ai-service-us-east.cognitiveservices.azure.com/" }, "indexProjections": { "selectors": [ { "targetIndexName": "vector-index-nasa", "parentKeyFieldName": "parent_id", "sourceContext": "/document/pages/*", "mappings": [ { "name": "text_vector", "source": "/document/pages/*/text_vector", "inputs": [] }, { "name": "chunk", "source": "/document/pages/*", "inputs": [] }, { "name": "title", "source": "/document/title", "inputs": [] }, { "name": "locations", "source": "/document/pages/*/locations", "inputs": [] } ] } ], "parameters": { "projectionMode": "skipIndexingParentDocuments" } }}
我个人感觉Skill很像Power Automate里的action,但是它的UI很弱,大多数时候你设置Skill都是直接编辑json文件,很多时候也不知道自己设得对不对。。。搞不清楚的话,在Debug Session里可以对Skill Set进行调试.
如果原生的Skill满足不了你,你还可以创建自定义的Skill,按照要求整一个Web API就行,具体例子可以看learn.microsoft.com/en-us/azure…顺便说一句,这个扩展的方法和SharePoint Search (on-Premise)的Content Enrichment Web Service很像(我十多年前就搞过这个。。。)
Index Mapping
这是创建index pipeline的最后一步,把源数据或者之前产生的中间结果给mapping到之前定义好的search index字段里,最后生成index文件(azure ai search的索引的每一条记录都是一个json document)
它实际上有好几种情况,一种是对数据源本身的字段做mapping,比如说数据源是一个cosmos db,它的json schema有*_city这个字段,我们要把它mapping到city*这个index字段。这个叫Field Mapping
"fieldMappings": [ { "sourceFieldName": "_city", "targetFieldName": "city", "mappingFunction": null }]
output field mapping用来mapping content enrichment新产生的数据。但是我感觉index projection也能处理同样的事情(至少在nasa earth book那个教程里,我没用到output field mapping),我对这块不是很了解。
前两个mapping都是配置在indexer(中文好像翻译成索引器,下文会介绍)里,但是index projection是配置在skill set里。官网说index projection的典型应用是处理Chunk Document产生一对多的父子关系。
"indexProjections": { "selectors": [ { "targetIndexName": "vector-index-nasa", "parentKeyFieldName": "parent_id", "sourceContext": "/document/pages/*", "mappings": [ { "name": "text_vector", "source": "/document/pages/*/text_vector", "inputs": [] }, { "name": "chunk", "source": "/document/pages/*", "inputs": [] }, { "name": "title", "source": "/document/title", "inputs": [] }, { "name": "locations", "source": "/document/pages/*/locations", "inputs": [] } ] } ], "parameters": { "projectionMode": "skipIndexingParentDocuments" } }
Indexer
上述所有步骤/配置(连接数据源,创建index字段,Skill Set,index mapping等等)会组成Indexer。Indexer是管理Azure Search Index数据的枢纽,你可以看到这个index的具体配置信息
以及执行的结果
待续
我本来想先把基本概念一次性讲完的,结果写search index就断断续续花了一周时间。下篇会讲Azure AI Search如何查询数据,以及如何用检索出来的数据来增强LLM生成的答案。