了解 Azure AI 搜索中的 OData 集合筛选器的工作原理

注释

Azure AI 搜索可通过Azure门户、REST API 和Azure SDK获取。

本文为使用复杂 lambda 表达式编写高级筛选器的开发人员提供了背景信息。 本文探讨了 Azure AI 搜索如何执行这些筛选器,并据此解释了为何需要为集合筛选器创建规则。

在 Azure AI 搜索中构建基于集合字段的筛选器时,可以结合使用 any 和 all 运算符与 lambda 表达式。 Lambda 表达式是引用范围变量的布尔表达式。 在使用 lambda 表达式的筛选器中,any 和 all 运算符类似于大多数编程语言中的 for 循环,范围变量充当循环变量的角色,而 Lambda 表达式充当循环的主体。 范围变量在循环迭代期间采用集合的“当前”值。

至少,从概念上来说,它就是这么运作的。 在事实上,Azure AI 搜索实现筛选器的方式与 for 循环的工作原理有很大的不同。 理想情况下,你察觉不到这种差异,但在某些情况下你会感受到这种差异。 最终结果是,在编写 Lambda 表达式时必须遵循某些规则。

注释

有关集合筛选器有哪些可用的规则(包括示例)的信息,请参阅排查 Azure AI 搜索中的 OData 集合筛选器问题。

集合筛选器为何受到限制

并非所有类型的集合都完全支持筛选器功能,这有三个根本原因:

  1. 特定的数据类型仅支持特定的运算符。 例如,使用比较运算符(如 true 和 false)来比较布尔值 lt 和 gt 是没有意义的。
  2. Azure AI 搜索 支持对类型为 Collection(Edm.ComplexType) 的字段进行关联筛选,但对这些字段执行字段级全文搜索则是非关联的。
  3. Azure AI 搜索使用倒排索引对所有类型的数据(包括集合)执行筛选器。

第一个原因只是 OData 语言和 EDM 类型系统的定义方式造成的。 本文的余下部分将更详细地解释后两个原因。

对复杂对象的集合应用多个筛选条件时,这些条件是关联的,因为它们将应用于集合中的每个对象。 例如,以下筛选器将返回至少提供一间价格低于 100 的豪华客房的酒店:

    Rooms/any(room: room/Type eq 'Deluxe Room' and room/BaseRate lt 100)

如果筛选是不相关的,上述筛选器可能会返回一家酒店,其中有一间客房是豪华型,另一间客房的基础价格低于100。 该结果没有任何意义,因为 Lambda 表达式的两个子句将应用到同一个范围变量,即 room。 这就是此类筛选器相关联的原因。

但是,对于全文搜索,无法引用特定的范围变量。 如果使用字段搜索发出如下所示的完整 Lucene 查询:

    Rooms/Type:deluxe AND Rooms/Description:"city view"

你可能会遇到这样的酒店,其中一个房间是豪华的,而另一个房间的描述中提到“市景”。 例如,满足 Id 为 1 条件的以下文档,将与查询相匹配:

{
  "value": [
    {
      "Id": "1",
      "Rooms": [
        { "Type": "deluxe", "Description": "Large garden view suite" },
        { "Type": "standard", "Description": "Standard city view room" }
      ]
    },
    {
      "Id": "2",
      "Rooms": [
        { "Type": "deluxe", "Description": "Courtyard motel room" }
      ]
    }
  ]
}

原因在于,Rooms/Type 引用了整个文档中 Rooms/Type 字段的所有已分析字词,Rooms/Description 与此类似,如下表所示。

如何存储 Rooms/Type 以用于全文搜索:

Rooms/Type 中的字词 文档 ID
豪华 1, 2
标准 1

如何存储 Rooms/Description 以用于全文搜索:

Rooms/Description 中的字词 文档 ID
庭院 2
城市 1
花园 1
大 1
汽车旅馆 2
房间 1, 2
标准 1
套件 1
视图 1

简单而言,上述筛选器指出“匹配其中某间客房的 Type 等于‘豪华客房’,且同一间客房 的 BaseRate 小于 100 的文档”,而该搜索查询则与此不同,它指出“匹配其中的 Rooms/Type 包含字词‘豪华’且 Rooms/Description 包含短语‘市景’的文档”。 在后一种情况下,不存在可与其字段相关联的独立房间的概念。

倒排索引和集合

你可能会注意到,lambda 表达式对复杂集合的限制比简单集合的限制要少得多,例如 Collection(Edm.Int32) 和 Collection(Edm.GeographyPoint)。 存在这种差异,因为Azure AI 搜索将复杂集合存储为子文档的实际集合,而简单集合根本不作为集合存储。

例如,假设某个在线零售商的索引中包含类似于 seasons 的可筛选字符串集合字段。 上传到此索引的某些文档可能如下所示:

{
  "value": [
    {
      "id": "1",
      "name": "Hiking boots",
      "seasons": ["spring", "summer", "fall"]
    },
    {
      "id": "2",
      "name": "Rain jacket",
      "seasons": ["spring", "fall", "winter"]
    },
    {
      "id": "3",
      "name": "Parka",
      "seasons": ["winter"]
    }
  ]
}

seasons 字段的值存储在称作“倒排索引”的结构中,如下所示:

术语 文档 ID
春天 1, 2
夏 1
秋 1, 2
冬 2、3

此数据结构旨在快速解答一个问题:给定的字词出现在哪些文档中? 解答此问题的过程更像是一个普通的相等性检查,而不是基于集合的循环。 事实上,正因如此,对于字符串集合,Azure AI 搜索仅允许使用 eq 作为 any 的 Lambda 表达式中的比较运算符。

接下来,我们将探讨如何将同一范围变量的多个相等性检查与 or 组合在一起。 它得益于代数和限定符的分布属性。 此表达式:

    seasons/any(s: s eq 'winter' or s eq 'fall')

等效于:

    seasons/any(s: s eq 'winter') or seasons/any(s: s eq 'fall')

这两个 any 子表达式中的每一个都可以使用倒排索引有效地执行。 此外,由于量词的否定律,这个表达式:

    seasons/all(s: s ne 'winter' and s ne 'fall')

等效于:

    not seasons/any(s: s eq 'winter' or s eq 'fall')

正因如此,我们可以将 all 与 ne 和 and 一起使用。

注释

尽管本文档并未详细介绍,但相同的原理也可以延伸到地理空间点集合的距离和交集测试。 因此,在 any 中:

  • geo.intersects 不可求反
  • 必须使用geo.distance或lt来比较le
  • 必须使用 or 而不是 and 合并表达式

相反的规则适用于 all。

在对支持 lt、gt、le 和 ge 运算符的数据类型集合进行筛选时,可以使用更多种表达式,例如 Collection(Edm.Int32)。 具体而言,可以在 and 中使用 or 和 any,前提是通过 范围比较 将底层比较表达式合并为 and,然后使用 or 进一步合并。 布尔表达式的这种结构称为反正态形式(DNF),否则称为“AND 的 OR”。 相反,这些数据类型的 lambda 表达式 all 必须采用连接法线形式(CNF),否则称为“OR 的 AND”。 Azure AI 搜索之所以允许此类范围比较,是因为它可以有效地使用倒排索引执行这些比较,就像它可以针对字符串执行快速字词查找一样。

下面汇总了有关 Lambda 表达式中允许的内容的基本原则。

  • 在 any 中,总是允许 正检查,例如相等性、范围比较、geo.intersects,或者将 geo.distance 与 lt 或 le 比较(在检查距离时,将“接近”视为相等)。
  • 在 any 中,始终允许 or。 只能对可以表达范围检查的数据类型使用 and,并且只能在使用“AND 的 OR”(DNF) 时使用它。
  • 在 all 中,规则将会反转。 只允许负检查,始终可以使用 and,只能对表达为“用 AND 连接的 OR”(CNF) 的范围检查使用 or。

在实践中,这些类型的筛选器是你最有可能使用的。 了解事物可能性的界限仍然有帮助。

有关允许和不允许的筛选器类型的具体示例,请参阅如何编写有效的集合筛选器。

后续步骤