最近基于ElasticStack搭建了一个日语搜索服务,发现日文的搜索相比英语和中文,有不少特殊之处,因此记录下用Elasticsearch搭建日语搜索引擎的一些要点。本文所有的示例,适用于Elastic6.X及7.X版本。
日语搜索的特殊性
以Elastic的介绍语「Elasticsearchは、予期した結果や、そうでないものも検索できるようにデータを集めて格納するElasticStackのコア製品です」为例。作为搜索引擎,当然希望用户能通过句子中的所有主要关键词,都能搜索到这条结果。
和英文一样,日语的动词根据时态语境等,有多种变化。如例句中的「集めて」表示现在进行时,属于动词的连用形的て形,其终止形(可以理解为动词的原型)是「集める」。一个日文动词可以有10余种活用变形。如果依赖单纯的分词,用户在搜索「集める」时将无法匹配到这个句子。
除动词外,日语的形容词也存在变形,如终止形「安い」可以有连用形「安く」、未然性「安かろ」、仮定形「安けれ」等多种变化。
和中文一样,日文中存在多音词,特别是人名、地名等,如「相楽」在做人名和地名时就有Sagara、Soraku、Saganaka等不同的发音。
同时日文中一个词还存在不同的拼写方式,如「空缶」=「空き缶」。
而作为搜索引擎,输入补全也是很重要的一个环节。从日语输入法来看,用户搜索时输入的形式也是多种多样,存在以下的可能性:
平假名,如「検索-けんさく」片假名全角,如「検索-ケンサク」片假名半角,如「検索-」汉字,如「検索」罗马字全角,如「検索-kennsaku」罗马字半角,如「検索-kennsaku」等等。这和中文拼音有点类似,在用户搜索结果或者做输入补全时,我们也希望能尽可能适应用户的输入习惯,提升用户体验。
Elasticsearch文本索引的过程
Elasticsearch(下文简称ES)作为一个比较成熟的搜索引擎,对上述这些问题,都有了一些解决方法
先复习一下ES的文本在进行索引时将经历哪些过程,将一个文本存入一个字段(Field)时,可以指定唯一的分析器(Analyzer),Analyzer的作用就是将源文本通过过滤、变形、分词等方式,转换为ES可以搜索的词元(Term),从而建立索引,即:
一个Analyzer内部,又由3部分构成
字符过滤器(CharacterFilter):,对文本进行字符过滤处理,如处理文本中的html标签字符。一个Analyzer中可包含0个或多个字符过滤器,多个按配置顺序依次进行处理。分词器(Tokenizer):对文本进行分词。一个Analyzer必需且只可包含一个Tokenizer。词元过滤器(Tokenfilter):对Tokenizer分出的词进行过滤处理。如转小写、停用词处理、同义词处理等。一个Analyzer可包含0个或多个词项过滤器,多个按配置顺序进行过滤。引用一张图说明应该更加形象
ES已经内置了一些Analyzers,但显然对于日文搜索这种较复杂的场景,一般需要根据需求创建自定义的Analyzer。
另外ES还有归一化处理器(Normalizers)的概念,可以将其理解为一个可以复用的Analyzers,比如我们的数据都是来源于英文网页,网页中的html字符,特殊字符的替换等等处理都是基本相同的,为了避免将这些通用的处理在每个Analyzer中都定义一遍,可以将其单独整理为一个Normalizer。
快速测试Analyzer
为了实现好的搜索效果,无疑会通过多种方式调整Analyzer的配置,为了更有效率,应该优先掌握快速测试Analyzer的方法,这部分内容详见如何快速测试Elasticsearch的Analyzer,此处不再赘述。
Elasticsearch日语分词器(Tokenizer)的比较与选择
日语分词是一个比较大的话题,因此单独开了一篇文章介绍和比较主流的开源日语分词项目。引用一下最终的结论
对于Elasticsearch,如果是项目初期,由于缺少数据,对于搜索结果优化还没有明确的目标,建议直接使用Kuromoji或者Sudachi,安装方便,功能也比较齐全。项目中后期,考虑到分词质量和效率的优化,可以更换为MeCab或Juman++。本文将以Kuromoji为例。
日语搜索相关的TokenFilter
在Tokenizer已经确定的基础上,日语搜索其他的优化都依靠Tokenfilter来完成,这其中包括ES内置的Tokenfilter以及Kuromoji附带的Tokenfilter,以下逐一介绍
LowercaseTokenFilter(小写过滤)
将英文转为小写,几乎任何大部分搜索的通用设置
CJKWidthTokenFilter(CJK宽度过滤)
将全角ASCII字符转换为半角ASCII字符
以及将半角片假名转换为全角
ja_stopTokenFilter(日语停止词过滤)
一般来讲,日语的停止词主要包括部分助词、助动词、连接词及标点符号等,Kuromoji默认使用的停止词参考lucene日语停止词源码。在此基础上也可以自己在配置中添加停止词
kuromoji_baseformTokenFilter(日语词根过滤)
将动词、形容词转换为该词的词根
kuromoji_readingformTokenFilter(日语读音过滤)
将单词转换为发音,发音可以是片假名或罗马字2种形式
当遇到多音词时,读音过滤仅会给出一个读音。
kuromoji_part_of_speechTokenFilter(日语语气词过滤)
语气词过滤与停止词过滤有一定重合之处,语气词过滤范围更广。停止词过滤的对象是固定的词语列表,停止词过滤则是根据词性过滤的,具体过滤的对象参考源代码。
kuromoji_stemmerTokenFilter(日语长音过滤)
去除一些单词末尾的长音,如「コンピューター」=「コンピュータ」
kuromoji_numberTokenFilter(日语数字过滤)
将汉字的数字转换为ASCII数字
日语全文检索Analyzer配置
基于上述这些组件,不难得出一个完整的日语全文检索Analyzer配置
其实这也正是kuromojianalyzer所使用的配置,因此上面等价于
这样的默认设置已经可以应对一般情况,采用默认设置的主要问题是词典未经打磨,一些新词语或者专业领域的分词不准确,如「東京スカイツリー」期待的分词结果是「東京/スカイツリー」,实际分词结果是「東京/スカイ/ツリー」。进而导致一些搜索排名不够理想。这个问题可以将词典切换到UniDic+NEologd,能更多覆盖新词及网络用语,从而得到一些改善。同时也需要根据用户搜索,不断维护自己的词典。而自定义词典,也能解决一词多拼以及多音词的问题。
至于本文开始提到的假名读音匹配问题,很容易想到加入kuromoji_readingform,这样索引最终存储的Term都是假名形式,确实可以解决假名输入的问题,但是这又会引发新的问题:一方面,kuromoji_readingform所转换的假名读音并不一定准确,特别是遇到一些不常见的拼写,比如「明るい」-「アカルイ」正确,「明るい」的送りがな拼写「明かるい」就会转换为错误的「メイカルイ」
另一方面,日文中相同的假名对应不同汉字也是极为常见,如「シアワセ」可以写作「幸せ」、「仕合わせ」等。
因此kuromoji_readingform并不适用于大多数场景,在输入补全,以及已知读音的人名、地名等搜索时,可以酌情加入。
日语自动补全的实现
Elasticsearch的补全(Suggester)有4种:TermSuggester和PhraseSuggester是根据输入查找形似的词或词组,主要用于输入纠错,常见的场景是你是不是要找XXX;ContextSuggester个人理解一般用于对自动补全加上其他字段的限定条件,相当于query中的filter;因此这里着重介绍最常用的CompletionSuggester。
CompletionSuggester需要响应每一个字符的输入,对性能要求非常高,因此ES为此使用了新的数据结构:完全装载到内存的FST(InMemoryFST),类型为