[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。

小米Mix2s修复距离传感器失灵

我的Mix2s不知道从什么时候开始出现了打电话时屏幕不灭,或者灭掉了之后又没法亮屏的问题,甚至还有打电话时有一定概率对方会完全听不见自己声音的情况,

原本我一直以为是刷的LineageOS和这硬件不太兼容,于是想着可能某次更新之后就解决了。

但我最近升了一个大版本之后发现这个问题依然存在,于是我网上查了几天,发现很多人都有这样的情况,而且和系统无关,全都有这种现象。

再仔细一查,发现似乎是因为距离传感器在长时间使用过程中被细小灰尘堵住了,导致它的超声波距离传感器灵敏度变差。

Mix2s的超声波距离传感器位于听筒两边,是从听筒的孔两边延伸出去的两条非常细的缝,长度大约各1厘米,用眼睛不太容易看出来,需要灯光位于一个特殊角度时你才可以发现听筒两边有一部分的缝比旁边要大一点点(如下图),用美工刀片是可以卡进去一点的。

修复方案那就是先用美工刀插进超声波缝,然后把里面固化的灰尘划出来,注意小心点别把听筒捅坏了,能划的灰都弄出来之后用能插得进缝的软毛刷狂捅一顿,再左右刷刷,尽量把能看到的灰都刷掉。

这样处理完之后距离传感器就可以恢复正常的感应距离了,也不那么容易出现卡在一个状态不变化的情况(但由于设计缺陷,某些情况下还是会有距离状态不变的问题,这个无法修复)

[SASS]指定import查找路径

本文为node包sass的使用笔记。

写这篇文章,是因为我碰到了引用node_modules目录中模块的scss文件的需求,但是它总是提示找不到: “Error: Can’t find stylesheet to import.”,于是我搜了搜,发现说是要在渲染参数中添加loadPaths数组来告诉sass去哪里找依赖文件,我在这个模块的typescript类型定义中也找到了这个参数名,但是我加上了之后并没有用,于是研究一番发现这个包使用的实际上并不是loadPaths,而是includePaths这个参数名,所以觉得有必要写篇笔记记录一下。

另外我发现有的解决方案中说可以在import的url前面加个~来表示这个文件在node_modules里,但我试了也没有用,这个特性似乎是webpack给sass引入的importer,所以单独使用sass的render api时此方式是不起作用的,如果使用的是sass-loader的话可以使用这个方法。

【佳佳拆解】台电幻影NP900C

这是之前买的台电幻影NP900C,三年质保,现在3年过了3个月就挂了,拆出来给大家看看。

我不知道这个板型是不是什么通用板型,反正我在淘宝上看到了一模一样的,带主控卖五十多块钱。

主控是慧荣SM2263XT,好像是AD的,在我尝试加焊闪存颗粒维修的时候,因为闪存不小心移位了一下,导致下面连锡了,然后一通电主控就炸了。

继续阅读【佳佳拆解】台电幻影NP900C

把3090塞进紧凑MATX机箱

上星期,我的系统盘(台电NP900C)用着用着就炸了,刚过保几个月就炸了,用量产工具检查了一下一个闪存颗粒失联了,数据恢复无望,还好里面只有系统和安装的软件,损失不大,尝试重新开卡当缓存用,现在重新买了个1TB的三星980Pro当系统盘用。

我猜测可能是因为直连CPU的m.2那个位置吸3090尾气太多,温度过高,又没有一个很好的风道散热,于是它迫不及待地热死了。

之前为了散热问题买了一个酷冷至尊Q500L,我标题说是MATX,但它其实也能放ATX主板,只是需要舍弃一些别的配置选项,本想把新的主板和显卡装进这个更扁的机箱,结果发现电源和显卡都太长,他俩的位置冲突了一点,电源是以前发过的长城巨龙,然后就放弃了,把这个机箱给了我的旧主板和cpu。

继续阅读把3090塞进紧凑MATX机箱

[Excel]根据身份证号码计算年龄

最近被迫大量使用excel,所以我也学了点它的函数用法,做点笔记

根据身份证号码计算年龄的使用场景应该挺多的,但我在网上始终没找到一个能得到最准确结果的答案,于是东拼西凑写了个

#以下公式需要把#以及后面的内容删除才可以使用,因为excel的公式并不存在注释语法
=FLOOR(		#把计算结果向下舍入,精度为1
	DATEDIF(	#计算日期差
		TEXT(	#把生日字符串格式化为日期
			MID("身份证号",7,8),#提取身份证的生日部分
			"0-00-00"
		),
		TODAY(),#获取今天的日期
		"M"	#以月为单位计算前两个参数的差值
	)/12,		#除以12变成年
1)

网上很多方法是直接计算两个日期之间的年差,没有考虑到过了生日才长一岁的问题,所以我计算的是月差,毕竟每年的月份是固定12个,那么只要用总月数除以12再舍掉小数就是正确的周岁结果了

[BiliBili]视频右键菜单工具(油猴脚本)

地址:https://gist.github.com/JiaJiaJiang/c54abe1227917af3d3825dfa098b9473

安装:在浏览器有tampermonkey插件的情况下,点上面链接里的RAW按钮安装

功能:

  • 右键视频变速按钮:添加新版播放器中被砍掉的右键菜单视频变速按钮
  • 跳过OP、ED快捷键:ctrl+左右键可以跳过一般op和ed长度的时间(1分30秒)
  • 右键截图按钮:点击后将当前视频画面完整截图并在新窗口中打开

 

[Node.js]ES模块和CommonJS模块的导入

在使用ES导入语法的文件中想要使用require导入一个CommonJS模块的话,可以使用module.createRequire来创建一个require函数。

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

 

在一般的使用CommonJS导入语法的文件中想要使用ES语法导入模块的话,可以使用import函数,要注意的是这是一个异步函数,返回一个Promise,所以需要await它或者使用then来等待返回结果,如下

(async()=>{
	const fs=await import('fs);
})();

 

[Browserify]异步插件导致write after end错误

写了个Browserify插件,之前一直没问题,但今天我改了点打包流程,结果一直报write after end错误,找了半个晚上,发现是因为我的插件函数写的是异步的,里面有个await异步读文件。

之前一直没出问题是因为后面的打包流程时间够长,能让这个插件正常执行完,但今天改了流程之后部分情况下很快就执行完了,于是当插件异步流程执行完后继续就出错了,修正方法是把异步文件读取改成同步的。

[Node.js]进程异常退出,错误码 3221226505

不是我想水一篇博文,在解决完前一篇博文的问题之后,立刻就又碰上另一个奇葩问题,进程直接跑着跑着没了,留下一个错误码3221226505。

在我左思右想把代码改来改去,包换来换去之后依然不能解决问题,然后发现是canvas包的问题,之所以一直没有发现是它,是因为出问题的代码里并没有用到这个包,它是在别的地方被引用到的,可能它的二进制模块破坏了node的环境,导致别的代码执行的时候触发了异常导致程序崩溃。

这个问题其实也不是第一次出现了,而且每次都是因为项目中同时有canvas和sharp两个模块才出现问题,在某些版本下不会出问题,某些版本下又会出现不一样的错误,这次就是和上次不一样所以才没能快速发现。

就这点破事,又浪费了我一个晚上,这时候我真想给搞出这bug的人屁股上来一脚。

IIS反向代理站点跳转域名被替换

今天一个破问题浪费我一下午的时间,我一直在找自己代码是不是有什么问题,或者是不是express有什么bug,结果到最后发现原来是iis的问题。

如果是在win上用iis反代站点的话,通常都会用iis的requestRouter插件,在他的设置页面Application Request Routing里网上的教程通常都勾着Reverse rewrite host in response headers这一项,就是因为这个设置导致后端返回的http头中location指定的跳转地址被替换为了此站点的地址,不知道这是个什么智障操作。

大龄单身狗的日常