Solr是一个用java开发的独立的企业级搜索应用服务器,它提供了类似于Web-service的API接口,它是基于Lucene的全文检索服务器,也算是Lucene的一个变种,很多一线互联网公司都在使用Solr,也算是一种成熟的解决方案.
官方主页:http://lucene.apache.org/solr/
Elasticsearch是一个采用java语言开发的,基于Lucene构造的开源,分布式的搜索引擎. 设计用于云计算中,能够达到实时搜索,稳定可靠. Elasticsearch的数据模型是JSON.
官方主页:http://www.elasticsearch.org/
Xunsearch 是一个高性能、全功能的全文检索解决方案,旨在帮助一般开发者针对既有的海量数据,快速而方便地建立自己的全文搜索引擎。
官方主页:http://www.xunsearch.com/
Zebra是一个用C语言实现的检索程序,特点是对大数据的支持,支持EMAIL,XML,MARC等格式的数据.
官方主页:https://www.indexdata.com/zebra
Nutch是一个用java实现的开源的web搜索引擎,包括爬虫crawler,索引引擎,查询引擎. 其中Nutch是基于Lucene的,Lucene为Nutch提供了文本索引和搜索的API.
对于应该使用Lucene还是使用Nutch,应该是如果你不需要抓取数据的话,应该使用Lucene,最常见的应用是:你有数据源,需要为这些数据提供一个搜索页面,在这种情况下,最好的方式是直接从数据库中取出数据,并用Lucene API建立索引.
官方主页:http://nutch.apache.org/
参考:https://blog.csdn.net/business122/article/details/78064092
Slor安装教程
需要环境:JAVA 8
cp -r jdk1.8.0_181/ ~/env/
cd solr-7.4.0/
export PATH=/home/username/env/jdk1.8.0_181/bin:$PATH
bin/solr start -e cloud
ps -ef|grep solr
# 停止Solr服务
bin/solr stop -all
bin/post -c testsolr example/exampledocs/* -p 9700
Slor配置详解
ES官方文档
ES可视化Kibana
启动ES
./../elasticsearch-6.3.0/bin/elasticsearch > logs/elasticsearch.log &
wget https://artifacts.elastic.co/downloads/kibana/kibana-6.3.0-linux-x86_64.tar.gz
tar -xzvf kibana-6.3.0-linux-x86_64.tar.gz
cd kibana-6.3.0-linux-x86_64
vi config/kibana.yml
启动Kibana
./bin/kibana
IK分词器安装
IK Analysis plugin integrates Lucene IK analyzer (http://code.google.com/p/ik-analyzer/) into elasticsearch, support customized dictionary.
ik_max_word 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合;
ik_smart 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。
SmartCN安装
The Smart Chinese Analysis plugin integrates Lucene’s Smart Chinese analysis module into elasticsearch.
分词结果查询Sample
POST max_synonyms/_analyze
{
"analyzer": "my_synonyms",
"text": "C7000处理器"
}
同义词安装
同义词一般格式:
简单扩展:我们可以把同义词列表中的任意一个词扩展成同义词列表所有的词。
举例 “jump,hop,leap”
简单收缩:把左边的多个同义词映射到了右边的单个词。它必须同时应用于索引和查询阶段,以确保查询词项映射到索引中存在的同一个值。
举例 “leap,hop => jump”
类型扩展:类型扩展是完全不同于简单收缩或扩张,并不是平等看待所有的同义词,而是扩大了词的意义,使被拓展的词更为通用。
举例"cat => cat,pet",“kitten => kitten,cat,pet”,“dog => dog,pet”“puppy => puppy,dog,pet”
同义词配置教程
PUT max
{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym",
"expand": true,
"synonyms_path" : "analysis/synonyms.dict"
}
},
"analyzer": {
"my_synonyms": {
"tokenizer": "ik_smart",
"filter": [
"lowercase",
"my_synonym_filter"
]
}
}
}
}
}
POST max/_doc/_mapping
{
"properties": {
"question": {
"type": "text"
},
"content": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "my_synonyms"
}
}
}
IK Analyzer在默认的停用词表中包含了一些关键字和特殊符号,如果同义词表中有相同的词,那么ES中的IK插件就会报错。
如果配置了自定义停用词表时,也要避免会与同义词表冲突,否则会报错:
"type": "illegal_argument_exception",
"reason": "term: ? was completely eliminated by analyzer"
解决办法:
illegal_list = ['%', '#', '+', '?']
illegal_words = ['be', 'a', 'no', "==", '"?"']
非法字符串过滤解决办法:
# 字符转半角
def strQ2B(str):
res_str = ""
for ichar in str:
inside_code = ord(ichar)
if inside_code == 12288:
inside_code = 32
elif 65281 <= inside_code <= 65374:
inside_code -= 65248
res_str += chr(inside_code)
return res_str
# 专治各种错误字符
def simplify_str(str, type="lower"):
str = strQ2B(str)
bad_char = ['(', ')', ',', ' ', '】', '【', '?', ' ', '=', '“', '”']
simpli_char = ['(', ')', ',', '', '', '', '?', '', '=', '"', '"']
for x, y in zip(bad_char, simpli_char):
str = str.replace(x, y)
if type == "upper":
return str.upper()
elif type == "lower":
return str.lower()
同义词配置过程小结:
1、如果使用同义词插件,优先选择ik_smart分词,因为这样分词不会将错误的分词结果对应的同义词引入到搜索结果当中
2、如果使用简单收缩模式,会将所有的搜索term替换,即使搜索结果中包含也不能匹配到
3、如果同义词表噪音较大,尽量使用类型扩展模式,再保证搜索term存在的情况下,其同义词的匹配也会计入得分
4、无论是检索还是问题相似度匹配,我对max(取匹配度最高的作为分类结果)和avg(取所有结果中平均得分最高的分类结果)都做了很多实验,max准确率远远高于avg,而且速度也比avg快很多。此结果是在数据噪声比较大的前提下。
Analyzer作用域解释
项目要求搜索时使用同义词+ik_smart(自定义词表),匹配时使用ik_smart(自定义词表),因此在不通的作用域以及场景下配置不同的Analyzer。
IK Analyzer 扩展配置
custom/words.list
custom/stopwords.txt
ElasticSearch 打分策略 ES评分机制
Lucene打分数学推导
通过得分和权重对ES搜索排名优化
ES官网给出的打分公式(Lucene):
s c o r e ( q , d ) = c o o r d ( q , d ) ∗ q u e r y N o r m ( q ) ∗ ∑ t ∈ q t f ( t ) ∗ i d f ( t ) 2 ∗ t . g e t B o o s t ( ) ∗ n o r m ( t , d ) score(q,d) = coord(q,d)*queryNorm(q)*{\sum_{t \in q}{tf(t)*{idf(t)}^2*t.getBoost()*norm(t,d)}} score(q,d)=coord(q,d)∗queryNorm(q)∗t∈q∑tf(t)∗idf(t)2∗t.getBoost()∗norm(t,d)
score(q,d) =
queryNorm(q)
· coord(q,d)
· ∑ (
tf(t in d)
· idf(t)²
· t.getBoost()
· norm(t,d)
) (t in q)
queryNorm(q) = 1 / sqrt(
idf(t1)*idf(t1)+idf(t2)*idf(t2)+...+idf(tn)*idf(tn)
)
coord(q,d) = overlap/maxoverlap
tf(t in d) = sqrt(frequency)
idf(t) = 1 + log (numDocs / (docFreq + 1))
norm(d) = 1 / sqrt(numTerms)
queryNorm(q)
对查询进行一个归一化,有几个分片就会有几个不同的queryNorm值
q u e r y N o r m ( q ) = 1 q . g e t B o o s t ( ) 2 ∗ ∑ t ∈ q ( i d f ( t ) ∗ t . g e t B o o s t ( ) ) 2 queryNorm(q) = \frac{1}{\sqrt{{q.getBoost()}^2*\sum_{t \in q}{(idf(t)*t.getBoost())^2}}} queryNorm(q)=q.getBoost()2∗∑t∈q(idf(t)∗t.getBoost())21
coord(q,d)
协调因子,暂时没找到计算的结果在哪里…coord官方介绍
tf(t in d)
词频数
idf(t)
逆词频数
t.getboost()
获取每个term的权重,需要人为调整
norm(d)
标准化因子
n o r m ( t , d ) = d . g e t B o o s t ( ) ∗ l e n g t h N o r m ( f i e l d ) ∗ ∏ f ∈ d f . g e t B o o s t ( ) norm(t,d) = d.getBoost()*lengthNorm(field)*\prod_{f \in d}{f.getBoost()} norm(t,d)=d.getBoost()∗lengthNorm(field)∗f∈d∏f.getBoost()
l e n g t h N o r m ( f ) = 1 nums of terms in field f lengthNorm(f) = \frac{1}{\sqrt{\text{nums of terms in field f}}} lengthNorm(f)=nums of terms in field f1
# 新建索引
PUT index
# 定义映射
POST index/max/_mapping
{
"properties": {
"question": {
"type": "text"
},
"content": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "my_synonyms"
}
}
}
# 批量插入数据
POST _bulk
{"index":{"_index": "index","_type": "max"}}
{"content":"C5000设置来电黑名单"}
# 查询
POST index/max/_search
{
"query" : { "match" : { "content" : "删除文件夹" }},
"highlight" : {
"pre_tags" : ["", ""],
"post_tags" : [" ", ""],
"fields" : {
"content" : {}
}
}
}
用文本替换将句子修改为bulk格式的方法:
replaceAll('\r\n','"}}\r\n{"index":{"_index": "index","_type": "fulltext"}}\r\n{ "create" :{"content":"')
PUT max_test
{
"settings": {
"number_of_shards": 1,
"similarity": {
"scripted_tfidf": {
"type": "scripted",
"script": {
"source": "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;"
}},
"scripted_bm25": {
"type": "scripted",
"script" :{
"source": "double k1 = 1.2;double b = 0.75;double idf = Math.log(1.0 + (field.docCount-term.docFreq+0.5)/(term.docFreq+0.5));double tfNorm = (doc.freq * (k1 + 1)) / (doc.freq + k1 * (1 - b + b * doc.length / field.avgLength));return query.boost * idf * tfNorm;"
}
}
}
},
"mappings": {
"_doc": {
"properties": {
"content": {
"type": "text",
"similarity": "scripted_bm25"
}
}
}
}
}
搭建过程:
工程部分目录结构:
.
└── rule
├── output
│ ├── class_key.list 用于batch_test,鉴别标注数据是否在已有类别中
│ ├── rule.data 生成规范的句子扩充
│ ├── synonyms.dict [1]用于ES_synonym同义词
│ ├── stopwords.txt [2]停用词表
│ └── words.list [3]生成的词表,用于自定义分词以及ES_IK分词
├── README.md
├── resource
│ ├── lexical20180504.txt.map 开发提供的近义词词表
│ └── rules20180504.rule 开发提供的规则
├── searchtool
│ ├── batch_test.py
│ ├── const.py
│ ├── db_type.es
│ ├── lx_main.py
│ ├── operate_db.py
│ ├── prodata.py
├── test
│ └── 0522full.rule 用于batch_test
[1]: elasticsearch-6.3.0/config/analysis/synonyms.dict
同时在ES新增index时配置,详见searchtool/db_type.es
"filter": {
"my_synonym_filter": {
"type": "synonym",
"expand": true,
"synonyms_path" : "analysis/synonyms.dict"
}
}
[2]:elasticsearch-6.3.0/plugins/elasticsearch-analysis-ik-6.3.1/config/custom/stopwords.txt
同时配置 elasticsearch-6.3.0/plugins/elasticsearch-analysis-ik-6.3.1/config/IKAnalyzer.cfg.xml
custom/stopwords.txt
[3]: elasticsearch-6.3.0/plugins/elasticsearch-analysis-ik-6.3.1/config/custom/words.list
同时配置 elasticsearch-6.3.0/plugins/elasticsearch-analysis-ik-6.3.1/config/IKAnalyzer.cfg.xml
custom/words.list
搜索流程:
def get_result(self, query, type="search_one"):
res_body = {}
# 替换非法字符(汉字字符+英文全角)
query = simplify_str(query)
# 去前后缀
query = self.del_prefix(query)
# 自定义分词
token_ques = self.tokenizer.max_forward_tokenizer(query)
if type == "term_boost":
# 设置 iterm Boost
term_dict = self.getTermBoost(token_ques)
# ES 搜索引擎查询
res_body["result"] = self.oper_es.searchByTermBoost(term_dict)
res_body["term_boost"] = str(term_dict)
elif type == "search_one":
res_body["result"] = self.oper_es.searchOne(token_ques)
return res_body
搜索query预处理
IK Analyzer分词插件只能为中文分词,而对于一些中间无空格的英文或者字符,不能解决。
所以自己实现了一个最大正向匹配分词的算法,参考链接:分词算法介绍
# 最大正向匹配分词算法
def max_forward_tokenizer(self, str):
assert len(self.wordset) > 0
s_index = 0
e_index = len(str)
temp_list = []
temp_str = ""
while s_index < e_index:
for i in reversed(range(s_index, e_index + 1)):
iter_str = str[s_index:i]
if iter_str in self.wordset:
if temp_str != "":
temp_list.append(temp_str)
temp_str = ""
temp_list.append(iter_str)
s_index += len(iter_str)
break
elif i == s_index:
temp_str += str[s_index]
s_index += 1
if temp_str != "":
temp_list.append(temp_str)
return ' '.join(temp_list)
term权重修改
ES官方文档以及国内的博客都说明了match搜索可以用通过bool的term搜索等价替代。
由于ES的Analyzer没有找到修改权重的方法,所以通过更换搜索方式解决。
POST max_token/_doc/_search?explain=true
{
"query": {
"bool": {
"should": [
{
"match": {
"content": {
"query": "手机",
"analyzer": "my_synonyms",
"boost": 2
}
}
},
{
"match": {
"content": {
"query": "性能",
"analyzer": "my_synonyms",
"boost": 3
}
}
},
{
"match": {
"content": {
"query": "参数",
"analyzer": "my_synonyms",
"boost": 3
}
}
}
]
}
}
}
搜索结果分析
在搜索流程上,最初是将IK分词后的结果设置每个term的权重,然后进行bool搜索。
但是这样的搜索结果性能上下降了很多,文档上提到match搜索最终都会在内部处理为bool搜索,理论上应该性能应该是等价的。
在进一步分析对比搜索结果后发现,"description": "weight(Synonym(content:手机 content:移动设备 content:集合) in 65902)
和weight(content:手机 in 62749)
从计算得分上存在差异。
虽然两者都是相加,但是match会将同一类同义词算作一个term来计算tfNorm&idf,而bool因提前做了同义词转换,每一个词都是一个term,匹配句子中没有同义词就不会引入计算。
最后采用了折中的办法,在自定义分词后,将每个词作为一个term,加权后使用IK Analyzer做搜索,当所有词的权重设置为1.0时,性能指标几乎和match方法一致。
而缺点是仅仅能设置一类同义词的权重,而不能精确到词。
XunSearch安装教程
ERROR: failed to compile xunsearch, see ‘setup.log’ for more detail
解决方式: 需要联网…