了解 Azure AI 搜索中的 OData 集合筛选器的工作原理
本文为使用复杂 lambda 表达式编写高级筛选器的开发人员提供了背景信息。 本文探讨了 Azure AI 搜索如何执行这些筛选器,并据此解释了为何需要为集合筛选器创建规则。
在 Azure AI 搜索中构建基于集合字段的筛选器时,可以结合使用 any
和 all
运算符与 lambda 表达式。 Lambda 表达式是引用范围变量的布尔表达式。 在使用 lambda 表达式的筛选器中,any
和 all
运算符类似于大多数编程语言中的 for
循环,范围变量充当循环变量的角色,而 Lambda 表达式充当循环的主体。 范围变量在循环迭代期间采用集合的“当前”值。
最起码,这在概念上解释了此类筛选器的工作原理。 在事实上,Azure AI 搜索实现筛选器的方式与 for
循环的工作原理有很大的不同。 理想情况下,你察觉不到这种差异,但在某些情况下你会感受到这种差异。 最终结果是,在编写 Lambda 表达式时必须遵循某些规则。
注意
有关集合筛选器有哪些可用的规则(包括示例)的信息,请参阅排查 Azure AI 搜索中的 OData 集合筛选器问题。
集合筛选器为何受到限制
并非所有类型的集合都完全支持筛选器功能,这有三个根本原因:
- 特定的数据类型仅支持特定的运算符。 例如,使用
lt
、gt
等运算符比较布尔值true
和false
是没有意义的。 - Azure AI 搜索不支持对
Collection(Edm.ComplexType)
类型的字段进行关联搜索。 - 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 |
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')
正因如此,我们可以将 all
与 ne
和 and
一起使用。
注意
尽管本文档并未详细介绍,但相同的原理也可以延伸到地理空间点集合的距离和交集测试。 因此,在 any
中:
geo.intersects
不可求反- 必须使用
lt
或le
比较geo.distance
- 必须使用
or
而不是and
合并表达式
相反的规则适用于 all
。
在对支持 lt
、gt
、le
和 ge
运算符的数据类型集合进行筛选时,可以使用更多种表达式,例如 Collection(Edm.Int32)
。 具体而言,可以在 any
中使用 and
以及 or
,前提是使用 and
将基础比较表达式合并到范围比较,然后使用 or
进一步合并。 这种布尔表达式结构称为析取范式 (DNF),也称为“AND 的 OR”。 相反,这些数据类型的all
Lambda 表达式必须采用合取范式 (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
。
在实践中,你最有可能使用这些筛选器类型。 不过,了解筛选器选项的界限仍有帮助。
有关允许和不允许的筛选器类型的具体示例,请参阅如何编写有效的集合筛选器。