🔍

【ElasticSearch】デフォルトでINSERTしたドキュメントに対してtermクエリで完全一致検索する【keyword型の明示】

2024/10/21に公開

ElasticSearchで、このようなドキュメントが格納されているとします。

{
  "id": "aaa",
  "foods": {
    "name": "orange-002",
    "type": "fruits"
  },
  {
    "name": "cabbage-001",
    "type": "vegetable"
  },
},
{
  "id": "bbb",
  "foods": {
    "name": "apple-005",
    "type": "fruits"
  }
}

実現したいこと

foods.nameorange-002のデータを検索したいです。
さらに matchクエリによる全文検索ではなく、termクエリによる完全一致検索を要件とします。

まずは答え

このようなクエリで検索可能です。

GET /_search
{
  "query": {
    "term": {
      "foods.name.keyword": "orange-002"
    }
  }
}

ポイントはfoods.name.keywordというフィールド名の指定です。

うまくできなかった検索クエリ

私は実装当初、このようなクエリを書いていました。

GET /_search
{
  "query": {
    "term": {
      "foods.name": "orange-002"
    }
  }
}

ところが検索結果は0件。
しかし、こちらのtermmatchにするとヒット。

GET /_search
{
  "query": {
    "match": {
      "foods.name": "orange-002"
    }
  }
}

matchクエリだとヒットするのにtermクエリだとヒットしないのはなぜだろう?
調べてみると、フィールドのマッピング・型が原因でした。

必要な前提知識

Elasticsearchのフィールドタイプ

Elasticsearchでは、ドキュメントのフィールド(プロパティ)にはいくつかの異なるタイプ = 型があり、それによってデータのインデックス方法や検索方法が変わります。
その中で、よく使われるのがtextkeywordという2つのフィールドタイプです。

text フィールド

textフィールドは、全文検索を目的としています。データはトークン化され、通常は形態素解析やステミングなどの処理を通じて検索可能な単語やフレーズに分解されます。
例えば、「This is a big apple」というフレーズが text フィールドにインデックスされる場合、これがトークン化されて、["this", "is", "a", "big", "apple"] のように複数の単語に分解されます。

このようなフィールドでは、部分一致や関連度の高いドキュメントを検索するために match クエリがよく使用されます。match クエリでは、トークン化された単語やフレーズに基づいて検索されるため、完全一致ではなく、近似的な一致が行われます。

keyword フィールド

一方、keywordフィールドは、文字列全体をそのまま扱います。トークン化されることなく、フィールドにインデックスされたデータがそのまま保持され、完全一致の検索に使用されます。このフィールドは、識別子やタグ、ID、カテゴリなど、解析が不要なデータ に適しています。

例えば、「This is a big apple」というフレーズが keyword フィールドにインデックスされる場合、フレーズ全体が1つのエンティティとして扱われ、部分一致は行われません。

text と keyword フィールドの違い

つまり、 text と keyword の主な違いをまとめるとこのようになります。

トークン化 適している検索 適しているデータ
text される 部分一致・関連性の検索(matchクエリ) タイトル、文書
keyword されない 完全一致(termクエリ) 識別子、カテゴリ

Elasticsearch のデフォルト動作

Elasticsearch では、ドキュメントをインデックスに挿入すると、デフォルトで text と keyword の両方のフィールドが自動的に作成されます。これにより、同じフィールドに対しても、全文検索と完全一致検索の両方を柔軟に使い分けることが可能です。

例えば、次のようにフィールドが定義されます。

{
  "properties": {
    "title": {
      "type": "text",
      "fields": {
        "keyword": {
          "type": "keyword"
        }
      }
    }
  }
}

ここでは、titleフィールドが text 型として定義されており、そのサブフィールドとしてtitle.keywordが keyword 型として定義されています。これにより、titleに対して全文検索を行うことも、title.keywordに対して完全一致の検索を行うことも可能になります。

うまくできなかった原因

termクエリをtext型のフィールドに対して使っていたことが原因でした。
トークン化されたデータに対し、完全一致検索をしても期待通りの結果は得られません。

    "term": {
      "foods.name": "orange-002" // トークン化されたデータに対してtermクエリはうまくいかない!
    }

解決策の詳細

正しいクエリのポイントはkeyword型を明示することです。
特定のフィールドに対して、keyword 型を指定することで、トークン化されたtextではなく文字列全体をそのまま検索対象にすることができます。

GET /_search
{
  "query": {
    "term": {
      "foods.name.keyword": "orange-002"
    }
  }
}

このクエリでは、foods.name.keywordに対して term クエリが使われており、トークン化されていないkeyword型のフィールドを対象にすることで完全一致検索が可能になっています。

まとめ

フィールドの型に適した検索方法を選ぶ

  • matchクエリは部分一致、termクエリは完全一致であること。
  • 部分一致にはtext型、完全一致にはkeyword型が適していること。

まずはこれらを覚えておくことが大切だということが分かりました。

ElasticSearchの仕様・デフォルト挙動・クエリの特性を理解する

私がこれまで触れることが多かったのはRDBやDynamoDB等のよくある一般的なデータベースでした。
ElasticSearchはデータベースとは全く異なる仕組みや挙動があります。
データを保存・取得するという点では近しいですが、それ故の先入観に気をつけて、全く異なるものを対象としていることを踏まえて理解・調査していく必要性を感じています。

GitHubで編集を提案
Fusic 技術ブログ

Discussion