はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Suggestersのシンタックス雰囲気まとめ (Elasticsearch)

はじめに

Elasticsearchには、Suggestersといういわゆる検索BOXのオートコンプリート等に使える問い合わせのAPIがあるようです。

先人が様々なカタチで利用方法やTIPS等をまとめていただいています。 

ただし、Suggerstersに限らないものの、製品がどんどん発展的にバージョンアップしているので、記事の当時と、問い合わせの考え方は変わっていないものの、クエリのシンタックスが変わっているものもいくつかあるように感じました。

いや変わっていないのかもしれませんが、公式リファレンス等、シンタックスとしての「suggest」や変数としての「xxx-suggest」など、「suggest」という文字がクエリ内に頻出するので*1、ひとまずコピペで試してみたいという時に個人的に混乱したので、今回、ひとまず現行バージョンのElasticsearchの公式リファレンスを元ネタに、サジェストのクエリや前提となるマッピングの関係を一枚絵にまとめてみることにしました。

対象のElasticsearchのバージョンは6.xです。

公式リファレンスのページはこちら。*2

www.elastic.co

Elasticsearch6.xのSuggestersのクエリのシンタックス

利用できるクエリは4つあります。

また、エンドポイントに「?typed_keys」をつけて問い合わせすると、検索結果にこの4つのクエリのどのタイプかがわかりやすくなる目印がつく"Returning the type of the suggester"という仕組みがあるのですがここでは省略します。

  1. Term suggester: ワードの誤記に絞って行う「もしかして」。レーベンシュタイン距離。なお、複数ワードは別々に問い合わせされる。
  2. Phrase Suggester: 入力された複数ワードをフレーズとみなして(≒並びを意識して)サジェスト。
  3. Completion Suggester: いわゆるオートコンプリート。他のものよりレスポンス重視。
  4. Context Suggester: 同じ検索語でも何を探しているかはコンテキストによるよねということで、あるカテゴリーのコンテキストの場合は、この系列のワードをサジェストする...というのが制御できるサジェスト。

私は、1はterm系、2はphrase系、3、4はcompletion系と覚えることにしました。

また、公式リファレンスをまとめたと言いながらやや私の視点での「こんな用途だよね」の言い回しに寄せていますので、(本件に限らずですが)正式なところは公式リファレンスをご参照ください。

それぞれ、インデックスへの格納の仕方等にも条件があります。 公式リファレンスの並びのように、1つずつ理解しようとすると、私はなかなか覚えられない予感がしたので、4つならべて確認することにしました。

ということでまとめたのが次↓の一枚絵です。

f:id:azotar:20181202092338p:plain
Suggerstersのシンタックスのイメージ: 【注】クリックして拡大してください

公式リファレンスを参考にしたということもありますが、ここではシンタックスを俯瞰することを目的に、上記は英単語の例に絞っています。 日本語で同じことをしようとするともう少し工夫が必要なようです。

以下、上記の図の補足です。

Term suggester

インデックスされているテキストに対して、数文字のみ誤記の入っている検索語に対するサジェストを取得しました。 クエリでは、text、termプロパティを指定します。suggest_mode等のオプションも指定できますが、ここでは省略しています。

Phrase Suggester

インデックスされているテキストに対して、数文字のみ誤記の入っている&単語の並びはおおよそそのままの検索語に対するサジェストを取得しました。 クエリでは、text、phraseを指定します。gram_sizeなどフレーズ検索を考慮した調整パラメータも指定できますね。

マッピングでのanalyzerの指定も重要です。

Completion Suggester

前提として、mappingでcompletionをtype指定するのと、インデックスには「input」というプロパティでサジェスト取得元の単語を格納します。 これに対して、suggestクエリで、prefixで検索語を、completionでCompletion Suggesterであること自体を示すのと対象フィールドを指定して、オートコンプリート用の候補を得ています。

Context Suggester

suggestクエリで、prefixで検索語を、completionでCompletion Suggesterであること自体を示すのと対象フィールドを指定して、サジェストを取得します。 ここで、contextsというプロパティで、(この例では)"place_type""cafe"であるような、コンテキストを指定しています。 これにより、"cafe"に該当する、"timmy's"が候補として得られているのですが、そもそも、インデックスには、"timmi'sがplace_typeがcafeおよびfoodであること"「input」「contexts」というプロパティで設定して格納してありますし、それができるように、mappingでtype:completion、contexts(のnameがplace_typeですよ、typeはcategoryですよ)といったことが規定されていることに注意ください。

category以外に、geoというのも指定できます。 そもそも上記の図では、サジェストの元ネタが1レコード、1コンテキストしか無いシンプルな例だったのですが、公式リファレンスではもう少し凝った例がありますので、この先は公式リファレンスを参照ください。

グローバルサジェスト text

シンタックスという意味では、こんな感じ↓で"text"フィールドを指定できます。 f:id:azotar:20181202123319p:plain

クエリのテキスト

前項の図の元ネタのクエリーのテキストを貼り付けます。 (ほぼ図にキャプチャしているとおりですが、kibanaのDevToolsで試し打ちした結果で一部変わっている部分もあります。)

// PUT test3
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "analysis": {
        "analyzer": {
          "trigram": {
            "type": "custom",
            "tokenizer": "standard",
            "filter": [
              "shingle"
            ]
          },
          "reverse": {
            "type": "custom",
            "tokenizer": "standard",
            "filter": [
              "reverse"
            ]
          }
        },
        "filter": {
          "shingle": {
            "type": "shingle",
            "min_shingle_size": 2,
            "max_shingle_size": 3
          }
        }
      }
    }
  },
  "mappings": {
    "_doc": {
      "properties": {
        "xxx_1_xxx": {
          "type": "keyword"
        },
        "xxx_2_xxx": {
          "type": "text",
          "fields": {
            "trigram": {
              "type": "text",
              "analyzer": "trigram"
            },
            "reverse": {
              "type": "text",
              "analyzer": "reverse"
            }
          }
        },
        "xxx_3_xxx": {
          "type": "completion"
        },
        "xxx_4_xxx": {
          "type": "completion",
          "contexts": [
            {
              "name": "place_type",
              "type": "category"
            }
          ]
        }
      }
    }
  }
}


// POST test3/_doc?refresh=true
{
  "xxx_1_xxx": "noble warriors",
  "xxx_2_xxx": "noble warriors",
  "xxx_3_xxx": {
    "input": [
      "Nevermind",
      "Nirvana"
    ],
    "weight": 34
  },
  "xxx_4_xxx": {
    "input": [
      "timmy's",
      "starbucks",
      "dunkin donuts"
    ],
    "contexts": {
      "place_type": [
        "cafe",
        "food"
      ]
    }
  }
} 



// POST test3/_search?pretty&typed_keys
{
  "suggest": {
    
    "EX1": {
       "text": "noble worriors",
       "term": {
          "field": "xxx_1_xxx"
      }
    },
    "EX2": {
      "text": "noble worriors",
      "phrase": {
        "field": "xxx_2_xxx.trigram",
        "size": 1,
        "gram_size": 3,
        "direct_generator": [
          {
            "field": "xxx_2_xxx.trigram",
            "suggest_mode": "always"
          }
        ]
      }
    },
    "EX3": {
      "prefix": "nir",
      "completion": {
        "field": "xxx_3_xxx"
      }
    },
    "EX4": {
      "prefix": "tim",
      "completion": {
        "field": "xxx_4_xxx",
        "size": 10,
        "contexts": {
          "place_type": [
            "cafe"
          ]
        }
      }
    }
  }
}


*1:そういえば、エンドポイントが「_suggest」から「_search」に集約される方向なのはありがたいですね。

*2:私の記事はふんわりな内容でおおきくバージョンによっては変わらなそうな内容が中心なので、通常currentにリンクするようにしていますが、今回は6.5のものにリンクしました。