前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【迅搜09】索引管理(二)增删改操作

【迅搜09】索引管理(二)增删改操作

作者头像
硬核项目经理
发布2023-12-19 16:06:08
1430
发布2023-12-19 16:06:08
举报

索引管理(二)增删改操作

今天我们来学习真正的,最核心的索引管理相关的操作。但其实今天的内容还更简单一些,为啥呢?因为索引管理中,最核心的就是对于数据的增、删、改呀。其实要往大了说,查询也是针对索引的操作,只不过相对来说,搜索引擎引用往往是读多写少,而且相比数据库来说,它的写还要少一些。

因此,XS 在 SDK 组件中,将索引对象和查询对象分开了。同样地,后端服务,也是通过 8383 和 8384 两个端口区分开了索引操作和查询操作。不过这也带来了一个问题,那就是索引的增、删、改操作是异步的,在查询的反馈上并不是完全及时的。

说了这些,其实就是要弄清楚我们的业务场景了。对于需要使用 XS 的系统来说,主要是文章、商品详情、存档数据这一类的信息。通常来说,文章文档类的应用可能会更多一些。这些应用往往修改的频率不高,而且就像我们的数据库设计,对于状态为未发布的文章,也完全没有必要进搜索引擎。大部分情况下,其实更多的前端调用还是在搜索上。根据这些业务场景,XS 的异步问题就完全不是问题了。当然,如果你想更快,更及时,那估计就是 ES 之类的了,但是,ES 也不是完全同步的哦,它也有刷分片级别设置的,默认情况下也不是直接就能马上读取到新写入的数据的,只是说,看到的效果比我们 XS 稍快一些。另外包括 Sphinx ,它对增量索引的支持都还没 XS 好呢(Sphinx绑定 MySQL 后全量索引速度非常快,它不推荐增量索引)。换句话说,搜索引擎的索引,应该是变动小的,而查询量,则是非常大的,需要全文检索分词的,这类应用,才是搜索引擎的主战场。

好吧,又扯了一遍搜索引擎的概念和应该在什么场景下使用搜索引擎。目的其实也是再次提醒大家一定要转变一下思维,要不看了 XS 的增、删、改功能之后,又用 MySQL 的思维来套,就会说 XS 的多垃圾呀什么的。了解之后就知道了吧,搜索引擎都是这个鸟样,千万不要直接用数据库的增、删、改思维来直接开骂哦。

好了,接下来我们就进入正文。

添加数据

添加数据的代码之前我们早就已经使用过的,没啥多说的。

代码语言:javascript
复制
$index = $xs->index; // 后期如果直接写一个 $index ,就是直接表示为 $xs->index 获得的 XSIndex 对象

$index2 = $xs->index->add(new XSDocument(['title'=>'添加一条','content'=>'添加一条'.date('YmdHis'),'id'=>uniqid()]));

var_dump($index === $index2);

add() 方法返回一个 XSIndex ,它自己本身,上面的代码我们就是来测一下 add() 返回的对象和调用 add() 方法时的对象是不是一样的,结果是全等于的 true 。

这里有什么问题呢?其实呀,这也是 XS 中一个比较被诟病的一点,添加操作,包括之后我们要学习的修改和删除操作,这些方法的返回值都只是一个 XSIndex 对象本身,没有其它内容。这样的话,我们就不知道这个操作是成功还是失败了。

在源码中,添加函数其实上调用的是修改的函数,这个我们在后面修改数据中再说。然后修改函数最终是通过之前我们学习过的 XSServer 对象中的 execCommand() 发送给服务端的。服务端在接收到之后返回的内容在 SDK 中没有处理,也没有返回或者记录。

这一点可能也会让大家比较困惑,其实 execCommand() 之后,是有返回值的,大家可以在源码中找到执行的地方打印它的返回值。

代码语言:javascript
复制
// /vendor/hightman/xunsearch/lib/XSIndex.class.php update() 方法下部
//…………………………
// execute cmd
if ($this->_bufSize > 0) {
  $this->appendBuffer(implode('', $cmds));
} else {
  for ($i = 0; $i < count($cmds) - 1; $i++) {
    $this->execCommand($cmds[$i]);
  }
  $this->execCommand($cmds[$i], XS_CMD_OK_RQST_FINISHED); // 打印这里
}
//…………………………

就能看到返回的是一个 XSCommand 对象,属性内容是:

代码语言:javascript
复制
XSCommand Object
(
    [cmd] => 128
    [arg1] => 0
    [arg2] => 250
    [buf] => 
    [buf1] => 
)

这样的,然后将 cmd 的值,也就是 128 放到 xs_cmd.inc.php 中查找,就会发现它对应的是 define('XS_CMD_OK', 128); 这个常量,意思很清楚了吧。

SDK 没有封装返回状态的结果,我觉得可能是因为这个 SDK 以完全面向对象的方式来写得,如果有问题直接会报错了,你可以再继续深入父类 XSServer 中 execCommand() 的源码进行查看。所以我们在调用 add() 之后,如果没报错,那么就可以认为是成功了。另一点就是由于索引的操作是异步的,返回的这个 128 状态也只是说服务端接收到了数据,完成了校验,但并不代表数据是正式插入成功了。

修改数据

修改数据使用的是 update() 方法。

代码语言:javascript
复制
$xs->index->update(new XSDocument(['title'=>'添加一条','content'=>'添加一条'.date('YmdHis'),'id'=>uniqid()]));

上面这条语句是有问题的,不知道各位同学发现了没有。

语法没问题,数据没问题,问题出在那个 id 字段上。我们使用的是 uniqid() 这个函数是 PHP 中生成不唯一字符串的。也就是说,上面的这个更新语句中,主键是一个新的 id 。不过幸好,在 XS 中,update() 的执行原理是根据主键 id ,先删除原来的数据,然后再添加一条。如果主键 id 指定的数据不存在,就是新添加一条数据。

划重点,先删除原来的,再添加一条新的进去。也就是说,这个更新也不是传统数据库层面的上更新,而是类似于很多 OLAP 大数据数据库的处理方式。这样的操作就会带来几个问题,我们来看下。

首先,主键 id 是可以重复的,可以有多条数据的主键是一样的。比如我们插入两条 id 一样的数据。

代码语言:javascript
复制
$xs->index->add(new XSDocument(['title'=>'添加一条1','content'=>'添加一条'.date('YmdHis'),'id'=>'123123123']));
$xs->index->add(new XSDocument(['title'=>'添加一条2','content'=>'添加一条'.date('YmdHis'),'id'=>'123123123']));

关于为啥主键 id 可以插入同样的数据问题,之前已经说过了,这里就不在赘述了,不记得的小伙伴可以翻看一下之前的文章。添加成功之后,执行下面的更新操作。

代码语言:javascript
复制
$xs->index->update(new XSDocument(['title'=>'添加一条3','pub_time'=>time(),'id'=>'123123123']));

这里我们更新 id 为 123123123 的数据,但内容产生了很大的变化,标题最后的数字是 3 ,还没有 content 了,出来的结果是什么呢?

代码语言:javascript
复制
> php vendor/hightman/xunsearch/util/Quest.php --show-query ./config/5-zyarticle-test1.ini "添加一条"
--------------------
解析后的 QUERY 语句:Query(<alldocuments>)
--------------------
在 1 条数据中,大约有 1 条包含  ,第 1-1 条,用时:0.0118 秒。

1. 添加一条3 #123123123# [100%,0.00]
 
Category_name:  Tags:  Pub_time:20221128  

看到结果了吧,数据只剩一条了,content 内容也没有了,但是 pub_time 有数据了。

这就是 update() 先删除,后添加的典型效果演示。如果你需要只更新其中某一个字段的值,也必须将所有的字段都带上,否则别的字段可能就没了哦。另外,删除多余的相同主键的数据其实在逻辑上是正确的,这个并没有其它多说的。

先别着急开骂,因为我们又要搬老大出来救场了。没错,ES 也是这样的!

如果你学过一点 ES 的话,那就会知道它可以通过 PUT 和 POST 两种请求方式来更新数据。不管使用哪个,如果直接在参数中使用字段更新,原来的文档数据就会被覆盖,就跟 XS 的效果是一样的,没写的字段就没有了。但 ES 可以通过指定 doc 字段,然后再更新,达到更新指定的字段效果。

总结一下,XS 中的 update() 相当于就是 ES 中的普通更新方式,但 XS 中没有提供 doc 的语法糖,只有先删后增这一种更新方式。

至于为什么搜索引擎都要这样来更新呢?因为倒排索引,之前我们已经学习过了,倒排索引是分词之后通过词项来建立和索引文档主键 id 的映射关系。如果是修改的话,需要的工作量非常大,需要遍历每个单词词项然后修改它所指向的 id 。而现在,我们先删,然后重新使用添加的过程进行索引。另外还有分数以及各种其它的计算都要重来一次,因此,直接删除再添加效率会更高一些,大概是这么个意思,但具体的原因和解释要更加的复杂,也不是我的水平所能理解的了,有兴趣的小伙伴可以自己再查找资料进行深入的学习。

和 add() 的差别

前面我们已经看到了,在使用 update() 的时候,如果主键数据不存在,就是新增。通过源码,相信大家也能看到 add() 里面就一行代码,直接就是去调用 update() ,但第二个参数设置成了 true 。那么,咱们直接用 update() 做新增不就好了?为啥还要一个 add() 呢?

其实从 add() 和 update() 的行为就可以看出来 ,add() 明显是少了一个判断的,而这个判断就是主键 id 数据是否存在。也就是我们一直在说的,add() 会忽略主键的唯一性,直接添加数据。而 update() 则不会,它会去进行查找判断,如果找到了,那么还得先删除,相对来说其实就是多了两个步骤。

由上可知,如果我们是针对大量数据的全量新建或者重建索引,那么 add() 的效率更好。而如果是日常的索引建立更新,比如说日常的添加修改文章、添加修改商品等等,则使用 update() 会更为保险,能够保证主键唯一性。

删除数据

在上面的添加和修改中,其实很多基础概念就已经讲完了,对于删除来说,没啥特别的东西,不过它有两种删除方式。

一是根据主键 id 进行删除,也是最推荐的方式。

代码语言:javascript
复制
$xs->index->del('6380e14c38b04');

这个参数就是 id 属性的字段值,我们在上面的测试代码中使用 add() 添加的是 uniqid() 类型的数据,所以 id 字段保存的内容就是这个样子的。除了单个 id 之外,我们也可以批量删除。

代码语言:javascript
复制
$xs->index->del(['6380e241c27e5','6380e2423b047']);

另一种就是根据分词词项删除,这个嘛,先看例子。

代码语言:javascript
复制
$xs->index->del('添加一条','title');

大家可以试试上面这条删除语句,不出意外的话,是删不掉数据的。这是为啥呢?又要提到我们关于分词的概念了。这里的第一个参数是一个词项,注意,是词项,就是我们之前说过的 term 。也就是说,倒排索引字典中需要有一个 “添加一条” 这样的完整的单词的词项索引,才会删除这条索引对应的文档。很明显,这一句话肯定是要被分词的。它会被分成什么词呢?在 SDK 的测试文件 Quest.php 后面增加的参数 --show-query ,就可以看到分词后的查询语句内容,大家可以使用 “添加一条” 来进行搜索,能看到它被拆分为这样的结果。

代码语言:javascript
复制
> php vendor/hightman/xunsearch/util/Quest.php --show-query ./config/5-zyarticle-test1.ini "添加一条"
--------------------
解析后的 QUERY 语句:Query((添@1 AND 加一@2 AND 条@3))
--------------------
…………………………

好了,后面的不用多说了,直接使用 “加一” 来删除好了。

代码语言:javascript
复制
$xs->index->del('加一','title');

这样我们之前测试的数据就都可以删掉了。

不过,并不推荐这种方式。为啥呢?没错,它很灵活,就像数据库中 Delete 语句时的 Where 条件一样。但是,如果你没有对分词和词项有清晰的了解,就很有可能删错或删多。毕竟,它不像数据库的 Where 是完全匹配的。因此,不是说不能用,只是说不太推荐而已。用不用,还是要看你自己的权衡咯。

批量操作

这个批量操作呀,还是要先拿数据库来做为例子。我们知道,在数据库操作时,如果有大量的写入,一条一条的 Insert 和一次 Insert 多个 Value 那样的批量插入相比,后者速度能提升不少。特别是如果数据库不在一个网段或者是远程连接数据库时。另外,之前我们学习过的 Redis 中的 Pipeline ,也是类似的效果,一次性批量提交一堆操作命令。而 XS 中的批量操作,是更类似于 Redis 的,因为它不止可以有 add() 操作,还可以有别的操作。

先来一个简单的测试。XS 中使用缓冲区的概念来实现批量操作,开始批量操作使用 openBuffer() 而结束则使用 closeBuffer() 。

代码语言:javascript
复制
$index = $xs->index;
$index->openBuffer();
$index->add(new XSDocument(['title'=>'添加一条','content'=>'添加一条'.date('YmdHis'),'id'=>uniqid()]));
sleep(60);
$index->add(new XSDocument(['title'=>'添加一条','content'=>'添加一条'.date('YmdHis'),'id'=>uniqid()]));
$index->add(new XSDocument(['title'=>'添加一条','content'=>'添加一条'.date('YmdHis'),'id'=>uniqid()]));
$index->closeBuffer();

在这个批量操作中,我们先开启 openBuffer() 然后使用 add() 添加一条数据。接着休息 60 秒,这时,你可以去尝试搜索查询数据,60秒内是查不到信息的,因为这时还没有提交。然后再 add() 两次,最后通过 closeBuffer() 关闭缓冲区实现数据提交。这时,再稍等几秒就可以查询到数据了。

openBuffer() 有一个参数,可以设置一个缓冲区大小的值。这个概念和我们之前在 Nginx 中的各种缓冲区大小的概念是类似的,也就是在批量操作内部的数据,如果超过了缓冲区设置的大小,直接就提交了,如果没有超过,就会继续往缓冲区添加。这个缓冲区的概念也不用多解释了吧,就是开辟的一片内存嘛。

有的同学可能会问,这个缓冲区大小要怎么设置呢?默认值是 4MB ,可以根据我们部署 XS 的服务器的内存大小和内存使用情况来设置。缓冲区越大,一次提交的数据就越多,网络频繁连接的次数就减少。大部分情况下其实可以不用设置,而如果有特殊需要,比如单个文档过大或者需要大量的全量操作索引时。

那么它的效果有那么明显吗?咱们可以来试试。

代码语言:javascript
复制
$index = $xs->index;
$time = microtime(true);
$index->openBuffer();
for($i=1;$i<=100000;$i++){
  $index->add(new XSDocument(['title'=>'添加一条'.$i,'content'=>'添加一条'.$i.date('YmdHis'),'id'=>$i]));
}
$index->closeBuffer();
echo microtime(true)-$time;
// 不使用buffer 91.203243017197
// 使用buffer 2.8002960681915

这一段测试是直接循环 100000 次添加 100000 条数据,可以看到我的测试结果写在下面的注释中了。差距还是非常明显的吧,又要搬出 ES 大佬了,在 ES 中,类似的功能是 _bulk

除了添加之外,在缓冲区中也可以执行其它操作。

代码语言:javascript
复制
$index->openBuffer(); 
...
$index->add($doc);
...
$index->del($doc);
...
$index->update($doc);
...
$index->closeBuffer();

这个就不测试了,大家可以自己试试哦。

清空索引

清空索引的代码我们其实也用过了。

代码语言:javascript
复制
$xs->index->clean();

这个就不多说了,没啥参数,一把清空整个索引项目里的所有文档数据,相当于 MySQL 中的 truncate 。在之前我们使用 的 SDK 提供的 Indexer.php ,也有 --clean 参数相当于调用这个函数。

但是需要注意的,clean() 清空索引是同步的操作,也就是说,一调用这个函数,马上进行查询,也查不到内容。数据马上被清空了,而且这个操作不可恢复,线上生产环境要慎用哦。同时,如果你想全量重建索引,使用 clean() 的话,因为后续的添加是异步的,所以会短暂出现索引库是空的情况,在这段时间内是没有任何数据的。要想避免这种情况,也就是想实现一边重建索引,一边还能继续查询,当索引重建完成后,查询到的也变成新数据的这种效果,就要使用下一个要学习的功能啦。

平滑重建索引

上面我们已经说过,要想平滑的,也就是不中断地完成索引地重建,就需要使用到平滑重建索引的功能。这个功能也是通过 XSIndex 的几个函数方法来实现的。

代码语言:javascript
复制
$index = $xs->index;
$index->stopRebuild();
$index->beginRebuild();
$time = microtime(true);
$index->openBuffer();
for($i=1;$i<=100000;$i++){
  $index->add(new XSDocument([
    'title'=>'添加一条'.$i,
    'content'=>'添加一条'.$i.date('YmdHis'),
    'pub_time'=>time(), // 增加pub_time数据
    'id'=>$i
  ]));
}
$index->closeBuffer();
$index->endRebuild();

stopRebuild() 方法,用于清除上次重建失败的错误状态。在重建过程中,可能因为各种原因导致重建工作意外终止,这时索引库会进入一个崩溃状态,出现 DB has been rebuilding 的错误。我们就需要先通过 stopRebuild() 清除错误状态。

接着通过 beginRebuild() 方法开始重建,这时你可以尝试继续访问查询数据,还是可以正常搜索到的。然后我们开始重建工作,针对之前的数据,我们增加了 pub_time 属性内容。前面的测试中,所有数据的 pub_time 字段是没有数据的,现在我们就给它加上。

操作完成之后,使用 endRebuild() 方法结束结束重建。

和全量添加索引一样,过一会我们再次查询数据,就会发现所有数据都有 pub_time 属性了。在这个过程中,服务一直都是可用的。

平滑重建的内部实现,相当于是在一个临时的区域开辟一个新的库,把所有数据先更新到新库,等到全部数据索引完成后,再用新库来替换老的库,从而保证服务的不中断。为确保重建的顺利完成,在重建时,不要对同一个项目开启多个进程、连接,避免同时交替重建引发错乱。

总结

今天的内容真的不复杂吧,只是针对索引数据的增、删、改操作,另外还加上了清空、批量操作以及平滑重建的内容。函数方法的使用都简单,重点还是需要转变很多思维,与数据的操作不同的地方都是需要关注的。比如说添加是异步的、修改是先删后增、删除如果按分词词项的注意点等等。

下篇文章,我们将继续学习 XSIndex 中剩余部分的内容。

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/xunsearch/source/9.php

参考文档:

http://www.xunsearch.com/doc/php/api/XSIndex#addExdata-detail

http://www.xunsearch.com/doc/php/guide/index.add

http://www.xunsearch.com/doc/php/guide/index.update

http://www.xunsearch.com/doc/php/guide/index.del

http://www.xunsearch.com/doc/php/guide/index.clean

http://www.xunsearch.com/doc/php/guide/index.rebuild

http://www.xunsearch.com/doc/php/guide/index.buffer

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农老张 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 索引管理(二)增删改操作
    • 添加数据
      • 修改数据
        • 和 add() 的差别
      • 删除数据
        • 批量操作
          • 清空索引
            • 平滑重建索引
              • 总结
              相关产品与服务
              数据库
              云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档