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

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

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

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

注意

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

集合筛选器为何受到限制

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

  1. 特定的数据类型仅支持特定的运算符。 例如,使用 ltgt 等运算符比较布尔值 truefalse 是没有意义的。
  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"

可能会返回提供一间豪华客房,并在另一间客房的描述中提到“市景”的酒店。 例如,以下 Id1 的文档将与查询相匹配:

{
  "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
city 1
花园 1
1
汽车旅馆 2
客房 1, 2
标准 1
套间 1
view 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')

正因如此,我们可以将 allneand 一起使用。

注意

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

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

相反的规则适用于 all

在对支持 ltgtlege 运算符的数据类型集合进行筛选时,可以使用更多种表达式,例如 Collection(Edm.Int32)。 具体而言,可以在 any 中使用 and 以及 or,前提是使用 and 将基础比较表达式合并到范围比较,然后使用 or 进一步合并。 这种布尔表达式结构称为析取范式 (DNF),也称为“AND 的 OR”。 相反,这些数据类型的all Lambda 表达式必须采用合取范式 (CNF),也称为“OR 的 AND”。 Azure AI 搜索之所以允许此类范围比较,是因为它可以有效地使用倒排索引执行这些比较,就像它可以针对字符串执行快速字词查找一样。

下面汇总了有关 Lambda 表达式中允许的元素的经验法则:

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

在实践中,你最有可能使用这些筛选器类型。 不过,了解筛选器选项的界限仍有帮助。

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

后续步骤