用 ElasticSearch 实现中文模糊搜索(分词 + 同义词搜索)
一、ElasticSearch 快速入门
快速入门见阮一峰的文章:全文搜索引擎 Elasticsearch 入门教程 。
二、环境安装
环境: 虚拟机 Debian10(X86_64),将虚拟机占用内存改成 2G ,且将虚拟机的 9200 端口和 8080 端口映射到宿主机。
(1) 安装 openjdk
sudo apt-get update
sudo apt-get install default-jdk
(2) 查看 openjdk 安装目录
sudo update-alternatives --config java
(3) 设置 JAVA_HOME 环境变量
# 打开 /etc/environment 文件
sudo nano /etc/environment
# 增加下面一行(安装目录见第 2 步)
JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
# 运行该文件
source /etc/environment
# 查看变量
echo $JAVA_HOME
三、安装 ElasticSearch
下载 ElasticSearch5.5.1 ,解压,直接运行
cd ~
mkdir doc-search
cd doc-search
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.5.1.zip
unzip elasticsearch-5.5.1.zip
cd elasticsearch-5.5.1/
./bin/elasticsearch
如果报错 "max virtual memory areas vm.maxmapcount [65530] is too low",则运行下面的命令:
sudo sysctl -w vm.max_map_count=262144
如果报错 "error='Not enough space' (errno=12)" ,则打开 elasticsearch-5.5.1/config/jvm.options ,修改堆内存大小,改为 1G :
# -Xms4g
# -Xmx4g
-Xms1g
-Xmx1g
如果正常启动,访问 localhost:9200 ,确认 elastcisearch 服务的信息:
curl localhost:9200
{
"name" : "BK_LpKY",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "XXIq-TxkQa2D-d9ZyfkNaw",
"version" : {
"number" : "5.5.1",
"build_hash" : "19c13d0",
"build_date" : "2017-07-18T20:44:24.823Z",
"build_snapshot" : false,
"lucene_version" : "6.6.0"
},
"tagline" : "You Know, for Search"
}
四、安装中文分词插件 ik ,配置扩展词典
(1) 下载安装中文分词插件 ik
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip
(2) 配置扩展词典
打开 elasticsearch-5.5.1/config/analysis-ik 目录,将 extra_single_word.dic 另存为 my_extra_word.dic ,打开 my_extra_word.dic ,增加自定义词,比如: "溢洪" 等词。
打开同目录下的 IKAnalyzer.cfg.xml 文件,在 properties 下增加扩展词典:
<entry key="ext_dict">my_extra_word.dic</entry>
重启 elasticsearch 。
五、配置同义词字典,建立索引及分词器
在 elasticsearch-5.5.1/config 目录下创建一个名为 "synonyms.dic" 的文件,输入同义词词组,格式如下:
西红柿,番茄 => 西红柿,番茄
社保,公积金 => 社保,公积金
溢洪,泄洪 => 泄洪,溢洪
创建索引:
# 删除原索引
curl -X DELETE 'localhost:9200/smarthydro?pretty=true'
# 创建索引
curl -X PUT 'localhost:9200/smarthydro?pretty=true' -d '{
"settings": {
"analysis": {
"analyzer": {
"by_smart": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["by_tfr", "by_sfr"],
"char_filter": ["by_cfr"]
}
},
"filter": {
"by_tfr": {
"type": "stop",
"stopwords": [" "]
},
"by_sfr": {
"type": "synonym",
"synonyms_path": "synonyms.dic"
}
},
"char_filter": {
"by_cfr": {
"type": "mapping",
"mappings": ["| => |"]
}
}
}
},
"mappings": {
"archive": {
"properties": {
"filename": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "by_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "by_smart"
},
"category": {
"type": "text"
},
"module": {
"type": "text"
},
"author": {
"type": "text"
},
"path": {
"type": "text"
}
}
}
}
}'
以上命令创建了一个名为 smarthydro 的索引,并在其下创建了一个类型为映射(mapping)的字符过滤器 "by_cfr" ,一个类型为停止(stop)的过滤器 "by_tfr" ,一个同义词过滤器 "by_sfr" (同义词在 elasticsearch-5.5.1/config/synonyms.dic 文件中指定),一个自定义分词器 "by_smart" (利用 ik_smart 分词,并应用过滤器 "by_tfr", "by_sfr" 以及字符过滤器 "by_cfr")。
另外,在 smarthydro 索引下创建了名为 archive 的类型,共有 filename, content, category, module, author, path 六个字段,其中 filename 和 content 字段的分词器为 ik_max_word 、搜索关键字的分词器为 by_smart 。
以下介绍各分词器,这是实现中文模糊搜索的重要概念。
(1) ik 插件自带的分词器 ik_smart
ik_smart 分词器针对汉语分词,能够智能地识别出汉语句子中的词语,并将其划分为有意义的词语组合。其特点是能够自动识别出单个汉字与词语之间的关系,并在分词时将其合理地分隔开来,同时不会将完整词语进行拆分。
例如,利用 ik_smart 对 "溢洪结构" 进行分词,结果为: "溢洪"、"结构" 。
curl 'localhost:9200/smarthydro/_analyze?pretty=true' -d '{
"analyzer": "ik_smart",
"text": "溢洪结构"
}'
{
"tokens" : [
{
"token" : "溢洪",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "结构",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
}
(2) ik 插件自带的分词器 ik_max_word
ik_max_word 分词器能够将文本切分为所有可能成词的组合,即将文本中的每个单位(汉字、数字、英文单词等)都尝试组合成词语,从而得到更全面的分词结果。相比 ik_smart 分词器, ik_max_word 更倾向于将较长的文本分解为更多的词语,适用于全文检索等需要更大范围的搜索场景。
例如,利用 ik_max_word 对 "溢洪结构" 进行分词,结果为: "溢洪"、"溢"、"洪"、"结构"、"结"、"构" 。
curl 'localhost:9200/smarthydro/_analyze?pretty=true' -d '{
"analyzer": "ik_max_word",
"text": "溢洪结构"
}'
{
"tokens" : [
{
"token" : "溢洪",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "溢",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "洪",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "结构",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "结",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "构",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 5
}
]
}
这里要注意的是,要实现单字分词("溢"、"洪"等),需要在 my_extra_word.dic 里面加入所有需要的单字。
一般来说 ik_max_word 用于字段内容分词、并插入索引,ik_smart 用于搜索关键字的分词。
(3) 自定义分词器 by_smart
by_smart 分词器先用 ik_smart 进行分词,之后,加入分词结果的所有同义词。例如:利用 by_smart 对 "溢洪结构" 进行分词,结果为: "泄洪"、"溢洪"、"结构" 。
curl 'localhost:9200/smarthydro/_analyze?pretty=true' -d '{
"analyzer": "by_smart",
"text": "溢洪结构"
}'
{
"tokens" : [
{
"token" : "泄洪",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0
},
{
"token" : "溢洪",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0
},
{
"token" : "结构",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
}
六、插入数据、搜索数据
(1) 插入数据
用 POST /{index_name}/{type_name} 向 elasticsearch 服务插入数据:
curl -X POST 'localhost:9200/smarthydro/archive' -d '{
"filename": "ANSYS 结构有限元高级分析方法与范例应用.pdf",
"path": "ANSYS 结构有限元高级分析方法与范例应用.pdf",
"content": ""
}'
curl -X POST 'localhost:9200/smarthydro/archive' -d '{
"filename": "Design of gravity dam,USBR-垦务局重力坝设计中文.pdf",
"path": "Design of gravity dam,USBR-垦务局重力坝设计中文.pdf",
"content": ""
}'
curl -X POST 'localhost:9200/smarthydro/archive' -d '{
"filename": "K078.天荒坪公司7#施工支洞渗水通道分析.pdf",
"path": "2013大坝年会/后加文章PDF/K078.天荒坪公司7#施工支洞渗水通道分析.pdf",
"content": ""
}'
curl -X POST 'localhost:9200/smarthydro/archive' -d '{
"filename": "K082.桐柏抽水蓄能电站下水库坝身溢洪道泄槽底板锚固筋型式试验研究.pdf",
"path": "2013大坝年会/后加文章PDF/K082.桐柏抽水蓄能电站下水库坝身溢洪道泄槽底板锚固筋型式试验研究.pdf",
"content": ""
}'
curl -X POST 'localhost:9200/smarthydro/archive' -d '{
"filename": "K083.深水垫条件下高拱坝护岸不护底水垫塘可行性研究 - 中文.pdf",
"path": "2013大坝年会/后加文章PDF/K083.深水垫条件下高拱坝护岸不护底水垫塘可行性研究 - 中文.pdf",
"content": ""
}'
完整的数据见 add-all.sh
插入数据时,elasticsearch 会用 analyzer 指定的分词器(本项目中是 ik_max_word)对字段文本进行分词,然后用分词结果作为键值插入数据索引中。
(2) 查询数据
用 POST /{index_name}/{type_name}/_search 查询数据,在 query 参数中指定搜索字段,如下:
curl -X POST 'localhost:9200/smarthydro/archive/_search?pretty=true' -d '{
"query": {
"match": {
"filename": "溢洪结构"
}
},
"from": 0,
"size": 1000,
"highlight": {
"fields": {
"filename": {}
}
},
"_source": {
"includes": [
"filename",
"content",
"category",
"module",
"author",
"path"
]
}
}'
{
"took" : 39,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 38,
"max_score" : 3.9375143,
"hits" : [
{
"_index" : "smarthydro",
"_type" : "archive",
"_id" : "AYi4q8QaJN1s5R57PpU5",
"_score" : 3.9375143,
"_source" : {
"path" : "规范/溢洪道设计规范.txt",
"filename" : "溢洪道设计规范.txt",
"content" : ""
},
"highlight" : {
"filename" : [
"<em>溢洪</em>道设计规范.txt"
]
}
},
{
"_index" : "smarthydro",
"_type" : "archive",
"_id" : "AYi4q8QLJN1s5R57PpU4",
"_score" : 3.8857627,
"_source" : {
"path" : "规范/溢洪道设计规范.pdf",
"filename" : "溢洪道设计规范.pdf",
"content" : ""
},
"highlight" : {
"filename" : [
"<em>溢洪</em>道设计规范.pdf"
]
}
}
]
}
}
前端页面如下:
七、高级使用问题
(1) 排序问题
有时会有匹配的 token 数更少但是排序在前的现象:
这个和 ES 中的匹配相关性分数计算方式有关,可参考 xguox 的文章 搜索引擎 ElasticSearch 的分数 (_score) 是怎么计算得出 。简单来说,相关性分数和匹配长度正相关,但和文档长度负相关。也就是说,匹配长度越大,分数越高,但另一方面,文档长度越大,分数越低。因此,若需要将搜索结果严格匹配长度排列,可在建立索引时,关闭 norms ,如下:
PUT /my_index
{
"mappings": {
"doc": {
"properties": {
"text": {
"type": "string",
"norms": { "enabled": false }
}
}
}
}
}
(2) 数字、年份问题
有时会有 “2022” 可以匹配到 “2022年7月27日xxx” 内容,但是 “2022年” 匹配不到。或者反过来, “2022年” 能匹配到,但是 “2022” 匹配不到。解决方法有两个:一个是在 my_extra_word.dic 中加入 “1000年、1001年、...、2999年、1月、...、12月、1日、...、31日” 等自定义词,然后重启 elasticsearch 服务,并重建索引;另一个方法是针对文本建两个字段,一个用 ik_max_word 分词,另一个用 ik_smart 分词,搜索时,关键字用 by_smart 分词,匹配到任意一个字段均可。