Elasticsearch7.2でkuromoji_ipadic_neologd_tokenizerのsearchモードとsynonym_token_filterを一緒に使うとエラーが出る
TL;DR
Elasticsearch7.2でKuromoji IPADic Neologd TokenizerのsearchモードとSynonym Token Filterを使うとエラーが出る。
エラーを回避するには、Synonym Token Filterを利用するanalyzerではkuromoji_tokenizerのnormalモードを使う
発生した問題
ElasticsearchでKuromoji IPADic Neologd TokenizerのsearchモードとSynonym Token Filterを一緒に使ったところ、インデックス作成時にエラーが出た。
/var/log/elasticsearch/[cluster name].log
のエラー
エラーメッセージ(クリックして表示)
[2019-09-19T05:57:48,919][DEBUG][o.e.a.a.i.t.p.TransportPutIndexTemplateAction] [n01] failed to put template [shops_template] java.lang.IllegalArgumentException: failed to build synonyms at org.elasticsearch.analysis.common.SynonymTokenFilterFactory.buildSynonyms(SynonymTokenFilterFactory.java:138) ~[?:?] at org.elasticsearch.analysis.common.SynonymTokenFilterFactory.getChainAwareTokenFilterFactory(SynonymTokenFilterFactory.java:90) ~[?:?] at org.elasticsearch.index.analysis.AnalyzerComponents.createComponents(AnalyzerComponents.java:84) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.analysis.CustomAnalyzerProvider.create(CustomAnalyzerProvider.java:63) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.analysis.CustomAnalyzerProvider.build(CustomAnalyzerProvider.java:50) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.analysis.AnalysisRegistry.produceAnalyzer(AnalysisRegistry.java:584) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.analysis.AnalysisRegistry.build(AnalysisRegistry.java:534) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.analysis.AnalysisRegistry.build(AnalysisRegistry.java:216) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.IndexService.<init>(IndexService.java:180) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.index.IndexModule.newIndexService(IndexModule.java:411) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.indices.IndicesService.createIndexService(IndicesService.java:563) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.indices.IndicesService.createIndex(IndicesService.java:512) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService.validateAndAddTemplate(MetaDataIndexTemplateService.java:235) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService.access$300(MetaDataIndexTemplateService.java:65) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.metadata.MetaDataIndexTemplateService$2.execute(MetaDataIndexTemplateService.java:176) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.ClusterStateUpdateTask.execute(ClusterStateUpdateTask.java:47) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.MasterService.executeTasks(MasterService.java:687) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.MasterService.calculateTaskOutputs(MasterService.java:310) ~[elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.MasterService.runTasks(MasterService.java:210) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.MasterService$Batcher.run(MasterService.java:142) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.TaskBatcher.runIfNotProcessed(TaskBatcher.java:150) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.cluster.service.TaskBatcher$BatchedTask.run(TaskBatcher.java:188) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingRunnable.run(ThreadContext.java:688) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor$TieBreakingPrioritizedRunnable.runAndClean(PrioritizedEsThreadPoolExecutor.java:252) [elasticsearch-7.3.2.jar:7.3.2] at org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor$TieBreakingPrioritizedRunnable.run(PrioritizedEsThreadPoolExecutor.java:215) [elasticsearch-7.3.2.jar:7.3.2] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?] at java.lang.Thread.run(Thread.java:835) [?:?] Caused by: java.text.ParseException: Invalid synonym rule at line 10 at org.apache.lucene.analysis.synonym.SolrSynonymParser.parse(SolrSynonymParser.java:72) ~[lucene-analyzers-common-8.1.0.jar:8.1.0 dbe5ed0b2f17677ca6c904ebae919363f2d36a0a - ishan - 2019-05-09 19:35:41] at org.elasticsearch.analysis.common.SynonymTokenFilterFactory.buildSynonyms(SynonymTokenFilterFactory.java:134) ~[?:?] ... 27 more Caused by: java.lang.IllegalArgumentException: term: 焼餃子 analyzed to a token (焼餃子) with position increment != 1 (got: 0) at org.apache.lucene.analysis.synonym.SynonymMap$Parser.analyze(SynonymMap.java:325) ~[lucene-analyzers-common-8.1.0.jar:8.1.0 dbe5ed0b2f17677ca6c904ebae919363f2d36a0a - ishan - 2019-05-09 19:35:41] at org.elasticsearch.analysis.common.ESSolrSynonymParser.analyze(ESSolrSynonymParser.java:57) ~[?:?] at org.apache.lucene.analysis.synonym.SolrSynonymParser.addInternal(SolrSynonymParser.java:114) ~[lucene-analyzers-common-8.1.0.jar:8.1.0 dbe5ed0b2f17677ca6c904ebae919363f2d36a0a - ishan - 2019-05-09 19:35:41] at org.apache.lucene.analysis.synonym.SolrSynonymParser.parse(SolrSynonymParser.java:70) ~[lucene-analyzers-common-8.1.0.jar:8.1.0 dbe5ed0b2f17677ca6c904ebae919363f2d36a0a - ishan - 2019-05-09 19:35:41] at org.elasticsearch.analysis.common.SynonymTokenFilterFactory.buildSynonyms(SynonymTokenFilterFactory.java:134) ~[?:?] ... 27 more [2019-09-19T05:58:00,832][INFO ][o.e.c.m.MetaDataCreateIndexService] [n01] [shops] creating index, cause [auto(bulk api)], templates [], shards [1]/[1], mappings [] [2019-09-19T05:58:00,983][INFO ][o.e.c.m.MetaDataMappingService] [n01] [shops/2DRfIkXNS66azf7EEvGuZw] create_mapping [_doc]
ちなみにエラーが出たときのAnalyzerとmapの設定はこれ。
"settings": { (省略) "analysis": { "filter": { "my_synonym": { "type": "synonym", "synonyms_path": "synonym.txt" } }, "tokenizer": { "kuromoji_search": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "search" } }, "analyzer": { "synonym_analyzer": { "type": "custom", "tokenizer": "kuromoji_search", ... "filter": ["my_synonym", ...] },
原因調査してわかったこと
ログに以下のエラーがあることから、synonym.txtの「焼餃子」を含む行の構文に問題があるらしい。
Caused by: java.lang.IllegalArgumentException: term: 焼餃子 analyzed to a token (焼餃子) with position increment != 1 (got: 0)
ちなみにsynonym.txtの中身はこんなかんじ
...省略... 焼き餃子, 焼きぎょうざ, 焼餃子
ここで、「焼餃子」をsynonym_analyzer
でtokenizeした結果を見てみる。
"settings": { (省略) "analysis": { "tokenizer": { "kuromoji_search": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "search" } }, "analyzer": { "kuromoji_search_analyzer": { "type": "custom", "tokenizer": "kuromoji_search", },
Analyzeした結果
$ curl -XPOST 'http://localhost:9200/hoge_index/_analyze?pretty' -H "Content-type: application/json" -d '{"analyzer": "kuromoji_search_analyzer", "text": "焼餃子"}' { "tokens" : [ { "token" : "焼", "start_offset" : 0, "end_offset" : 1, "type" : "word", "position" : 0 }, { "token" : "焼餃子", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0, "positionLength" : 2 }, { "token" : "餃子", "start_offset" : 1, "end_offset" : 3, "type" : "word", "position" : 1 } ] }
エラーメッセージにあった、position increment != 1 (got: 0)
のposition
っぽいものがある。
Analyzerのtokenizerにkuromoji_ipadic_neologd_tokenizer
のsearch
モードを利用すると、synonymのanalyzeの結果として、複合語を考慮した出力がされるが、「焼」と「焼餃子」でpositionの値が一緒であるためにpositionのincrementのエラーが出ているっぽい。
ソースコードを追っていってもその様子はけっこう容易に理解できた。
ただ、positionのcheckはlucene側に実装があったが、なんのためのチェックかはコメントなどもなくわからなかった。
次に過去のIssueを調べた結果、以下のIssueコメントを見つけた。
synonymを使いたいときは、normalモードを使えとのこと。 そのとおりにしたらうまくいった。
一応試しにnormalモードのAnalyzerを使って「焼餃子」Analyzeしてみる。
Analyzer
"analysis": { "tokenizer": { "kuromoji_normal": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "normal" } }, "analyzer": { "kuromoji_normal_analyzer": { "type": "custom", "tokenizer": "kuromoji_normal" } ...
Analyzeした結果
$ curl -XPOST 'http://localhost:9200/hoge_index/_analyze?pretty' -H "Content-type: application/json" -d '{"analyzer": "kuromoji_normal_analyzer", "text": "焼餃子"}' { "tokens" : [ { "token" : "焼", "start_offset" : 0, "end_offset" : 1, "type" : "word", "position" : 0 }, { "token" : "餃子", "start_offset" : 1, "end_offset" : 3, "type" : "word", "position" : 1 } ] }
normal
モードだと複合語である「焼餃子」が消えて同じpositionをとるtokenがなくなっていた。
最終的な構成
normal
モードで動くとはいえ、複合語も考慮したインデキシングはしてほしいので、最終的には以下のようにした。
Analyzerの構成
"analysis": { "filter": { "my_synonym": { "type": "synonym", "synonyms_path": "synonym.txt" } }, "tokenizer": { "kuromoji_normal": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "normal", "discard_punctuation": "true", "user_dictionary": "userdict.txt" }, "kuromoji_search": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "normal", "discard_punctuation": "true", "user_dictionary": "userdict.txt" } }, "analyzer": { "kuromoji_search_analyzer": { "type": "custom", "tokenizer": "kuromoji_search", "char_filter": ["kuromoji_iteration_mark"], "filter": [ "lowercase", "cjk_width", "kuromoji_baseform", "kuromoji_part_of_speech" ] } "synonym_analyzer": { "type": "custom", "tokenizer": "kuromoji_normal" "char_filter": ["kuromoji_iteration_mark"], "filter": [ "lowercase", "cjk_width", "kuromoji_baseform", "kuromoji_part_of_speech", "my_synonym" ] }, ... } }
mapping
... "properties": { "hogehoge": { "type": "text", "fields": { "kuromoji_field": { "type": "text", "analyzer": "kuromoji_search_analyzer" }, "synonym_field": { "type": "text", "analyzer": "synonym_analyzer" }, .... } }, ...
query
curl -XGET 'http://localhost:9200/hoge_index/_search?pretty' -H 'Content-Type: application/json' -d'{ "query": { "multi_match" : { "type" : "cross_fields", "query" : "焼餃子", "fields" : ["hogehoge.kuromoji_field^5", "hogehoge.synonym_field"], "operator" : "and" } } }'
その他試したこと1
上記を見ると、The tokenizer parameter controls the tokenizers that will be used to tokenize the synonym
とあるので、以下のようにsynonym
filterのパラメータにtokenizer
を渡せばsynonymのTokenizerだけnormal
モードを使ってその他の単語のTokenizeはsearch
モードを使うことができるのかと思ったけど、最初と同様のエラーがでてできなかった。
"analysis": { "filter": { "ja_synonym": { "type": "synonym", "synonyms_path": "synonym.txt", "tokenizer": "kuromoji_normal" } }, "tokenizer": { "kuromoji_normal": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "normal" }, "kuromoji_search": { "type": "kuromoji_ipadic_neologd_tokenizer", "mode": "search" } }, "analyzer": { "kuromoji_search_analyzer": { "type": "custom", "tokenizer": "kuromoji_normal", "filter": ["ja_synonym"] }
その他試したこと2
ちなみに、このブログを見ると、
Synonym Token Filter を日本語と一緒に使用する場合は、辞書の内容もトークナイザーの仕様にあわせて単語を分割して登録しておく必要があります。
とあるが、このやり方だとインデックス作成時はエラーが出ないが、一つの単語として認識してくれないのでシノニムとしては使えなかった。
Elasticsearch5.3あたりのドキュメントを見ると、The tokenizer parameter controls the tokenizers that will be used to tokenize the synonym, and defaults to the whitespace tokenizer.
とあるので、こちらのバージョンでの話なのかもしれない。