如何在 Azure AI 搜索中形成结果

本文介绍如何在 Azure AI 搜索中处理查询响应。 响应的结构由查询本身内部的参数决定,如搜索文档 (REST)SearchResults 类 (Azure for .NET) 中所述。

查询中的参数决定了:

  • 字段选择
  • 在查询的索引中找到的匹配项计数
  • Paging
  • 响应中的结果数(默认不超过 50 个)
  • 排序顺序
  • 突出显示结果中的术语,并匹配正文中的全部或部分术语

结果的构成

结果采用表格形式,由所有“可检索”字段组成,或者仅限于 $select 参数中指定的那些字段。 行是匹配的文档。

可以选择在搜索结果中包含哪些字段。 尽管搜索文档可能包含大量字段,但通常只需少量的几个字段就能表示结果中的每个文档。 在查询请求中,追加 $select=<field list> 以指定要在响应中显示哪些“可检索”字段。

选择的字段应该可供对比和区分文档,并提供足够的信息来邀请用户一端做出点击响应。 在电子商务网站上,这些字段可能是产品名称、说明、品牌、颜色、尺寸、价格和评级。 对于内置的 hotels-sample 索引,它可能是以下示例中的“select”字段:

POST /indexes/hotels-sample-index/docs/search?api-version=2024-07-01 
    {  
      "search": "sandy beaches",
      "select": "HotelId, HotelName, Description, Rating, Address/City"
      "count": true
    }

意外结果提示

有时,搜索结果的内容出乎意料。 例如,你可能发现某些结果似乎是重复的,或者某个本应靠前显示的结果却出现在结果列表中的较后位置。 如果查询结果不符合预期,可以尝试对查询进行以下修改,然后查看结果是否有所改善:

  • searchMode=any(默认)更改为 searchMode=all,从而得出符合所有条件而不是某个条件的匹配项。 在查询包含布尔运算符时更应如此。

  • 使用不同的词法分析器或自定义分析器进行试验,看它是否改变了查询结果。 默认分析器分解包含连字符的单词并将单词缩减为词根形式,这通常可提高查询响应的稳定性。 但是,如果需要保留连字符,或者字符串中包含特殊字符,则你可能需要配置自定义分析器,以确保索引包含正确格式的标记。 有关详细信息,请参阅部分字词搜索和包含特殊字符(连接符、通配符、正则表达式、模式)的模式

计数匹配项

count 参数返回索引中被认为与查询匹配的文档数。 若要返回计数,请将 $count=true 添加到查询请求。 搜索服务没有规定最大值。 根据查询和文档内容,计数可能与索引中的每个文档一样多。

当索引稳定时,计数是准确的。 如果系统正在主动添加、更新或删除文档,则计数将是近似值,不包括任何未完全编制索引的文档。

计数不会受到搜索服务上的日常维护或其他工作负载的影响。 但是,如果有多个分区和单个副本,则在分区重启时,可能会遇到文档计数的短期波动(几分钟)。

提示

若要检查索引操作,可以通过在空搜索 search=* 查询中添加 $count=true 来确认索引是否包含预期数量的文档。 结果是索引中文档的完整计数。

在测试查询语法时,通过 $count=true 可以快速判断修改返回的结果是更多还是更少,这可能是有用的反馈。

分页结果

默认情况下,搜索引擎最多返回前 50 个匹配项。 前 50 个匹配项由搜索分数决定,假设查询是全文搜索或语义。 否则,前 50 个匹配项是任意顺序的完全匹配查询(其中统一“@searchScore=1.0”表示任意排名)。

上限是每页搜索结果返回 1,000 个文档,因此你可以将 top 设置为在第一个结果中最多返回 1000 个文档。 在较新的预览 API 中,如果使用混合查询,则可以指定 maxTextRecallSize 以返回最多 10,000 个文档。

若要控制结果集中返回的所有文档的分页,请将参数 $top$skip 参数添加到 GET 查询请求,或者将 topskip 添加到 POST 查询请求。 以下列表解释了相关逻辑。

  • 返回第一组 15 个匹配文档,以及匹配项总计数:GET /indexes/<INDEX-NAME>/docs?search=<QUERY STRING>&$top=15&$skip=0&$count=true

  • 跳过前 15 个匹配文档,返回第二组 15 个文档:$top=15&$skip=15。 重复第三组 15 个匹配文档:$top=15&$skip=30

如果基础索引会发生变化,则无法保证分页查询的结果稳定。 分页会更改每页的 $skip 值,但每个查询是独立的,并针对查询时存在于索引中的当前数据视图运行(换言之,对于在常规用途数据库等位置中发现的结果,不存在结果缓存或快照)。

以下示例展示了如何获取重复项。 假设某个索引包含四个文档:

{ "id": "1", "rating": 5 }
{ "id": "2", "rating": 3 }
{ "id": "3", "rating": 2 }
{ "id": "4", "rating": 1 }

现在假设你希望每次返回按评级排序的两个结果。 你将执行此查询来获取第一页结果:$top=2&$skip=0&$orderby=rating desc,将生成以下结果:

{ "id": "1", "rating": 5 }
{ "id": "2", "rating": 3 }

在服务上,假设在两次查询调用之间将第五个文档添加到索引中:{ "id": "5", "rating": 4 }。 片刻之后,你执行查询来提取第二页:$top=2&$skip=2&$orderby=rating desc,将获得以下结果:

{ "id": "2", "rating": 3 }
{ "id": "3", "rating": 2 }

请注意,文档 2 提取了两次。 这是因为,新文档 5 的评级值较大,因此它排在文档 2 的前面,并出现在第一页中。 尽管这种行为可能让人意外,但它却是搜索引擎的典型行为。

分页浏览大量结果

如果使用 $top$skip,则将允许搜索查询分页浏览 100,000 个结果,但如果结果大于 100,000,该怎么办? 若要翻页浏览此大型响应,请使用排序顺序范围筛选器作为 $skip 的解决方法。

在此解决方法中,排序和筛选将应用于文档 ID 字段,或每个文档独有的其他字段。 唯一字段必须在搜索索引中具有 filterable 属性和 sortable 属性。

  1. 发出查询以返回已排序结果的完整页面。

    POST /indexes/good-books/docs/search?api-version=2024-07-01
        {  
          "search": "divine secrets",
          "top": 50,
          "orderby": "id asc"
        }
    
  2. 选择搜索查询返回的最后一个结果。 此处仅显示具有一个“id”值的示例结果。

    {
        "id": "50"
    }
    
  3. 在范围查询中使用“id”值来提取下一页结果。 此“id”字段应具有唯一值,否则分页可能包含重复结果。

    POST /indexes/good-books/docs/search?api-version=2024-07-01
        {  
          "search": "divine secrets",
          "top": 50,
          "orderby": "id asc",
          "filter": "id ge 50"
        }
    
  4. 在查询返回零结果时,分页结束。

注意

仅当字段首次添加到索引时,才能启用“filterable”和“sortable”属性,不能在现有字段上启用这些属性。

对结果排序

在全文搜索查询中,结果可按以下依据排名:

  • 搜索评分
  • 基于“可排序”字段的排序顺序

还可以通过添加评分配置文件来提升在特定字段中找到的任何匹配项。

按搜索评分排序

对于全文搜索查询,结果将按照搜索评分自动排名,搜索评分是根据文档中的字词频率和邻近性计算的,根据搜索字词,匹配项越多或者匹配程度越高的文档,其评分越高。

“@search.score”范围没有限制,在较旧的服务上为 0 到 1.00(不含)。

对于任一算法,“@search.score”等于 1.00 表示未评分或未排序的结果集,其中 1.0 评分在所有结果中都是一致的。 如果查询形式是模糊搜索、通配符或正则表达式查询,或者是空搜索 (search=*),则会出现未评分的结果。 如果需要对未评分的结果进行排名,请考虑使用 $orderby 表达式实现此目的。

使用 $orderby 排序

如果一致的排序是一项应用程序要求,则可以在字段上定义 $orderby 表达式。 只有在编制索引时设置为“可排序”的字段才可用于对结果排序。

$orderby 中常用的字段包括评级、日期和位置。 如果要按位置进行筛选,则需要筛选器表达式调用 geo.distance() 函数以及字段名称。

数值字段 (Edm.Double, Edm.Int32, Edm.Int64) 按数值顺序排序(例如,1, 2, 10, 11, 20)。

字符串字段(EDM.String、EDM.ComplexType 子字段)按 ASCII 排序顺序或 Unicode 排序顺序排序,具体取决于语言。 不能对任何类型的集合进行排序。

  • 字符串字段中的数字内容按字母顺序排序 (1, 10, 11, 2, 20)。

  • 大写字母字符串排序在小写字母之前 (APPLE, Apple, BANANA, Banana, apple, banana)。 要更改此行为,可以分配文本规范化程序以在排序之前对文本进行预处理。 对某一字段使用小写分词器对排序行为没有影响,因为 Azure AI 搜索对字段的非分析副本进行排序。

  • 以变音符号开头的字符串出现在最后 (Äpfel, Öffnen, Üben)

使用评分配置文件提升相关性

提高排序一致性的另一种方法是使用自定义评分配置文件。 使用评分配置文件可以提高在特定字段中有匹配项的项的分数,从而可以让你更好地控制搜索结果中各个项的排名。 这一附加的评分逻辑有助于覆盖副本之间的细微差异,因为每个文档的搜索评分会在更大程度上拉开差距。 我们建议对此方法使用排名算法

突出显示

命中项突出显示是指对结果中的匹配字词应用文本格式设置(例如粗体或黄色突出显示),以便轻松找到匹配项。 突出显示适合用于较长的内容字段(如“说明”字段),因为在这些字段中,匹配内容不是立即就能看到。

请注意,突出显示将应用于单个术语。 整个字段的内容没有突出显示功能。 如果想要突出显示某个短语,则必须在带引号的查询字符串中提供匹配术语(或短语)。 本部分将进一步介绍此方法。

查询请求上提供了命中词突出显示说明。 在引擎中触发查询扩展的查询(例如模糊搜索和通配符搜索)对命中项突出显示的支持有限。

命中项突出显示的要求

  • 字段必须是 Edm.StringCollection(Edm.String)
  • 必须将字段的属性设置为可搜索

在请求中指定突出显示

若要返回突出显示的字词,请在查询请求中包含“highlight”参数。 该参数设置为逗号分隔的字段列表。

默认情况下,格式标记为 <em>,但你可以使用 highlightPreTaghighlightPostTag 参数来替代标记。 客户端代码将处理响应(例如,应用粗体字体或黄色背景)。

POST /indexes/good-books/docs/search?api-version=2024-07-01
    {  
      "search": "divine secrets",  
      "highlight": "title, original_title",
      "highlightPreTag": "<b>",
      "highlightPostTag": "</b>"
    }

默认情况下,Azure AI 搜索最多为每个字段返回五处突出显示。 你可以通过追加一个短划线并后接一个整数来调整此数字。 例如,"highlight": "description-10" 在“description”字段中返回匹配内容中最多 10 个突出显示的字词。

突出显示的结果

将突出显示添加到查询时,响应将包含每条结果的“@search.highlights”,使应用程序代码能够以该结构为目标。 为“highlight”指定的字段列表包含在响应中。

在关键字搜索中,将单独扫描每个字词。 对“divine secrets”的查询将返回包含其中任一字词的任何文档的匹配项。

突出显示了短语查询的屏幕截图。

关键字搜索突出显示

在突出显示的字段中,格式设置将应用于完整字词。 例如,在根据“The Divine Secrets of the Ya-Ya Sisterhood”进行匹配时,格式设置将分别应用于每个字词,即使它们是连续的。

"@odata.count": 39,
"value": [
    {
        "@search.score": 19.593246,
        "@search.highlights": {
            "original_title": [
                "<em>Divine</em> <em>Secrets</em> of the Ya-Ya Sisterhood"
            ],
            "title": [
                "<em>Divine</em> <em>Secrets</em> of the Ya-Ya Sisterhood"
            ]
        },
        "original_title": "Divine Secrets of the Ya-Ya Sisterhood",
        "title": "Divine Secrets of the Ya-Ya Sisterhood"
    },
    {
        "@search.score": 12.779835,
        "@search.highlights": {
            "original_title": [
                "<em>Divine</em> Madness"
            ],
            "title": [
                "<em>Divine</em> Madness (Cherub, #5)"
            ]
        },
        "original_title": "Divine Madness",
        "title": "Divine Madness (Cherub, #5)"
    },
    {
        "@search.score": 12.62534,
        "@search.highlights": {
            "original_title": [
                "Grave <em>Secrets</em>"
            ],
            "title": [
                "Grave <em>Secrets</em> (Temperance Brennan, #5)"
            ]
        },
        "original_title": "Grave Secrets",
        "title": "Grave Secrets (Temperance Brennan, #5)"
    }
]

短语搜索突出显示

完整字词格式设置甚至会应用于短语搜索,其中的多个字词用双引号括住。 以下示例与前面的查询相同,只不过“divine secrets”是以引号括住的短语形式提交的(某些 REST 客户端要求使用反斜杠 \" 来转义内部引号):

POST /indexes/good-books/docs/search?api-version=2024-07-01 
    {  
      "search": "\"divine secrets\"",
      "select": "title,original_title",
      "highlight": "title",
      "highlightPreTag": "<b>",
      "highlightPostTag": "</b>",
      "count": true
    }

由于条件现在具有这两个字词,因此在搜索索引中只找到一个匹配项。 对上述查询的响应如下所示:

{
    "@odata.count": 1,
    "value": [
        {
            "@search.score": 19.593246,
            "@search.highlights": {
                "title": [
                    "<b>Divine</b> <b>Secrets</b> of the Ya-Ya Sisterhood"
                ]
            },
            "original_title": "Divine Secrets of the Ya-Ya Sisterhood",
            "title": "Divine Secrets of the Ya-Ya Sisterhood"
        }
    ]
}

早期服务上的短语突出显示

在 2020 年 7 月 15 日之前创建的搜索服务为短语查询实现不同的突出显示体验。

以下示例假设查询字符串包含以引号括住的短语“super bowl”。 在 2020 年 7 月之前,短语中的任何字词都会突出显示:

"@search.highlights": {
    "sentence": [
        "The <em>super</em> <em>bowl</em> is <em>super</em> awesome with a <em>bowl</em> of chips"
   ]

对于在 2020 年 7 月之后创建的搜索服务,只会在“@search.highlights”中返回与完整短语查询匹配的短语:

"@search.highlights": {
    "sentence": [
        "The <em>super</em> <em>bowl</em> is super awesome with a bowl of chips"
   ]

后续步骤

若要快速为客户端生成搜索页面,请考虑以下选项: