Python全文搜索引擎库之redisearch-py

redisearch-py是一个使用RediSearch Redis 模块API 的 Python 搜索引擎库。它是 RediSearch 的“官方”客户端,应该被视为其规范的客户端实现。

创建redisearch-py实例

当您创建 redisearch-py 客户端实例时,唯一需要的参数是索引的名称。

1
2
3
from redisearch import Client

client = Client("my-index")

要使用用户名和/或密码进行连接,请将这些选项传递给客户端初始化程序。

1
client = Client("my-index", host='localhost', port='6666', password="my-password")

检查 RediSearch 索引是否存在

要检查 RediSearch 索引是否存在,请使用命令并在索引不存在时FT.INFO捕获引发的错误。ResponseError

1
2
3
4
5
6
7
8
9
from redis import ResponseError
from redisearch import Client

client = Client("my-index")

try:
client.info()
except ResponseError:
# Index does not exist. We need to create it!

但是实际开发中我们通常会在创建索引前或先删除已有的索引(类比sql建表)

1
2
3
4
5
6
7
8
9
10
from redisearch.client import Client, Query
from redisearch import TextField

client = Client('idx')
try:
client.drop_index()
except:
pass

client.create_index([TextField('txt')])

索引

创建索引

您只需要在创建索引时,使用IndexDefinition实例来定义一个搜索索引。

RediSearch 索引通过观察键前缀来跟踪 Redis 数据库中的hash。如果从 Redis 添加、更新或删除以客户端配置的键前缀开头的hash,RediSearch 将在索引中进行同步变更。您可以在初始化RediSearch客户端时,在IndexDefinition参数中配置键前缀。

注意:创建索引后,RediSearch 会在这些键的哈希值发生变化时持续索引这些键。

IndexDefinition也需要一个SCHEMA。以指定要从索引遵循的哈希中索引哪些字段。字段类型有:

  • TextField
  • TagField
  • NumericField
  • GeoField

有关这些字段类型含义的更多信息,请参阅有关命令的RediSearch 文档FT.CREATE

使用 redisearch-py,SCHEMA是一个可迭代的Field实例。拥有IndexDefinition实例后,您可以通过将字段列表传递给create_index()方法来创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from redis import ResponseError
from redisearch import Client, IndexDefinition, TextField

SCHEMA = (
TextField("title", weight=5.0),
TextField("body")
)

client = Client("idx:blog")

definition = IndexDefinition(prefix=['blog:'])

try:
client.drop_index()
except:
pass

# 如果索引存在则删除重建
client.create_index(SCHEMA, definition=definition)

修改索引

修改索引的方法,redisearch-py说明中并没有提到,但我从源码中看到其封装了一个FT.ALTER的命令,而RediSearch修改索引的命令就是用的这个。

需要说明的是,该修改并非无限制的修改,首先索引必须存在是前提,其次已存在的属性是不允许修改的,仅能新增属性。

1
2
3
4
# 为索引增加国家字段
res = client.alter_schema_add((
TagField("country")
))

删除索引

1
2
3
4
5
# 默认删除索引下的文档
Client('myIndex', host='localhost', port='6666').drop_index()

# 默认仅删除索引,不删除文档
Client('myIndex', host='localhost', port='6666').dropindex()

从源码中发现提供了两个删除文档的方法,并且作者建议在RediSearch 2.0使用dropindex替换drop_index,但是如果你仍然想在删除索引时删除对应的文档,可以添加参数,dropindex(delete_documents=True)即可以同步删除文档(其实drop_index自带了默认值为True的参数,所以暂时可用的)。

查看索引信息

1
2
client = Client('idx:test', host='localhost', port='6666')
client.info()

结果:

1
{'index_name': 'idx:test', 'index_options': [], 'index_definition': ['key_type', 'HASH', 'prefixes', ['test:'], 'default_score', '1'], 'attributes': [['identifier', 'name', 'attribute', 'name', 'type', 'TEXT', 'WEIGHT', '1'], ['identifier', 'age', 'attribute', 'age', 'type', 'NUMERIC']], 'num_docs': '0', 'max_doc_id': '0', 'num_terms': '0', 'num_records': '0', 'inverted_sz_mb': '0', 'vector_index_sz_mb': '0', 'total_inverted_index_blocks': '7074', 'offset_vectors_sz_mb': '0', 'doc_table_size_mb': '0', 'sortable_values_size_mb': '0', 'key_table_size_mb': '0', 'records_per_doc_avg': '-nan', 'bytes_per_record_avg': '-nan', 'offsets_per_term_avg': '-nan', 'offset_bits_per_record_avg': '-nan', 'hash_indexing_failures': '0', 'total_indexing_time': '0', 'indexing': '0', 'percent_indexed': '1', 'number_of_uses': 2, 'gc_stats': ['bytes_collected', '0', 'total_ms_run', '0', 'total_cycles', '0', 'average_cycle_time_ms', '-nan', 'last_run_time_ms', '0', 'gc_numeric_trees_missed', '0', 'gc_blocks_denied', '0'], 'cursor_stats': ['global_idle', 0, 'global_total', 0, 'index_capacity', 128, 'index_total', 0], 'dialect_stats': ['dialect_1', 0, 'dialect_2', 0, 'dialect_3', 0]}

文档

创建文档

RediSearch 2.0 索引会跟踪具有您定义的键前缀的hash(创建索引时也要设置好前缀),因此如果您想将文档添加到索引,您只需要创建一个具有这些前缀的hash。

1
2
3
4
5
6
# RediSearch 2.0 创建文档的方式.
doc = {
'title': 'RediSearch',
'body': 'Redisearch adds querying, indexing, and full-text search to Redis'
}
client.redis.hset('blog:1', mapping=doc)

RediSearch 的过去版本要求您调用该add_document() 方法。此方法已弃用(实际官方并没有废弃,只是没有了该方法的文档,但方法依然可用),但我们将其用法包含在此处以供参考。

1
2
3
4
5
6
# RediSearch 1.x 中创建文档的方式
client.add_document(
"blog:2",
title="RediSearch",
body="Redisearch implements a search engine on top of redis",
)

修改文档

修改文档依然可以使用add_document方法,参数replace代表是否替换原来文档;如果只是想要更新已有文档中的某些字段,而不是替换整个文档,这时就需要用到 partial 选项,

例如我们仅仅更新指定电影发行国家:

1
client.add_document("movie:020001", country="中国", replace=True, partial=True)

注意:partial为True时,replace必须为True,否则无效

另外你也可以使用redis哈希修改的方式进行文档修改:client.redis.hset

删除文档

删除文档我们使用delete_document方法,但是默认只会将文档移出索引,并不会删除该文档,还是可以用client.get(docid)的方式来获取

如果想要在移除索引文档的同时将文档一并删除,就需要在执行函数的时候将参数delete_actual_document设为True即可。

如:强制删除某文档:

1
client.delete_document("movie:0001", delete_actual_document=True)

查询文档

基本查询

使用该search()方法执行全文text字段的搜索。此方法不采用 RediSearch 命令可用的许多选项FT.SEARCH

1
2
# 全文搜索wizards(仅TextField字段中)
res = client.search("wizards")

结果对象

结果包装在一个Result对象中,该对象包括结果数和匹配文档列表。

1
2
3
4
>>> print(res.total)
2
>>> print(res.docs[0].title)
"Wizard Story 2: Evil Wizards Strike Back"

构建复杂查询

后续示例我们约定按照已建好的索引进行演示,演示数据后续会放出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from redisearch import Client, TextField, NumericField, TagField, Query, IndexDefinition
from redisearch.aggregation import AggregateRequest

client = Client(index_name='idx:movie', host='localhost', port='6666')
try:
client.drop_index()
except:
pass
client.create_index(
(
TextField("id", sortable=True),
TagField("genre"),
TagField("language"),
TagField("country"),
NumericField("rating", sortable=True),
NumericField("votes", sortable=True),
NumericField("duration", sortable=True),
NumericField("year", sortable=True),
TextField("date_released"),
TextField("name"),
TextField("description"),
TextField("poster"),
TextField("original_name"),
TextField("alias"),
), definition=IndexDefinition(prefix=['movie:'], language="chinese")
)

您可以使用该Query对象来构建复杂的查询:

1
2
3
q = Query("夏天").verbatim().no_content().language("chinese").with_scores().paging(0, 5)
res = client.search(q)
res

结果:

1
Result{2 total, docs: [Document {'id': 'movie:1300299', 'payload': None, 'score': 4.0}, Document {'id': 'movie:1858711', 'payload': None, 'score': 3.0}]}

有关这些选项的说明,请参阅命令的RediSearch 文档FT.SEARCH

查询语法

查询的默认行为是在索引中的所有字段中运行全文搜索(所有TextField字段中)以查找查询中所有术语的交集。

client.search("evil wizards")表示过滤所有TextField字段中包含“evil”和“wizard”的所有文档,Query()初始化程序的字符串具有 RediSearch 中可用的全部查询语法。

全文搜索
查询所有包含波兰的文档
1
2
3
q = Query("波兰").language("chinese").highlight()
res = client.search(q)
res

结果:

1
Result{3 total, docs: [Document {'id': 'movie:1303418', 'payload': None, 'rating': '9.2', 'votes': '27133', 'alias': '生存还是毁灭 / 戏谍人生 / 扮嘢奇兵 / 生死攸关 / 嗨!我的元首 / 生或死 / 生死问题,是死?是活? / 生死大逃亡', 'description': '<b>波兰</b>华沙有一个剧团,其中有一名演员Bronski(Tom Dugan 饰)极其擅长模仿德国纳粹领袖希特勒。Maria Tura(卡洛·朗白 Carole Lombard 饰)和Joseph Tura(...', 'genre': '喜剧', 'duration': '5940', 'lang': '英语', 'name': '你逃我也逃', 'original_name': 'To Be or Not to Be', 'date_released': '1942-03-06', 'year': '1942', 'poster': 'https://wmdb.querydata.org/movie/poster/1607014476724-f79d22.jpg', 'country': '美国'}, Document {'id': 'movie:1296736', 'payload': None, 'rating': '9.2', 'votes': '409576', 'alias': '钢琴战曲(港) / 战地琴人(台) / 战地琴声 / 钢琴师', 'description': '史标曼(艾德里安•布洛迪 Adrien Brody 饰)是<b>波兰</b>一家电台的钢琴师。二战即将爆发之时,他们全家被迫被赶进华沙的犹太区。在战争的颠沛流离中,家人和亲戚最终被纳粹杀害,而标曼本人也受尽种种羞辱...', 'genre': '剧情', 'duration': '9000', 'lang': '英语', 'name': '钢琴家', 'original_name': 'The Pianist', 'date_released': '2002-05-24', 'year': '2002', 'poster': 'https://wmdb.querydata.org/movie/poster/1606347998445-205f46.jpg', 'country': '法国'}, Document {'id': 'movie:1295124', 'payload': None, 'rating': '9.5', 'votes': '843435', 'alias': '舒特拉的名单(港) / 辛德勒名单', 'description': '1939年,<b>波兰</b>在纳粹德国的统治下,党卫军对犹太人进行了隔离统治。德国商人奥斯卡·辛德勒(连姆·尼森 Liam Neeson 饰)来到德军统治下的克拉科夫,开设了一间搪瓷厂,生产军需用品。凭着出众的社...', 'genre': '历史', 'duration': '11700', 'lang': '英语', 'name': '辛德勒的名单', 'original_name': 'Schindler&#39;s List', 'date_released': '1993-11-30', 'year': '1993', 'poster': 'https://wmdb.querydata.org/movie/poster/1606049156251-837fb3.jpg', 'country': '美国'}]}
查询所有包含战争且包含美国的文档
1
2
3
q = Query("战争 美国").language("chinese").highlight()
res = client.search(q)
res

结果:

1
Result{1 total, docs: [Document {'id': 'movie:1292849', 'payload': None, 'alias': '雷霆救兵(港) / 抢救雷恩大兵(台) / 拯救大兵雷恩', 'description': '瑞恩(马特•达蒙 Matt Damon饰 )是二战期间的<b>美国</b>伞兵,被困在了敌人后方。更不幸的是,他的三个兄弟全部在<b>战争</b>中死亡,如果他也遇难,家中的老母亲将无依无靠。<b>美国</b>作战总指挥部知道了这个情况,毅...', 'votes': '506703', 'rating': '9.0', 'original_name': 'Saving Private Ryan', 'name': '拯救大兵瑞恩', 'date_released': '1998-07-24', 'duration': '10140', 'lang': '英语', 'genre': '剧情', 'year': '1998', 'poster': 'https://wmdb.querydata.org/movie/poster/1611403093938-02aacc.jpg', 'country': '美国'}]}
查询所有包含战争或包含美国的文档
1
2
3
q = Query("战争|美国").language("chinese").highlight()
res = client.search(q)
res

结果:

1
Result{37 total, docs: [Document {'id': 'movie:1292062', 'payload': None, 'rating': '8.5', 'votes': '279900', 'alias': '美丽有罪(港) / <b>美国</b>心·玫瑰情(台) / <b>美国</b>大美人 / <b>美国</b>美人 / <b>美国</b>少女 / 红蔷薇', 'description': '莱斯特(凯文·史派西 Kevin Spacey 饰)跟许多中年男人一样,遇到了各种各样的人生难题。他在一个广告公司工作,成绩平平,但是妻子却比他混得出色,一派女强人作风。这个平凡的男人还有一个未成年的...', 'genre': '剧情', 'duration': '7320', 'lang': '英语', 'name': '<b>美国</b>丽人', 'original_name': 'American Beauty', 'date_released': '1999-09-08', 'year': '1999', 'poster': 'https://wmdb.querydata.org/movie/poster/1607520913673-7gf31a.jpg', 'country': '美国'}, Document {'id': 'movie:1419005', 'payload': None, 'rating': '8.5', 'votes': '5842', 'alias': '阿尔及利亚的<b>战争</b> / The Battle of Algiers', 'description': '一九五四年十月一日,以法属阿尔及利亚首都阿尔及耳的卡斯巴为中心,爆发了阿尔及利亚人反抗运动。这是由于阿拉伯人憎恨法国人长期统治而引起的。人们四处搞破坏,法国政府发觉事态严重,便于一九五七年十月七日,派...', 'genre': '历史', 'duration': '7260', 'lang': '法语', 'name': '阿尔及尔之战', 'original_name': 'La battaglia di Algeri', 'date_released': '1966-09-08', 'year': '1966', 'poster': 'https://wmdb.querydata.org/movie/poster/1606734542298-9cge87.jpg', 'country': '意大利'}, Document {'id': 'movie:1296736', 'payload': None, 'rating': '9.2', 'votes': '409576', 'alias': '钢琴战曲(港) / 战地琴人(台) / 战地琴声 / 钢琴师', 'description': '史标曼(艾德里安•布洛迪 Adrien Brody 饰)是波兰一家电台的钢琴师。二战即将爆发之时,他们全家被迫被赶进华沙的犹太区。在<b>战争</b>的颠沛流离中,家人和亲戚最终被纳粹杀害,而标曼本人也受尽种种羞辱...', 'genre': '剧情', 'duration': '9000', 'lang': '英语', 'name': '钢琴家', 'original_name': 'The Pianist', 'date_released': '2002-05-24', 'year': '2002', 'poster': 'https://wmdb.querydata.org/movie/poster/1606347998445-205f46.jpg', 'country': '法国'}, Document {'id': 'movie:1422186', 'payload': None, 'rating': '8.6', 'votes': '6461', 'alias': '见证(台) / 炎628 / 过来瞧瞧 / 屠杀安魂曲 / 炙628 / Come And See / Idi i smotri', 'description': '这是一部很特殊的<b>战争</b>题材电影,它真实地描写了德占区人民的悲惨遭遇和场面,以及人们面对突如其来的灾难的恐惧,反映了<b>战争</b>的真实面目。它既不同于《斯大林格勒保卫战》、《攻占柏林》这些正面战场的血肉横飞、排山...', 'genre': '战争', 'duration': '8520', 'lang': '白俄罗斯语', 'name': '自己去看', 'original_name': 'Иди и смотри', 'date_released': '1985-10-17', 'year': '1985', 'poster': 'https://wmdb.querydata.org/movie/poster/1613161487535-5931f8.jpg', 'country': '苏联'}, ……]}
查询所有包含战争但不包含美国的文档
1
2
3
q = Query("战争 -美国").language("chinese").highlight()
res = client.search(q)
res

结果:

1
Result{10 total, docs: [Document {'id': 'movie:1419005', 'payload': None, 'alias': '阿尔及利亚的<b>战争</b> / The Battle of Algiers', 'description': '一九五四年十月一日,以法属阿尔及利亚首都阿尔及耳的卡斯巴为中心,爆发了阿尔及利亚人反抗运动。这是由于阿拉伯人憎恨法国人长期统治而引起的。人们四处搞破坏,法国政府发觉事态严重,便于一九五七年十月七日,派...', 'votes': '5842', 'rating': '8.5', 'original_name': 'La battaglia di Algeri', 'name': '阿尔及尔之战', 'date_released': '1966-09-08', 'duration': '7260', 'lang': '法语', 'genre': '历史', 'year': '1966', 'poster': 'https://wmdb.querydata.org/movie/poster/1606734542298-9cge87.jpg', 'country': '意大利'}, Document {'id': 'movie:1422186', 'payload': None, 'alias': '见证(台) / 炎628 / 过来瞧瞧 / 屠杀安魂曲 / 炙628 / Come And See / Idi i smotri', 'description': '这是一部很特殊的<b>战争</b>题材电影,它真实地描写了德占区人民的悲惨遭遇和场面,以及人们面对突如其来的灾难的恐惧,反映了<b>战争</b>的真实面目。它既不同于《斯大林格勒保卫战》、《攻占柏林》这些正面战场的血肉横飞、排山...', 'votes': '6461', 'rating': '8.6', 'original_name': 'Иди и смотри', 'name': '自己去看', 'date_released': '1985-10-17', 'duration': '8520', 'lang': '白俄罗斯语', 'genre': '战争', 'year': '1985', 'poster': 'https://wmdb.querydata.org/movie/poster/1613161487535-5931f8.jpg', 'country': '苏联'}, Document {'id': 'movie:3592854', 'payload': None, 'alias': '末日先锋:战甲飞车(港) / 疯狂麦斯:愤怒道(台) / 冲锋飞车队4 / 迷雾追魂手4 / 冲锋追魂手4 / 疯狂麦克斯4 / 疯狂迈斯:怒途 / Mad Max 4: Fury Road', 'description': '未来世界,水资源短缺引发了连绵的<b>战争</b>。人们相互厮杀,争夺有限的资源,地球变成了血腥十足的杀戮死战场。面容恐怖的不死乔在戈壁山谷建立了难以撼动的强大武装王国,他手下的战郎驾驶装备尖端武器的战车四下抢掠,...', 'votes': '396919', 'rating': '8.6', 'original_name': 'Mad Max: Fury Road', 'name': '疯狂的麦克斯4:狂暴之路', 'date_released': '2015-05-14', 'duration': '7200', 'lang': '英语', 'genre': '冒险', 'year': '2015', 'poster': 'https://wmdb.querydata.org/movie/poster/1605981675337-a01c84.jpg', 'country': '澳大利亚'}, ……]}
模糊搜索
前置匹配
1
2
3
q = Query("*国").language("chinese").highlight().return_fields("alias", "name", "description")
res = client.search(q)
res

结果

1
Result{54 total, docs: [Document {'id': 'movie:1309115', 'payload': None, 'alias': '希特拉的最后十二夜(港) / <b>帝国</b>毁灭(台) / <b>帝国</b>陷落 / Downfall / The Downfall: Hitler and the End of the Third Reich', 'name': '<b>帝国</b>的毁灭', 'description': '这是一部纪实性电影,逼真地反映了希特勒人生的最后12天,第三<b>帝国</b>最后的日子。苏联红军已经攻入柏林,希特勒(布鲁诺·甘茨 Bruno Ganz 饰)和情妇爱娃(茱莉安·柯勒 Juliane Köhle...'}, Document {'id': 'movie:1296528', 'payload': None, 'alias': '星球大战第五集:<b>帝国</b>反击战 / 星际大战五部曲:<b>帝国</b>大反击 / 星球大战5:<b>帝国</b>反击战', 'name': '星球大战2:<b>帝国</b>反击战', 'description': '维德勋爵(大卫•普劳斯 David Prowse 饰)把抵抗力量赶出基地,并派出数以千计的探针寻找卢克•天行者(马克•哈米尔 Mark Hamill 饰)。抵抗力量埋伏到冰天雪地的哈斯星,卢克在一次巡...'}, Document {'id': 'movie:1292969', 'payload': None, 'alias': '光荣之路', 'name': '光荣之路', 'description': '1916年,第一次世界大战期间的<b>法国</b>,德法两军的战争如火如荼。值此关键时刻,<b>法国</b>陆军将军布洛拉德(Adolphe Menjou 饰)向陆军上尉达克斯(柯克·道格拉斯 Kirk Douglas 饰)率领...'}, Document {'id': 'movie:1293663', 'payload': None, 'alias': 'Tengoku to jigoku / High and Low', 'name': '<b>天国</b>与地狱', 'description': '在权藤今吾(三船敏郎 Toshiro Mifune)家里,举行了一场关于民族鞋业的董事会议,讨论关于新鞋的质量问题。权藤发表的意见慷慨激昂,他觉得不应该只顾降低成本提高利润,而忽视了鞋子本身的质量。此...'}, Document {'id': 'movie:1294958', 'payload': None, 'alias': '桂河桥', 'name': '桂河大桥', 'description': '二战期间,日军占领了缅甸边境的一个战俘营。出于战略需要,日军将在缅甸与<b>泰国</b>交界修建一条大桥,同时希望战俘营里战俘出力,但英军战俘代表尼科森上校(亚利克·基尼斯 Alec Guinness 饰)认为这违...'}, Document {'id': 'movie:1292589', 'payload': None, 'alias': '纽伦堡大审判 / 劫后升平', 'name': '纽伦堡的审判', 'description': '讲述二战后在纽伦堡提审<b>德国</b>纳粹计划的法律关系者,三个被告提审的原因是给犹太人施行断种手术。担任主审判长的是美国人赫鲁特,他主张其中两个被告无罪;而<b>德国</b>司法部长亚林克竟对此事保持沉默,但检查官罗森上校却...'}, Document {'id': 'movie:1292260', 'payload': None, 'alias': '当代启示录', 'name': '现代启示录', 'description': '越战后期,美军上尉威拉德(马丁•辛 Martin Sheen 饰)奉命沿湄公河而上,搜寻脱离美军在柬埔寨建立了自己的<b>王国</b>的科茨(马龙•白兰度 Marlon Brando 饰)上校,将他带回或杀死。科茨...'}, Document {'id': 'movie:1292062', 'payload': None, 'alias': '美丽有罪(港) / <b>美国</b>心·玫瑰情(台) / <b>美国</b>大美人 / <b>美国</b>美人 / <b>美国</b>少女 / 红蔷薇', 'name': '<b>美国</b>丽人', 'description': '莱斯特(凯文·史派西 Kevin Spacey 饰)跟许多中年男人一样,遇到了各种各样的人生难题。他在一个广告公司工作,成绩平平,但是妻子却比他混得出色,一派女强人作风。这个平凡的男人还有一个未成年的...'}, Document {'id': 'movie:1303418', 'payload': None, 'alias': '生存还是毁灭 / 戏谍人生 / 扮嘢奇兵 / 生死攸关 / 嗨!我的元首 / 生或死 / 生死问题,是死?是活? / 生死大逃亡', 'name': '你逃我也逃', 'description': '波兰华沙有一个剧团,其中有一名演员Bronski(Tom Dugan 饰)极其擅长模仿<b>德国</b>纳粹领袖希特勒。Maria Tura(卡洛·朗白 Carole Lombard 饰)和Joseph Tura(...'}, Document {'id': 'movie:1300299', 'payload': None, 'alias': '谋杀回忆 / 杀手回忆录 / Salinui chueok / Memories of Murder', 'name': '杀人回忆', 'description': '1986年,<b>韩国</b>京畿道华城郡,热得发昏的夏天,在田野边发现一具女尸,早已发臭。小镇警察朴探员(宋康昊饰)和汉城来的苏探员(金相庆饰)接手案件,唯一可证实的是这具女尸生前被强奸过。线索的严重缺乏让毫无经...'}]}
后置匹配
1
2
3
q = Query("美*").language("chinese").highlight().return_fields("alias", "name", "description")
res = client.search(q)
res

结果

1
Result{46 total, docs: [Document {'id': 'movie:1292260', 'payload': None, 'alias': '当代启示录', 'name': '现代启示录', 'description': '越战后期,<b>美军</b>上尉威拉德(马丁•辛 Martin Sheen 饰)奉命沿湄公河而上,搜寻脱离<b>美军</b>在柬埔寨建立了自己的王国的科茨(马龙•白兰度 Marlon Brando 饰)上校,将他带回或杀死。科茨...'}, Document {'id': 'movie:1306029', 'payload': None, 'alias': '有你终生<b>美丽</b>(港) / <b>美丽</b>境界(台) / 完美大脑', 'name': '<b>美丽</b>心灵', 'description': '本片是关于20世纪伟大数学家小约翰•福布斯-纳什的人物传记片。小约翰•福布斯-纳什(拉塞尔•克劳)在念研究生时,便发表了著名的博弈理论,该理论虽只有短短26页,却在经济、军事等领域产生了深远的影响。...'}, Document {'id': 'movie:1300055', 'payload': None, 'alias': '烈血焚城(港) / 金甲部队(台) / 金甲战士', 'name': '全金属外壳', 'description': '越战期间,<b>美军</b>大量征兵。大批年轻人应征入伍,在新兵营接受“残忍”的训练。“傻瓜”比尔运动神经不发达,常常犯错而连累所有人一起受罚。“小丑”(马修•莫迪恩 Matthew Modine 饰)奉命帮助比尔...'}, Document {'id': 'movie:1292270', 'payload': None, 'alias': '噩梦挽歌(台) / 迷上瘾(港) / 梦的挽歌', 'name': '梦之安魂曲', 'description': '哈瑞(杰瑞德·莱托 Jared Leto 饰)和玛丽安(詹妮弗·康纳利 Jennifer Connelly 饰)彼此相爱,梦想着开个服装店,梦想着有个<b>美好</b>的明天。然而他们都离不开毒品,并想着以销毒赚得...'}, Document {'id': 'movie:1297127', 'payload': None, 'alias': "华盛顿政客(港) / 华府风云(台) / 史密斯游<b>美</b>京 / 史密斯先生上<b>美</b>京 / 民主万岁 / Frank Capra's Mr. Smith Goes to Washington", 'name': '史密斯先生到华盛顿', 'description': '美国的一个小镇,Jefferson Smith(詹姆斯·斯图尔特 James Stewart饰)是当地的童子军的首领,深受青少年们的喜爱,被选为新的参议员,来到了华盛顿。遇到了他父亲的老朋友,同为参议...'}, Document {'id': 'movie:1299131', 'payload': None, 'alias': '教父续集(港) / 教父II', 'name': '教父2', 'description': '迈克尔(阿尔·帕西诺 饰)是<b>美利坚</b>黑手党科利昂家族的头目。 迈克尔的父亲维托·安多里尼(罗伯特·德尼罗 饰)出生于意大利科利昂镇。1901年,维托的父亲安东尼奥、兄长保罗、母亲(玛丽亚·卡塔 饰)都死...'}, Document {'id': 'movie:1294100', 'payload': None, 'alias': '我们生活的<b>美好</b>时代', 'name': '黄金时代', 'description': '故事发生在1945年,弗雷德(达纳·安德鲁斯 Dana Andrews 饰),艾尔(弗雷德里克·马奇 Fredric March 饰)和霍莫(哈罗德·拉塞尔 Harold Russell 饰)是三名刚...'}, Document {'id': 'movie:1294947', 'payload': None, 'alias': '龙虎榜(港) / 第三集中营(台) / 胜利大逃亡 / 绝处逢生 / 胜利逃亡', 'name': '大逃亡', 'description': '二战期间,德军的战俘营里,每个人都在渴望着自由。<b>美国人</b>希尔(史蒂夫•麦奎因 Steve McQueen 饰)在进入战俘营的第一天起,就一直计划着越狱。虽然他的十多次逃跑都以失败告终,但希尔从未放弃。这...'}, Document {'id': 'movie:1292065', 'payload': None, 'alias': '疤面人 / 疤脸人', 'name': '疤面煞星', 'description': '古巴难民青年托尼(阿尔•帕西诺 Al Pacino 饰)逃难来到了<b>美国</b>的迈阿密,成了一个典型的天不怕、地不怕的<b>美国</b>街头小混混。托尼在当地的一个毒枭手下干活,因其心狠手辣、胆大心细,十分出色地帮老大完成...'}, Document {'id': 'movie:1293749', 'payload': None, 'alias': "莫负少年头(港) / 风云人物(台) / 美满人生(澳) / <b>美好</b>人生 / 哀乐人生 / 美好生活 / Frank Capra's It's a Wonderful Life", 'name': '生活多美好', 'description': '乔治(詹姆斯•斯图尔特 James Stewart 饰)在圣诞节前准备自杀,这时上帝传来旨意,派天使拯救他,并让他了解到自己一生的使命——拯救那些不幸的人。乔治小时候左耳有疾,在贝德福德镇上的一家药店...'}]}
模糊匹配
1
2
3
q = Query("%美国%").language("chinese").highlight().return_fields("alias", "name", "description")
res = client.search(q)
res

结果:

1
Result{68 total, docs: [Document {'id': 'movie:1292260', 'payload': None, 'alias': '当代启示录', 'name': '现代启示录', 'description': '越战后期,美军上尉威拉德(马丁•辛 Martin Sheen 饰)奉命沿湄公河而上,搜寻脱离美军在柬埔寨建立了自己的<b>王国</b>的科茨(马龙•白兰度 Marlon Brando 饰)上校,将他带回或杀死。科茨...'}, Document {'id': 'movie:1309115', 'payload': None, 'alias': '希特拉的最后十二夜(港) / <b>帝国</b>毁灭(台) / <b>帝国</b>陷落 / Downfall / The Downfall: Hitler and the End of the Third Reich', 'name': '<b>帝国</b>的毁灭', 'description': '这是一部纪实性电影,逼真地反映了希特勒人生的最后12天,第三<b>帝国</b>最后的日子。苏联红军已经攻入柏林,希特勒(布鲁诺·甘茨 Bruno Ganz 饰)和情妇爱娃(茱莉安·柯勒 Juliane Köhle...'}, Document {'id': 'movie:1296528', 'payload': None, 'alias': '星球大战第五集:<b>帝国</b>反击战 / 星际大战五部曲:<b>帝国</b>大反击 / 星球大战5:<b>帝国</b>反击战', 'name': '星球大战2:<b>帝国</b>反击战', 'description': '维德勋爵(大卫•普劳斯 David Prowse 饰)把抵抗力量赶出基地,并派出数以千计的探针寻找卢克•天行者(马克•哈米尔 Mark Hamill 饰)。抵抗力量埋伏到冰天雪地的哈斯星,卢克在一次巡...'}, Document {'id': 'movie:1306029', 'payload': None, 'alias': '有你终生<b>美丽</b>(港) / <b>美丽</b>境界(台) / 完美大脑', 'name': '<b>美丽</b>心灵', 'description': '本片是关于20世纪伟大数学家小约翰•福布斯-纳什的人物传记片。小约翰•福布斯-纳什(拉塞尔•克劳)在念研究生时,便发表了著名的博弈理论,该理论虽只有短短26页,却在经济、军事等领域产生了深远的影响。...'}, Document {'id': 'movie:1292969', 'payload': None, 'alias': '光荣之路', 'name': '光荣之路', 'description': '1916年,第一次世界大战期间的<b>法国</b>,德法两军的战争如火如荼。值此关键时刻,<b>法国</b>陆军将军布洛拉德(Adolphe Menjou 饰)向陆军上尉达克斯(柯克·道格拉斯 Kirk Douglas 饰)率领...'}, Document {'id': 'movie:1293663', 'payload': None, 'alias': 'Tengoku to jigoku / High and Low', 'name': '<b>天国</b>与地狱', 'description': '在权藤今吾(三船敏郎 Toshiro Mifune)家里,举行了一场关于民族鞋业的董事会议,讨论关于新鞋的质量问题。权藤发表的意见慷慨激昂,他觉得不应该只顾降低成本提高利润,而忽视了鞋子本身的质量。此...'}, Document {'id': 'movie:1294958', 'payload': None, 'alias': '桂河桥', 'name': '桂河大桥', 'description': '二战期间,日军占领了缅甸边境的一个战俘营。出于战略需要,日军将在缅甸与<b>泰国</b>交界修建一条大桥,同时希望战俘营里战俘出力,但英军战俘代表尼科森上校(亚利克·基尼斯 Alec Guinness 饰)认为这违...'}, Document {'id': 'movie:1300055', 'payload': None, 'alias': '烈血焚城(港) / 金甲部队(台) / 金甲战士', 'name': '全金属外壳', 'description': '越战期间,<b>美军</b>大量征兵。大批年轻人应征入伍,在新兵营接受“残忍”的训练。“傻瓜”比尔运动神经不发达,常常犯错而连累所有人一起受罚。“小丑”(马修•莫迪恩 Matthew Modine 饰)奉命帮助比尔...'}, Document {'id': 'movie:1292062', 'payload': None, 'alias': '<b>美丽</b>有罪(港) / 美国心·玫瑰情(台) / 美国大美人 / 美国美人 / 美国少女 / 红蔷薇', 'name': '美国丽人', 'description': '莱斯特(凯文·史派西 Kevin Spacey 饰)跟许多中年男人一样,遇到了各种各样的人生难题。他在一个广告公司工作,成绩平平,但是妻子却比他混得出色,一派女强人作风。这个平凡的男人还有一个未成年的...'}, Document {'id': 'movie:1292270', 'payload': None, 'alias': '噩梦挽歌(台) / 迷上瘾(港) / 梦的挽歌', 'name': '梦之安魂曲', 'description': '哈瑞(杰瑞德·莱托 Jared Leto 饰)和玛丽安(詹妮弗·康纳利 Jennifer Connelly 饰)彼此相爱,梦想着开个服装店,梦想着有个<b>美好</b>的明天。然而他们都离不开毒品,并想着以销毒赚得...'}]}

模糊匹配是由很大限制的,他基于Levenshtein距离(LD)进行模糊匹配。术语的模糊匹配是通过在术语周围加“%”来实现的,模糊匹配的最大LD为3,确切的说这只是一种相识度查询,并非一般意义上的模糊搜索,但是:
如果仔细观察会发现通过精确匹配时,不仅能够将完整value值查询出来,而且还查询出其他处于文档某个位置的key,请看以下例子:

1
2
3
q = Query("美国").language("chinese").highlight().return_fields("name", "alias", "description").paging(0, 5)
res = client.search(q)
res

结果:

1
Result{27 total, docs: [Document {'id': 'movie:1292062', 'payload': None, 'name': '<b>美国</b>丽人', 'alias': '美丽有罪(港) / <b>美国</b>心·玫瑰情(台) / <b>美国</b>大美人 / <b>美国</b>美人 / <b>美国</b>少女 / 红蔷薇', 'description': '莱斯特(凯文·史派西 Kevin Spacey 饰)跟许多中年男人一样,遇到了各种各样的人生难题。他在一个广告公司工作,成绩平平,但是妻子却比他混得出色,一派女强人作风。这个平凡的男人还有一个未成年的...'}, Document {'id': 'movie:1292065', 'payload': None, 'name': '疤面煞星', 'alias': '疤面人 / 疤脸人', 'description': '古巴难民青年托尼(阿尔•帕西诺 Al Pacino 饰)逃难来到了<b>美国</b>的迈阿密,成了一个典型的天不怕、地不怕的<b>美国</b>街头小混混。托尼在当地的一个毒枭手下干活,因其心狠手辣、胆大心细,十分出色地帮老大完成...'}, Document {'id': 'movie:1293527', 'payload': None, 'name': '<b>美国</b>X档案', 'alias': '野兽良民(港) / <b>美国</b>历史档案 / <b>美国</b>X历史', 'description': '德瑞克(爱德华•诺顿 Edward Norton 饰)的父亲在他很小的时候就被一名黑人毒贩射杀,从此给他幼小的心灵埋下了仇恨的种子。原来德瑞克功课很好,是老师眼中的好学生。但自从父亲遇难后,他就将一切...'}, Document {'id': 'movie:1292849', 'payload': None, 'name': '拯救大兵瑞恩', 'alias': '雷霆救兵(港) / 抢救雷恩大兵(台) / 拯救大兵雷恩', 'description': '瑞恩(马特•达蒙 Matt Damon饰 )是二战期间的<b>美国</b>伞兵,被困在了敌人后方。更不幸的是,他的三个兄弟全部在战争中死亡,如果他也遇难,家中的老母亲将无依无靠。<b>美国</b>作战总指挥部知道了这个情况,毅...'}, Document {'id': 'movie:1292262', 'payload': None, 'name': '<b>美国</b>往事', 'alias': '四海兄弟(台) / 义薄云天(港)', 'description': '1933年,纽约流氓Noodles(罗伯特·德·尼罗 饰)因向哈洛伦警司(布鲁斯·巴伦堡 饰)通风报信害死了三名同伙而被追杀。逃亡之前,他打开了存放帮派基金的手提箱,里面却只有报纸。 1968年,已改...'}]}

之所以会出现这样的效果是因为redisearch对文本进行了分词,其使用的工具是friso相比es的ik还是弱一些前者主要是对中文分词,体积小可移植性强。

字段查询

通过字段查询也可以实现模糊搜索,条件表达式可参考官网上给的sqlredisearch的对照表

SQL 表达式RediSearch 等效表达式注意
WHERE x='foo' AND y='bar'@x:foo @y:bar为减少歧义,建议使用 (@x:foo) (@y:bar)
WHERE x='foo' AND y!='bar'@x:foo -@y:bar
WHERE x='foo' OR y='bar'(@x:foo)|(@y:bar)
WHERE x IN ('foo', 'bar','hello world')@x:(foo|bar|"hello world")引号内表示确定(必须完全匹配)的短语
WHERE y='foo' AND x NOT IN ('foo','bar')@y:foo (-@x:foo) (-@x:bar)
WHERE x NOT IN ('foo','bar')-@x:(foo|bar)
WHERE num BETWEEN 10 AND 20@num:[10 20]
WHERE num >= 10@num:[10 +inf]
WHERE num > 10@num:[(10 +inf]
WHERE num < 10@num:[-inf (10]
WHERE num <= 10@num:[-inf 10]
WHERE num < 10 OR num > 20@num:[-inf (10] | @num:[(20 +inf]
WHERE name LIKE 'john%'@name:john*
标签过滤

主要过滤TagField类型的字段

查询country为苏联或印度的电影

1
2
3
4
5
q = Query("@country:{苏联|印度}").language("chinese").highlight().return_fields("name", "country",).paging(0, 15)
# 等价于
# q = Query("(@country:{苏联})|(@country:{印度})").language("chinese").highlight().return_fields("name", "country",).paging(0, 15)
res = client.search(q)
res

结果:

1
Result{7 total, docs: [Document {'id': 'movie:35652715', 'payload': None, 'name': '杰伊·比姆', 'country': '印度'}, Document {'id': 'movie:26387939', 'payload': None, 'name': '摔跤吧!爸爸', 'country': '印度'}, Document {'id': 'movie:3793023', 'payload': None, 'name': '三傻大闹宝莱坞', 'country': '印度'}, Document {'id': 'movie:1422186', 'payload': None, 'name': '自己去看', 'country': '苏联'}, Document {'id': 'movie:1306019', 'payload': None, 'name': '大地之歌', 'country': '印度'}, Document {'id': 'movie:1300034', 'payload': None, 'name': '德尔苏·乌扎拉', 'country': '苏联'}, Document {'id': 'movie:2363506', 'payload': None, 'name': '地球上的星星', 'country': '印度'}]}
数字过滤

主要过滤NumericField类型的字段

查询评分在9.4到9.5的电影(包含边界)

1
2
3
q = Query("@rating:[9.4 9.5]").language("chinese").highlight().return_fields("name", "rating",).paging(0, 15).sort_by("rating")
res = client.search(q)
res

结果:

1
Result{10 total, docs: [Document {'id': 'movie:1293182', 'payload': None, 'rating': '9.4', 'name': '十二怒汉'}, Document {'id': 'movie:3011091', 'payload': None, 'rating': '9.4', 'name': '忠犬八公的故事'}, Document {'id': 'movie:1291561', 'payload': None, 'rating': '9.4', 'name': '千与千寻'}, Document {'id': 'movie:1291567', 'payload': None, 'rating': '9.4', 'name': '千与千寻'}, Document {'id': 'movie:1295644', 'payload': None, 'rating': '9.4', 'name': '这个杀手不太冷'}, Document {'id': 'movie:1303408', 'payload': None, 'rating': '9.5', 'name': '福尔摩斯二世'}, Document {'id': 'movie:1295124', 'payload': None, 'rating': '9.5', 'name': '辛德勒的名单'}, Document {'id': 'movie:34961898', 'payload': None, 'rating': '9.5', 'name': '汉密尔顿'}, Document {'id': 'movie:1292063', 'payload': None, 'rating': '9.5', 'name': '美丽人生'}, Document {'id': 'movie:1292720', 'payload': None, 'rating': '9.5', 'name': '阿甘正传'}]}

查询评分大于9.5且小于9.6的电影(不包含边界)

1
2
3
q = Query("@rating:[(9.4 (9.6]").language("chinese").highlight().return_fields("name", "rating",).paging(0, 15).sort_by("rating")
res = client.search(q)
res

结果

1
Result{5 total, docs: [Document {'id': 'movie:1303408', 'payload': None, 'rating': '9.5', 'name': '福尔摩斯二世'}, Document {'id': 'movie:1295124', 'payload': None, 'rating': '9.5', 'name': '辛德勒的名单'}, Document {'id': 'movie:34961898', 'payload': None, 'rating': '9.5', 'name': '汉密尔顿'}, Document {'id': 'movie:1292063', 'payload': None, 'rating': '9.5', 'name': '美丽人生'}, Document {'id': 'movie:1292720', 'payload': None, 'rating': '9.5', 'name': '阿甘正传'}]}

查询评分大于等于9.6分的电影

1
2
3
4
q = Query("@rating:[9.6 inf]").language("chinese").highlight().return_fields("name", "rating", ).paging(0,15).sort_by(
"rating")
res = client.search(q)
res

结果:

1
Result{2 total, docs: [Document {'id': 'movie:1296141', 'payload': None, 'rating': '9.6', 'name': '控方证人'}, Document {'id': 'movie:1292052', 'payload': None, 'rating': '9.7', 'name': '肖申克的救赎'}]}

查询评分小于等于7.5的电影

1
2
3
4
q = Query("@rating:[-inf 7.5]").language("chinese").highlight().return_fields("name", "rating", ).paging(0,15).sort_by(
"rating")
res = client.search(q)
res

结果:

1
Result{2 total, docs: [Document {'id': 'movie:6893932', 'payload': None, 'rating': '0', 'name': '壮志凌云2:独行侠'}, Document {'id': 'movie:1315316', 'payload': None, 'rating': '7.3', 'name': '无间道风云'}]}

查询评分小于等于7.5或大于等于9.6的电影

1
2
3
4
5
# q = Query("(@rating:[-inf 7.5])|@rating:[9.6 inf]").language("chinese").highlight().return_fields("name", "rating", ).paging(0,15).sort_by("rating")
# 等价于否定查询
q = Query("-@rating:[(7.5 (9.6]").language("chinese").highlight().return_fields("name", "rating", ).paging(0,15).sort_by("rating")
res = client.search(q)
res

结果:

1
Result{4 total, docs: [Document {'id': 'movie:6893932', 'payload': None, 'rating': '0', 'name': '壮志凌云2:独行侠'}, Document {'id': 'movie:1315316', 'payload': None, 'rating': '7.3', 'name': '无间道风云'}, Document {'id': 'movie:1296141', 'payload': None, 'rating': '9.6', 'name': '控方证人'}, Document {'id': 'movie:1292052', 'payload': None, 'rating': '9.7', 'name': '肖申克的救赎'}]}

查询评分为9.5的电影

1
2
3
4
5
6
7
8
from redisearch import NumericFilter

# q = Query("@rating:[9.5 9.5]").return_fields("name", "rating", )
# 等价于使用filter过滤,对应命令行FT.SEARCH idx:movie * FILTER rating 9.5 9.5 RETURN 2 name rating
filters = NumericFilter("rating", 9.5, 9.5)
q = Query("*").add_filter(filters).return_fields("name", "rating", )
res = client.search(q)
res

结果:

1
Result{5 total, docs: [Document {'id': 'movie:34961898', 'payload': None, 'name': '汉密尔顿', 'rating': '9.5'}, Document {'id': 'movie:1292720', 'payload': None, 'name': '阿甘正传', 'rating': '9.5'}, Document {'id': 'movie:1303408', 'payload': None, 'name': '福尔摩斯二世', 'rating': '9.5'}, Document {'id': 'movie:1292063', 'payload': None, 'name': '美丽人生', 'rating': '9.5'}, Document {'id': 'movie:1295124', 'payload': None, 'name': '辛德勒的名单', 'rating': '9.5'}]}
多字段类型混合查询

查询简介中包含战争,但不包含美国,且国家为法国或日本的电影

1
2
3
q = Query("(@description:(战争 -美国)) (@country:{法国|日本})").language("chinese").highlight().return_fields("name", "description", "country").paging(0, 10)
res = client.search(q)
res

结果:

1
Result{2 total, docs: [Document {'id': 'movie:1293318', 'payload': None, 'name': '萤火虫之墓', 'description': '美日<b>战争</b>爆发,14岁的清太带着年幼的妹妹到处逃命,当他们到达防空洞的时候,母亲已身受重伤,没过多久便不久人世。两兄妹自此过着相依为命的日子。他们只好投靠了母亲的姐妹,纵使他们把家里所有的家当都送给了阿...', 'country': '日本'}, Document {'id': 'movie:1296736', 'payload': None, 'name': '钢琴家', 'description': '史标曼(艾德里安•布洛迪 Adrien Brody 饰)是波兰一家电台的钢琴师。二战即将爆发之时,他们全家被迫被赶进华沙的犹太区。在<b>战争</b>的颠沛流离中,家人和亲戚最终被纳粹杀害,而标曼本人也受尽种种羞辱...', 'country': '法国'}]}

分组聚合查询

要进行聚合查询,需要借助类的实例传递AggregateRequestClient的实例的方法search()

查询各种类型的对应的电影数量

RediSearch 命令行: FT.AGGREGATE idx:movie * GROUPBY 1 @genre REDUCE COUNT 0 AS genre_nums SORTBY 2 @genre_nums DESC LIMIT 0 5

1
2
3
4
5
6
from redisearch.aggregation import Desc
from redisearch import reducers

request = AggregateRequest("*").group_by("@genre", reducers.count().alias("genre_nums")).sort_by(Desc("@genre_nums")).limit(0, 5)
res = client.aggregate(request)
res.rows

结果:

1
2
3
4
5
[['genre', '剧情', 'genre_nums', '79'],
['genre', '犯罪', 'genre_nums', '24'],
['genre', '冒险', 'genre_nums', '18'],
['genre', '喜剧', 'genre_nums', '17'],
['genre', '爱情', 'genre_nums', '16']]
Reducer

从示例中注意,我们使用了模块中的对象reducers。 有关在对结果进行分组时可以使用的reducers函数的更多示例,请参阅官方RediSearch 文档。

Reducer 函数包括一个alias()为 reducer 的结果指定特定名称的方法。如果您不提供名称,RediSearch 将生成一个。

1
2
3
request = AggregateRequest("*").group_by("@genre", reducers.count()).limit(0, 5)
res = client.aggregate(request)
res.rows

结果:

1
2
3
4
5
[['genre', '犯罪', '__generated_aliascount', '24'],
['genre', '惊悚', '__generated_aliascount', '11'],
['genre', '科幻', '__generated_aliascount', '10'],
['genre', '爱情', '__generated_aliascount', '16'],
['genre', '动作', '__generated_aliascount', '15']]
按零个、一个或多个字段分组

group_by语句可以将零个、单个字段名称作为字符串,或将多个字段名称作为字符串列表。

查询所有电影投票数之和(即我们mysql里的只聚合不分组)

1
2
3
request = AggregateRequest("*").group_by([], reducers.sum("@votes").alias("votes_sums"))
res = client.aggregate(request)
res.rows

结果:

1
[['votes_sums', '77890535']]

查询每个国家每个年份电影数

等价RediSearch 命令行:FT.AGGREGATE idx:movie * GROUPBY 2 @country @year REDUCE COUNT 0 AS sums SORTBY 4 @sums DESC @year DESC LIMIT 0 10

1
2
3
request = AggregateRequest("*").group_by(["@country", "@year"], reducers.count().alias("sums")).sort_by(Desc("@sums"), Desc("@year")).limit(0, 10)
res = client.aggregate(request)
res.rows

结果:

1
2
3
4
5
6
7
8
9
10
[['country', '美国', 'year', '1999', 'sums', '6'],
['country', '美国', 'year', '1995', 'sums', '6'],
['country', '美国', 'year', '2019', 'sums', '4'],
['country', '美国', 'year', '2014', 'sums', '4'],
['country', '美国', 'year', '2010', 'sums', '4'],
['country', '美国', 'year', '2007', 'sums', '4'],
['country', '美国', 'year', '2004', 'sums', '4'],
['country', '美国', 'year', '2003', 'sums', '4'],
['country', '美国', 'year', '1998', 'sums', '4'],
['country', '美国', 'year', '1994', 'sums', '4']]

查询每个国家每个年份电影数之后,获取最多的电影数

等价RediSearch 命令行:FT.AGGREGATE idx:movie * GROUPBY 2 @country @year REDUCE COUNT 0 AS sums GROUPBY 0 REDUCE MAX 1 @sums AS country_year_max_nums

1
2
3
request = AggregateRequest("*").group_by(["@country", "@year"], reducers.count().alias("sums")).group_by([], reducers.max("@sums").alias("country_year_max_nums"))
res = client.aggregate(request)
res.rows

结果:

1
[['country_year_max_nums', '6']]
聚合结果对象

聚合查询返回一个AggregateResult对象,该对象包含为查询返回的行和一个游标(如果您使用的是游标 API)

filter

在 reducer 函数运行后,使用filter进一步聚合查询的结果(听起来类似sql中的having)。例如,计算每年上映的电影数量,只返回上映数量高于 5 的年份:

等价RediSearch 命令行:FT.AGGREGATE idx:movie * GROUPBY 1 @year REDUCE COUNT 0 AS year_nums FILTER "@year_nums>5"

1
2
3
request = AggregateRequest("*").group_by("@year", reducers.count().alias("year_nums")).filter("@year_nums>5")
res = client.aggregate(request)
res.rows

结果:

1
2
3
4
5
6
7
8
[['year', '2004', 'year_nums', '7'],
['year', '2009', 'year_nums', '6'],
['year', '2003', 'year_nums', '6'],
['year', '2001', 'year_nums', '6'],
['year', '1995', 'year_nums', '8'],
['year', '1999', 'year_nums', '6'],
['year', '2019', 'year_nums', '6'],
['year', '1957', 'year_nums', '6']]

参考文献:

https://redis.io/docs/stack/search/

https://github.com/RediSearch/redisearch-py