[node.js]在redis上使用数据搜索

reids本身是一个基于键值对数据存储的内存数据库,也就是只能通过数据的key来获取数据项目,那么它自然也就没有任何数据搜索方面的功能,只能依靠一定规则生成的key来获取数据。

虽然它的本体是这样,但redis也提供了几个模块为其添加了一部分搜索功能的支持,并将这些模块整合为了redis-stack,官网介绍:https://redis.io/docs/stack/,我暂时还没测试它是否可以直接替代原本的redis实例,但就命令形式上来看,应该是兼容的。

本文是对于redis-stack官网上提供的node.js示例记录的笔记。

安装

redis-stack的安装我就不介绍了,直接从官网下载就好,或者使用docker之类的,本文只记录如何使用。

要在node.js中使用redis-stack的相关特性,我们需要redis-om模块,使用npm i redis-om安装进需要它的项目,关于该模块更详细的API介绍可以去其npm包页面上或者github仓库查看:https://github.com/redis/redis-om-node,不过要注意的是官方的示例中有些地方是错的,我会在下面对应的部分说明。

初始化连接和数据结构

本文假设你已经开启了顶层await,如果没有的话把下面所有的代码包进一个async函数里执行即可。

首先从中取出几个目前需要的类 本文来自luojia.me

const { Client ,Entity, Schema } = require('redis-om');

接着创建一个客户端对象

const client = await new Client().open('redis://[[用户名]:密码]@服务地址:端口');

创建一个class,用于指代一类数据,并为它定义数据项目和类型

class Poi extends Entity {} //注意,该类的类名会成为redis中键的一部分,所以尽量不要起太长的名字
//是的没错,这个类只需要这样一行就完成创建了。
//你也可以自己在里面添加getter来根据redis返回的条目生成数据。

//json数据结构定义,由于此数据将以json文本形式存储在redis中,因此也建议不要给属性起太长的名字,在业务许可的范围内越短越好
const schema = new Schema(Poi , {
	time: { type: 'date' }, //定义为日期,在实际json中将以时间戳形式存储
	num: { type: 'number',sortable:true },//定义为数字,并将其定义为可排序的
	msg: { type: 'string' },//定义为普通字符串
	types: { type: 'string[]' },//定义为字符串数组
	value: { type: 'text' },//定义为可执行全文索引的字符串
	isPoi: { type: 'boolean' }, //定义为布尔型数据
	pos: { type: 'point' }, //定义为坐标类型
});

以上就是目前redis-stack支持进行搜索的所有类型了,这里放个链接,关于对数据的附加定义和后续如果有任何更新可以看这个区块:https://github.com/redis/redis-om-node#-define-an-entity-and-a-schema本文来自luojia.me

接下来我们需要创建一个抽象的数据仓库对象,该对象就是主要用于进行搜索和操作数据的对象。

const repo = client.fetchRepository(schema);

//紧接着我们可以执行一次创建索引的指令,如果不存在索引,则进行搜索时会报错
await repo.createIndex();
//不用担心重复执行该指令会有副作用,当有效的索引存在时它不会进行任何操作
//如果你想要手动清除索引,可以执行 await repo.dropIndex()

到此为止对于该数据库的初始化工作就完成了,接下来就是数据的创建和搜索部分。

创建数据

这个包的作者非常热衷于给同一个功能创建很多使用方法,在创建数据方面,我们可以有以下两种流程 本文来自luojia.me

1.首先创建数据条目,然后保存它

const entity= repo.createEntity(); //创建条目
entity.entityId // 可以通过该属性获得该条目随机生成的id,比如 '01FJYWEYRHYFT8YTEGQBABJ43J'
entity.time=new Date();
entity.num=123;
entity.pos={ longitude: -81.6764187, latitude: 41.5080462 };

const id=await repo.save(entity);//保存到redis中

在数据类型中已经定义了属性,但是在这里没有赋值的,会变成null。

2.直接创建并保存条目

const entity=await repo.createAndSave({
	time: new Date(),
	num: 123,
	pos: { longitude: -81.6764187, latitude: 41.5080462 },
});
entity.entityId  //可以通过该属性获得该条目随机生成的id

数据保存完成后在redis中的形式默认是这样的,键名中的冒号:是分隔符,可以自己定义为其它字符,本文不做介绍

  • 键: 数据类名:条目id,在本例中为Poi:01FJYWEYRHYFT8YTEGQBABJ43J
  • 值: JSON字符串

如果条目中所有值都是null,那么保存该条目会使其直接被删除。 本文来自luojia.me

数据操作

有了数据之后我觉得基本的数据操作也需要简单介绍一下,因为和原本的redis包操作方式不太一样。

  • 通过id获取数据: await repo.fetch(条目id); 使用fetch获取数据时,如果id对应的数据不存在,则会返回null
  • 通过id删除数据: await repo.remove(条目id)
  • 为数据设置过期时间: await repo.expire(条目id,秒数)

数据搜索

接下来就是重头戏了,使用搜索功能获取我们需要的所有数据,而不是通过固定的id获取单个数据。官网的说明链接在这里:https://github.com/redis/redis-om-node#-using-redisearch,如前面所说,作者非常喜欢给一个功能定义好几种用法,但我在这里只写最简短的一种,我觉得其它写法都又臭又长,喜欢别的写法的就自己去看官方文档。

搜索始于repo.search(),然后在后面接上条件语句,在官方readme中有一部分的search不是作为方法调用的,而是直接当属性获取其where,亲测是错误的。

下面我尽量在简短的示例代码中包含大部分要点,并在注释里写上作用 本文来自luojia.me

const results=await repo.search()	//该语句还没结束,为了方便注释,我把不同的部分分到了不同行里
	.where('num').eq(123)			//搜索num等于123的数据条目
	.and('msg').not.eq('poi')		//使用'与'条件搜索msg不等于poi的条目
	.or('isPoi').true()				//使用'或'条件搜索isPoi为true的条目
	.or(search=>search.where('value').match('poi').and('num').gt(1))
	//↑使用'或'条件且嵌套子条件,该子条件里需要满足对value字段全文搜索匹配poi并且num大于1
	.return.all()

这里再单独列一下不同类型可以使用的搜索方法

String 字符串
  • 等于 : eq(str)
  • 不等于 : not.eq(str)
Number 数字
  • 等于 : eq(x)
  • 大于 : gt(x)
  • 大于等于 : gte(x)
  • 小于 : lt(x)
  • 小于等于 : lte(x)
  • 在a和b之间 : between(a,b) //包含首尾
  • 在上面几个方法前加上not.就是相反条件
Boolean 布尔型
  • 为true : true()  //讲道理像这样用关键词做函数名真的不可取
  • 为false : false()
  • 也可以用eq(true或false)判断是否为true或false
  • 同样,以上方法前面加上not.就是相反条件
Date 日期
  • 判断是否在某一天中 : on(x)eq(x)   //x可为Date对象、符合ISO 8601的字符串或以秒为单位的unix时间戳,以下相同
  • 在时间x之前 : before(x)lt(x)
  • 在时间x之后 : after(x)gt(x)
  • 在时间x当天或之前 : onOrBefore(x)lte(x)
  • 在时间x当天或之后 : onOrAfter(x)gte(x)
  • 在时间a和b之间 : between(a,b)
  • 同样,以上方法前面加上not.就是相反条件 本文来自luojia.me

注意,官方文档上写的是Date类型由于存储为数字,所以也可以使用数字的搜索方法,因此上面所有带on的方法意思就产生了歧义,我暂时还没测试它到底是只要在当天就可以匹配还是必须要完全相同才可以匹配。

String[] 字符串数组 本文来自luojia.me
  • 数组中包含指定字符串 : contain(str)
  • 数组中包含指定字符串中的一个 : containOneOf(a,b,c, ... )
  • 以上方法前面加上not.就是相反条件
Text 全文索引
  • 匹配内容 : match(str)
  • 精确匹配 : matchExact(str)  //此模式下单词之间的空格不会被认为是关键词分隔符,而是作为关键词的一部分一起搜索
  • 以上方法前面加上not.就是相反条件
在match中可以使用*符号进行词后模糊查询,比如repo.search().where('value').match('poi*'),注意该符号只能放在词后,不能在词前,也不能在matchExact里使用,出现这些用法都将报错。
Point 坐标

坐标搜索需要使用嵌套搜索,嵌套在inRadius方法内,首先写个示例

await repo.search().where('pos').inRadius(
	circle => circle.origin(-81.7758995, 41.4976393).radius(50).meter
)

这里使用origin(经度,纬度)定义搜索中心,使用radius(x)定义搜索半径,后面可以跟以下属性作为单位。

  • mile
  • foot
  • kilometer
  • meter

作者定义了3种写法,包括复数、简写,为了语义明确,我这里只列了单数形式 本文来自luojia.me

如果没有指定搜索中心,默认为(0,0),如果未指定单位,默认为meter(米)。

到这里针对不同类型数据的搜索方法就全部讲完了。

排序

对于我们的搜索结果,可以对其进行预排序再返回,只需要在前面的搜索语句后面加上.sortBy(字段,‘asc'或'desc')即可,asc为从小到大排序,desc为从大到小排序,对于排序方向还有两个简写函数.sortAscending(字段名).sortDescending(字段名)

redis-om有两种存储模式,一个是json,一个是hash,这两种模式下可以排序的数据类型是不同的,详情看这里,我就不多做解释了。

获取结果

搜索写完了就该获取搜索结果了,方法很简单,就是在上面搜索的语句后面加上.return.all(),该方法会返回所有满足条件的结果,完整示例

const results=await repo.search().where('num').eq(123).return.all();

results会成为一个结果数组,存放着所有结果对象。 本文来自luojia.me

有时我们不需要返回所有结果,比如只要获取结果中的第一行可以这么写 : .return.first(),这时候results就不是数组了,而是一个单独的条目对象;

或者我们需要返回结果中的一部分,可以用分页方法 : .return.page(偏移量,数量)

又或者我们同时还想获得结果的总数量 :  .return.count(),这时候results也不是数组,返回的是一个数字;

如果主要结果里的最小值,可以直接使用  .return.min() ,这个是我在源码里翻到的,readme中没有列举,还有maxminIdminKeymaxIdmaxKeypageOfIdspageOfKeysfirstId firstKeyallIdsallKeys

另外经测试,search后面不一定非要跟where,也可以直接写and、or之类的方法,因此那些由程序生成的条件就不需要处理这个例外去凑一个where了,可以直接调用and或or。



本文发布于 https://luojia.me

本站文章未经文下加注授权不得拷贝发布。

本博客使用Disqus评论系统,如果看不到评论框,请尝试爬墙。