全文检索基本概念

全文搜索

1.索引过程

索引过程如下:

 ┏━━━━━┓   ┏━━━━┓   ┏━━━━━━┓
┃ 预处理 ┃┈┈→┃ 分词  ┃┈┈→┃ 反向索引 ┃
┗━━━━━┛   ┗━━━━┛   ┗━━━━━━┛

2.预处理

2.1 去标点

标点符号对文本检索没有作用,可以去除。

2.2 去停止词

停止词一般是虚词、语气词。极度高频词对检索没有意义。英文停止词一般有:

a an and are as at be but by for if in into is it no not of on or such that the their then there these they this to was will with 

中文停止词一般有:

也 了 仍 从 以 使 则 却 又 及 对 就 并 很 或 把 是 的 着 给 而 被 让 在 还 比 等 当 与 于 但 

2.3 字符转换

为了提高转写结果的准确性,需要对字符进行转换处理,包括:

  • 大小写转换
"Milos Raonic (born 1990) is a Canadian professional tennis player." --> "milos raonic born 1990 is a canadian professional tennis player" 
  • 全角半角转换
"contact@cnblogs.com" --> "contact@cnblogs.com" 
  • 其他字符转换
"①③⑨⑤⑥⑨⑧⑤⑤⑧③" --> "13956985583" 

2.4 词根还原

拼音语言存在复数、时态、阴性阳性、等词形变换,需要进行词根还原。

"connected", "connecting", "connection" --> "connect" 

有的搜索引擎不使用词根还原。考虑词形变换非常复杂,为了提高recall采用词根还原,使用table lookup策略。

3.分词

分词是为了识别出文档中的词,词是最小的能够独立活动的有意义的语言成分。信息处理只要涉及到句法、语义就需要以词为基本单位。

英文词和词之间有自然分割,中文是没有分隔符的。中文分词的精确度受到歧义、未登录词的影响,造成很大的困难。中文分词的一般方法:

  • 基于规则的分词
  • 基于统计的分词
  • 基于规则和统计向结合的分词
  • 基于理解的分词

我们使用ik分词组件,ik有两种分词策略:smart策略、max word策略。例如这个句子:

1939年的德国,9岁的小女孩莉赛尔和弟弟被迫送往慕尼黑远郊的 寄养家庭。6岁的弟弟不幸死在了路途中。在冷清的葬礼后,莉赛尔 意外得到她的第一本书《掘墓人手册》。 

看分词的结果,先看smart策略:

1939年/德国/9岁/小女孩/莉/赛/尔/和/弟弟/被迫/送往/慕尼黑/远郊/ 寄养/家庭/6岁/弟弟/不幸/死/路/途中/冷清/葬礼/后/莉/赛/尔/意外/ 得/到她/第一/本书/掘墓人/手册 

分词结果中一些词不准确,比如“1939年”、“得/到她”,因为分词不准确,当用户搜索“1939”、“得到”就检索不到这个文档,尽管文档中出现了这些词。还有“莉/赛/尔”,ik不能识别这个词,分割成了三个独立的字。如果用户搜索“赛莉尔”,只要不严格规定出现的顺序,也能搜出文档。

再使用max word策略:

1939/年/德国/9/岁/小女孩/小女/女孩/莉/赛/尔/和/弟弟/被迫/迫/送 往/慕尼黑/慕/尼/黑/远郊/郊/寄养/寄/养家/家庭/家/庭/6/岁/弟弟/不 幸/死/路途/途中/途/中/冷清/冷/清/葬礼/葬/礼/后/莉/赛/尔/意外/得 到/到她/第一本/第一/一本书/一本/一/本书/本/书/掘墓人/墓/人手/ 手册/册 

max word多分出了很多词,这样就增加了检索的召回率,但是正确率有所降低,也增加了索引的体积。平台的索引策略:

  • 对fulltext字段使用max_word分词索引
  • match_query检索条件smart分词
  • span_near_query检索检索条件max_word分词,与索引分词果一致

Elasticsearch支持分词接口,比如这个接口: http://localhost:9200/cr/_analyze?text=1939年的德国&analyzer=ik_smart 可以执行一个分词计算,使用的分词器是ik_smart。在分词结果中可以看到每个词的位置和类型。

ik组件可以使用自定义词典,配置文件:/server/elasticsearch/config/ik/IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://localhost/ext_dict.php</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">http://localhost/ext_stopwords.php</entry>
</properties>

修改本地词典和停止词文件,需要在每个Elasticsearch节点上修改文件,修改后重启。

使用远程字典可以集中维护字典文件,并且不需要重启Elasticsearch。ik每分钟向远程地址发出请求,请求有一个“If-Modified-Since”消息头。如果字典没有变动,返回“304 Not Modified”,ik不会更新字典。如果字典内容有变动,返回字典内容和新的“Last-Modified”,ik更新字典。

4.反向索引

反向索引也叫倒排索引,存储了词在文档中的位置。比如有下面三个文档,已经做好了分词:

  • doc1: A/BC/D/EF
  • doc2: BC/D/XY/Z/Z
  • doc3: BC/E/MN/XY

首先统计这三个文档中出现的所有的词,给这些词编号,并且统计词频:

词 编号 词频
A 1 1
BC 2 3
D 3 2
EF 4 1
XY 5 2
Z 6 2
E 7 1
MN 8 1

然后统计这些词出现在哪些文档中,以及出现的位置:

词编号 文档和位置
1 (doc1,0)
2 (doc1,1),(doc2,0),(doc3,0)
3 (doc1,2),(doc2,1)
4 (doc1,3)
5 (doc2,2),(doc3,3)
6 (doc2,3,4)
7 (doc3,1)
8 (doc3,2)

这样反向索引就建立起来了。现在进行检索,输入“BDC”。首先对输入条件进行分词,得到“BC/D”,分别检索BC和D的集合,查反向索引:

{doc1, doc2, doc3} AND {doc1, doc2} = {doc1, doc2} 

得到搜索结果集合:doc1和doc2。

Elasticsearch是一个分布式的全文检索数据库,封装了Lucene的功能。Lecene实现了分词和倒排索引的功能。Lucene的索引是分域存储的,比纯粹的文本索引更加复杂,具体的原理可以看Lucene代码。

5.检索

5.1 term检索

term检索的条件写法,用SQL表达就是:

SELECT * FROM index/type WHERE status='yes' 

term是不分词检索,也就是对检索条件不分词。所以一般对非文本字段、不分词的文本字段使用这样的检索。对分词文本字段用term检索要慎重。比如对这样的文档:

fulltext: AB/CD/E/FG 

如果用term检索:

WHERE fulltext='CDE' 

由于文档的fulltext字段中不存在“CDE”这个词,所以检索不到文档。

5.2 matchQuery检索

matchQuery条件写法:

SELECT * FROM index/type WHERE fulltext=matchQuery('ABE') 

matchQuery是分词检索,先对检索条件进行分词,得到“AB/E”,然后寻找fulltext中同时包含“AB”和“E”的文档。我们对fulltext进行ik max word分词,matchQuery使用ik smart分词,这样能够最大限度的避免分词不准确对效果的影响。

5.3 spanNearQuery检索

spanNearQuery条件写法:

SELECT * FROM index/type WHERE fulltext=spanNearQuery('ABE') 

spanNearQuery是分词检索,同时要求条件在文档中一定要连续出现。所以spanNearQuery要求索引和检索一定要使用完全相同的分词策略。比如对“远郊寄养家庭”,分词后是“远郊/郊/寄养/寄/养家/家庭/家/庭”,如果检索条件是:

WHERE fulltext=spanNearQuery('寄养家庭') 

搜索时先对条件使用同样的分词策略,然后判断fulltext中是否连续出现搜索词。

5.4 query检索

query条件写法:

SELECT * FROM index/type WHERE fulltext=query('chin*') 

query也是分词检索,首先在字典中寻找符合“chin*”通配符的词,比如“china”、“chinese”等等。然后寻找含有任一词的文档。

6.排序

把关键词从文档的海洋中检索出来只是万里长征走完了第一步,后面还有一件更重要、难度也更大的事情:排序。对于海量数据来说,排序不合理和检索不正确造成的后果其实没有多大的区别(甚至要更严重,个人观点)。

Lucene默认的排序方式是根据关键词文档相关性,默认的算法是TF-IDF。TF词频(Term Frequency),IDF逆向文件频率(Inverse Document Frequency)。

具体的公式这里就不写了,维基百科上有个例子,https://zh.wikipedia.org/wiki/TF-IDF ,这里说明一下:

首先计算单词的TF-IDF:假如一篇文件的总词语数是100个,而词语“母牛”出现了3次,那么“母牛”一词在该文件中的词频就是3/100=0.03。一个计算文件频率(DF)的方法是测定有多少份文件出现过“母牛”一词,然后除以文件集里包含的文件总数。所以,如果“母牛”一词在1,000份文件出现过,而文件总数是10,000,000份的话,其逆向文件频率就是log(10,000,000 / 1,000)=4。最后的TF-IDF的分数为0.03 * 4=0.12。

再计算关键词与文档的相关性:根据关键字k1,k2,k3进行搜索结果的相关性就变成TF1 * IDF1 + TF2 * IDF2 + TF3 * IDF3。比如document1的term总量为1000,k1,k2,k3在document1出现的次数是100,200,50。包含了k1, k2, k3的document总量分别是 1000,10000,5000。document set的总量为10000。 TF1 = 100/1000 = 0.1 TF2 = 200/1000 = 0.2 TF3 = 50/1000 = 0.05 IDF1 = In(10000/1000) = In (10) = 2.3 IDF2 = In(10000/10000) = In (1) = 0; IDF3 = In(10000/5000) = In (2) = 0.69 这样关键字k1,k2,k3与document1的相关性 = 0.1 * 2.3 + 0.2 * 0 + 0.05 * 0.69 = 0.2645 其中k1比k3的比重在document1要大,k2的比重是0.

这是理论上的相关性算法,实际上可以根据其他因素来修正。比如Google的page rank算法,会提前根据连接数和引用数算出网页的page rank得分,计算关键词相关性的时候还会考虑关键词在网页上出现的位置(title、meta、正文标题、正文内容、侧边栏等等),给出不同的相关度。也可以根据某个业务属性强制排序(比如create_time等等)。

posted on 2016-08-18 11:05  小陆  阅读(2208)  评论(0编辑  收藏  举报