聚合

分组、投影和聚合函数

聚合是一种处理搜索查询结果的方式。聚合允许您对结果数据进行分组、排序和转换,并从中提取分析洞察。与在其他数据库和搜索引擎中的聚合查询非常相似,它们可用于创建分析报告,或执行分面搜索风格的查询。

例如,索引 Web 服务器的日志时,您可以按小时、国家或其他任何细分方式创建唯一用户报告。或者,您可以创建关于错误、警告等的不同报告。

核心概念

聚合查询的基本思想如下

  • 执行搜索查询,筛选出要处理的记录。
  • 构建一个操作管道,通过零个或多个序列转换结果
    • 分组和归约:按结果中的字段进行分组,并对每个组应用归约函数。
    • 排序:根据一个或多个字段对结果进行排序。
    • 应用转换:对管道中的字段应用数学和字符串函数,可以选择创建新字段或替换现有字段。
    • 限制:限制结果数量,无论结果如何排序。
    • 过滤:根据与值相关的谓词(查询后)对结果进行过滤。

管道是动态且可重入的,每个操作都可以重复。例如,您可以按属性 X 分组,按组大小对前 100 个结果进行排序,然后按属性 Y 分组,并按其他属性对结果进行排序,最后对输出应用转换。

图 1:聚合管道示例

聚合请求格式

聚合请求的语法定义如下

FT.AGGREGATE
  {index_name:string}
  {query_string:string}
  [VERBATIM]
  [LOAD {nargs:integer} {property:string} ...]
  [GROUPBY
    {nargs:integer} {property:string} ...
    REDUCE
      {FUNC:string}
      {nargs:integer} {arg:string} ...
      [AS {name:string}]
    ...
  ] ...
  [SORTBY
    {nargs:integer} {string} ...
    [MAX {num:integer}] ...
  ] ...
  [APPLY
    {EXPR:string}
    AS {name:string}
  ] ...
  [FILTER {EXPR:string}] ...
  [LIMIT {offset:integer} {num:integer} ] ...
  [PARAMS {nargs} {name} {value} ... ]

参数详情

可以接受可变数量参数的参数采用 param {nargs} {property_1... property_N} 的形式表示。参数的第一个参数是紧随其后的参数数量。这允许 Redis 避免在您的某个参数与另一个参数同名时出现解析歧义。例如,要按名字、姓氏和国家/地区排序,可以指定 SORTBY 6 firstName ASC lastName DESC country ASC

  • index_name:执行查询所针对的索引。

  • query_string:检索文档的基本过滤查询。其语法与搜索查询完全相同,包括过滤器、联合、非、可选等。

  • LOAD {nargs} {property} ...:从文档 HASH 对象加载文档字段。通常应避免这样做。聚合所需的字段应存储为 SORTABLE(并可选择 UNF 以避免任何归一化),这样它们可以以非常低的延迟提供给聚合管道。LOAD 会显著损害聚合查询的性能,因为每个处理过的记录都需要对 Redis 键执行相当于 HMGET 的操作,当对数百万个键执行此操作时,处理时间会非常高。可以使用 @__key 加载文档 ID。

  • GROUPBY {nargs} {property} ...:根据管道中的一个或多个属性对结果进行分组。每个组应至少有一个归约器(见下文),这是一个处理组条目的函数,用于计数或执行多个聚合操作(见下文)。

  • REDUCE {func} {nargs} {arg} ... [AS {name}]:使用归约函数将每个组中的匹配结果归约成一个记录。例如,COUNT 将计算组中记录的数量。有关可用归约器的更多详细信息,请参阅下面的“归约器”部分。

    归约器可以使用可选参数 AS {name} 指定自己的属性名称。如果未指定名称,则结果名称将是归约函数的名称和分组属性。例如,如果未为按属性 @foo 的 COUNT_DISTINCT 指定名称,则结果名称将为 count_distinct(@foo)

  • SORTBY {nargs} {property} {ASC|DESC} [MAX {num}]:使用属性列表对管道排序,直到 SORTBY 所在的位置。默认情况下,排序是升序的,但可以为每个属性添加 ASCDESCnargs 是排序参数的数量,包括 ASC 和 DESC。例如:SORTBY 4 @foo ASC @bar DESC

    MAX 用于优化排序,仅对前 n 个最大元素进行排序。尽管它与 LIMIT 无关,但在常见查询中通常只需要 SORTBY … MAX

  • APPLY {expr} AS {name}:对一个或多个属性应用一对一转换,并将结果存储为管道中的新属性,或使用此转换替换任何属性。expr 是一个表达式,可用于对数值属性执行算术运算,或根据属性类型(见下文)应用的函数,或它们的任意组合。例如:APPLY "sqrt(@foo)/log(@bar) + 5" AS baz 将在管道中动态评估此表达式,并将结果存储为一个名为 baz 的新属性,该属性可供后续的 APPLY / SORTBY / GROUPBY / REDUCE 操作引用。

  • LIMIT {offset} {num}。限制返回的结果数量,仅返回从索引 offset(从零开始)开始的 num 个结果。如上所述,如果您只想限制排序操作的输出,使用 SORTBY … MAX 会效率得多。

    但是,limit 可以用于限制结果而不排序,或用于对由 SORTBY MAX 确定的前 n 个最大结果进行分页。例如,获取前 100 个结果中的第 50-100 个结果最有效的方式是表示为 SORTBY 1 @foo MAX 100 LIMIT 50 50。从 SORTBY 中删除 MAX 将导致管道对所有记录进行排序,然后对第 50-100 个结果进行分页。

  • FILTER {expr}。使用与每个结果中的值相关的谓词表达式过滤结果。这些表达式在查询后应用,并与管道的当前状态相关。有关完整详细信息,请参阅下面的“FILTER 表达式”。

  • PARAMS {nargs} {name} {value}。定义一个或多个值参数。每个参数都有一个名称和一个值。参数可以在查询字符串中通过 $ 后跟参数名称来引用,例如 $user,搜索查询中对参数名称的每个此类引用都将由相应的参数值替换。例如,使用参数定义 PARAMS 4 lon 29.69465 lat 34.95126,表达式 @loc:[$lon $lat 10 km] 将被评估为 @loc:[29.69465 34.95126 10 km]。在不允许具体值的地方(例如字段名称 @loc 中),不能在查询字符串中引用参数。

示例

网站访问日志可能如下所示,每个记录都有以下字段/属性

  • url (文本,可排序)
  • timestamp (数字,可排序) - 访问条目的 Unix 时间戳。
  • country (标签,可排序)
  • user_id (文本,可排序,未索引)

示例 1:按小时计算唯一用户,按时间顺序排序。

第一步是确定索引名称和过滤查询。过滤查询 * 表示“获取所有记录”

FT.AGGREGATE myIndex "*"

接下来,按小时对结果进行分组。数据包含秒级分辨率的 Unix 时间戳格式的访问时间,因此您需要提取时间戳中的小时部分。为此,添加一个 APPLY 步骤,该步骤从时间戳中去除小时以下的信息,并将其存储为一个新属性,hour

FT.AGGREGATE myIndex "*"
  APPLY "@timestamp - (@timestamp % 3600)" AS hour

接下来,按小时对结果进行分组,并统计每小时的唯一用户 ID 数量。这通过 GROUPBY/REDUCE 步骤完成

FT.AGGREGATE myIndex "*"
  APPLY "@timestamp - (@timestamp % 3600)" AS hour

  GROUPBY 1 @hour
  	REDUCE COUNT_DISTINCT 1 @user_id AS num_users

接下来,按小时升序对结果进行排序

FT.AGGREGATE myIndex "*"
  APPLY "@timestamp - (@timestamp % 3600)" AS hour

  GROUPBY 1 @hour
  	REDUCE COUNT_DISTINCT 1 @user_id AS num_users

  SORTBY 2 @hour ASC

最后一步,将小时格式化为人类可读的时间戳。这通过调用转换函数 timefmt 完成,该函数用于格式化 Unix 时间戳。您可以指定一个格式传递给系统的 strftime 函数(参阅文档),但如果不指定,则等同于向 strftime 指定 %FT%TZ

FT.AGGREGATE myIndex "*"
  APPLY "@timestamp - (@timestamp % 3600)" AS hour

  GROUPBY 1 @hour
  	REDUCE COUNT_DISTINCT 1 @user_id AS num_users

  SORTBY 2 @hour ASC

  APPLY timefmt(@hour) AS hour

示例 2:按天和国家对特定 URL 的访问进行排序

下一个示例按 url 进行过滤,将时间戳转换为其日期部分,并按天和国家进行分组,统计每组的访问次数,然后按天升序、国家降序进行排序。

FT.AGGREGATE myIndex "@url:\"about.html\""
    APPLY "@timestamp - (@timestamp % 86400)" AS day
    GROUPBY 2 @day @country
    	REDUCE count 0 AS num_visits
    SORTBY 4 @day ASC @country DESC

GROUPBY 归约器

GROUPBY 的工作方式类似于 SQL 的 GROUP BY 子句,并根据每个记录中的一个或多个属性创建结果组。对于每个组,Redis 返回组键(即组中所有记录共有的值)以及零个或多个 REDUCE 子句的结果。

管道中的每个 GROUPBY 步骤都可以伴随零个或多个 REDUCE 子句。归约器对组中的每个记录应用累积函数,并将其归约成代表该组的单个记录。处理完成后,GROUPBY 步骤上游的所有记录都会发出其归约后的记录。

例如,最简单的归约器是 COUNT,它只是简单地计算每个组中的记录数量。

如果一个 GROUPBY 步骤存在多个 REDUCE 子句,每个归约器独立处理每个结果,并一次性写入最终输出。每个归约器可以使用可选参数 AS 指定自己的别名。如果未指定 AS,则别名是归约函数及其参数,例如 count_distinct(foo,bar)

支持的 GROUPBY 归约器

COUNT

格式

REDUCE COUNT 0

描述

计算每个组中的记录数量

COUNT_DISTINCT

格式

REDUCE COUNT_DISTINCT 1 {property}

描述

计算 property 的不同值的数量。

注意
该归约器为每个组创建一个哈希集合,并对每个记录进行哈希处理。如果组很大,这可能会消耗大量内存。

COUNT_DISTINCTISH

格式

REDUCE COUNT_DISTINCTISH 1 {property}

描述

与 COUNT_DISTINCT 相同,提供近似值而非精确计数,对于大型组消耗更少的内存和 CPU。

注意
该归约器为每个组使用HyperLogLog计数器,错误率约为 3%,每个组占用 1024 字节的恒定空间。这意味着它非常适合少数超大组,但不适合许多小组。在前一种情况下,它可能比 COUNT_DISTINCT 快一个数量级,并且消耗的内存少得多,但同样,它并不适合所有用例。

SUM

格式

REDUCE SUM 1 {property}

描述

返回组中给定属性的所有数值的总和。组中的非数值按 0 计算。

MIN

格式

REDUCE MIN 1 {property}

描述

返回属性的最小值,无论它是字符串、数字还是 NULL。

MAX

格式

REDUCE MAX 1 {property}

描述

返回属性的最大值,无论它是字符串、数字还是 NULL。

AVG

格式

REDUCE AVG 1 {property}

描述

返回数值属性的平均值。这等同于按总和和计数进行归约,然后将它们的比率作为一个 APPLY 步骤应用。

STDDEV

格式

REDUCE STDDEV 1 {property}

描述

返回组中数值属性的标准差

QUANTILE

格式

REDUCE QUANTILE 2 {property} {quantile}

描述

返回结果中给定分位数位置的数值属性值。分位数表示为 0 到 1 之间的数字。例如,中位数可以表示为 0.5 处的分位数,例如 REDUCE QUANTILE 2 @foo 0.5 AS median

如果需要多个分位数,只需为每个分位数重复 QUANTILE 归约器即可。例如,REDUCE QUANTILE 2 @foo 0.5 AS median REDUCE QUANTILE 2 @foo 0.99 AS p99

TOLIST

格式

REDUCE TOLIST 1 {property}

描述

将给定属性的所有不同值合并到一个数组中。

FIRST_VALUE

格式

REDUCE FIRST_VALUE {nargs} {property} [BY {property} [ASC|DESC]]

描述

返回组中给定属性的第一个或顶部值,可以选择将其与另一个属性进行比较。例如,您可以提取组中最年长用户的姓名

REDUCE FIRST_VALUE 4 @name BY @age DESC

如果未指定 BY,则返回组中遇到的第一个值。

如果您希望获取按相同值排序的组中的顶部或底部值,最好使用 MIN/MAX 归约器,但通过执行 REDUCE FIRST_VALUE 4 @foo BY @foo DESC 也可以达到相同的效果。

RANDOM_SAMPLE

格式

REDUCE RANDOM_SAMPLE {nargs} {property} {sample_size}

描述

对组元素进行指定大小的蓄水池抽样,并返回抽样项的均匀分布数组。

APPLY 表达式

APPLY 对每个记录中的一个或多个属性执行一对一转换。它将结果存储为管道中的新属性,或使用此转换替换任何属性。

转换表示为算术表达式和内置函数的组合。评估函数和表达式是递归嵌套的,可以无限制地组合。例如:sqrt(log(foo) * floor(@bar/baz)) + (3^@qaz % 6) 或简单地 @foo/@bar

如果表达式或函数应用于与预期类型不匹配的值,则不会发出错误,结果将设置为 NULL 值。

APPLY 步骤必须具有由 AS 参数确定的显式别名。

表达式中的字面量

  • 数字表示为整数或浮点数,例如 23.141-34inf-inf 也是可以接受的。
  • 字符串用单引号或双引号引用。双引号引用的字符串中可以使用单引号,反之亦然。标点符号可以用反斜杠转义。例如 "foo's bar"'foo\'s bar'"foo \"bar\""
  • 任何字面量或子表达式都可以用括号括起来以解决运算符优先级的歧义。

算术运算

对于数值表达式和属性,支持加法 (+)、减法 (-)、乘法 (*)、除法 (/)、取模 (%) 和幂 (^)。不支持位逻辑运算符。

请注意,这些运算符仅适用于数值和数值子表达式。例如,任何尝试将字符串乘以数字的操作都将导致 NULL 输出。

字段 APPLY 函数列表

函数 描述 示例
exists(s) 检查文档中是否存在某个字段。 exists(@field)

数值 APPLY 函数列表

函数 描述 示例
log(x) 返回数字、属性或子表达式的对数 log(@foo)
abs(x) 返回数值表达式的绝对值 abs(@foo-@bar)
ceil(x) 向上取整到不小于 x 的最小整数 ceil(@foo/3.14)
floor(x) 向下取整到不大于 x 的最大整数 floor(@foo/3.14)
log2(x) 返回 x 的以 2 为底的对数 log2(2^@foo)
exp(x) 返回 x 的指数,例如 e^x exp(@foo)
sqrt(x) 返回 x 的平方根 sqrt(@foo)

字符串 APPLY 函数列表

函数
upper(s) 返回 s 的大写形式 upper('hello world')
lower(s) 返回 s 的小写形式 lower("HELLO WORLD")
startswith(s1,s2) 如果 s2 是 s1 的前缀,则返回 1,否则返回 0 startswith(@field, "company")
contains(s1,s2) 返回 s2 在 s1 中出现的次数,否则返回 0。如果 s2 是空字符串,则返回 length(s1) + 1 contains(@field, "pa")
strlen(s) 返回 s 的长度 strlen(@t)
substr(s, offset, count) 返回 s 的子字符串,从 offset 开始,包含 count 个字符。
如果 offset 为负数,则表示从字符串末尾开始的距离。
如果 count 为 -1,则表示“从 offset 开始的字符串剩余部分”。
substr("hello", 0, 3)
substr("hello", -2, -1)
format( fmt, ...) 使用 fmt 后面的参数格式化字符串。
目前仅支持格式参数 %s,它适用于所有类型的参数。
format("Hello, %s, you are %s years old", @name, @age)
matched_terms([max_terms=100]) 返回与每个记录匹配的查询词(最多 100 个),作为一个列表。如果指定了限制,Redis 将根据查询顺序返回找到的前 N 个匹配项。 matched_terms()
split(s, [sep=","], [strip=" "]) 按字符串 sep 中的任何字符分割字符串,并去除 strip 中的任何字符。如果只指定了 s,则按逗号分割并去除空格。输出是一个数组。 split("foo,bar")

日期/时间 APPLY 函数列表

函数 描述
timefmt(x, [fmt]) 根据数值时间戳值 x 返回格式化的时间字符串。
参阅strftime 获取格式化选项。
不指定 fmt 等同于 %FT%TZ
parsetime(timesharing, [fmt]) timefmt() 的反向操作 - 使用给定的格式字符串解析时间格式
day(timestamp) 将 Unix 时间戳四舍五入到当前天的午夜 (00:00)。
hour(timestamp) 将 Unix 时间戳四舍五入到当前小时的开始。
minute(timestamp) 将 Unix 时间戳四舍五入到当前分钟的开始。
month(timestamp) 将 unix 时间戳四舍五入到当前月的开始。
dayofweek(timestamp) 将 Unix 时间戳转换为星期几数字(周日 = 0)。
dayofmonth(timestamp) 将 Unix 时间戳转换为当月第几天数字 (1 .. 31)。
dayofyear(timestamp) 将 Unix 时间戳转换为一年中的第几天数字 (0 .. 365)。
year(timestamp) 将 Unix 时间戳转换为当前年份(例如 2018)。
monthofyear(timestamp) 将 Unix 时间戳转换为当前月份 (0 .. 11)。

地理位置 APPLY 函数列表

函数 描述 示例
geodistance(field,field) 返回距离(以米为单位)。 geodistance(@field1,@field2)
geodistance(field,"lon,lat") 返回距离(以米为单位)。 geodistance(@field,"1.2,-3.4")
geodistance(field,lon,lat) 返回距离(以米为单位)。 geodistance(@field,1.2,-3.4)
geodistance("lon,lat",field) 返回距离(以米为单位)。 geodistance("1.2,-3.4",@field)
geodistance("lon,lat","lon,lat") 返回距离(以米为单位)。 geodistance("1.2,-3.4","5.6,-7.8")
geodistance("lon,lat",lon,lat) 返回距离(以米为单位)。 geodistance("1.2,-3.4",5.6,-7.8)
geodistance(lon,lat,field) 返回距离(以米为单位)。 geodistance(1.2,-3.4,@field)
geodistance(lon,lat,"lon,lat") 返回距离(以米为单位)。 geodistance(1.2,-3.4,"5.6,-7.8")
geodistance(lon,lat,lon,lat) 返回距离(以米为单位)。 geodistance(1.2,-3.4,5.6,-7.8)
FT.AGGREGATE myIdx "*"  LOAD 1 location  APPLY "geodistance(@location,\"-1.1,2.2\")" AS dist

要检索距离

FT.AGGREGATE myIdx "*"  LOAD 1 location  APPLY "geodistance(@location,\"-1.1,2.2\")" AS dist

注意:必须使用 LOAD 预加载地理字段。

结果也可以按距离排序

FT.AGGREGATE idx "*" LOAD 1 @location FILTER "exists(@location)" APPLY "geodistance(@location,-117.824722,33.68590)" AS dist SORTBY 2 @dist DESC

注意:确保没有缺少位置,否则 SORTBY 将不返回任何结果。使用 FILTER 确保您对所有有效位置进行排序。

FILTER 表达式

FILTER 表达式使用与结果集中的值相关的谓词来过滤结果。

FILTER 表达式在查询后进行评估,并与管道的当前状态相关。因此,它们对于根据组计算修剪结果非常有用。请注意,过滤器未索引,不会加快处理速度。

FILTER 表达式遵循 APPLY 表达式的语法,并添加了条件 ==!=<<=>>=。两个或多个谓词可以使用逻辑 AND (&&) 和 OR (||) 组合。单个谓词可以使用 NOT 前缀 (!) 否定。

例如,过滤用户名为“foo”且年龄小于 20 的所有结果,可以表示为

FT.AGGREGATE
  ...
  FILTER "@name=='foo' && @age < 20"
  ...

可以添加多个过滤步骤,尽管在管道的同一阶段,将多个谓词组合到一个过滤步骤中更有效。

游标 API

FT.AGGREGATE ... WITHCURSOR [COUNT {read size} MAXIDLE {idle timeout}]
FT.CURSOR READ {idx} {cid} [COUNT {read size}]
FT.CURSOR DEL {idx} {cid}

您可以使用游标与FT.AGGREGATE命令结合使用,使用 WITHCURSOR 关键字。游标允许您仅消费响应的一部分,从而可以根据需要获取更多结果。这比使用带有 offset 的 LIMIT 要快得多,因为查询只执行一次,其状态存储在服务器上。

要使用游标,请在FT.AGGREGATE中指定 WITHCURSOR 关键字。例如

FT.AGGREGATE idx * WITHCURSOR

这将返回一个包含两个元素的数组响应。第一个元素是实际的(部分)结果,第二个元素是游标 ID。然后可以将游标 ID 反复提供给FT.CURSOR READ,直到游标 ID 为 0,表示所有结果已返回。

要从现有游标读取,请使用FT.CURSOR READ。例如

FT.CURSOR READ idx 342459320

假设 342459320FT.AGGREGATE请求返回的游标 ID,这里是一个伪代码示例

response, cursor = FT.AGGREGATE "idx" "redis" "WITHCURSOR";
while (1) {
  processResponse(response)
  if (!cursor) {
    break;
  }
  response, cursor = FT.CURSOR read "idx" cursor
}

请注意,即使游标为 0,也可能仍会返回部分结果。

游标设置

读取大小

您可以使用 COUNT 参数控制每次游标获取读取多少行。此参数可以在FT.AGGREGATE(紧随 WITHCURSOR 之后)或FT.CURSOR READ中指定。

以下示例每次将读取 10 行

FT.AGGREGATE idx query WITHCURSOR COUNT 10

您也可以在 CURSOR READ 中指定 COUNT 来覆盖此设置。以下示例最多将返回 50 个结果

FT.CURSOR READ idx 342459320 COUNT 50

默认读取大小为 1000。

超时和限制

由于游标是有状态资源,会占用服务器内存,因此它们的生命周期有限。为防止孤儿/过期游标,游标具有空闲超时值。如果在空闲超时前游标上没有任何活动,则游标将被删除。每次使用 CURSOR READ 读取游标时,空闲计时器都会重置为 0。

默认的空闲超时为 300000 毫秒(或 300 秒)。您可以在创建游标时使用 MAXIDLE 关键字修改空闲超时。请注意,该值不能超过默认的 300 秒。

例如,要设置十秒的限制

FT.AGGREGATE idx query WITHCURSOR MAXIDLE 10000

其他游标命令

游标可以使用 CURSOR DEL 命令显式删除。例如

FT.CURSOR DEL idx 342459320

请注意,如果游标的所有结果已返回,或已超时,则游标将自动删除。

所有空闲游标可以使用 FT.CURSOR GC idx 0 命令同时强制清除。默认情况下,Redis 使用一种延迟节流的方法进行垃圾回收,每 500 次操作或每秒(以较晚者为准)收集空闲游标。

评价本页
返回顶部 ↑