概要
以前Prefix Query の注意 - Carpe Diemで述べたように、PrefixQueryはsearch queryがnot_analyzed
になるので意図しない結果になることがあります。
一方で前方一致は検索の利便性を向上させる上でメリットが大きいので、入れておきたい要素でもあります。
今回は前方一致を考える上で何が良いかを調べてみました。
環境
- Elasticsearch 5.2.1
Prefix Query
メリット
- 文字通り前方一致のクエリであることが分かる
デメリット
- search analyzerは指定できないので、index時にnormalizeで小文字などにしてしまうと、大文字による検索でヒットしない
- 内部的にはregexpによる一致なので短いと重い
特に後者は短い文字でも表示する場合だと非常に無駄が多く、
ref:Elasticsearch Queries, or Term Queries are Really Fast! | Elastic
この表を見て分かるように4文字→5文字に変わるだけで倍の速度になりますし、3文字以下だと非常に遅いことがわかります。
Regexp Query
次にぱっと浮かぶのはRegex Queryでしょうか。
メリット
- 正規表現で前方一致を表現できる(
abc.*
など)
デメリット
- Prefix Queryで説明したように非常に重いです。
Edge ngram
他に浮かぶのはEdge ngramではないでしょうか。
メリット
- 必ず前方からngramしていくので前方一致として使える
デメリット
- ngramなので検索結果にノイズが入りやすい
Edge ngramのノイズ
どういったノイズが出るかですが、例えばquick brown
で登録すると
q qu qui quic quick b br bro brow brown
と登録されます。この時にbaby
などで検索すると、
b ba bab baby
と同じように分解されるのですが、この時にb
が一致してしまうため、スコアは低いですが結果に混ざってしまいます。
検証
実際にElasticsearchを使って検証してみます。
analyzer
まずはanalyzerを登録します。普通のedge_ngram
です。
$ curl -XPUT 'localhost:9200/my_index' -d ' { "settings": { "analysis": { "filter": { "autocomplete_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } }, "analyzer": { "autocomplete": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "autocomplete_filter" ] } } } } } '
mapping
次に検索に使うフィールドにanalyzerを適用します。
$ curl -XPUT 'localhost:9200/my_index/_mapping/my_type' -d ' { "my_type": { "properties": { "name": { "type": "string", "analyzer": "autocomplete" } } } } '
データ投入
データを投入します。
$ curl -XPOST 'localhost:9200/my_index/my_type/_bulk' -d ' { "index": { "_id": 1 }} { "name": "Brown foxes" } { "index": { "_id": 2 }} { "name": "Yellow furballs" } '
検索クエリ
$ curl 'localhost:9200/my_index/my_type/_search?pretty' -d ' { "query": { "match": { "name": "brown fo" } } } '
結果
{ "took" : 25, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 2, "max_score" : 1.072439, "hits" : [ { "_index" : "my_index", "_type" : "my_type", "_id" : "1", "_score" : 1.072439, "_source" : { "name" : "Brown foxes" } }, { "_index" : "my_index", "_type" : "my_type", "_id" : "2", "_score" : 0.43214047, "_source" : { "name" : "Yellow furballs" } } ] } }
Yellow furballs
の方も混ざっています。これはfo
の方のf
があるせいです。
確認のためクエリのexplain
クエリをexplainしてみると
$ curl 'localhost:9200/my_index/my_type/_validate/query?explain' -d ' { "query": { "match": { "name": "brown fo" } } } '
以下のようにf
があることが分かります。
(name:b name:br name:bro name:brow name:brown) (name:f name:fo)
確認のためスコアのexplain
スコアのexplainをしてみると
$ curl 'localhost:9200/my_index/my_type/_search?pretty' -d ' { "explain": true, "query": { "match": { "name": "brown fo" } } } '
結果
{ "_shard" : 0, "_node" : "-IxQc6BlRHKC2ZOQ7gAFYw", "_index" : "my_index", "_type" : "my_type", "_id" : "2", "_score" : 0.043822706, "_source" : { "name" : "Yellow furballs" }, "_explanation" : { "value" : 0.043822702, "description" : "sum of:", "details" : [ { "value" : 0.043822702, "description" : "product of:", "details" : [ { "value" : 0.087645404, "description" : "sum of:", "details" : [ { "value" : 0.087645404, "description" : "sum of:", "details" : [ { "value" : 0.087645404, "description" : "weight(name:f in 1) [PerFieldSimilarity], result of:",
と、f
によるスコアが出ています。
解決方法
ではこの問題をどう解決するかというと、searchのときのanalyzerにはedge ngramを使わなければいいのです。
検索クエリ
analyzerをstandard
にしてみます。
$ curl 'localhost:9200/my_index/my_type/_search?pretty' -d ' { "query": { "match": { "name": { "query": "brown fo", "analyzer": "standard" } } } } '
結果
{ "took" : 10, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 1, "max_score" : 0.8271048, "hits" : [ { "_index" : "my_index", "_type" : "my_type", "_id" : "1", "_score" : 0.8271048, "_source" : { "name" : "Brown foxes" } } ] } }
Brown foxes
だけ反応するようになりました。
まとめ
前方一致の場合はIndex AnalyzerのみEdge ngramにし、検索時は通常のanalyzerにすることが最も効率よくできそうです。