CLOVER🍀

That was when it all began.

ElasticsearchのSuggesterで、サジェストを試してみる

ファセットに続いて、サジェストをElasticsearchで試してみようと思います。

Elasticsearchでは、Suggesterというものを使うことになるみたいです。

Suggesters | Elasticsearch Reference [6.4] | Elastic

参考になりそうなエントリも、ちらほらと。

RailsでElasticsearch: サジェスト (Suggest) 機能でオートコンプリート - Rails Webook

Elasticsearchで、漢字データでも平仮名でサジェスト取得

elasticsearchのCompletion Suggesterをさわる

Elasticsearchで日本語人名検索を実装した時のまとめ - 炎と硝煙にむせる開発現場から

Elasticsearchのサジェスト機能を使った話

Suggesterのうち、Completion Suggesterというものがサジェストとしてはよく使われているようです。

こちらを含めて、いくつかSuggersterを使ってみましょう。

準備

インデックス/マッピングの定義は、以下のようにしています。

{
  "settings": {
    "index": {
      "number_of_shards" : 5,
      "number_of_replicas" : 1,
      "analysis": {
        "tokenizer": {
          "kuromoji_tokenizer_search": {
            "type": "kuromoji_tokenizer",
            "mode": "search",
            "discard_punctuation" : "true",
            "user_dictionary" : "userdict_ja.txt"
          }
        },
        "filter": {
          "kuromoji_no_romaji_readingform": {
            "type": "kuromoji_readingform",
            "use_romaji": false
          },
          "kana_filter": {
            "type" : "icu_transform",
            "id": "Katakana-Hiragana"
          }
        },
        "analyzer": {
          "kuromoji_analyzer": {
            "type": "custom",
            "tokenizer": "kuromoji_tokenizer_search",
            "filter": ["kuromoji_baseform",
                       "kuromoji_part_of_speech",
                       "cjk_width",
                       "stop",
                       "ja_stop",
                       "kuromoji_stemmer",
                       "lowercase"]
          },
          "kuromoji_reading_analyzer": {
            "type": "custom",
            "tokenizer": "kuromoji_tokenizer_search",
            "filter": ["kuromoji_baseform",
                       "kuromoji_part_of_speech",
                       "cjk_width",
                       "stop",
                       "ja_stop",
                       "kuromoji_stemmer",
                       "kuromoji_no_romaji_readingform",
                       "kana_filter",
                       "lowercase"]
          }
        }
      }
    }
  },
  "mappings": {
    "mytype": {
      "_source": { "enabled": true },
      "_all": { "enabled": true },
      "properties": {
        "content": {
          "type": "string",
          "store": "yes",
          "index": "not_analyzed",
          "copy_to": [ "content_ja", "content_completion_ja", "content_reading_ja", "content_reading_completion_ja" ]
        },
        "content_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" },
        "content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" },
        "content_reading_ja": {
          "type": "string",
          "store": "yes",
          "index": "analyzed",
          "analyzer": "kuromoji_reading_analyzer",
          "search_analyzer": "simple"
        },
        "content_reading_completion_ja": {
          "type": "completion",
          "analyzer": "kuromoji_reading_analyzer",
          "search_analyzer": "simple"
        }
      }
    }
  }
}

いくつかサジェスト用にフィールドを用意していますが、そちらはcopy_toでまずは値を入れるようにしています。

フィールドは、それぞれ

  • content … 登録した値をそのまま登録
  • content_ja … 形態素解析した結果を登録
  • content_completion_ja … 形態素解析した結果を登録するが、Completion Suggester用に使う
  • content_reading_ja … 登録した値の読み仮名を登録
  • content_reading_completion_ja … 登録した値の読み仮名を登録するが、Completion Suggester用に使う

という使い方をします。

なお、ICUのフィルターを使っているので、事前にICU Analysis Pluginをインストールしておきます。今回は、カタカナ→ひらがな変換に使用しています。

# sudo -u elasticsearch /usr/share/elasticsearch/bin/plugin install analysis-icu

こういうデータを放り込みます。

[
  {
    "content": "吾輩は猫である"
  },
  {
    "content": "我が名は青春のエッセイドラゴン"
  },
  {
    "content": "下町ロケット"
  },
  {
    "content": "北斗の拳"
  },
  {
    "content": "進撃の巨人"
  }
]

なんでこのデータかなのですが、過去にApache Solrで書いたこちらのエントリのElasticsearch版として書いています。

Apache Solr 5.xのSuggesterを使って、サジェストとDid You Mean? - CLOVER

では、ここから試していってみます。

Term Suggester

最初は、Term Suggester。

Term suggester | Elasticsearch Reference [6.4] | Elastic

単語(term)と編集距離(edit distance)による、シンプルなSuggesterです。

こんな感じで、微妙に誤字ったクエリを用意。

{
  "suggest": {
    "my-suggestion-1": {
      "text": "ドラコン",
      "term": {
        "field": "content_ja",
        "size": 10
      }
    },
    "my-suggestion-2": {
      "text": "ろけって",
      "term": {
        "field": "content_reading_ja",
        "size": 10
      }
    }
  }
}

この形式の場合は、Search APIを使って実行します。

$ curl -XGET 'http://localhost:9200/myindex/mytype/_search?pretty' -d @[クエリを書いたファイル]

結果。

{
  "took" : 698,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 5,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "mytype",
      "_id" : "AVLaDrunMoVksaujBZ9v",
      "_score" : 1.0,
      "_source" : {
        "content" : "吾輩は猫である"
      }
    }, {

〜省略〜

  "suggest" : {
    "my-suggestion-2" : [ {
      "text" : "ろけって",
      "offset" : 0,
      "length" : 4,
      "options" : [ {
        "text" : "ろけっと",
        "score" : 0.75,
        "freq" : 1
      } ]
    } ],
    "my-suggestion-1" : [ {
      "text" : "ドラコン",
      "offset" : 0,
      "length" : 4,
      "options" : [ {
        "text" : "ドラゴン",
        "score" : 0.75,
        "freq" : 1
      } ]
    } ]
  }
}

正しい単語になって戻って来ていますね。

また、以下のような形式にした場合は、Suggest APIを使います。

{
  "my-suggestion-1": {
    "text": "ドラコン",
    "term": {
      "field": "content_ja",
      "size": 10
    }
  },
  "my-suggestion-2": {
    "text": "ろけって",
    "term": {
      "field": "content_reading_ja",
      "size": 10
    }
  }
}

こういうコマンドになります。typeはいらないみたいです。

$ curl -XGET 'http://localhost:9200/myindex/_suggest?pretty' -d @[クエリを書いたファイル]

結果。

{
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "my-suggestion-2" : [ {
    "text" : "ろけって",
    "offset" : 0,
    "length" : 4,
    "options" : [ {
      "text" : "ろけっと",
      "score" : 0.75,
      "freq" : 1
    } ]
  } ],
  "my-suggestion-1" : [ {
    "text" : "ドラコン",
    "offset" : 0,
    "length" : 4,
    "options" : [ {
      "text" : "ドラゴン",
      "score" : 0.75,
      "freq" : 1
    } ]
  } ]
}

こちらの場合は、検索結果がレスポンスに含まれません。

Search APIを使う場合でも、sizeを0にしたり

{
  "size": 0,
  "suggest": {
    "my-suggestion-1": {
      "text": "ドラコン",
      "term": {
        "field": "content_ja",
        "size": 10
      }
    },
    "my-suggestion-2": {
      "text": "ろけって",
      "term": {
        "field": "content_reading_ja",
        "size": 10
      }
    }
  }
}

search_type=countを使えばよさそう?

$ curl -XGET 'http://localhost:9200/myindex/mytype/_search?pretty&search_type=count'

Completion Suggester

続いて、Completion Suggesterです。前方一致によるSuggesterだそうな。

Completion Suggester | Elasticsearch Reference [6.4] | Elastic

こちらを使う場合は、準備としてフィールドのtypeに「completion」としておく必要があるようです。

先ほどのマッピングで示したCompletion Suggester用に用意したフィールドのtypeも、「completion」としてあります。

        "content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" },

        "content_reading_completion_ja": {
          "type": "completion",
          "analyzer": "kuromoji_reading_analyzer",
          "search_analyzer": "simple"
        }

で、実行するクエリとしてはこんなのを用意。

{
  "my-suggest-1": {
    "text": "我",
    "completion": {
      "field": "content_completion_ja"
    }
  },
  "my-suggest-2": {
    "text": "吾",
    "completion": {
      "field": "content_completion_ja"
    }
  },
  "my-suggest-3": {
    "text": "下",
    "completion": {
      "field": "content_completion_ja"
    }
  },
  "my-suggest-4": {
    "text": "わが",
    "completion": {
      "field": "content_reading_completion_ja"
    }
  }
}

普通に形態素解析した単語と読みに対して、前方一致でヒットさせることを狙います。

結果。

{
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "my-suggest-1" : [ {
    "text" : "我",
    "offset" : 0,
    "length" : 1,
    "options" : [ {
      "text" : "我が名は青春のエッセイドラゴン",
      "score" : 1.0
    } ]
  } ],
  "my-suggest-4" : [ {
    "text" : "わが",
    "offset" : 0,
    "length" : 2,
    "options" : [ {
      "text" : "吾輩は猫である",
      "score" : 1.0
    }, {
      "text" : "我が名は青春のエッセイドラゴン",
      "score" : 1.0
    } ]
  } ],
  "my-suggest-2" : [ {
    "text" : "吾",
    "offset" : 0,
    "length" : 1,
    "options" : [ {
      "text" : "吾輩は猫である",
      "score" : 1.0
    } ]
  } ],
  "my-suggest-3" : [ {
    "text" : "下",
    "offset" : 0,
    "length" : 1,
    "options" : [ {
      "text" : "下町ロケット",
      "score" : 1.0
    } ]
  } ]
}

うまくいってそうな感じですね。

ここで、ちょっとマッピングの定義を変更。

  "mappings": {
    "mytype": {
      "_source": { "enabled": true },
      "_all": { "enabled": true },
      "properties": {
        "content": {
          "type": "string",
          "store": "yes",
          "index": "not_analyzed",
          "copy_to": [ "content_ja", "content_reading_ja", "content_reading_completion_ja" ]
        },
        "content_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" },
        "content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" },
        "content_reading_ja": {
          "type": "string",
          "store": "yes",
          "index": "analyzed",
          "analyzer": "kuromoji_reading_analyzer",
          "search_analyzer": "simple"
        },
        "content_reading_completion_ja": {
          "type": "completion",
          "analyzer": "kuromoji_reading_analyzer",
          "search_analyzer": "simple"
        }
      }
    }
  }
}

「content_completion_ja」フィールドへのcopy_toを解除して、データを登録し直します。

          "copy_to": [ "content_ja", "content_reading_ja", "content_reading_completion_ja" ]

登録するデータ。

[
  {
    "content": "吾輩は猫である",
    "content_completion_ja": {
      "input": "吾輩は猫である",
      "output": "夏目漱石"
    }
  }
]

ちょっと形式が変わりました。inputとoutputを取ることができ、inputは登録する値ですが、サジェスト時に得られる値をoutputで指定することができます。

この状態で、こういうクエリを発行すると。

{
  "my-suggest-1": {
    "text": "吾",
    "completion": {
      "field": "content_completion_ja"
    }
  }
}

「吾」で「夏目漱石」がサジェストされます。

{
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "my-suggest-1" : [ {
    "text" : "吾",
    "offset" : 0,
    "length" : 1,
    "options" : [ {
      "text" : "夏目漱石",
      "score" : 1.0
    } ]
  } ]
}

とりあえず、マッピング定義とデータは戻しておきます。

Phrase Suggester

最後は、Phrase Suggester。

Phrase Suggester | Elasticsearch Reference [6.4] | Elastic

こちらも編集距離を使ったSuggesterのようなのですが、Did You Mean?を実現するのに使われるようです。

なんですが…ちょっと今回はうまく使えませんでした。これ系、Solrの時もブログエントリを書く時には失敗したんですよねぇ…。

どこかでデータをたくさん入れて、試してみますか。

一応、こういうクエリを作って

{
  "size": 0,
  "suggest": {
    "my-suggest-1": {
      "text": "吾輩は犬である",
      "phrase": {
        "field": "content",
        "size": 10
      }
    }
  }
}

Search APIに投げると動くのですが

  "suggest" : {
    "my-suggest-1" : [ {
      "text" : "吾輩は犬である",
      "offset" : 0,
      "length" : 7,
      "options" : [ {
        "text" : "吾輩は猫である",
        "score" : 0.7842417
      } ]
    } ]
  }
}

これだとTerm Suggesterと変わらないですからねぇ…。

まとめ

ElasticsearchのSuggesterを使って、サジェスト機能をちょっと試してみました。

サジェストというものはけっこう悩ましいイメージがあって、Suggesterみたいな機能を使って実現することもあれば、専用のインデックスを定義して作り込むケースもあるかと思います。

…自分は、Solrでは後者を最近やりました。
Apache Solr 5.xで、サジェストを実装することを考える - CLOVER

こういうエントリも参考になりそうですね。
Elasticsearch キーワードサジェスト日本語のための設計 – Hello! Elasticsearch. – Medium