教程:在 Azure AI 搜索中为 RAG 设计索引
索引包含可搜索的文本和矢量内容以及配置。 在使用聊天模型进行响应的 RAG 模式中,需要一个包含可在查询时传递给 LLM 的内容块的索引。
在本教程中,你将了解:
- 了解为 RAG 生成的索引架构的特征
- 创建可容纳矢量和混合查询的索引
- 添加矢量配置文件和配置
- 添加结构化数据
- 添加筛选
先决条件
包含 Python 扩展和 Jupyter 包的 Visual Studio Code。 有关详细信息,请参阅 Visual Studio Code 中的 Python。
本练习的输出是 JSON 格式的索引定义。 此时,它尚未上传到 Azure AI 搜索,因此本练习对云服务或权限没有任何要求。
查看 RAG 的架构注意事项
在对话搜索中,LLM(而不是搜索引擎)编写用户看到的响应,因此不需要考虑在搜索结果中显示哪些字段,以及各个搜索文档的表示是否与用户一致。 根据问题的不同,LLM 可能会从索引中返回逐字内容,或者更有可能重新打包内容以获得更好的答案。
围绕块进行组织
当 LLM 生成响应时,它们会对消息输入的内容块进行操作,虽然它们需要知道块的源以便于引用,但最重要的是消息输入的质量及其与用户问题的相关性。 无论这些内容块来自一个文档还是一千个文档,LLM 都会引入信息或基础数据,并使用系统提示中提供的说明来制定响应。
块是架构的焦点,每个块是 RAG 模式中搜索文档的定义元素。 可以将索引视为大量块的集合,这与可能具有更多结构的传统搜索文档不同,例如包含名称、描述、类别和地址的统一内容的字段。
使用生成的数据进行增强
在本教程中,示例数据包括 PDF 和来自 NASA Earth Book 的内容。 此内容描述性强,信息量丰富,多次提及了全球的地理、国家和地区。 所有文本内容都以块的形式捕获,但重复出现的地名实例为向索引添加结构创造了机会。 使用技能,可以识别文本中的实体,并在索引中捕获实体,以便在查询和筛选器中使用。 在本教程中,我们包括了一个实体识别技能,用于识别和提取位置实体,将其加载到可搜索和可筛选的 locations
字段中。 向索引中添加结构化内容可提供更多用于筛选、相关性改进和更具针对性的答案的选项。
父子字段位于一个还是两个索引中?
分块内容通常源自较大的文档。 尽管架构是围绕块组织的,但你也可以在父级别捕获属性和内容。 这些属性的示例包括父文件路径、标题、作者、出版日期或摘要。
架构设计中的一个转折点是,是要为父内容和子内容/分块内容创建两个索引,还是创建为每个块重复父元素的单个索引。
在本教程中,由于所有文本块都源自单个父级 (NASA Earth Book),因此不需要专用于提升父字段级别的单独索引。 但是,如果从多个父 PDF 编制索引,则可能需要父子索引对来捕获特定于级别的字段,然后向父索引发送查找查询以检索与每个块相关的字段。
架构注意事项清单
在 Azure AI 搜索中,最适合 RAG 工作负载的索引具有以下特征:
返回与查询相关且可供 LLM 读取的块。 LLM 可以处理一定程度的块状脏数据,例如标记、冗余和不完整的字符串。 虽然块需要可读且与问题相关,但它们不需要是完美的。
维护文档块与父文档的属性(例如文件名、文件类型、标题、作者等)之间的父子关系。 为了回答查询,可以从索引中的任何位置拉取块。 与提供块的父文档的关联对于上下文、引述和后续查询很有用。
满足你要创建的查询。 应有用于矢量和混合内容的字段,并且这些字段应该归因于支持特定的查询行为。 一次只能查询一个索引(无联接),因此字段集合应该定义所有可搜索的内容。
架构应该是扁平的(没有复杂的类型或结构)。 此要求特定于 Azure AI 搜索中的 RAG 模式。
尽管 Azure AI 搜索无法联接索引,但你可以创建保留父子关系的索引,然后在搜索逻辑中使用顺序或并行查询从两者中拉取数据。 本练习包括同一索引和单独索引中的父子元素的模板,其中使用查找查询检索来自父索引的信息。
为 RAG 工作负荷创建索引
LLM 的最小索引被设计用于存储内容块。 如果你要执行相似性搜索以获取高度相关的结果,它通常包括矢量字段。 它还包括非矢量字段,以便向 LLM 提供人类可读的输入进行对话式搜索。 搜索结果中的非矢量分块内容成为发送到 LLM 的基础数据。
打开 Visual Studio Code 并创建一个新文件。 对于本练习而言,它不必是 Python 文件类型。
下面是支持矢量和混合搜索的 RAG 解决方案的最小索引定义。 请查看它以了解所需元素的简介:索引名称、字段和矢量字段的配置部分。
{ "name": "example-minimal-index", "fields": [ { "name": "id", "type": "Edm.String", "key": true }, { "name": "chunked_content", "type": "Edm.String", "searchable": true, "retrievable": true }, { "name": "chunked_content_vectorized", "type": "Edm.Single", "dimensions": 1536, "vectorSearchProfile": "my-vector-profile", "searchable": true, "retrievable": false, "stored": false }, { "name": "metadata", "type": "Edm.String", "retrievable": true, "searchable": true, "filterable": true } ], "vectorSearch": { "algorithms": [ { "name": "my-algo-config", "kind": "hnsw", "hnswParameters": { } } ], "profiles": [ { "name": "my-vector-profile", "algorithm": "my-algo-config" } ] } }
字段必须包含关键字段(在本例中为
"id"
),并且应包含用于相似性搜索的矢量块和用于 LLM 输入的非矢量块。矢量字段与在查询时确定搜索路径的算法相关联。 索引有一个 vectorSearch 节,用于指定多个算法配置。 矢量字段还具有特定类型和附加属性,用于嵌入模型维度。
Edm.Single
是一种适用于常用 LLM 的数据类型。 有关矢量字段的详细信息,请参阅创建矢量索引。元数据字段可能是父文件路径、创建日期或内容类型,对于筛选器很有用。
下面是教程源代码和 Earth Book 内容的索引架构。
与基本架构一样,它是围绕块进行组织的。
chunk_id
唯一标识每个块。text_vector
字段是块的嵌入。 非矢量chunk
字段是可读的字符串。title
映射到 Blob 的唯一元数据存储路径。parent_id
是仅有的父级字段,它是父文件 URI 的 base64 编码版本。该架构还包括一个
locations
字段,用于存储由索引管道创建的生成内容。from azure.identity import DefaultAzureCredential from azure.identity import get_bearer_token_provider from azure.search.documents.indexes import SearchIndexClient from azure.search.documents.indexes.models import ( SearchField, SearchFieldDataType, VectorSearch, HnswAlgorithmConfiguration, VectorSearchProfile, AzureOpenAIVectorizer, AzureOpenAIVectorizerParameters, SearchIndex ) credential = DefaultAzureCredential() # Create a search index index_name = "py-rag-tutorial-idx" index_client = SearchIndexClient(endpoint=AZURE_SEARCH_SERVICE, credential=credential) fields = [ SearchField(name="parent_id", type=SearchFieldDataType.String), SearchField(name="title", type=SearchFieldDataType.String), SearchField(name="locations", type=SearchFieldDataType.Collection(SearchFieldDataType.String), filterable=True), SearchField(name="chunk_id", type=SearchFieldDataType.String, key=True, sortable=True, filterable=True, facetable=True, analyzer_name="keyword"), SearchField(name="chunk", type=SearchFieldDataType.String, sortable=False, filterable=False, facetable=False), SearchField(name="text_vector", type=SearchFieldDataType.Collection(SearchFieldDataType.Single), vector_search_dimensions=1024, vector_search_profile_name="myHnswProfile") ] # Configure the vector search configuration vector_search = VectorSearch( algorithms=[ HnswAlgorithmConfiguration(name="myHnsw"), ], profiles=[ VectorSearchProfile( name="myHnswProfile", algorithm_configuration_name="myHnsw", vectorizer_name="myOpenAI", ) ], vectorizers=[ AzureOpenAIVectorizer( vectorizer_name="myOpenAI", kind="azureOpenAI", parameters=AzureOpenAIVectorizerParameters( resource_url=AZURE_OPENAI_ACCOUNT, deployment_name="text-embedding-3-large", model_name="text-embedding-3-large" ), ), ], ) # Create the search index index = SearchIndex(name=index_name, fields=fields, vector_search=vector_search) result = index_client.create_or_update_index(index) print(f"{result.name} created")
对于更近似地模拟结构化内容的索引架构,可为父和子(分块)字段创建单独的索引。 需要使用索引投影来同时协调两个索引的索引编制。 查询针对子索引执行。 查询逻辑包括一个查找查询,它使用 parent_id 从父索引检索内容。
子索引中的字段:
- ID
- chunk
- chunkVectcor
- parent_id
父索引中的字段(你希望是“其中之一”的所有内容):
- parent_id
- 父级字段(名称、标题、类别)