デモ環境でだけ検索できない OpenSearchのDynamic Mappingにハマった話

新田 幸子

はじめに

「OpenSearchがローカル環境では動くのに、デモ環境だけ特定フィールドで検索できない」

こんな不可解な事象に遭遇したことはありませんか?私自身最近そのような事象に遭遇したのですが、原因を追いかけてみると、OpenSearchのマッピングにまつわる落とし穴がありました。この記事では、その調査の経緯と合わせて、OpenSearchのマッピングの仕組みをゼロから解説します。


目次

  1. 事象の概要
  2. OpenSearchのマッピングとは?
  3. フィールド型の違い:keyword vs text
  4. Dynamic Mappingの仕組み
  5. 原因の特定:なぜデモ環境だけ失敗したのか
  6. 解決策と再発防止策
  7. まとめ

事象の概要

今回起きた問題を整理するとこうなります。

  • ローカル環境:特定フィールドでの検索が正常に動作する
  • デモ環境:同じクエリを投げても検索結果が0件になる(エラーにはならない)

アプリケーション側のコードは同一。インフラの設定も同じはず。なのになぜ?

調査の結果、原因は 「デモ環境でのみインデックスのマッピング更新に失敗しており、Dynamic Field Mappingが適用されていた」 ことでした。

本来 keyword 型であるべきフィールドが text 型として登録されており、アプリ側の keyword を前提とした検索クエリが正しく機能しなかったのです。


OpenSearchのマッピングとは?

OpenSearch(およびElasticsearch)においてマッピングとは、インデックス内のフィールドがどのような型として扱われるかを定義したスキーマのことです。

RDBMSで言えば、テーブルのカラム定義に相当します。

PUT /my-index
{
  "mappings": {
    "properties": {
      "title":      { "type": "text" },
      "item_code":  { "type": "keyword" },
      "created_at": { "type": "date" },
      "score":      { "type": "integer" }
    }
  }
}

マッピングを正しく定義しておくことで、OpenSearchはデータを適切な形式で保存・インデックス化し、効率的な検索が可能になります。

マッピングの主な型

用途
text 全文検索用。アナライザによってトークン分割される
keyword 完全一致・集計・ソート用。分割されない
date 日付・日時
integer / long / float 数値
boolean 真偽値
nested オブジェクトの配列

フィールド型の違い:keyword vs text

今回の事象の核心は keywordtext の違い です。この2つは一見どちらも「文字列」ですが、内部の扱いが大きく異なります。

text 型:全文検索のために分割される

text 型のフィールドは、インデックス登録時にアナライザによってトークン(単語)に分割されます。標準アナライザはハイフンや空白を区切り文字として認識します。

例:"0000-11111-2222" のようなハイフン区切りのコード値を text 型で保存すると…

"0000-11111-2222"
  → ["0000", "11111", "2222"]  ← ハイフンで分割・小文字化される

"0000" という部分文字列では検索できますが、"0000-11111-2222" という値そのままでは完全一致しません。 集計(aggregation)にも使えません。

keyword 型:そのまま保存される

keyword 型は分割も変換も行わず、値をそのまま1つのトークンとして保存します。

"0000-11111-2222"
  → ["0000-11111-2222"]  ← そのまま

こちらは完全一致検索・ソート・集計に適しています。今回のようなハイフン区切りのコード値やIDなど、完全一致で扱いたいフィールドに使います。

アプリ側が keyword を期待しているとどうなるか

アプリケーション側で次のような term クエリを投げているとします。

{
  "query": {
    "term": {
      "item_code": "0000-11111-2222"
    }
  }
}

term クエリはアナライザを通さない完全一致クエリです。keyword 型のフィールドであれば "0000-11111-2222" がそのままトークンとして存在するのでヒットします。しかし text 型になっていると、インデックス上のトークンは ["0000", "11111", "2222"] に分割されており、"0000-11111-2222" というトークンは存在しません。結果、エラーにはならず、ただ0件が返ってくるという挙動になります。

これがまさに今回の事象の正体でした。


Dynamic Mappingの仕組み

では、なぜフィールド型が意図しない text になってしまったのでしょうか。ここで登場するのが Dynamic Mapping です。

Dynamic Mappingとは

OpenSearchでは、マッピングを事前に定義していない状態でドキュメントを登録しようとすると、OpenSearchが自動的にフィールドの型を推測してマッピングを作成します。これが Dynamic Mapping です。

// マッピング未定義のインデックスにドキュメントを投入すると…
PUT /my-index/_doc/1
{
  "item_code": "0000-11111-2222",
  "score": 42
}

// OpenSearchが自動的に以下のようなマッピングを作成する
// "item_code" → text + keyword (multi-field)
// "score"     → long

Dynamic Mappingの型推測ルール

データ 推測される型
"hello" のような文字列 text(+ keyword のサブフィールド)
42 のような整数 long
3.14 のような小数 float
true / false boolean
"2024-01-01" のような日付形式文字列 date

文字列の場合、デフォルトでは text 型として登録され、さらに .keyword というサブフィールドも自動生成されます。

"item_code": {
  "type": "text",
  "fields": {
    "keyword": {
      "type": "keyword",
      "ignore_above": 256
    }
  }
}

Dynamic Mappingが意図しない型を生む場面

問題になるのは、アプリ側が item_code というフィールドをそのまま keyword として検索しているのに、Dynamic Mapping によって text 型(with .keyword サブフィールド)として登録されてしまった場合です。

term クエリで "item_code": "0000-11111-2222" と検索しても、text 型のフィールドには完全一致しません。本来は item_code.keyword で検索する必要があるのですが、アプリ側はそれを期待していない——ここでミスマッチが発生します。


原因の特定:なぜデモ環境だけ失敗したのか

今回の根本原因は 「デモ環境でのみ、インデックス作成時のマッピング定義の適用(PUT mappings)に失敗していた」 ことです。

推測される経緯

  1. デモ環境でインデックスが作成された
  2. 何らかの原因でマッピングの更新処理が失敗した(権限エラー、タイミング問題、設定ミスなど)
  3. マッピングが未定義のままドキュメントが投入された
  4. Dynamic Mapping によってフィールド型が自動推測された
  5. item_code などの文字列フィールドが text 型として登録された
  6. アプリ側の term クエリが機能しなくなった

ローカル環境ではマッピング定義が正しく適用されていたため、同じコードでも動作に差が出たのです。

確認方法:現在のマッピングを見る

インデックスのマッピングは以下のAPIで確認できます。

GET /my-index/_mapping

レスポンス例(意図した状態):

{
  "my-index": {
    "mappings": {
      "properties": {
        "item_code": {
          "type": "keyword"   ← これが正しい
        }
      }
    }
  }
}

Dynamic Mapping が適用されてしまった状態:

{
  "my-index": {
    "mappings": {
      "properties": {
        "item_code": {
          "type": "text",     ← text になってしまっている
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}

解決策と再発防止策

今回の対応:根本原因を修正してインデックスを再作成

今回は次の手順で解決しました。

  1. マッピング更新処理が失敗していたコードのバグを修正する
  2. 既存のインデックスを全削除する
  3. 正しいマッピングが適用された状態でインデックスを再作成し、ドキュメントを再投入する

デモ環境はデータを消しても問題がなかったため、このシンプルな方法が使えました。

# インデックスの全削除
DELETE /my-index

# 正しいマッピングで再作成(アプリの起動処理などで自動実行されるのが理想)
PUT /my-index
{
  "mappings": {
    "properties": {
      "item_code": { "type": "keyword" },
      "title":     { "type": "text" }
    }
  }
}

本番環境でデータを消せない場合:Reindex API

本番環境など既存データを維持したまま対応する場合は、Reindex API を使います。新しいマッピングで別インデックスを作り、データを移行したうえでエイリアスを切り替える方法です。

# 1. 正しいマッピングで新インデックスを作成
PUT /my-index-v2
{
  "mappings": {
    "properties": {
      "item_code": { "type": "keyword" },
      "title":     { "type": "text" }
    }
  }
}

# 2. Reindex APIでデータを移行
POST /_reindex
{
  "source": { "index": "my-index" },
  "dest":   { "index": "my-index-v2" }
}

# 3. エイリアスを切り替えてアプリ側への影響をゼロにする
POST /_aliases
{
  "actions": [
    { "remove": { "index": "my-index",    "alias": "search-alias" } },
    { "add":    { "index": "my-index-v2", "alias": "search-alias" } }
  ]
}

データ量が多い場合はReindexに時間がかかるため、切り替え中の二重書き込み戦略なども検討が必要になります。

Dynamic Mappingの無効化は本当に有効か?

「そもそもDynamic Mappingを無効にすれば今回のような問題は起きないのでは?」という考え方もあります。dynamic の設定でその挙動を制御できます。

挙動
true(デフォルト) 未知のフィールドを自動マッピング
false 未知のフィールドを無視(インデックス化しない)
strict 未知のフィールドがあればエラーを返す

一見 strict にしておけば意図しないマッピングが生まれず安全に思えます。しかし実際には 「マッピングに定義されていないフィールドを含むドキュメントが来た瞬間にドキュメント投入がエラーになる」 という副作用があります。

アプリ側のコード変更でフィールドが追加されたとき、マッピングの更新を忘れているとドキュメントが一切登録できなくなる——つまり検索以前に、インデックスへの書き込み自体が止まるリスクがあります。false も同様で、フィールドが静かに無視されてデータが欠損するという別の危険があります。

Dynamic Mappingの無効化はスキーマ管理を厳密に運用できるチームには有効な選択肢ですが、「とりあえず strict にしておけば安心」とは言い切れないことは意識しておく必要があります。

今回の事象に対する本質的な再発防止策は、マッピング更新処理の信頼性を高めること(冪等に実行できるようにする、適用後に検証する、など)と言えるでしょう。


まとめ

今回の事象と教訓を整理します。

ポイント 内容
事象 デモ環境のみ特定フィールドで検索不可
根本原因 マッピング更新処理のバグ → Dynamic Mappingが適用された
技術的な原因 keyword 型であるべきフィールドが text 型になり、term クエリが機能しなかった
今回の解決策 バグ修正 → インデックス全削除 → 再作成・再投入
本番での対応 Reindex APIで新インデックスへ移行しエイリアスを切り替え
予防策 マッピング更新処理の冪等性確保・適用後の検証

OpenSearchのマッピングは「設定したから大丈夫」ではなく、適用されているかを確認する習慣が大切です。特に環境ごとに構築手順が微妙に異なるケースでは、今回のような差異が生まれやすいので注意しましょう。

この記事が同じ落とし穴にはまっている方の助けになれば幸いです。

参考ドキュメント

新田 幸子/ Drupalエンジニア

北海道在住のDrupalエンジニアです。バックエンドを主に担当しています。三度の飯よりAIが好きです。趣味はドライブ、猫と遊ぶことです。

新田 幸子 の書いた記事一覧

最新の関連記事

Download 資料ダウンロード

Drupalでの開発・運用、サーバー構築、Webサイト構築全般、制作費用などに関してお気軽にご相談ください。


Contact お問い合わせ

Drupalでの開発・運用、サーバー構築、Webサイト構築全般についてお気軽にご相談ください。専門スタッフによるDrupal無料相談も行なっております。