lucene搜索原理讲解

来源:互联网 发布:c语言循环从1加到100 编辑:程序博客网 时间:2024/06/05 04:33

文档内容分为两部分:

1.索引过程;

2.搜索过程;

注:涉及到实现部分打算在另外一篇文章单独写;

索引过程:

简单的讲,索引过程分为创建索引节点,构建schema,原数据采集,分词,构建doc,提交索引;

打个比方,我们现在有一个用户表,定义为:

idintnamevarcharaddressvarchar现在,要为这个表建立索引,那么首先,我们就按上面的步骤依次干活:

我们先创建一个名为user的索引节点,构建schema结构与表相同,把varchar类型指定分词器,下面开始读一条记录出来

id9527name张三address我的家在东北,Big house对address进行分词:

提取句子,去除标点符号:我的家在东北 Big house

去掉字母上的音调符号,将字母统一转换成小写:我的家在东北 big house

去除常用词,将单词还原为词干形式:我 家 东北 big house

这里涉及到分词算法和策略:机械分词算法:最大匹配算法=最大正向匹配和最大逆向匹配,智能分词算法:nlp;分词策略:最大粒度分词/最小粒度分词

最大匹配算法,简单的说,就是参考定义的词典,对文本依次正向扫描和反向扫描,在词典中匹配上的结果捞出来,把两组结果做合并取最优集,剩下的字单独列出来;

nlp就是基于语义分析,对文本做场景化,语义化的分析,得到词项,这个需要大量语义化的训练效果才比较好;

最大粒度就是按词典能匹配上的最大的词保留下来,大词不进行二次细分,适用于结果较多,需要输入更精确的词才出结果的场景;

最小粒度就是按词典将所有匹配上的词都记录下来,适用于用户搜索意图不明确,搜索词较少的场景,如果结果较多,则体验不大好;

到这一步的时候,每个分出来的词叫词汇单元(token),token与字段结合形成项(term):<address,我>,<address,家>,<address,东北>,<address,big>,<address,house>

没有分词的字段直接以字段名+字段值组成term:<id,9527>,<name,张三>

实际上在切分为token时,lucene还干了一些事情,就是记录了文本值和一些元数据:原始文本从起点到终点的偏移量,词汇单元的类型,以及位置增量。

一般来说,位置增量为1,表示每个单次存在于域中连续的位置上,位置增量因子会直接影响短语查询和跨度查询,因为这些查询需要知道域中各个项之间的距离。

如果位置增量大于1,则允许词汇单元之间有空隙,可以用这个空隙来表示被删除的单词,比如被删掉的停用词;

如果位置增量为0,表示该词汇单元放置在前一个词汇单元的位置上,同义词分词器可以通过0增量来表示插入的同义词,这个做法使得短语查询时,输入任意一个同义词都能匹配到同一个结果。

偏移量通常是用于高亮显示搜索结果,减少重复分析的代价;

在把原始文本组成term之后,下一步就是构建doc,这里的doc实际上可以理解为多个field列的集合,真实存储还是按单个字段的列来存放:

address

我 文档号 位置增量/偏移量(token带入的信息)

家 文档号 位置增量/偏移量

···

然后提交索引:这个过程说白了就是为你把构建好的信息保存下来,这里面就涉及到一些存储细节,以及优化策略;

首先,在做一次commit之后,lucene会为你这次commit保存正向信息和反向信息:


正向信息:

  • 按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)
  • 也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了那些词。
  • 既然是层次结构,则每个层次都保存了本层次的信息以及下一层次的元信息,也即属性信息,比如一本介绍中国地理的书,应该首先介绍中国地理的概况,以及中国包含多少个省,每个省介绍本省的基本概况及包含多少个市,每个市介绍本市的基本概况及包含多少个县,每个县具体介绍每个县的具体情况。
  • 如上图,包含正向信息的文件有:
    • segments_N保存了此索引包含多少个段,每个段包含多少篇文档。
    • XXX.fnm保存了此段包含了多少个域,每个域的名称及索引方式。
    • XXX.fdx,XXX.fdt保存了此段包含的所有文档,每篇文档包含了多少域,每个域保存了那些信息。
    • XXX.tvx,XXX.tvd,XXX.tvf保存了此段包含多少文档,每篇文档包含了多少域,每个域包含了多少词,每个词的字符串,位置等信息。

反向信息:

  • 保存了词典到倒排表的映射:词(Term) –> 文档(Document)
  • 如上图,包含反向信息的文件有:
    • XXX.tis,XXX.tii保存了词典(Term Dictionary),也即此段包含的所有的词按字典顺序的排序。
    • XXX.frq保存了倒排表,也即包含每个词的文档ID列表。
    • XXX.prx保存了倒排表中每个词在包含此词的文档中的位置。

正向文件存储占比:90%,反向文件(倒排索引)占比:10%

为了减小倒排索引的存储量,做了一些小动作,比如:term对应的docId列表用差值存储;

为了便于快速找到文档号,对正向文件采用跳表结构,减少遍历次数;

.del文件实际上是在segment合并后,不会立即删除segment,因为有可能如scroll类型的查询还在引用之前的segment,所以这里实际上维护了一个.del文件,在所有对于segment的引用都去掉后,segment才会真正删除;

这里每个segment可以看作一个完整的索引文件,在查询时会打开所有的segment进行查询并统计,所以这里会涉及到最大文件句柄数和segment数量大小的设置;

但是segment占了较大内存,只能把segment放到磁盘,lucene为了加快搜索速度,减少磁盘读写,就将segment进行压缩,将segment的前缀以FST结构压缩在内存;搜索时先在内存找到前缀,在根据前缀到磁盘上的segment找到后缀,拿到term对应的doc列表;这里只是由segment牵出了一点搜索的过程,实际上的搜索过程下面继续写;


搜索过程:

简单来说,搜索过程分为:查询分词,过滤,排序,聚合,返回;从功能上来说,常用的有短语查询,布尔查询,模糊查询,多字段查询,高亮,同义词,近义词,建议词,拼写检查等;

现在模拟一个搜索场景,要在用户索引中搜索“东北的家”相关的结果;

搜索框输入搜索词:东北的家;

分词:这里需要跟索引时,用同一款分词器,这样分出来的结果才能最大化相似,这里经过分词后,得到两个词:东北,家;

分词后,需要根据指定的查询字段,将词和字段结合为term:<address,东北>,<address,家>;

然后在内存中找到address这一列,根据FST的原理,东(e4,b8,9c)北(e5,8c,97),家(e5,ae,b6),转换成byte再以16进制表示;构建为FST结构为:

Tip:

flag

e5

flag

8c

Addr_8c

flag

ae

Addr_ae

Addr_8c指向97,Addr_ae指向b6;
tim:

97

Addr_doc1

Addr_doc2

b6

Addr_doc2

Addr_doc3


以此类推,所有列的term都以这个结构压缩到内存中,

1.通过FST匹配前缀找到后缀词块位置。前缀即非叶子节点,后缀即叶子节点;
2. 根据词块位置,读取磁盘中tim文件中后缀块并找到后缀和相应的倒排表位置信息。 
3. 根据倒排表位置去doc文件中加载倒排表,最后拿出倒排表的结果;

然后进行条件过滤,其实条件过滤跟query没有明显的界限,query和filter可以相互转换,过滤的过程也是按生成term到对应列的FST中找前缀对应的后缀拿到doc列表;区别在于,由于过滤多是非文本的过滤条件,query会对结果集评分,filter则不会,所以过滤效率比query查询快,在query和filter之间有顺序的区别,lucene是先执行filter再从结果集中去query;这里的细节在于,filter的过程在于把原有FST结构裁剪,然后query在裁剪后的FST结构下快速找到doc列表;两者独立维护自己的缓存;

结果集拿到后,下一步就是对结果排序;排序分两种,lucene自带的根据词向量模型相关度排序,第二种就是自定义排序;

词向量模型:过程大概是这样的,首先算出每个term在doc中的权重weight(t,d)后面简称w(t,d),

公式:w(t,d)=tf(t,d) x log(n/df(t));

tf(t,d) = term在doc出现的次数;

n=总文档数;

df(t)=索引中包含term的文档数;

doc是由term组成:doc(term1,term2,...term n);

对应算出权重:doc(w1,w2,...w n);

query也可以理解为term组成:query(term1,term2,...term n);

对应算出权重:query(w1,w2,...w n);

这里query为了和doc维度上匹配,会把没有的维度补0,query和doc就分别形成了一个N维的词向量;

然后取query和doc的cos夹角,夹角越小,相关度约大,所以这里两个向量取cos值的公式为:score(q,d) = cos(q,d) = doc(w1,w2...)·query(w1,w2...) / (|doc(w1,w2...)| x |query(w1,w2...)|);

自定义排序,就是按某列升序/降序,或者为某列自定义权重组合公式,来实现查询时灵活加权;底层实现细节就是把排序列全部加载到内存,做完排序,在根据请求参数collect指定数量的doc;



原创粉丝点击