教程:在 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 的基础数据。

  1. 打开 Visual Studio Code 并创建一个新文件。 对于本练习而言,它不必是 Python 文件类型。

  2. 下面是支持矢量和混合搜索的 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 的数据类型。 有关矢量字段的详细信息,请参阅创建矢量索引

    元数据字段可能是父文件路径、创建日期或内容类型,对于筛选器很有用。

  3. 下面是教程源代码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")  
    
  4. 对于更近似地模拟结构化内容的索引架构,可为父和子(分块)字段创建单独的索引。 需要使用索引投影来同时协调两个索引的索引编制。 查询针对子索引执行。 查询逻辑包括一个查找查询,它使用 parent_id 从父索引检索内容。

    子索引中的字段:

    • ID
    • chunk
    • chunkVectcor
    • parent_id

    父索引中的字段(你希望是“其中之一”的所有内容):

    • parent_id
    • 父级字段(名称、标题、类别)

下一步