Redis查询之RediSearch和RedisJSON讲解
1 Redis查询
1.1 RedisMod介绍
首先介绍下RedisMod
这个东西,它是一系列Redis
的增强模块。有了RedisMod
的支持,Redis
的功能将变得非常强大。目前RedisMod
中包含了如下增强模块:
RediSearch
:一个功能齐全的搜索引擎;RedisJSON
:对JSON
类型的原生支持;RedisTimeSeries
:时序数据库支持;RedisGraph
:图数据库支持;RedisBloom
:概率性数据的原生支持;RedisGears
:可编程的数据处理;RedisAI
:机器学习的实时模型管理和部署。
1.2 安装Redis
Redis
这些模块都是依赖于Redis
,因此先要安装Redis
点击了解Redis单机安装
点击了解Redis集群安装
1.3 RediSearch+RedisJSON安装
1.3.1 下载安装
下载RediSearch+RedisJSON
地址:https://redis.com/redis-enterprise-software/download-center/software/
在 redis
安装目录下新建 module
文件夹,把获取到的rejson.so
和module-enterprise.so
(可以重命名为redissearch.so
)文件 放到 module
文件夹中
1.3.2 修改配置
修改 文件 为可执行权限
chmod +x rejson.so
chmod +x module-enterprise.so
修改 redis.conf,搜索 loadmodule
loadmodule /root/software/redis-6.0.6/module/rejson.so
loadmodule /root/software/redis-6.0.6/module/module-enterprise.so
重启 redis
/usr/local/redis-6.2.6/bin/redis-cli -a 123456 shutdown
/usr/local/redis-6.2.6/bin/redis-server conf/redis.conf
1.4 RedisJSON操作
命令行体验 json
的操作
1.4.1 基本操作
1.4.1.1 保存操作JSON.SET
语法:
JSON.SET <key> <path> <json> [NX | XX]
参数说明:
- 对于新的
Key
,path
需要使用$
或.
- 对于已经存在
Key
,在进行保存操作之后,原来path
路径的值将会被替换掉; NX
:表示只有Key
不存在,才执行保存操作XX
:表示只有Key
存在,才执行保存操作- 通过命令
type doc
可以查看到存储进去的数据是ReJSON-RL类型
1.4.1.2 读取操作JSON.GET
语法:
JSON.GET <key>
[INDENT indentation-string]
[NEWLINE line-break-string]
[SPACE space-string]
[path ...]
参数说明:
- 允许使用多个
path
进行查询 INDENT
查询结果替换掉默认缩进字符(用于返回Pretty-formatted JSON
)NEWLINE
查询结果替换掉默认换行符(用于返回Pretty-formatted JSON
)SPACE
查询结果替换掉默认空格(用于返回Pretty-formatted JSON
)- 获取
JSON
对象中的属性时需要以.
开头
1.4.1.3 批量读取操作JSON.MGET
语法:
JSON.MGET <key> [key ...] <path>
参数说明:
- 最后一个参数作为
path
进行处理 - 遍历每一个
Key
的path
,如果不存在,则返回null
例子:
先保存两条记录
JSON.SET doc1 $ '{"a":1, "b": 2, "nested": {"a": 3}, "c": null}'
JSON.SET doc2 $ '{"a":4, "b": 5, "nested": {"a": 6}, "c": null}'
再进行mget操作
JSON.MGET doc1 doc2 $..a
执行结果:
1) "[1,3]"
2) "[4,6]"
注意
:$..a
是一个 JSONPath
表达式,它表示从 JSON
文档的根开始,递归地查找所有键名为 a 的值。具体解释如下:
$
:表示 JSON 文档的根。..
:表示递归下降操作符,这个操作符会查找所有层级中符合条件的键。a
:表示要查找的键名。
1.4.1.4 删除操作JSON.DEL
语法:
JSON.DEL <key> [path]
参数说明:
path
是可选的,如果没有输入,则默认整个Key
删除掉
例子:
JSON.DEL doc $..a
结果:
"2"
1.4.1.5 其他命令
除了上面的几种常见操作,官方还支持如下命令,官方命令地址:https://redis.io/docs/stack/json/commands/
常用命令:
JSON.NUMINCRBY,JSON.NUMMULTBY,JSON.STRAPPEND,JSON.STRLEN
数组命令:
JSON.ARRAPPEND,JSON.ARRINDEX,JSON.ARRINSERT,JSON.ARRLEN,JSON.ARRPOP,JSON.ARRTRIM
对象命令:
JSON.OBJKEYS,JSON.OBJLEN
组件命令
JSON.TYPE,JSON.DEBUG,JSON.FORGET,JSON.RESP
1.4.1.6 综合操作
创建一个 json_1
127.0.0.1:6379> JSON.SET json_1 . '{"name":"zz","age":22,"msg":"hello"}'
OK
设置 json_1 的 key=name 的值为 zhangsan
127.0.0.1:6379> JSON.SET json_1 .name '"zhangsan"'
OK
获得整个 json_1
127.0.0.1:6379> JSON.GET json_1
"{\"name\":\"zhangsan\",\"age\":22,\"msg\":\"hello\"}"
获得 json_1 键为 name 的值
127.0.0.1:6379> JSON.GET json_1 .name
"\"zhangsan\""
往 json_1 中添加一个数组对象
127.0.0.1:6379> json.set json_1 .list '[2,3,4]'
OK
往 json_1 的 list 对象中添加一个元素 6
127.0.0.1:6379> json.arrappend json_1 .list 6
(integer) 4
查看所有元素
127.0.0.1:6379> json.get json_1
"{\"name\":\"zhangsan\",\"age\":22,\"msg\":\"hello\",\"list\":[2,3,4,6]}"
体验下来,感觉 Redis
原生支持 json 之后,对于 redis 的操作更加灵活了。
想象空间更大了,一切复杂信息的存储皆可 JSON,并且操作十分简单,省去了序列化、反序列化的操作,
1.4.2 Java 来操作 redis Json
当然我们还是要在一个 Java
工程中去操作一下:
package com.kkarch.rejson;
import com.redislabs.modules.rejson.JReJSON;
import com.redislabs.modules.rejson.Path;
import redis.clients.jedis.Jedis;
import java.util.Arrays;
public class ReJsonMain {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.0.110",6379);
jedis.auth("123456");
JReJSON redisClient = new JReJSON(jedis);
System.out.println("初始化 json");
redisClient.set("json_2",new Object());
redisClient.set("json_2","zhangsan",new Path(".name"));
redisClient.set("json_2",21,new Path(".age"));
redisClient.set("json_2","hello",new Path(".msg"));
redisClient.set("json_2",Arrays.asList(9,8,7),new Path(".arr"));
Object result = null;
result = redisClient.get("json_2");
System.out.println(result);
System.out.println("设置 name=lisi");
redisClient.set("json_2","lisi",new Path(".name"));
result = redisClient.get("json_2");
System.out.println(result);
System.out.println("在数组追加一个值:21");
redisClient.arrAppend("json_2", new Path(".arr"), 21);
result = redisClient.get("json_2");
System.out.println(result);
}
}
结果:
初始化 json
{name=zhangsan, age=21.0, msg=hello, arr=[9.0, 8.0, 7.0]}
设置 name=lisi
{name=lisi, age=21.0, msg=hello, arr=[9.0, 8.0, 7.0]}
在数组追加一个值:21
{name=lisi, age=21.0, msg=hello, arr=[9.0, 8.0, 7.0, 21.0]}
1.5 RediSearch操作
通过RediSearch
模块,Redis
可以变成一个功能强大的全文搜索引擎,并且原生支持中文搜索,下面我们就来体验下
1.5.1 查询语法
RediSearch
的搜索语法比较复杂,不过我们可以对比SQL
来使用它,具体可以参考如下
SQL Condition | RediSearch Equivalent | 注释 |
---|---|---|
where x='foo' and y='bar' | @x:foo @y:bar | for less ambiguity use (@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' ) | @x:(foo| bar) | quotes means exact phrase |
where y='foo' and x not in ('foo' ,'bar' ) | @y:foo (-@x:foo)(-@x: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* |
1.5.2 建立索引
使用 RediSearch
来搜索数据之前,我们得先创建下索引,建立索引的语法有点复杂,我们先来看下;
FT.CREATE {index}
[ON {data_type}]
[PREFIX {count} {prefix} [{prefix} ..]
[LANGUAGE {default_lang}]
SCHEMA {identifier} [AS {attribute}]
[TEXT | NUMERIC | GEO | TAG ] [CASESENSITIVE]
[SORTABLE] [NOINDEX]] ...
使用FT.CREATE
命令可以建立索引,语法中的参数意义如下;
index
:索引名称;data_type
:建立索引的数据类型,目前支持JSON
或者HASH
两种;PREFIX
:通过它可以选择需要建立索引的数据前缀,比如PREFIX 1 "product:"
表示为键中以product:
为前缀的数据建立索引;LANGUAGE
:指定TEXT
类型属性的默认语言,使用chinese
可以设置为中文;SCHEMA
:索引的字段identifier
:指定属性名称;attribute
:指定属性别名;TEXT | NUMERIC | GEO | TAG
:这些都是属性可选的类型;SORTABLE
:指定属性可以进行排序。
看了语法可能不太好理解,直接对一个商品数据建立索引试试就懂了;
FT.CREATE
productIdx
ON JSON
PREFIX 1 "product:"
LANGUAGE chinese
SCHEMA $.id AS id NUMERIC
$.name AS name TEXT $.subTitle AS subTitle TEXT
$.price AS price NUMERIC
SORTABLE $.brandName AS brandName TAG
1.5.3 操作
建立完索引后,我们就可以使用FT.SEARCH
对数据进行查看了,比如使用*
可以查询全部;
FT.SEARCH productIdx *
由于我们设置了price
字段为SORTABLE
,我们可以以price
降序返回商品信息
FT.SEARCH productIdx * SORTBY price DESC
指定返回的字段;
FT.SEARCH productIdx * RETURN 3 name subTitle price
我们把brandName
设置为了TAG
类型,我们可以使用如下语句查询品牌为小米或苹果的商品;
FT.SEARCH productIdx '@brandName:{小米 | 苹果}'
由于price
是NUMERIC
类型,我们可以使用如下语句查询价格在500~1000
的商品;
FT.SEARCH productIdx '@price:[500 1000]'
还可以通过前缀进行模糊查询,类似于SQL
中的LIKE
,使用*
表示;
FT.SEARCH productIdx '@name:小米*'
在FT.SEARCH
中直接指定搜索关键词,可以对所有TEXT
类型的属性进行全局搜索,支持中文搜索,比如我们搜索下包含黑色字段的商品;
FT.SEARCH productIdx '黑色'
当然我们也可以指定搜索的字段,比如搜索副标题中带有红色字段的商品;
FT.SEARCH productIdx '@subTitle:红色'
通过FT.DROPINDEX
命令可以删除索引,如果加入DD
选项的话,会连数据一起删除;
FT.DROPINDEX productIdx
通过FT.INFO
命令可以查看索引状态;
FT.INFO productIdx
1.5.4 Java 中操作RediSearch
对于 Java 项目直接选用 Jedis4.0
以上版本就可以使用 RediSearch 提供的搜索功能,Jedis
在 4.0 以上版本自动支持 RediSearch
,编写 Jedis
连接 RediSearch
测试用例,用 RediSearch 命令创建如下,
1.5.4.1 Jedis 创建 RediSearch 客户端
@Bean
public UnifiedJedis unifiedJedis(GenericObjectPoolConfig jedisPoolConfig) {
UnifiedJedis client;
if (StringUtils.isNotEmpty(password)) {
client = new JedisPooled(jedisPoolConfig, host, port, timeout, password, database);
} else {
client = new JedisPooled(jedisPoolConfig, host, port, timeout, null, database);
}
return client;
}
1.5.4.2 Jedis 创建索引
Schema schema = new Schema()
.addSortableTextField("goodsName", 1.0)
.addSortableTagField("tag", "|");
IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.HASH)
.setPrefixes("idx:goods")
.setLanguage("chinese"); # 设置支持中文分词
client.ftCreate(idxName,
IndexOptions.defaultOptions().setDefinition(rule),
schema);
1.5.4.3 Jedis 添加索引源数据
public boolean addGoodsIndex(String keyPrefix, Goods goods) {
Map<String, String> hash = MyBeanUtil.toMap(goods);
hash.put("_language", "chinese");
client.hset("idx:goods" + goods.getGoodsId(), MyBeanUtil.toMap(goods));
return true;
}
1.5.4.4 Jedis 中文查询
public SearchResult search(String goodsIdxName, SearchObjVO searchObjVO, Page<SearchPageGoodsVO> page) {
// 查询关键字
String keyword = searchObjVO.getKeyword();
String queryKey = String.format("@goodsName:(%s)", keyword);
Query q = new Query(queryKey);
String sort = searchObjVO.getSidx();
String order = searchObjVO.getOrder();
// 查询是否排序
if (StringUtils.isNotBlank(sort)) {
q.setSortBy(sort, Constants.SORT_ASC.equals(order));
}
// 设置中文分词查询
q.setLanguage("chinese");
// 设置分页
q.limit((int) page.offset(), (int) page.getSize());
// 返回查询结果
return client.ftSearch(goodsIdxName, q);
}