当前位置: 首页 > news >正文

ES应用_Lucene知识

前言

近期的工作逐渐从移动端转向Java后端,现正在做一个Elastic Search(ES)相关的应用,需要提供关键词全文检索及聚合筛选功能。在翻阅了一堆文档后发现,原来ES就是分布式版的Lucene,内核还是Lucene。这就让我联想起了两三年前在移动端上基于Lucene做的聊天记录全文检索SDK,借此机会,重新回顾了Lucene的基本原理及实践,在此整理一番。当然现在基于最新的Lucene 8.0 API在后端更容易实现相同功能了。

背景

我们在使用微信、钉钉时,对于聊天记录全局搜索功能应该不陌生。以微信为例,输入一个或者多个关键字,微信能立即告诉你有多少个会话、每个会话里有多少条匹配的聊天记录,并且,你可以查看具体某个会话的所有匹配的聊天记录并跳转到其上下文。本文就以此需求为背景,阐述如何采用Lucene实现消息全文检索功能的,以此回顾Lucene的原理及实践。

1. Lucene原理浅析

Lucene是啥?简单说就是一个基于Java的高效的全文索引库。

全文检索是啥?简单说就是对非结构化的数据(也叫做全文数据)先建立索引,再对索引进行搜索的过程叫做全文检索(Full-text Search,FTS)。

全文检索分为两个阶段,创建索引(Indexing)和搜索索引(Search),Indexing就是从结构化和非结构化数据中提取信息、并重新组织信息形成一定的结构的过程,其产物就是有结构的数据——索引。Search,根据用户的查询请求,搜索创建的索引,返回匹配的结果集。

索引是一系列Document的集合,Document是一系列Field的集合,Filed是一系列可能出现的Term的集合,Term是一系列bytes的集合,同一个bytes序列,在不同的Field中被认为是不同的Term,因此Term由一对值组成:Field name(string)和Field value(bytes)。一个索引由多个子索引即段(Segments)组成,段是一个完全独立的索引,将会被单独搜索。

Lucene索引的组织、存储结构是倒排索引(Inverted Index)。例如有一系列Document,Document中有一个Field,名叫content,所有在Field:content中可能出现的字符串叫作Term,例如一个Term(Field Name:content,Field value:"lucene")。对这些Document进行组织会得到下面结构:

左边保存的是Term Dictionary,每个Term都指向包含此Term的Document链表,成为倒排表(Posting List)。

有了索引,在数据量大的时候能大大加速搜索的速度,相对于顺序扫描,全文检索的优势就在于:一次索引,多次使用。

上图中,如果我们要搜索既包含“lucene”又包含“solr”的文档(即输入框输入“lucene solr”),那么就需要对两个Document有序链表做合并,示意图如下:

为了快速定位到Term词典中的Term,Lucene还引入了TermIndex,其结构就是单词查找树(Trie树),利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。Trie树不会包含所有的Term,它包含的是Term的一些前缀。通过Term Index就可以快速地定位到Term dictionary的某个offset,然后从这个位置再往后顺序查找。

如上图,整理来说,某个字段的关键字查询会经历Term Index到Term Dictionary,再到Posting List。多个关键字查询,需要对多个关键字的Posting List进行交集、并集、差集等链表操作。

Lucene创建索引过程

1. 创建Document。

2. 将Document传给分词器(Tokenizer),对需要分词的Filed值拆分成一个个单独的单词,去掉标点符号,去除停词(Stop words,例如英文中的"a","the","and","as"等,不同分词器都不同的停词表),经过Tokenizer后得到词元(Token)。

3. 将Token传给语言处理器(Linguistic Processor)对Token进行处理,对于英文,要转换成小写(Lowercase)、将单词缩减为词根形式(Stemming,例如"cars"转换成"car")、将单词转变为词根形式(Lemmatization,例如"drove"转换成"drive")等。经过语言处理的Token就成为了词(Term)。

4. 将Term传给索引器(Indexer),创建字典,对字典进行排序,合并相同的Term成为文档倒排链表(Posting List)。示意图如下:

(图)TermDictionary -> PostingList 数据结构

Document Frequency:文档频次,表示共有多少个Document包含此Term。

Frequency:词频率,表示Term在Document中出现的次数。

此外,只知道Frequency还不够,有时还需要记录Term在Document中出现的位置(Position),位置又包括两种:

1) 字符位置,记录该Term在文章中的第几个字符,关键字高亮时定位用。

2) 关键词位置,记录该Term在文章中是第几个关键词(有些词可能在经过分词器事被过滤掉了,剩下的有效的词,按顺序排列),短语查询(PharseQuery)时需要用,Lucene记录的是关键词位置。

那么一个Posting List中一个包含DOC、Frequency、Position的链表节点存储信息如下:

Term

Document ID

Frequency

Position

nim

1

2

2,6

2

1

3

上述节点信息说明Term "nim"在文章1中出现了2次,分别在第2和第6关键字位置上(中间还有三个关键字);在文章2中出现了1次,在第3关键字位置上。

5. 将索引写入磁盘。

DONE

Lucene搜索过程:

1. 用户输入查询语句,该查询语句是有语法的,例如 AND、OR、NOT等。

2. 对查询语句进行词法分析,识别token和关键字(AND、OR、NOT),对token进行语言处理(同创建索引过程)得到一系列Term。

3. 对查询语句进行语法分析,形成语法树(查询树)。

4. 从磁盘加载索引到内存,通过语法树搜索索引,得到每个Term的Document链表(这里就需要经历Term Index、Term Dictionary得到Posting List),根据语法对文档链表做并集、交集、差操作,得到符合的文档列表。

5. 计算上述得到的Document与查询语句相关性,对结果进行排序,返回查询结果给用户。

DONE

2. Lucene VS 传统数据库

1)全文检索≠SQL like "%keyword%",由于数据库索引不是为全文索引设计的,因此,在使用like "%keyword%"时,数据库索引根本不起作用的,在使用like进行结构化查询时,搜索过程就变成顺序扫描(Serial Scanning),这对于含有模糊查询的数据库服务来说,性能是有危害的。如果要对多个关键词进行模糊匹配:like "%keyword1%" and like "%keyword2%" and ...,其效率就会大打折扣了。而Lucene最核心的特征就是通过特殊的倒排索引结构实现了传统数据库不擅长的全文索引机制,速度更快。

2)Lucene通过词元(Term)进行匹配,通过语言分析接口进行关键字拆分,能实现对中文的支持。而Like可以看成是字符串的正则匹配,从匹配效果来说,Lucene会更加丰富,例如,Like不能匹配词序颠倒的情况,不能匹配词根的变化形式(复数、过去式、大小写等)。

3)Lucene对匹配结果有相似度评价,也可以自定义评价算法,能够根据评分进行排序给出结果,而Like没有这一能力。

4)在搜索过程中的内存开销上来说,在返回所有结果集,在匹配条目数大的时候,需要大量的内存存放临时结果集。

5)Lucene可定制性强,可以方便的定制符合应用需要的索引规则,Like难定制,对于一个关键字需要多种匹配规则的需求,难以实现,Like也可能会搜索出一些我们并不期望的结果。

3. Field设计&构造Document

要满足上述的需求,并考虑未来新增的需求,首先我们要思考下如何设计要存储的文档(Document,可以看作是行),并考虑哪些域(Field,可以看作是列)需要索引、需要分词、需要存储、需要排序?下面是针对聊天记录搜索设计的Document:

Field1: type 文档的类型,例如消息、通讯录、收藏等。

Field2: subtype 文档的子类型,预留。

Field3: dataId 数据来源ID,例如消息的主键。

Field4: id 数据来源的产生者ID,例如会话ID,通讯录账号等。

Field5: time 数据来源产生的时间,例如收发消息的时间。

Field6: content 用于进行全文检索的数据内容。例如消息内容,通讯录昵称等。

分析:

哪些需要索引?

删除Document可能的情况有:

delete(type) 例如删除所有消息文档。

delete(type, dataId) 例如删除某条消息文档。

delete(type, id) 例如删掉某个会话的消息文档。

那么在type,dataId,id上应该建索引(倒排索引),Lucene的删除本质上就是先查询再删除的过程,两个不同Field的Term做AND查询,实际上就是两个Term对应的Posting List的做合并(两个docid集合做交集)。与SQL的多列索引有些不同(在树中完成多列值到行记录的快速查找)。

哪些需要分词?

明显就只有content字段,需要支持全文检索。

哪些需要排序?

我们需要按照id进行分组(Lucene的分组查询,要进行group的Filed必须要排序),需要根据time进行倒序排列。因此id,time需要排序。

这里Field6可以再拆分成两个Field,一个Field专门用于分词、构造索引、搜索;另一个Field用于存储原文内容。这与分词策略有关,如果你使用的是Lucene标准的分词器或者自定义分词器,那么可以不用拆分。

最终Document设计如下:

Field

Index?

Tokenized?

Sort?

Store?

type

×

×

subtype

×

×

×

dataid

×

×

id

×

time

×

×

content

×

×

original content

×

×

×

4.分词算法的选择,中文分词器入门到放弃

Lucene提供了常用的分词器:

1. WhitespaceAnalyzer

使用空格作为间隔符的词汇分割分词器。分词器不做词汇过滤,也不进行小写字符转换。由于不完成单词过滤和小写字符转换功能,也不需要过滤词库支持。词汇分割策略上简单使用非英文字符作为分割符,不需要分词词库支持,不适合中文环境。

2. StandardAnalyzer

标准分析器是Lucene内置的分析器,会根据空格和符号来完成分词,会将语汇单元转成小写形式,并去除停用词及标点符号,还可以完成数字、字母、E-mail地址、IP地址以及中文字符的分析处理,对中文采用单字切分的方式,还可以支持过滤词表,用来代替StopAnalyzer能够实现的过滤功能。

3. KeywordAnalyzer

会把整个输入作为一个单独词汇单元,方便特殊类型的文本进行索引和检索。针对邮政编码,地址等文本信息使用关键词分词器进行索引项建立非常方便。

4. PerFieldAnalyzerWrapper

主要用在针对不同的Field采用不同的Analyzer的场合。比如对于文件名,需要使用KeywordAnalyzer,而对于文件内容只使用StandardAnalyzer就可以了。

除此之外,还有StopAnalyzer,SimpleAnalyzer等等。

中文分词器:

1. IKAnalyzer

IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版本开始,IKAnalyzer已经推出了3个大版本。它采用了特有的正向迭代最细粒度切分算法,实现了以词典为基础的正反向全切分,以及正反向最大匹配切分两种方法。IKAnalyzer是第三方实现的分词器,继承自Lucene的Analyzer类,针对中文文本进行处理。最新的IK Analyzer2012版本支持细粒度切分和智能切分。举个官方的例子:

文本原文:

智能分词结果:

ikanalyzer | 是 | 一个 | 开源 | 的 | 基于 | java | 语言 | 开发 | 的 | 轻量级 | 的 | 中文 | 分词 | 工具包 | 从 | 2006年 | 12月 | 推出 | 1.0版 | 开始 | ikanalyzer | 已经 | 推 | 出了 | 3个 | 大 | 版本

最细粒度分词结果:

ikanalyzer | 是 | 一个 | 一 | 个 | 开源 | 的 | 基于 | java | 语言 | 开发 | 的 | 轻量级| 量级 | 的 | 中文 | 分词 | 工具包 | 工具 | 包 | 从 | 2006 | 年 | 12 | 月 | 推出 | 1.0 | 版 | 开始 | ikanalyzer | 已经 | 推出 | 出了 | 3 | 个 | 大 | 版本

2. MMSeg4J

采用MMSeg 算法实现的中文分词器,基于正向最大匹配。官方说:词语的正确识别率达到了 98.41%,也是值得推荐的中文分词器之一。mmseg4j maxword 分词效果举个例子:

文本原文1:中华人民共和国

分词结果:中华 | 华人 | 人民 | 共和 | 国 |

文本原文2:今天真热,是游泳的好日子。

今天 | 天真 | 热 | 是 | 游泳 | 的 | 好 | 日子 |

研究下微信的聊天记录全文检索,总结一下:

- 对于英文,支持前缀匹配(PrefixQuery),例如原串“James”,你搜索“Jam”是可以搜到,但你搜“me”是搜不到的,因为“me”不是前缀。对于原串“Lebron James”,你搜“james lebron”也是能搜到的。搜索时空格是And的关系,只要查询串切出的Term都在同一个原串中匹配,无论顺序,这个文档就算命中了,这里即是,搜索既包含Term "james",又包含Term "lebron"的Document。

- 对于数字,支持前缀匹配(PrefixQuery),在不考虑性能、存储开销的情况下,这个体验并不好,比如电话号码1815***9769,我只记得后三位769,就搜不到了。

- 对于中文,支持短语匹配(PhraseQuery),例如原串“中华人民共和国”,你搜索“中华”是可以匹配的,你搜“中国”,就搜不到了。这是因为短语匹配要求查询串中每个token在原串中的关键词位置是要连续的。即,短语是由一串token组成的,一个查询短语和索引匹配,需要短语中的token按照同样的顺序出现在文档中。

例如:keyword“中国”,在构造PhraseQuery时,先会经过分词器拿到Term,例如经过单切的分词器拿到的Term1 "中",Term2 "国",通过Term List来构造PhraseQuery。在查询时,会检索到"中"在文档x的第0关键字,"国"在文档x的第6关键字,两个Term都在文档x中(两个Term找到的docid做交集,得到结果docid= x),随后检查关键字位置,发现并不连续,那么则淘汰文档x。

- 对于符号,一般都是单个字符,需要匹配。你搜“。”,可能会匹配上万的聊天记录出来。

- 对于日文、韩文等其他国家语言、输入法自带的特殊表情符号,也是支持的。

那么对于上述需求,应该选择什么分词器合适呢?下面说说我从入门到放弃的过程:

首先,IKAnalyzer中文分词器一看就很接地气,果断整进来,可自定义停词表,自定义词库,666啊,接完就发现了一堆问题。就例如输入串“中华人民共和国”,你搜索“人民”,可以搜到,搜索“民”,没结果;仔细分析一下,首先IK肯定不是单字切分的,那么你搜单个字,很大可能会搜不到;其次,通过IK构造的QueryParser再parse输入串得到的Query,Query逻辑是:查询串与原串切分出来的token必须全匹配,不能是前缀匹配或者后缀匹配此外,可能你会想,我可以引入词库,让分词更加合理,但是这并不能有效的解决上面的问题,还会使得APP的资源包变大,你还得去维护词库的更新。综上,对于上述需求,IK是个很优秀的中文分词器,但并不合适我们的场景。那么考虑下单字切分?

接着看看StandardAnalyzer,对于中文采用单字切分方式。经测试,能满足对中文短语匹配的需求,但是!英文就无法支持前缀匹配了。例如原串“James”,经过标准分词器后形成token “james”,你的输入串“jam”在搜索时经过标准分词器产生的token是“jam”,无法全匹配原串的token。你必须输入完整的“james”才能命中。此外,还遇到了停词符被过滤、特殊符号无法搜索等问题。最后只能考虑自定义分词器了。

5. 实现分词算法

其实对于在中文分词器上折腾的过程,我已经把一个输入串,拆分成token的过程实现了,并可以判断token所属的类型。WhitespaceAnalyzer看起来功能很弱,但是个好东西,用它我就可以自己实现分词算法,将token采用空格分隔去形成一个String交给空格分词器去产生token去建立索引。因此,自定义分词器,我并没有继承Analyzer去实现里面的接口,直接使用自实现的分词算法+WhitespaceAnalyzer

自实现分词算法,更多是要对字符进行判断、分类,有些是连续字符合并成一个token(例如英文),有些是一个字符一个token(例如英文符号),对于中文及其他外国语言、特殊字符,为了满足上述需求,用单字切,搜索结果是最全的。对于数字,考虑到用户体验,也一个数字一个token。

这个分词算法什么时候会用到呢?

第一,构造Document的时候,需要对中文、数字进行单切写入索引。

第二,搜索的时候,需要对中文、数字进行短语匹配,不能单切。

第三,高亮搜索结果的时候,单切与否都可以,得看产品需求了。例如紫薇说的“你们一起看雪看星星看月亮...”,搜索“看雪”,不单切的高亮结果是“你们一起看雪看星星看月亮...”,单切的高亮结果是“你们一起看雪看星星看月亮...”,三个“看”都会被高亮。

这里还需注意的是,要进行大小写转换,统一转成小写。

6. 输入串预处理与多条件查询

实现了Document的存储,那么如何查询呢?

无论是实现上面需求1(query all)还是需求2(query session),都一样,无非是query all时不需要指定id,因为涉及多条件查询,需要使用到BooleanQuery,支持嵌套。

这里需要对输入串做个预处理:经过分词算法(非SingleMode模式),分离出两组token,一组是中文和数字,将采用短语匹配(PhraseQuery),一组是英文和符号,将采用前缀匹配(PrefixQuery)。

此外,值得注意的是,如果你的查询结果不需要依据相关性评分进行排序,那么可以使用过滤条件:BooleanClause.Occur.FILTER代替BooleanClause.Occur.MUST,MUST会参与评分计算,Filter不会,但依然有MUST的效果。

7. 分组查询,是否需要引入得分?

上述Like方式,使用了group by id order by time desc 的分组、排序。

在Lucene中我们实现类似的功能怎么做呢?最脑残的想法就是把所有满足条件的消息查询来,拿到docId列表,然后拿到所有对应的Document,取出Filed id里的值去做分组吧。这种做法显然效率低,在实测过程中,我们会发现Lucene根据docid去拿出Document相对是比较慢的,I/O上有瓶颈,这种方式显然不可取。

搜了下官网文档,Lucene为了解决这类问题,提供了用于分组操作的模块group,group主要用于处理含有某个相同Field值的不同document的分组统计。Lucene的分组有一个很重要的前提,即要进行group的Field必须是SortedDocValuesField类型的,切记!这也是我们在构建Document的时候Filed id为什么加上排序域的原因,这应该也是非结构化分组查询效率保证的一个关键点。其实,一般来说,对于结构化查询GROUP BY实际上也同样会进行排序操作,与ORDER BY 相比,GROUP BY主要多了排序之后的分组操作。对要用来排序的域,Lucene会从倒排索引中将Document中该字段的值都读出来,放到一个大小为maxDoc的数组中,因此排序域必须设为索引域,但绝对不能是tokenzied字段。因此上述Field id(group by的域要加上排序域,排序域必须是索引域)即有index,又有sort。

8. 分页查询

实测中发现,Lucene通过倒排索引搜索得到符合条件的文档ID是非常快的,但发现根据docId获取Document是相对比较慢的,如果符合的文档ID有几万个,那么一次性取出所有对应的Document,从而取出各个Filed的值来构造返回结果,可能要消耗几秒时间,这个用户体验并不好。现实世界中,移动端屏幕小,能展示的信息量本身就不多,也不可能一次性要显示几万条结果给用户。因此,针对此问题,可以采用分页方式来大大改善用户体验。分页一般可以分为两种,一种是类似Google搜索结果一样,每页显示固定条数,你可以点下一页,也可以跳转到第N页;还有一种就是移动端常用的,通过上拉加载下一页数据。Lucene也提供了IndexSearcher.searchAfter API,可以根据锚点,查询下一页的数据。

9. 删除索引

本质上删除索引也就是先查询出所有符合条件的文档ID再删除(Lucene delete API传入的参数就是一个Query或者Term),删除时指定的Field,必须要建立索引,否则删除会失败。需要注意的是调用IndexWriter.delete之后并不是真正的删除,只是把这些文档放入回收站中,如果不执行IndexWriter.commit,查询时还能查到被删除的数据。在commit之前,你可以调用IndexWriter.rollabck回滚到之前没有修改的状态。IndexWriter在没有commit或者close的情况下,它所进行的修改是不起效的。

10.读写并发与近实时搜索

Lucene提供了3种修改索引的方法,索引新文档、删除文档、更新文档(本质上是先删除文档再添加文档),主要集中在IndexWriter和IndexReader两个类上,其中IndexWriter主要负责对索引的写入和索引的整体维护(合并、优化等);IndexReader负责从索引中删除文档。在执行增删改操作时,Lucene为了避免损坏索引文件,制定了一系列并发规则:

1)任意数量的只读操作可以同时执行(并行、并发),即多个线程或者多进程可以同时对一个索引进行搜索。

2)正在被修改的索引也可以同时执行任意数量的只读操作,即一个线程正在对索引进行添删改文档操作或者一个索引正在被优化,另一个线程依然可以进行搜索。

3)用一个时刻,同一个索引只允许执行一个修改操作,即任一时刻,不允许同时有多个IndexWriter对象对同一个索引进行修改操作。这里要注意的是IndexReader和IndexWriter都能够删除索引,但有一定的限制:在使用IndexWriter向索引加入文档前,必须先关闭执行删除操作的IndexReader实例.相对的,在使用IndexReader删除前,必须先关闭执行添加文档操作的IndexWriter实例。当然,任一时刻,不允许有多个IndexReader执行删除文档操作,下一个IndexReader应该在上一个IndexReader对象close之后才能执行。

上面的规则说的,同一个时间,不允许使用多个IndexWriter或IndexReader实例对同一个索引进行修改。但IndexWriter和IndexReader都是线程安全的,这两个类的实例都可以被多个线程共享,Lucene会对不同线程中所有对索引修改的操作进行同步处理,保证修改操作是排队顺序执行的,因此APP不需要做额外的同步处理,但需要APP确保这两个类的对象对索引的修改操作不能重叠,即当IndexWriter对象在对索引进行修改操作时,IndexReader对象不能对该索引进行修改,相对的,当IndexReader对象正在对索引进行修改操作时,IndexWriter对象同样不能对索引进行修改。

如果违背上述规则,就会损坏索引文件,Lucenen的设计者为了避免开发者对并发性有错误的理解,他们通过锁机制尽可能避免对索引文件的意外损坏。Lucene采用的是文件锁,保证同一时刻只允许执行单一进程的代码。

write.lock文件用于阻止进程试图并行的修改一个索引,IndexWriter对象在实例化时获得了write.lock文件,直到IndexWriter对象close之后才释放。当IndexReader对象在删除文档时也需要获得write.lock文件。如果锁文件write.lock存在于目录内,那么创建新的IndexWriter就会失败,抛出LockObtainFailedException异常。

除了write.lock,Lucene还有commit.lock。当对段(Segment)进行读或合并操作时,就需要用到commit.lock文件。在IndexReader对象读取段文件之前会获取commit.lock文件,在这个锁文件中对所有的索引段进行了命名,只有当IndexReader对象已经打开并读取完所有的段后,才会释放这个锁文件。IndexWriter对象在创建新的段之前,也需要获得commit.lock文件,并一直对其进行维护,直到该对象执行诸如段合并等操作,并将无用的索引文件移除完毕之后才释放。因此,commit.lock的创建可能比write.lock更为频繁,但commit.lock绝不能过长时间地锁定索引,因为在commit锁文件生存期内,索引文件都只能被打开或删除,并且只有一小部分的段文件被写入磁盘里。

对于读写并发,我的做法是,IndexWriter单例模式,只有一个线程(HandlerThread)能够操作此IndexWriter对象,所有的修改操作封装成一个任务往线程里投递,顺序执行,对于写操作做延时执行,例如短时间内一直在高频的收消息,等消息累计到一定量或者等收消息频率下降时再批量写入索引。IndexReader只用于查询,单例模式,供多线程共享。

搞定读写并发问题,接下来我们还会遇到一个问题,就是我们写入或者删除的数据如何立即反应到搜索上呢?这是一个实时搜索的问题。Lucene有一个近实时搜索的概念。

近实时搜索(near-real-time,NRT)可以搜索IndexWriter还未commit的内容,介于immediate和eventual之间,在数据比较大、更新较频繁的情况下使用。IndexReader的重建想要看到新的结果就需要重新打开一个IndexReader,DirectoryReader提供了DirectoryReader.openIfChanged(DirectoryReader oldReader)函数,只有索引有变化时才建立新IndexReader(这里不是完全打开一个new IndexReader,会复用old IndexReader的一些资源,并入新索引,降低一些开销), 否则返回oldReader

在实际应用中,我们会并行的进行建索引、删索引、搜索、打开新IndexReader、关闭old IndexReader,操作比较复杂还有线程安全问题。为了简化使用流程,Lucene提供了SearcherManager extends ReferenceManager<IndexSearcher>管理IndexReader的重建和关闭,保证了线程安全,封装了IndexSearcher的生成。主要提供如下三个接口:

- maybeRefresh:尝试打开新的IndexReader,本质调用的是DirectoryReader.openIfChanged。

- acquire:获取当前已打开的最新IndexSearcher

- release:释放不用的引用,本质调的是IndexReader.close(),当一个IndexReader内部的引用计数为0时,会关闭自己释放资源。

11. 高亮算法

高亮算法要实现的就是输入原串和查询串,返回需要高亮的区间,一般是在界面显示的时候才会使用,因此动态计算出此区间即可。

那么高亮算法就是输入原串(也就是查询匹配结果)String result,查询串String query,输出高亮区间集合。

此外我们还需要引入两个参数:

1)boolean prefixMatch :比较 SQL Like和Lucene两种方式搜索,我们也知道对于英文Lucene是前缀匹配,但Like并不是,对于原串“abc bc”,搜索“bc”,对于Like方式,高亮结果应该是“abc bc”,对于Lucene方式,应该是“abc bc”,这样是最真实的反应Lucene的匹配结果,当然也有可能策划要求你高亮结果与Like方式一样,也是合理的,因此提取这个参数,可供配置。

2)boolean chineseSingleMode:中文是否单字切分,例如对于原串“美利坚合众国,简称美国”,你搜索“美国”,对于中文不单字切分,连续的中文字连起来构成一个短语,其高亮结果是“美利坚合众国,简称美国”,这也是Lucene对于中文短语匹配的结果。如果单字切分,其高亮结果为“美利坚合众国,简称美国”。还有一种特殊情况,例如原串“徐 文”,注意中间有空格,分词时跳过了空格,原串产生两个Term“徐”和“文”,他们在关键字位置分别是0和1是连续的,那么你搜索“徐文”进行短语匹配,是能匹配的,但高亮时原串中有空格,高亮区间会空,这时候,你可以对原串进行特殊处理,也可以对搜索串采用单字切分的方式去高亮。

此外,产生的区间有可能有交集,例如高亮结果区间[0,1],[1,2],应该合并成[0,2];高亮区间的顺序也可能颠倒,应该排序输出,例如[5,7],[1,2],应该输出[1,2],[5,7];区间也可能是连续的,应该合并,例如,[0,1],[2,3],应该合并成[0,3]输出。

12. 小结

本文回顾了自己在做消息全文检索时Lucene的实践经历,从索引结构设计、分词器选择、自定义分词算法,到搜索、聚合、分页查询,最后到近实时搜索与高亮算法,较为完整的阐述了一个Lucene开发过程的会遇到的各种问题和解决思路。ES是基于Lucene的分布式的Web服务,ES集群上每个分片都是一个分离的Lucene实例。因此,弄清楚Lucene的原理,有助于ES的快速上手。


http://www.taodudu.cc/news/show-8456980.html

相关文章:

  • ElasticSearch那些事儿(九)
  • 代码注释之程序猿天真可爱无国界!
  • 一文看懂TCP/IP中的相关知识
  • 开发数据产品+AI产品通关上岸课程
  • AI产品经理-借力
  • 七种令人惊叹的人工智能工具
  • cin.ignore()
  • delete 与 delete[ ] 区别
  • C++信号处理 [ signal()函数 raise()函数 ]
  • C++存储类
  • 第13章 宣泄的拥抱
  • 【附源码】Java计算机毕业设计喜枫日料店自助点餐系统(程序+LW+部署)
  • Java实现餐厅自助点餐系统【附源码】
  • 微信小程序java高校食堂窗口自助点餐系统uniapp
  • 基于微信小程序的食堂窗口自助点餐系统设计与实现-计算机毕业设计源码+LW文档
  • 自助点餐系统(三)
  • JAVA实现自助点餐系统【附源码】
  • 【Vue H5项目实战】从0到1的自助点餐系统—— 项目页面布局(Vue3.2 + Vite + TS + Vant + Pinia + Nodejs + MongoDB)
  • P5706 【深基2.例8】再分肥宅水 C语言
  • 再分肥宅水
  • (JAVA)P5706 【深基2.例8】再分肥宅水
  • 【洛谷算法题】P5706-再分肥宅水【入门1顺序结构】Java题解
  • 洛谷P5706 再分肥宅水
  • (已更新)市肥宅中心论坛类小程序源码 apache协议开源
  • 从Hello world到算法! 第12题【再分肥宅水】
  • 知乎网论文重查入口 快码论文
  • 维普论文查重入口降重
  • 维普论文查重入口官网?paperpass降重后的报告怎么下载
  • 犹如白皮书般详细的Css文本控制总结
  • IDT + FisherVector (by C++)我的实践