首页 > 技术文章 > Elastic Search和MySQL的部分对比(持续更新)

sakurafalling 2022-02-16 23:27 原文

我们一般用ES做分布式的实时全文搜索,而考虑在MySQL中也存在全文索引这种类似的东西,今天主要记录一下这两者在全文搜索和联合查询之间的些许不同

 

MySQL的全文索引与ES的倒排索引

MySQL

在MySQL中我们用fulltext index表示全文索引,用于(可能会用于?反正我不用)全文搜索,具体的用法如下所示

select * from user where match(name,information) aganist ('yza','hahahahah');

其中name和information都要建立全文索引(但是注意其匹配全文索引得有个最长和最短的匹配项的要求)

这种模式在MySQL5.8之前只有MyISAM存储引擎支持,在5.8之后innodb和myisam都支持全文索引,这种方式比like xxx%会快很多,但是也存在很多的问题。

  1. 首先就是全文索引不支持搜索高亮。
  2. 其次原理类似于分词,然后存储,类似于“请叫我yz",就会分为“请叫,叫我,我y,yz”这种两个两个的词,不能动态的分成若干长度的词,比如我想两个,三个的分就无法实现(请叫我,yz),另外对于搜索结果会以搜索条件整个单词作为匹配项,例如如果我要搜索“草莓橘子”,就只会去匹配含有“草莓橘子”的列,不会去考虑匹配“草莓”和“橘子”,这个ES就可以做到。
  3. 然后就是如果一个列上有全文索引则一定会用上,即使有性能更好的其他索引也不会用上。

Elastic Search

而在ES中,对于全文搜索采用的则是倒排索引

什么是倒排索引?简而言之,我们一般都是去寻找这个文章中出现了哪几个单词,频率是多少,而倒排索引则是记录某个单词在哪些文档中出现过,并记录此文档的id和出现的位置,存入所谓的倒排列表中,这就是所谓的倒排索引。

而根据上面的描述,我们很容易得出,倒排索引主要记录的有两个,一为出现了哪些单词,二是这些单词都出现在哪些文档中。

  • 对于第一个我们使用词典存储

    存储于内存中,当然也不可能直接存储每个单词在内存中,ES对于词典使用的是类似于字典树的存储,但是做了些许优化,主要是单词共享前缀和共享后缀,这样就能降低存词典所用的空间,另外还加入了一些压缩算法,更进一步的节省内存。

    其次,就算这样压缩了每个单词,其实还是会很大(因为一个文章会有很多的词语啊),在真实的内存中,只会保存一些单词的前缀,而通过这个前缀我们可以快速的定位到磁盘真正的词典块(以block划分),再去这些块中去找到对应的单词(由于是排序好了的,所以说理论上是o(logn)的时间复杂度,当然可能会遇到目标的单词并不在这一块上(因为某个前缀可能会有很多个对应项,会存在很多个块),这个时候再次去遍历下一块就好了),这样就进一步的压缩了词典的大小。

  • 对于第二个则是存储在倒排列表中

    存储于磁盘上,以段的形式存储(参考操作系统中文件的分块和分段)这个主要是考虑同步的问题,尽量避免了冲突而加锁,因为对于倒排文件,修改一个地方的索引(比如某个文档的某个单词被修改,就得从倒排索引中这个单词所对应的文档记录列表中删去这个文档的信息,这其实是一个很麻烦的过程,要重新建立一个新的索引,很浪费时间),在使用段了之后,对于一个地方的修改,只用将该段删除(注意,整个索引其实并不建议真的删除,所以这里的删除其实是逻辑删除,加入.de文件l表示已经被删除,在执行某次查询之后会根据.del文件对查询结果做进一步的过滤)然后再插入一个新的索引就行了,不用重建整个索引。

 

MySQL联合查询和ES联合查询

MySQL

在MySQL中其实对于联合查询的效率比较低(除非建立联合索引,当然更别说join了)

例如,我们在user表上有两个索引,一个idx_name,一个idx_information

select * from user where name=''yuanzhe" and information="xxx"

这个时候就只会使用一个索引,不会两个都是用,而对于第二个条件,则会去内存中的第一个索引查询结果集合中筛选,所以说其实不是真正的联合查询!!!

小扩展:在join中联合join就更加不堪入目,一般有三种查询方式,循环嵌套查询,哈希查询,排序表查询。默认会使用循环嵌套查询

例如   a left join b on a.id =b.id where a.id=‘123456’

这个时候执行情况是,在a中取出符合条件的行,再去b中一个个的查询(假设a中符合条件 M条,b中有N条)。

如果b的id有索引(基于索引的循环嵌套查询),那么就还好,时间复杂度也就o(M*logN)(将a的M条一个个拿出,循环去b中查找,因为b的id有索引,所以走b+树索引的logN,所以说要小表驱动大表啊!!!!!)

如果b的id没有索引(基于块的循环嵌套查询),那么就惨了,只能将a的记录加载到内存中(join buffer),然后将b的记录也加载到内存中去对比,时间复杂度o(N*M),而且如果join buffer放不下,还得分块去,磁盘IO飞起,所以说join的时候还是建建索引吧。

这里记录一下剩下两种查询方式

  • hash查询

    这种查询方式的大致思路是,将a中的数据用hash表存储起来,然后不断用b的列去比较,由于hash表倾向于o(1)时间复杂度,所以说如果数据分散的合理,冲突不多,其实时间复杂度也就还好,但是哪有这么好的美事呢?

  • 排序表查询

    这种查询的大致思路是将a和b的符合条件的数据集合排序,然后就像类似于我们寻找两个排序数组的公共数字,时间复杂度o(N+M),如果两张表的数据已经排序过了(建了索引),那么这种方式其实还挺好的,但是如果没有排序,就得去基于内存分块排序了(类比于 file sort)。

Elastic Search

对于ES而言,实现真正的联合索引步骤为:先分别去根据倒排索引查找对应的两种条件的结果集合,然后将两种结果集合取并集,这里取并集也有两种方式:

  • 基于跳表的合并

    基于跳表的合并,其实就是将两种条件查询的结果集合A和B,取比较少的那一个(假设A的长度比B长),那么就遍历B的集合,不断去A中查找是否存在一样的值(跳表时间复杂度o(logn),稳定的很),然后将匹配到的值记录在最终结果集合中,没有匹配到的记录就舍弃。

  • 基于bitset的合并

    基于bitset的合并,其实就是使用bitset去记录哪些记录存不存在,类似于redis中的bitmap,不过这里ES对于bitset做了一定的优化,因为判断1<N<10000000的集合中,某个数字是否存在,这种集合如果结果比较少(可能就几千个,大多数情况下都不会超过总记录数的1/10),就会形成一种非常稀疏的bitmap,浪费了很多空间。

    就好比某次查询返回的文档是1,10000000,如果用原来的bitmap,就需要记录中间的9999999个0和2个1,这样内存就不划算。

    ES做的优化是,取结果记录的前16位作为block的标识,取后16位作为block中表示这个记录中的某一位,就类似于我们使用65535*M+N去记录某个文档是否存在于结果集合中,用(M,N)去记录,这个时候就节省了很多内存。

 

推荐阅读