Elasticsearchで「めうはめうめうめう」を探す (形態素解析+N-Gram)

ホンモノのElasticsearch初心者です。ログの解析はしないのでKibanaもFluentdも出てきません。関係無いけど常体で記事書くの、慣れていないせいか割りと難しく感じます……。だいたい敬体で書いてから常体に修正してしまってます。この段落も敬体で書いてしまいました。

目的

「めうはめうめうめう」を検索できるようにする。

形態素解析 vs. N-Gram

形態素解析N-Gramの特徴に関しては下の記事が分かりやすかった。

第6回 N-gramと形態素解析との比較:検索エンジンを作る|gihyo.jp … 技術評論社

形態素解析(今回はプラグインも用意されているKuromojiという形態素解析器を利用した)単体である程度求めている結果を得ることは出来た。

elasticsearch-kuromojiについては導入から設定まで @9215 さんの記事がかなり詳しく、非常に助かった。自分はとりあえず全てデフォルト設定のままKuromojiを使っている。

Elasticsearch 日本語で全文検索 その1 — Hello! Elasticsearch. — Medium

Elasticsearch 日本語で全文検索 その2 — Hello! Elasticsearch. — Medium

Elasticsearch 日本語で全文検索 その3 — Hello! Elasticsearch. — Medium

ところが、形態素解析の性質上、未知語(辞書、今回はIPAdicに載っていない単語)にあたると的外れな分かち書きをした上に、正しくない活用語尾でインデックスしてしまうため、「ぽよぽよ」や「めう」が検索できなくなってしまう。硬めのテキストならさほど問題にならないのかもしれないが、検索対象がTwitterのツイートなので、これはかなり手痛い。

実際によく検索しそうで辞書に載っていなさそうな文字列をAnalyzerに通してみると、「ぽよぽよ」→「ぽい/ぽい」、「めう」→「め」、「めうはめうめうめう」→「め/はめ/うめ/うめる」、と、割りと残念な結果が返ってくる。

N-Gram の導入

これは結構キツい……ので、インデックスの肥大化覚悟でN-Gramも導入した。ElasticsearchのnGramトークナイザーはデフォルトで1/2-Gramを作るようだが、日本語では3-Gramが適切らしいので、そんな感じにAnalyzerとTokenizerを設定。

ja_ngramというCustom Analyzerからja_ngram_tokenizerというnGram(Elasticsearch組み込み)ベースのCustom Tokenizerを呼ぶように。

{  
    "index":{  
        "analysis":{  
            "analyzer":{  
                "ja_ngram":{  
                    "type":"custom",
                    "tokenizer":"ja_ngram_tokenizer"
                }
            },
            "tokenizer":{  
                "ja_ngram_tokenizer":{  
                    "type":"nGram",
                    "min_gram":"2",
                    "max_gram":"3",
                    "token_chars":[  
                        "letter",
                        "digit"
                    ]
                }
            }
        }
    }
}

こんな感じですね。Elasticsearchではmulti-fieldsと言って、一つのフィールドに複数のAnalysisを設定できるっぽい。便利。

Multi-fields

と、いうことで、実際にMappingをそのように設定する。content形態素解析を利用したデータ、content.ngramで先ほど設定したja_ngramでAnalyzeされたデータにアクセスできる。

{  
    "mappings":{  
        "tweets":{  
            "properties":{  
                "content":{  
                    "type":"string",
                    "analyzer":"kuromoji",
                    "fields":{  
                        "ngram":{  
                            "type":"string",
                            "analyzer":"ja_ngram"
                        }
                    }
                }
            }
        }
    }
}

実際に検索する

"fields": ["content", "content.ngram"] みたいな感じのフィールド指定を加える事で両方から拾ってくるようになります。体感的にはN-Gramから出た結果を色濃くする(["content", "content.ngram^3"])と、求めていた結果に近いものが得られるんじゃないかな、という感じです。

お疲れ様でした。