Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

OpenSearchのDynamic mapping仕様検証

この記事はSansan Advent Calendar 2025の17日目の記事です。

こんにちは。
技術本部 Sansan Engineering Unit Infrastructureグループ
プロダクトインフラエンジニアをしている渡邉です。

最近は息子と一緒にウルトラマンオメガにハマっています。
本当なら平成ウルトラマンの歴史を語りたいところですが、
ここはTech Blog……ということで、
今回はOpenSearchのDynamic mappingについて書きます。

1.はじめに

Sansan Engineering Unitでは、各種サーバーやバッチ処理、Lambdaなどから出力されるログを収集し、 その検索・分析基盤の一部としてOpenSearchを利用しています。

Index Templateを定義して型を事前にそろえることはできますが、
テンプレートに定義のないフィールドは、OpenSearchのDynamic mappingによって「よしなに型推論されてドキュメントとして取り込まれる」ことになります。

とても便利なこの仕組みですが、
実際に運用してみると次のような事象に遭遇することがあります。

  • date型のつもりで投入していたフィールドがtextとして扱われ、日付フィルターが効かない
  • 大文字・小文字が厳密に別扱いされてしまい、検索が期待どおりにヒットしない
  • 最初に投入されたログの形式をもとにmappingが確定してしまい、後続のログで型不一致が発生する

そこで今回は、OpenSearchの公式ドキュメントに記載されているDynamic mappingの仕様をあらためて整理しつつ、 その挙動の詳細を、いくつかのパターンで検証していくことにしました。

2. 検証環境

今回の検証は、社内の開発用OpenSearchドメインに対して行いました。
Amazon OpenSearch Service上のドメインに対して、手元の端末からcurl等を用いて、 インデックス作成やドキュメント投入を行いながら挙動を確認しています。

▼ 検証に利用したOpenSearchドメインの設定(クリックで展開)

  • サービス: Amazon OpenSearch Service
  • ドメイン名: ma-duuk-omega(ブログ用に名称を変更しています)
  • リージョン: ap-northeast-1
  • バージョン: OpenSearch 2.13
  • ドメインエンドポイント (VPC):
    https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com
  • ノード構成:
    • データノード数: 1
    • インスタンスタイプ: t3.medium.search
    • アベイラビリティーゾーン: 1-AZ
  • ストレージ:
    • EBS タイプ: gp3
    • サイズ: 10 GiB
    • IOPS: 3000
    • スループット: 125 MiB/秒

※ 今回の検証は、検証用OpenSearchドメインに対して行いました。

3. Dynamic mappingの公式仕様をざっくり整理

ここでは、OpenSearchのDynamic mappingがどのようにフィールド型を推論するのか、公式ドキュメント(Dynamic mapping | OpenSearch documentation)の内容をベースに整理しておきます。

Dynamic mappingは、投入されたドキュメントに新しいフィールドが含まれていた場合に、その値をもとにOpenSearchが自動でフィールド型を判断し、mappingを更新する仕組みです。
公式で定義されている推論ルールは以下のとおりです。

■ Dynamic mapping rules(自動推論ルール)

  • null
    フィールドはmappingに追加されない(値が存在しない扱い)。

  • boolean(true / false)
    boolean型として扱われる。

  • 数値(integer / float)
    JSONの整数はlong、小数はfloatとして扱われる。

  • object({})
    JSONオブジェクトはobjectとして追加され、中のフィールドも動的に推論される。

  • array([])
    専用のarray型はなく、最初の非null要素の型をもとに推論される。

  • string("")
    通常はtext + keywordのマルチフィールドとして扱われる。
    文字列がdate formatと一致すればdateとして扱われる。
    numeric_detectionが有効なら、数値文字列は数値型として扱われる。

■ 推論されない型について

OpenSearchが動的に判定する型は前述のとおり限定的で、
keyword単体やnestedgeo_pointgeo_shapeなどの特殊な型はDynamic mappingでは生成されません

これらのフィールドがドキュメントに含まれていても、専用の型としては扱われず、
JSONの形状に応じて文字列なら text + keyword、オブジェクトならobjectといった
一般的な型として分類されます。

そのため、これらの型を利用したい場合は、明示的にmappingを定義しておく必要があります。

■ Dynamic Template

なお、Dynamic mappingの挙動をカスタマイズする Dynamic Templateという機能もあります。
フィールド名のパターンやJSON型に応じてcustom mappingを適用できる仕組みですが、今回の記事ではDynamic mappingの“デフォルト挙動”を検証することを目的としているため、Templateは使用しません。

4. 検証

それでは早速、いくつかのケースで Dynamic mappingの挙動を確認していきます。

■ 検証①:文字列(string)の扱い

Dynamic mappingにおける文字列の基本挙動を確認します。
numeric_detectionはデフォルト(false)のままです。

投入データ

curl -X PUT "https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/dynamic-test/_doc/1" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Watanabe",
    "id_string": "12345",
    "long_string_number": "999999999999999",
    "message": "Hello OpenSearch!"
  }'

※ 実環境では認証およびアクセス制御を設定していますが、記事中では簡略化のため省略しています。

作成された mapping

{
  "properties": {
    "name": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    },
    "id_string": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    },
    "long_string_number": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    },
    "message": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    }
  }
}

コメント

  • すべての文字列はtext + keywordとして登録される
  • "12345"など数値に見える文字列もstringとして登録される
  • keywordにはデフォルトで ignore_above: 256が付与される
    • ignore_above: 256は256文字を超える値をkeywordとしてはインデックスしない設定で、ドキュメント自体は登録されるものの、terms集計や完全一致検索の対象外になります。
  • numeric_detectionを有効にすると、数値文字列はlong / floatとして登録される

■ 検証②:boolean / nullの扱い

Dynamic mappingにおけるboolean値とnullの挙動を確認します。

投入データ

curl -X PUT "https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/dynamic-test/_doc/2" \
  -H "Content-Type: application/json" \
  -d '{
    "flag_true": true,
    "flag_false": false,
    "flag_str_true": "true",
    "flag_str_false": "false",
    "value_is_null": null
  }'

作成された mapping

{
  "properties": {
    "flag_true": {
      "type": "boolean"
    },
    "flag_false": {
      "type": "boolean"
    },
    "flag_str_true": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    },
    "flag_str_false": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    }
  }
}

コメント

  • true / falseはbooleanとして登録される
  • "true" / "false"のような文字列はstringとして登録される(text + keyword)
  • nullのフィールドはmappingに生成されない

■ 検証③:数値(integer / float)の扱い

Dynamic mappingにおける数値の型推論を確認します。
公式仕様では、整数値はlong、小数値はfloatとして扱われます。

投入データ

curl -X PUT \
  "https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/dynamic-test/_doc/3" \
  -H "Content-Type: application/json" \
  -d '{
    "int_value": 42,
    "float_value": 1.23,
    "large_int": 999999999999,
    "negative_int": -123
  }'

作成された mapping

{
  "properties": {
    "int_value": {
      "type": "long"
    },
    "float_value": {
      "type": "float"
    },
    "large_int": {
      "type": "long"
    },
    "negative_int": {
      "type": "long"
    }
  }
}

コメント

  • JSONの整数値はすべて longとして登録される
  • 小数値はfloatとして登録される
  • 大きな整数値も例外なくlongとして登録される
  • 負の値もlongとして登録される

■ 検証④:dateの自動判定(date_detection)

Dynamic mappingでは、文字列がdateフォーマットに一致すると自動的にdateとして扱われます。

投入データ

curl -X PUT \
  "https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/dynamic-test/_doc/4" \
  -H "Content-Type: application/json" \
  -d '{
    "date_iso": "2025-12-17T09:00:00Z",
    "date_simple": "2025-12-17",
    "date_compact": "20251217",
    "date_invalid": "2025-13-01"
  }'

作成された mapping

{
  "properties": {
    "date_iso": {
      "type": "date"
    },
    "date_simple": {
      "type": "date"
    },
    "date_compact": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    },
    "date_invalid": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    }
  }
}

コメント

  • ISO8601形式(2025-12-17T09:00:00Z)はdateとして登録される
  • 2025-12-17のようなシンプルな日付表現もdateとして登録される
  • 20251217のような8桁形式はdateと判定されない
  • 不正な日付(2025-13-01など)はtextとして登録される

■ 検証⑤:object/nestedの扱い

Dynamic mappingでは、JSONのオブジェクトはobjectとして扱われます。

投入データ

curl -X PUT \
  "https://vpc-ma-duuk-omega-xxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/dynamic-test/_doc/5" \
  -H "Content-Type: application/json" \
  -d '{
    "user": {
      "flags": { "admin": true }
    }
  }'

作成された mapping

{
  "properties": {
    "user": {
      "properties": {
        "flags": {
          "properties": {
            "admin": { "type": "boolean" }
          }
        }
      }
    }
  }
}

コメント

  • JSONのオブジェクトは自動的にobjectとして登録される
  • 入れ子構造もすべてobjectに展開される
  • Dynamic mappingではnestedは自動生成されない

5. まとめ

OpenSearchのDynamic mappingについて、その実際の挙動をいくつか検証してみました。
概ね公式仕様を読み解いたとおりの動作ではありましたが、個人的な感覚とギャップがあったのは次の点です。

  • 文字列は常にtext + keywordとして扱われ、数値に見える文字列であってもstring扱いになる
  • JSONの整数値はlong、小数値はfloatとして推論される
  • dateはフォーマット依存で、ISO8601やyyyy-MM-ddは判定される一方、20251217のようなcompact形式はtext扱いになる
  • JSONのオブジェクトはすべてobjectとして扱われ、nestedgeo_pointのような特殊な型はDynamic mappingでは生成されない

こうして確認してみると、Dynamic mappingは便利な反面、
入力データの揺れや想定外のフォーマットによって、意図しない型が割り当てられてしまう可能性もあります。
多様なサービスから様々な形式のドキュメントが飛んでくる環境では、
型の不整合が検索精度の低下や可観測性の劣化につながるケースもあります。

そのため運用では、Dynamic mappingにすべてを任せるのではなく、

  • 必要なフィールドは明示的にmappingを定義する
  • 数値や日付はフォーマットを揃える
  • インデックスごとに投入されるデータ構造を一定に保つ

といった工夫が重要だと感じました。

本検証が、OpenSearchを用いた構築・運用の参考になれば幸いです。

6. we are hiring

Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

© Sansan, Inc.