使用 Elasticsearch 做一个好用的日语搜索引擎及自动补全
最近基于 Elastic Stack 搭建了一个日语搜索服务,发现日文的搜索相比英语和中文,有不少特殊之处,因此记录下用 Elasticsearch 搭建日语搜索引擎的一些要点。本文所有的示例,适用于 Elastic 6.X 及 7.X 版本。
日语搜索的特殊性
以 Elastic 的介绍语「Elasticsearchは、予期した結果や、そうでないものも検索できるようにデータを集めて格納するElastic Stackのコア製品です」为例。作为搜索引擎,当然希望用户能通过句子中的所有主要关键词,都能搜索到这条结果。
和英文一样,日语的动词根据时态语境等,有多种变化。如例句中的「集めて」表示现在进行时,属于动词的连用形的て形,其终止形(可以理解为动词的原型)是「集める」。一个日文动词可以有 10 余种活用变形。如果依赖单纯的分词,用户在搜索「集める」时将无法匹配到这个句子。
除动词外,日语的形容词也存在变形,如终止形「安い」可以有连用形「安く」、未然性「安かろ」、仮定形「安けれ」等多种变化。
和中文一样,日文中存在多音词,特别是人名、地名等,如「相楽」在做人名和地名时就有 Sagara、Soraku、Saganaka 等不同的发音。
同时日文中一个词还存在不同的拼写方式,如「空缶」 = 「空き缶」。
而作为搜索引擎,输入补全也是很重要的一个环节。从日语输入法来看,用户搜索时输入的形式也是多种多样,存在以下的可能性:
- 平假名, 如「検索 -> けんさく」
- 片假名全角,如 「検索 -> ケンサク」
- 片假名半角,如「検索 -> ケンサク」
- 汉字,如 「検索」
- 罗马字全角,如「検索 -> kennsaku」
- 罗马字半角,如「検索 -> kennsaku」
等等。这和中文拼音有点类似,在用户搜索结果或者做输入补全时,我们也希望能尽可能适应用户的输入习惯,提升用户体验。
Elasticsearch 文本索引的过程
Elasticsearch (下文简称 ES)作为一个比较成熟的搜索引擎,对上述这些问题,都有了一些解决方法
先复习一下 ES 的文本在进行索引时将经历哪些过程,将一个文本存入一个字段 (Field) 时,可以指定唯一的分析器(Analyzer),Analyzer 的作用就是将源文本通过过滤、变形、分词等方式,转换为 ES 可以搜索的词元(Term),从而建立索引,即:
graph LR Text --> Analyzer Analyzer --> Term
复制代码
一个 Analyzer 内部,又由 3 部分构成
- 字符过滤器 (Character Filter): ,对文本进行字符过滤处理,如处理文本中的 html 标签字符。一个 Analyzer 中可包含 0 个或多个字符过滤器,多个按配置顺序依次进行处理。
- 分词器 (Tokenizer): 对文本进行分词。一个 Analyzer 必需且只可包含一个 Tokenizer。
- 词元过滤器 (Token filter): 对 Tokenizer 分出的词进行过滤处理。如转小写、停用词处理、同义词处理等。一个 Analyzer 可包含 0 个或多个词项过滤器,多个按配置顺序进行过滤。
引用一张图说明应该更加形象
ES 已经内置了一些 Analyzers,但显然对于日文搜索这种较复杂的场景,一般需要根据需求创建自定义的 Analyzer。
另外 ES 还有归一化处理器 (Normalizers)的概念,可以将其理解为一个可以复用的 Analyzers, 比如我们的数据都是来源于英文网页,网页中的 html 字符,特殊字符的替换等等处理都是基本相同的,为了避免将这些通用的处理在每个 Analyzer 中都定义一遍,可以将其单独整理为一个 Normalizer。
快速测试 Analyzer
为了实现好的搜索效果,无疑会通过多种方式调整 Analyzer 的配置,为了更有效率,应该优先掌握快速测试 Analyzer 的方法, 这部分内容详见 如何快速测试 Elasticsearch 的 Analyzer, 此处不再赘述。
Elasticsearch 日语分词器 (Tokenizer) 的比较与选择
日语分词是一个比较大的话题,因此单独开了一篇文章介绍和比较主流的开源日语分词项目。引用一下最终的结论
算法/模型 | 实现语言 | 词典 | 处理速度 | ES 插件 | Lisence | |
---|---|---|---|---|---|---|
MeCab | CRF | C++ | 可选 | 最高 | 有 | GPL/LGPL/BSD |
Kuromoji | Viterbi | Java | 可选, 默认 ipadic | 中 | 内置 | Apache License v2.0 |
Juman++ | RNNLM | C++ | 自制 | 高 | 无 | Apache License v2.0 |
KyTea | SVM 等 | C++ | UniDic | 中 | 有 | Apache License v2.0 |
Sudachi | Lattice LSTM | Java | UniDic + NEologd | 中 | 有 | Apache License v2.0 |
nagisa | Bi-LSTM | Python | ipadic | 低 | 无 | MIT |
对于 Elasticsearch,如果是项目初期,由于缺少数据,对于搜索结果优化还没有明确的目标,建议直接使用 Kuromoji 或者 Sudachi,安装方便,功能也比较齐全。项目中后期,考虑到分词质量和效率的优化,可以更换为 MeCab 或 Juman++。 本文将以 Kuromoji 为例。
日语搜索相关的 Token Filter
在 Tokenizer 已经确定的基础上,日语搜索其他的优化都依靠 Token filter 来完成,这其中包括 ES 内置的 Token filter 以及 Kuromoji 附带的 Token filter,以下逐一介绍
Lowercase Token Filter (小写过滤)
将英文转为小写, 几乎任何大部分搜索的通用设置
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["lowercase"],"text": "Ironman"
}Response
{"tokens": [{"token": "ironman","start_offset": 0,"end_offset": 7,"type": "word","position": 0}]
}
复制代码
CJK Width Token Filter (CJK 宽度过滤)
将全角 ASCII 字符 转换为半角 ASCII 字符
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["cjk_width"],"text": "kennsaku"
}{"tokens": [{"token": "kennsaku","start_offset": 0,"end_offset": 8,"type": "word","position": 0}]
}
复制代码
以及将半角片假名转换为全角
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["cjk_width"],"text": "ケンサク"
}{"tokens": [{"token": "ケンサク","start_offset": 0,"end_offset": 4,"type": "word","position": 0}]
}
复制代码
ja_stop
Token Filter (日语停止词过滤)
一般来讲,日语的停止词主要包括部分助词、助动词、连接词及标点符号等,Kuromoji 默认使用的停止词参考lucene 日语停止词源码。 在此基础上也可以自己在配置中添加停止词
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["ja_stop"],"text": "Kuromojiのストップワード"
}{"tokens": [{"token": "Kuromoji","start_offset": 0,"end_offset": 8,"type": "word","position": 0},{"token": "ストップ","start_offset": 9,"end_offset": 13,"type": "word","position": 2},{"token": "ワード","start_offset": 13,"end_offset": 16,"type": "word","position": 3}]
}
复制代码
kuromoji_baseform
Token Filter (日语词根过滤)
将动词、形容词转换为该词的词根
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["kuromoji_baseform"],"text": "飲み"
}{"tokens": [{"token": "飲む","start_offset": 0,"end_offset": 2,"type": "word","position": 0}]
}
复制代码
kuromoji_readingform
Token Filter (日语读音过滤)
将单词转换为发音,发音可以是片假名或罗马字 2 种形式
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["kuromoji_readingform"],"text": "寿司"
}{"tokens": [{"token": "スシ","start_offset": 0,"end_offset": 2,"type": "word","position": 0}]
}
复制代码
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": [{"type": "kuromoji_readingform", "use_romaji": true}],"text": "寿司"
}{"tokens": [{"token": "sushi","start_offset": 0,"end_offset": 2,"type": "word","position": 0}]
}
复制代码
当遇到多音词时,读音过滤仅会给出一个读音。
kuromoji_part_of_speech
Token Filter (日语语气词过滤)
语气词过滤与停止词过滤有一定重合之处,语气词过滤范围更广。停止词过滤的对象是固定的词语列表,停止词过滤则是根据词性过滤的,具体过滤的对象参考源代码。
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["kuromoji_part_of_speech"],"text": "寿司がおいしいね"
}{"tokens": [{"token": "寿司","start_offset": 0,"end_offset": 2,"type": "word","position": 0},{"token": "おいしい","start_offset": 3,"end_offset": 7,"type": "word","position": 2}]
}
复制代码
kuromoji_stemmer
Token Filter (日语长音过滤)
去除一些单词末尾的长音, 如「コンピューター」 => 「コンピュータ」
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["kuromoji_stemmer"],"text": "コンピューター"
}{"tokens": [{"token": "コンピュータ","start_offset": 0,"end_offset": 7,"type": "word","position": 0}]
}
复制代码
kuromoji_number
Token Filter (日语数字过滤)
将汉字的数字转换为 ASCII 数字
POST _analyze
{"tokenizer": "kuromoji_tokenizer","filter": ["kuromoji_number"],"text": "一〇〇〇"
}{"tokens": [{"token": "1000","start_offset": 0,"end_offset": 4,"type": "word","position": 0}]
}
复制代码
日语全文检索 Analyzer 配置
基于上述这些组件,不难得出一个完整的日语全文检索 Analyzer 配置
PUT my_index
{"settings": {"analysis": {"analyzer": {"ja_fulltext_analyzer": {"type": "custom","tokenizer": "kuromoji_tokenizer","filter": ["cjk_width","lowercase","kuromoji_stemmer","ja_stop","kuromoji_part_of_speech","kuromoji_baseform"]}}}},"mappings": {"my_type": {"properties": {"title": {"type": "text","analyzer": "ja_fulltext_analyzer"}}}}
}
复制代码
其实这也正是 kuromoji
analyzer 所使用的配置,因此上面等价于
PUT my_index
{"mappings": {"my_type": {"properties": {"title": {"type": "text","analyzer": "kuromoji"}}}}
}
复制代码
这样的默认设置已经可以应对一般情况,采用默认设置的主要问题是词典未经打磨,一些新词语或者专业领域的分词不准确,如「東京スカイツリー」期待的分词结果是 「東京/スカイツリー」,实际分词结果是「東京/スカイ/ツリー」。进而导致一些搜索排名不够理想。这个问题可以将词典切换到 UniDic + NEologd,能更多覆盖新词及网络用语,从而得到一些改善。同时也需要根据用户搜索,不断维护自己的词典。而自定义词典,也能解决一词多拼以及多音词的问题。
至于本文开始提到的假名读音匹配问题,很容易想到加入 kuromoji_readingform
,这样索引最终存储的 Term 都是假名形式,确实可以解决假名输入的问题,但是这又会引发新的问题:
一方面,kuromoji_readingform
所转换的假名读音并不一定准确,特别是遇到一些不常见的拼写,比如「明るい」-> 「アカルイ」正确,「明るい」的送りがな拼写「明かるい」就会转换为错误的「メイ・カルイ」
另一方面,日文中相同的假名对应不同汉字也是极为常见,如「シアワセ」可以写作「幸せ」、「仕合わせ」等。
因此kuromoji_readingform
并不适用于大多数场景,在输入补全,以及已知读音的人名、地名等搜索时,可以酌情加入。
日语自动补全的实现
Elasticsearch 的补全(Suggester)有 4 种:Term Suggester 和 Phrase Suggester 是根据输入查找形似的词或词组,主要用于输入纠错,常见的场景是"你是不是要找 XXX";Context Suggester 个人理解一般用于对自动补全加上其他字段的限定条件,相当于 query 中的 filter;因此这里着重介绍最常用的 Completion Suggester。
Completion Suggester 需要响应每一个字符的输入,对性能要求非常高,因此 ES 为此使用了新的数据结构:完全装载到内存的 FST(In Memory FST), 类型为 completion
。众所周知,ES 的数据类型主要采用的是倒排索引(Inverse Index), 但由于 Term 数据量非常大,又引入了 term dictionary 和 term index,因此一个搜索请求会经过以下的流程。
graph LR TI[Term Index]TD[Term Dictionary]PL[Posting List]Query --> TI TI --> TDTD --> TermTerm --> PLPL --> Documents
复制代码
completion 则省略了 term dictionary 和 term index,也不需要从多个 nodes 合并结果,仅适用内存就能完成计算,因此性能非常高。但由于仅使用了 FST 一种数据结构,只能实现前缀搜索。
了解了这些背景知识,来考虑一下如何构建日语的自动补全。
和全文检索不同,在自动补全中,对读音和罗马字的匹配有非常强的需求,比如用户在输入「銀魂」。按照用户的输入顺序,实际产生的字符应当是
- gin
- ぎん
- 銀
- 銀 t
- 銀 tama
- 銀魂
理想状况应当让上述的所有输入都能匹配到「銀魂」,那么如何实现这样一个自动补全呢。常见的方法是针对汉字、假名、罗马字各准备一个字段,在输入时同时对 3 个字段做自动补全,然后再合并补全的结果。
来看一个实际的例子, 下面建立的索引中,创建了 2 种 Token Filter,kuromoji_readingform
可以将文本转换为片假名,romaji_readingform
则可以将文本转换为罗马字,将其与kuromoji
Analyzer 组合,就得到了对应的自定义 Analyzer ja_reading_analyzer
和 ja_romaji_analyzer
。
对于 title 字段,分别用不同的 Analyzer 进行索引:
title
: text 类型,使用kuromoji
Analyzer, 用于普通关键词搜索title.suggestion
: completion 类型, 使用kuromoji
Analyzer,用于带汉字的自动补全title.reading
: completion 类型, 使用ja_reading_analyzer
Analyzer,用于假名的自动补全title.romaji
: completion 类型, 使用ja_romaji_analyzer
Analyzer,用于罗马字的自动补全
PUT my_index
{"settings": {"analysis": {"filter": {"katakana_readingform": {"type": "kuromoji_readingform","use_romaji": "false"},"romaji_readingform": {"type": "kuromoji_readingform","use_romaji": "true"}},"analyzer": {"ja_reading_analyzer": {"type": "custom","filter": ["cjk_width","lowercase","kuromoji_stemmer","ja_stop","kuromoji_part_of_speech","kuromoji_baseform","katakana_readingform"],"tokenizer": "kuromoji_tokenizer"},"ja_romaji_analyzer": {"type": "custom","filter": ["cjk_width","lowercase","kuromoji_stemmer","ja_stop","kuromoji_part_of_speech","kuromoji_baseform","romaji_readingform"],"tokenizer": "kuromoji_tokenizer"}}}},"mappings": {"my_type": {"properties": {"title": {"type": "text","analyzer": "kuromoji","fields": {"reading": {"type": "completion","analyzer": "ja_reading_analyzer","preserve_separators": false,"preserve_position_increments": false,"max_input_length": 20},"romaji": {"type": "completion","analyzer": "ja_romaji_analyzer","preserve_separators": false,"preserve_position_increments": false,"max_input_length": 20},"suggestion": {"type": "completion","analyzer": "kuromoji","preserve_separators": false,"preserve_position_increments": false,"max_input_length": 20}}}}}}
}
复制代码
插入示例数据
POST _bulk
{ "index": { "_index": "my_index", "_type": "my_type", "_id": 1} }
{ "title": "銀魂" }
复制代码
然后运行自动补全的查询
GET my_index/_search
{"suggest": {"title": {"prefix": "gin","completion": {"field": "title.suggestion","size": 20}},"titleReading": {"prefix": "gin","completion": {"field": "title.reading","size": 20}},"titleRomaji": {"prefix": "gin","completion": {"field": "title.romaji","size": 20}}}
}
复制代码
可以看到不同输入的命中情况
- gin: 命中
title.romaji
- ぎん: 命中
title.reading
和title.romaji
- 銀: 命中
title.suggestion
,title.reading
和title.romaji
- 銀 t: 命中
title.romaji
- 銀たま: 命中
title.reading
和title.romaji
- 銀魂: 命中
title.suggestion
,title.reading
和title.romaji
References
- All About Analyzers
- Writing analyzers
- Elasticsearch 6 を使ったデータ検証
- Elasticsearch キーワードサジェスト日本語のための設計
- Elasticsearchを日本語で使う設定のまとめ
- Lucene 查询原理
使用 Elasticsearch 做一个好用的日语搜索引擎及自动补全相关推荐
- java中使用kuromoji_使用Elasticsearch做一个好用的日语搜索引擎及自动补全
最近基于 Elastic Stack 搭建了一个日语搜索服务,发现日文的搜索相比英语和中文,有不少特殊之处,因此记录下用 Elasticsearch 搭建日语搜索引擎的一些要点.本文所有的示例,适用于 ...
- ElasticSearch从入门到精通--第七话(自动补全、拼音分词器、自定义分词、数据同步方案)
ElasticSearch从入门到精通–第七话(自动补全.拼音分词器.自定义分词.数据同步方案) 使用拼音分词 可以引入elasticsearch的拼音分词插件,地址:https://github.c ...
- ElasticSearch六:搜索模板与suggest search(自动补全)、地图位置检索功能实现、FileBeat与LogSteash使用
目录 search template template入门案例 记录template实现重复调用 保存template到ES 调用template执行搜索 查询已定义的template 删除已定义的t ...
- elasticsearch搜素关键字自动补全(suggest)
elasticsearch搜素关键字自动补全顾名思义 在搜索框搜索时能有提示列表可供选择. 最终效果如下: 该搜索优化功能是elasticsearch自带的即suggest,suggest即存储一个词 ...
- Elasticsearch 分布式搜索引擎 -- 自动补全(拼音分词器、自定义分词器、自动补全查询、实现搜索框自动补全)
文章目录 1. 自动补全 1.1 拼音分词器 1.2.1 自定义分词器 1.2.2 小结 1.2 自动补全 1.3 实现酒店搜索框自动补全 1.3.1 修改酒店映射结构 1.3.2 修改HotelDo ...
- SpringCloud(9)— Elasticsearch聚合和自动补全
SpringCloud(9)- Elasticsearch聚合和自动补全 一 数据聚合 1.聚合的分类 聚合(aggregations)可以实现对文档数据的统计,分析,运算.常见的聚合有三种: 1.桶 ...
- python的cfg是什么模块_cfg4py:一个严肃的Python配置模块应有的风格-层级式、部署环境自适应、自动补全...
关于如何配置一个Python程序,如果你还停留在使用ini,json,yaml,xml等文件格式之争,或者还在为使用哪个库来加载配置纠结,只能说明你还没有开发过一个严肃认真的大型Python应用程序. ...
- 8s yaml 配置生成_cfg4py:一个严肃的Python配置模块应有的风格-层级式、部署环境自适应、自动补全...
关于如何配置一个Python程序,如果你还停留在使用ini,json,yaml,xml等文件格式之争,或者还在为使用哪个库来加载配置纠结,只能说明你还没有开发过一个严肃认真的大型Python应用程序. ...
- Elasticsearch高级使用-自动补全
一.概念 注意事项 为了避免搜索同音字,搜索时不要使用拼音分词器 二.拼音分词器 官网https://github.com/medcl/elasticsearch-analysis-pinyin 安装 ...
最新文章
- linux x window system下载,linux的x window system
- c#后台如何导出excel到本地_C#后台导出Excel
- IDEA查找接口实现类及快速实现接口的方法及常用快捷键
- PHPCMS调用相关
- 使用python构建向量空间_使用Docker构建Python数据科学容器
- 试题11 蒜头君的随机数(排序和去重)
- 如何使用VC6.0创建DLL
- 取PE文件OriginalFilename-解析VERSION资源
- 最新最简单的黑苹果Mac Windows双系统教程(单双系统通用)
- ImportError: attempted relative import with no known parent package
- 京东java前后端联调_前端工程化、组件化实践JDM分享
- 计算机安装Hp1005打印机,hp1005打印机安装方法
- 消灭泡泡糖游戏java编程_JAVA面向对象编程课程设计——泡泡堂
- web系统中的结构化数据标记
- SQL视图View的总结和使用
- 《格兰杰计量经济学文集(第一卷)》
- FORTRAN学习笔记
- Linux系统中的ddns:dhcp+dns=ddns (花生壳)
- 直升机空气动力学基础--005翼型的升阻比和气动力矩
- 阿斯汤加瑜伽(Ashtanga Yoga)第一序列学习与实践笔记(一)
热门文章
- 计算机英语短文,计算机英语短文
- 目前最稳定的Win10 21H2正式版系统 V2022
- 八边形 八角图形 Python Turtle绘制
- 使用React Native 和 微信小程序 编写的一款阅读类app ———《轻松一刻》
- 海康、大华、tplink监控摄像头和硬盘录像机接入GB28181平台配置细节
- 物理学基本原理----电磁感应
- Patching more than once will result in the union of all True param eters being patched monkey.pat
- U盘内容被病毒隐藏的解决方案
- python计算相似矩阵
- H5 和 C3 的新特性