RediSearch模块

人们在使用Redis的时候,通常只会使用Redis构建一些功能较为单一 的应用,比如稍早之前介绍过的锁、自动补全、排行榜、社交关系 等;但有些人也会使用Redis构建非常复杂的应用,比如全文搜索引 擎。因为全文搜索引擎的复杂性及其底层数据结构的特殊性,仅使用 Redis提供的现成数据结构是很难实现一个功能完备的全文搜索引擎 的,即使勉强实现了效率也不会太高。

为了解决上述问题,RedisLabs基于Redis的模块机制,在Redis顶层实 现了全文搜索引擎RediSearch( https://github.com/RedisLabsModules/RediSearch )。这个全文搜 索引擎的功能非常强大,它能够为多种语言的文档建立索引,并通过 高效的反向索引数据结构快速地进行检索操作。

本节我们将会学习RediSearch的基本使用方法,其中包括:

·如何下载、编译和载入RediSearch模块。 ·如何使用RediSearch建立索引。 ·如何将文档添加至索引。 ·如何在索引中实施检索。

在本节的最后,我们还会更详细地了解到RediSearch最常见的一系列API。

下载与编译

与之前介绍过的两个模块一样,下载RediSearch模块源码的操作也可 以通过执行以下命令来完成:

$ git clone https://github.com/RedisLabsModules/RediSearch.git
正复制到'RediSearch'...
remote: Enumerating objects: 268, done.
remote: Counting objects: 100% (268/268), done.
remote: Compressing objects: 100% (170/170), done.
remote: Total 19951 (delta 162), reused 158 (delta 97), pack-reused 19683
接收对象中: 100% (19951/19951), 15.20 MiB | 5.33 MiB/s,完成.
处理delta中: 100% (14181/14181),完成.

在取得RediSearch模块的源码之后,我们只需要进入源码目录,然后 执行make命令即可开始编译:

$ cd RediSearch/
$ make
*** Raw Makefile build uses CMake. Use CMake directly!
*** e.g.
mkdir build && cd build
cmake .. && make && redis-server --loadmodule ./redisearch.so
***
...
Scanning dependencies of target metaphone
[ 1%] Building C object src/dep/phonetics/CMakeFiles/metaphone.dir/double_
metaphone.c.o
[ 1%] Built target metaphone
...
[100%] Linking C executable test_priority_queue
[100%] Built target test_priority_queue
cp build-compat/redisearch.so src
cp build-compat/redisearch.so src/redisearch.so

编译RediSearch需要用到CMake,如果你的计 算机尚未安装CMake,那么请先安装CMake,然后再执行编译操作。

编译完成的模块文件redisearch.so将被放到源码文件夹的/src文件夹 当中,我们只需要在Redis客户端中载入这个模块文件即可:

redis> MODULE LOAD /Users/huangz/RediSearch/src/redisearch.so
OK

使用示例

RediSearch支持非常丰富的功能,但它最重要的功能还是实施全文检 索。本节将会介绍使用RediSearch构建全文索引并执行检索操作的具 体方法。

首先,构建索引的工作可以通过执行FT.CREATE命令来完成:

redis> MODULE LOAD /Users/huangz/RediSearch/src/redisearch.so
OK

在上述命令中,我们创建了一个名为databases的索引,该索引可以存 储包含title和description两个字段的文档。

在建立起索引之后,可以通过FT.ADD命令将文档添加至索引。比如在 以下代码中,我们就将3个不同的文档添加到了databases索引中:

redis> FT.ADD databases doc1 1.0 LANGUAGE "chinese" FIELDS title "Redis" description "Redis是一个
使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。"
OK
redis> FT.ADD databases doc2 1.0 LANGUAGE "chinese" FIELDS title "MySQL" descri
ption "MySQL是一个开放源代码的关系数据库管理系统。"
OK
redis> FT.ADD databases doc3 1.0 LANGUAGE "chinese" FIELDS title "PostgreSQL" descri
ption "PostgreSQL是自由的对象-关系数据库服务器。"
O

以上面执行的第一条FT.ADD命令为例,databases、doc1和1.0分别为 文档所属的索引、文档的ID以及文档的分值,而紧接着的 LANGUAGE"chinese"则指明了该文档使用的语言,至于之后的FIELDS选 项则分别提供了文档title字段的值以及description字段的值。

在将文档添加到索引之后,我们就可以检索索引中的文档了。在以下 两次命令调用中,我们分别在索引中查找了与"Redis"相关的文档以及 与"关系数据库"相关的文档:

redis> FT.SEARCH databases "Redis" LANGUAGE "chinese"
1
doc1
title
Redis
description
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。
redis> FT.SEARCH databases "关系数据库" LANGUAGE "chinese"
2
doc3
title
PostgreSQL
description
PostgreSQL是自由的对象-关系数据库服务器。
doc2
title
MySQL
description
MySQL是一个开放源代码的关系数据库管理系统。

根据命令的执行结果可知,第一次检索返回了一个文档,而第二次检 索则返回了两个文档。

API简介

在了解了RediSearch的基本用法之后,现在是时候学习更多 RediSearch的常见API使用方法了。因为RediSearch具有非常强大的功 能,所以它的API非常多,也非常复杂。本节只会对RediSearch最常用 的API做基本介绍,完整的API参考信息可以查看RediSearch的命令文 档( https://oss.redislabs.com/redisearch/Commands.html )。

  1. 创建索引

    用户可以通过FT.CREATE命令创建具有指定名字的索引。每个索引可以 包含任意多个字段,字段的类型可以是文本、数字、地理位置或者标签:

    FT.CREATE name SCHEMA [field [WEIGHT n] [TEXT | NUMERIC | GEO | TAG] [SORTABLE]] [field ...]

    通过可选的WEIGHT选项,用户可以为每个字段设置相应的权重,如果 不设置则默认使用1.0作为权重。

    举个例子,通过执行以下命令,我们可以创建出名为locations的索 引,该索引带有文本字段user和地理位置字段location:

    redis> FT.CREATE locations SCHEMA user TEXT location GEO
    OK

    又比如,通过执行以下命令,我们可以创建出名为categories的索 引,它带有文本字段item、数字字段price和标签字段tags:

    redis> FT.CREATE categories SCHEMA item TEXT price NUMERIC tags TAG
    OK

    字段的另一个可选项SORTABLE可以将字段设置为可排序字段,带有该 属性的字段可以在实施检索时通过SORTBY选项进行排序。

    FT.CREATE命令的复杂度为O(1),它在执行成功时返回OK,执行失败 时返回相应的错误。

  2. 添加文档至索引

在创建索引之后,用户可以通过执行FT.ADD命令将给定的文档添加到索引中:

FT.ADD index docId score FIELDS field value [field value]

最基本的FT.ADD命令接受index、docId、score以及任意多个fieldvalue键值对作为参数,其中:

  • index用于指定文档所属的索引,docId用于指定文档ID,score用于 指定文档分值。文档的分值可以介于0.0至1.0之间,如果没有特殊要 求则一般设置为1.0。

  • FIELDS选项之后的任意多个field-value键值对则用于指定文档包含 的键值对。

举个例子,通过执行以下命令,我们可以将物品MI8SE添加到索引中:

redis> FT.ADD categories item10086 1.0 FIELDS item MI8SE price 1999 tags "phone,MI,4G"
OK

其中categories为索引的名字,item10086为文档ID,1.0为分值,而 文档包含的3个键值对分别为item-MI8SE、price-1999和tags- "phone,MI,4G"。

(1)设置文档的语言 用户在将文档添加至索引的时候,可以通过可选的LANGUAGE选项为文 档设置相应的语言,以便RediSearch根据语言对文档做相应的处理:

FT.ADD index docId score [LANGUAGE lang] FIELDS field value [field value]

LANGUAGE选项的值可以是以下任意一个,它们分别对应阿拉伯语、中 文、丹麦语、荷兰语、英语、芬兰语等多种语言:

·"arabic" ·"chinese" ·"danish" ·"dutch" ·"english" ·"finnish" ·"french" ·"german" ·"hungarian" ·"italian" ·"norwegian" ·"portuguese" ·"romanian" ·"russian" ·"spanish" ·"swedish" ·"tamil" ·"turkish"

注意,必须根据文档内容设置正确的语言,因为不正确的语言设置将 导致文档无法被检索。比如,如果我们在添加中文文档的时候不将语 言设置为中文,那么之后的搜索结果将会出现错误:

# 创建索引
redis> FT.CREATE memo SCHEMA content TEXT
OK
# 添加中文文档
redis> FT.ADD memo m1 1.0 LANGUAGE "chinese" FIELDS content "我爱吃鸡蛋"
OK
# 添加非中文文档(默认做英文处理)
redis> FT.ADD memo m2 1.0 FIELDS content "别忘了买鸡蛋"
OK
# 搜索“鸡蛋”只能检索到一个文档
redis> FT.SEARCH memo "鸡蛋"
1
m1
content
我爱吃鸡蛋

(2)只索引而不存储文档 如果用户在执行FT.ADD命令时给定了可选的NOSAVE选项,那么命令只 会为文档建立索引,并不会存储文档本身:

FT.ADD index docId score [NOSAVE] FIELDS field value [field value]

单纯被索引而未被存储的文档在被检索的时候只会返回文档的ID而不 会返回文档本身。

举个例子,如果我们在索引memo中添加一个与西红柿相关的文档,但 是并不存储它:

redis> FT.ADD memo m3 1.0 NOSAVE LANGUAGE "chinese" FIELDS content "西红柿炒鸡蛋是我最喜欢的菜式之一"
OK

那么当我们尝试检索这个文档的时候,命令只会返回被匹配文档的ID ——m3,而不是文档本身:

# 找到一条结果,它的ID为m3
redis> FT.SEARCH memo "西红柿" LANGUAGE "chinese"
1
m3

(3)使用新文档代替旧文档 如果用户在执行FT.ADD命令时给定了可选的REPLACE选项,并且索引中 存在与给定ID相同的文档,那么命令将使用新文档代替旧文档,效果 与先移除旧文档然后再添加新文档一样:

FT.ADD index docId score [REPLACE] FIELDS field value [field value]

举个例子,假设我们建立了date-memo索引用于存储带有日期的备忘 录,它的定义如下:

redis> FT.CREATE date-memo SCHEMA date TEXT content TEXT
OK

在建立这个索引之后,我们向它添加了ID为m1的文档:

redis> FT.ADD date-memo m1 1.0 LANGUAGE "chinese" FIELDS date "2020.5.6" content "去张三家吃饭"
OK
redis> FT.GET date-memo m1
date
2020.5.6
content
去张三家吃饭

之后,如果我们再次向该索引添加ID为m1的文档,并给定可选项 REPLACE,那么旧的m1文档将被替换成新的m1文档:

redis> FT.ADD date-memo m1 1.0 REPLACE LANGUAGE "chinese" FIELDS date "2022.1.1" content "去时代
广场庆祝元旦"
OK
redis> FT.GET date-memo m1
date
2022.1.1
content
去时代广场庆祝元旦

与此相反,如果我们尝试向索引添加ID重复的文档,但是并不使用 REPLACE选项,那么命令将拒绝执行这个添加操作:

redis> FT.ADD date-memo m1 1.0 LANGUAGE "chinese" FIELDS date "2021.6.1" content "儿童节快乐!"
Document already exists

(4)部分更新文档 在一些情况下,我们可能只是想要更新已有文档中的某些字段,而不 是要替换整个文档,这时就需要用到PARTIAL可选项了:

FT.ADD index docId score [REPLACE PARTIAL] FIELDS field value [field value]

因为部分更新是文档更新的一种特殊情形,所以PARTIAL可选项必须与 REPLACE可选项同时使用。在默认情况下,用户在定义索引的时候设定 文档有多少个字段,我们在添加文档的时候就需要提供多少个字段, 但是在启用了PARTIAL可选项并且旧文档已经存在的情况下,用户只需 要给定想要更新的字段及其新值就可以了,至于那些未给定新值的字 段将继续沿用已有的旧值。

举个例子,对于以下这个备忘:

redis> FT.GET date-memo m1
date
2022.1.1
content
去时代广场庆祝元旦

如果我们不想修改备忘的时间而只想修改备忘的内容,那么只需要执 行以下命令即可:

redis> FT.ADD date-memo m1 1.0 REPLACE PARTIAL LANGUAGE "chinese" FIELDS content "去江滨公园参加元
旦倒数活动"
OK
redis> FT.GET date-memo m1
date
2022.1.1
content
去江滨公园参加元旦倒数活动

FT.ADD命令的复杂度为O(N),其中N为文档包含的字段总数。这个命 令在成功执行时返回OK,出错时返回相应的错误信息。

  1. 添加散列至索引

为了让用户可以快速地将存储在Redis散列中的文档添加至散列, RediSearch提供了FT.ADDHASH命令:

FT.ADDHASH index hash score [LANGUAGE lang] [REPLACE]

这个命令接受的参数与FT.ADD命令基本相同,唯一的不同之处在于, 用户只需要在hash参数中提供散列的键名,命令就会把散列中包含的 字段和值作为文档添加到索引中。

比如,假设有以下散列:

redis> HGETALL m2
date
2021.8.1
content
庆祝建军节

那么我们可以通过执行以下命令,将这个散列(文档)添加到索引中:

redis> FT.ADDHASH date-memo m2 1.0 LANGUAGE "chinese"
OK

之后我们就可以基于这个文档进行检索和执行取值操作了:

redis> FT.GET date-memo m2
date
2021.8.1
content
庆祝建军节
redis> FT.SEARCH date-memo "建军节" LANGUAGE "chinese"
1
m2
date
2021.8.1
content
庆祝建军节

FT.ADDHASH命令的复杂度为O(N),其中N为散列包含的字段数量。这 个命令在成功执行时返回OK,出错时返回相应的错误信息。

  1. 查看索引信息

在创建索引之后,用户可以通过执行FT.INFO命令查看索引的相关信 息,比如索引包含的文档数量、索引包含的唯一字段数量、索引在内 存中的分布信息等:

FT.INFO index

以下是对索引date-memo执行FT.INFO命令的结果:

redis> FT.INFO date-memo
index_name
date-memo
index_options
fields
date
type
TEXT
WEIGHT
1
content
type
TEXT
WEIGHT
1
num_docs
2
max_doc_id
7
num_terms
23
num_records
10
inverted_sz_mb
5.7220458984375e-05
total_inverted_index_blocks
96
offset_vectors_sz_mb
2.6702880859375e-05
doc_table_size_mb
0.00057220458984375
sortable_values_size_mb
0
key_table_size_mb
6.389617919921875e-05
records_per_doc_avg
5
bytes_per_record_avg
6
offsets_per_term_avg
2.7999999999999998
offset_bits_per_record_avg
8
gc_stats
current_hz
1
bytes_collected
108
effectiv_cycles_rate
0.0023544800523217788
cursor_stats
global_idle
0
global_total
0
index_capacity
128
index_total
0

FT.INFO命令的复杂度为O(1),它在成功执行时将返回一个嵌套数组作为结果。

  1. 检索文档

通过执行FT.SEARCH命令,用户可以根据给定的文本在索引中查找与之匹配的文档:

FT.SEARCH index query [LANGUAGE lang]

最基本的FT.SEARCH命令只需要给定两个参数,其中index用于指定被 检索索引的名字,而query则用于指定被查找的文本,如果该文本使用 的不是默认的英语,那么用户还需要通过LANGUAGE选项指定检索时使 用的语言。

比如,通过执行以下命令,我们可以从索引databases中检索与中文文 本"数据库"有关的文档:

redis> FT.SEARCH databases "数据库" LANGUAGE "chinese"
3
doc3
title
PostgreSQL
description
PostgreSQL是自由的对象-关系数据库服务器。
doc2
title
MySQL
description
MySQL是一个开放源代码的关系数据库管理系统。
doc1
title
Redis
description
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。

FT.SEARCH命令的复杂度为O(N*M),其中N为被检索文档的数量,而M 则是文本包含的单词数量。这个命令在执行成功时将返回一个数组作 为结果,数组的第一个元素为被匹配文档的总数量,而后续元素则为 被匹配的文档或文档ID。

(1)只返回匹配文档的ID 如果用户在检索时只想获取被匹配文档的ID而不是文档本身,那么只需要在执行命令的时候提供NOCONTENT可选项即可:

FT.SEARCH index query [NOCONTENT]

以下是一个使用NOCONTENT可选项的例子:

redis> FT.SEARCH databases "数据库" LANGUAGE "chinese" NOCONTENT
3
doc3
doc2
doc1

可以看到,FT.SEARCH命令只返回了被匹配文档的ID而没有返回文档本 身。 (2)只返回指定的字段 在默认情况下,FT.SEARCH命令将返回文档包含的所有字段,但用户也 可以通过RETURN可选项让命令只返回指定的字段:

FT.SEARCH index query [RETURN num field ...]

用户需要通过num参数指定自己想要返回的字段数量,并在之后给出具 体字段的名字。

举个例子,假设我们现在想要在databases索引中检索与关键字"数据 库"有关的文档,并且只想获取文档中的title字段,那么只需要执行 以下命令即可:

redis> FT.SEARCH databases "数据库" LANGUAGE "chinese" RETURN 1 "title"
3
doc3
title
PostgreSQL
doc2
title
MySQL
doc1
title
Redis

可以看到,除了必须返回的文档ID之外,检索结果现在只会返回文档 的title字段。

(3)只返回指定数值区间内的文档 通过使用可选的FILTER选项,用户可以让FT.SEARCH命令只返回结果文 档中,指定数值字段符合给定区间的文档:

FT.SEARCH index query [FILTER numeric_field min max]

FILTER字段接受numeric_field、min和max这3个参数作为输入,其中 numeric_field参数用于指定文档中的数值字段,而min和max参数则用 于指定具体的数值区间。这个数值区间可以像ZRANGE之类的命令一 样,使用-inf和+inf来指定无限值,或者使用左括号(表示开区间。

举个例子,假设现在有一个日志索引logs,它的定义及包含的文档如下:

# 使用SORTABLE选项将time字段设置为可排序字段,以便之后进行排序
redis> FT.CREATE logs 1.0 SCHEMA time NUMERIC SORTABLE content TEXT
OK
redis> FT.ADD logs log1 1.0 FIELDS time 1500000000 content "log1: blah blah"
OK
redis> FT.ADD logs log2 1.0 FIELDS time 1600000000 content "log2: blah blah"
OK
redis> FT.ADD logs log3 1.0 FIELDS time 1700000000 content "log3: blah blah"
OK
redis> FT.ADD logs log4 1.0 FIELDS time 1800000000 content "log4: blah blah"
OK
redis> FT.ADD logs log5 1.0 FIELDS time 1900000000 content "log5: blah blah"
OK

如果我们尝试在索引中查找所有包含单词"blah"的文档,那么命令将返回5个文档:

redis> FT.SEARCH logs "blah" NOCONTENT
5
log5
log4
log3
log2
log1

但如果我们使用FILTER选项,让命令只返回结果文档中time字段小于 等于1700000000的文档,那么命令将只返回3个文档:

redis> FT.SEARCH logs "blah" FILTER "time" -inf 1700000000 NOCONTENT
3
log3
log2
log1

(4)根据指定规则排序 通过SORTBY可选项,用户可以让FT.SEARCH命令根据指定字段的大小按 顺序或者逆序返回结果:

FT.SEARCH index query [SORTBY field [ASC|DESC]]

比如,通过执行以下命令,我们可以根据time字段的大小排序搜索结果:

# 根据time的大小顺序排序(从小到大)
redis> FT.SEARCH logs "blah" SORTBY time ASC NOCONTENT
5
log1
log2
log3
log4
log5
# 根据time的大小逆序排序(从大到小)
redis> FT.SEARCH logs "blah" SORTBY time DESC NOCONTENT
5
log5
log4
log3
log2
log1

ASC参数或DESC参数是可选的,如果省略,那么SORTBY默认使用ASC作 为参数,因此以下两条命令是完全等效的:

FT.SEARCH logs "blah" SORTBY time ASC NOCONTENT
FT.SEARCH logs "blah" SORTBY time NOCONTENT

使用SORTBY排序的字段必须在创建索引时使用 SORTABLE可选项进行声明,否则SORTBY将得出错误的结果。

(5)限制命令返回的结果数量 通过LIMIT选项,用户可以让FT.SEARCH命令只返回指定数量的文档:

FT.SEARCH index query [LIMIT offset num]

选项中的offset参数用于指定在返回结果前需要跳过的文档数量,而 num参数则用于指定需要返回的最大文档数量。

比如,假设我们想要让命令返回排在结果最前面的3个文档,那么可以 执行以下命令:

redis> FT.SEARCH logs "blah" LIMIT 0 3 NOCONTENT
5
log5
log4
log3

又比如,如果我们想要让命令跳过结果中的第一个文档,并返回之后 跟着的两个文档,那么可以执行以下命令:

redis> FT.SEARCH logs "blah" LIMIT 1 2 NOCONTENT
5
log4
log3
  1. 从索引中获取文档

用户可以通过FT.GET命令或者FT.MGET命令,从指定的索引中获取一个或多个文档:

FT.GET index docId
FT.MGET index docId [docId ...]

比如,通过执行以下命令,我们可以从logs索引中取出ID为log1的文档:

redis> FT.GET logs log1
time
1500000000
content
log1: blah blah

或者通过执行以下命令,从logs索引中取出ID为log1、log2和log3的这3个文档:

redis> FT.MGET logs log1 log2 log3
time
1500000000
content
log1: blah blah
time
1600000000
content
log2: blah blah
time
1700000000
content
log3: blah blah

如果用户给定的索引或文档ID不存在,那么命令将返回相应的空值或错误:

# 文档不存在
redis> FT.GET logs log10086
# 索引不存在
redis> FT.GET not_exists_index doc1
Unknown Index name

FT.GET命令的复杂度为O(1),而FT.MGET命令的复杂度则为O(N), 其中N为用户给定的文档数量。

  1. 从索引中移除文档 当用户不再需要索引中的某个文档时,可以通过执行以下命令将其移除出索引:

FT.DEL index docID

比如,通过执行以下命令,我们可以从索引logs中移除ID为log5的文档:

redis> FT.DEL logs log5
1

注意,单纯地执行FT.DEL只会将文档移除出索引,但是并不会删除被 移除的文档。因此我们仍然可以像之前一样,使用FT.GET命令获取被 移除的log5文档:

redis> FT.GET logs log5
time
1900000000
content
log5: blah blah

不过由于log5文档已经被移除出索引logs了,所以它不会再出现在检 索结果中:

redis> FT.SEARCH "logs" "blah" NOCONTENT
4
log4
log3
log2
log1

如果用户想要在移除索引文档的同时将文档一并删除,就需要在执行 命令的同时使用可选项DD(Delete Document,删除文档):

FT.DEL index docID [DD]

作为例子,以下代码演示了从索引中移除并删除log4文档的方法:

redis> FT.DEL logs log4 DD
1

在此之后,我们将无法再获取log4文档:

redis> FT.GET logs log4

也无法再在检索操作中查找这个文档:

redis> FT.SEARCH "logs" "blah" NOCONTENT
3
log3
log2
log1

FT.DEL命令的复杂度为O(1),它在成功移除文档时返回1,因为文档 不存在等原因而导致移除失败时返回0。

  1. 移除索引

当用户不再需要某个索引的时候,可以通过执行以下命令将其移除:

FT.DROP index [KEEPDOCS]

在默认情况下,FT.DROP命令在移除索引的同时也会删除索引中的所有 文档,如果用户想要保留被索引的文档,那么只需要在执行命令的时 候使用KEEPDOCS可选项即可。

比如,通过执行以下命令,我们可以移除logs索引并删除其属下的所 有文档:

redis> FT.DROP logs
OK

执行以下命令将移除date-memo索引,但是索引中的所有文档都会被保留:

redis> FT.DROP date-memo KEEPDOCS
OK

FT.DROP命令的复杂度为O(N),其中N为命令执行时被删除的文档数 量。这个命令在执行成功时返回OK,出错时返回相应的错误。