この記事は ミライトデザイン Advent Calendar 2025 の 16 日目の記事となっています。
初めに
最近 OpenSearch に入門したいモチベがあったので、入門のついでにアドカレを書くことにしました。
筆者が気になることを気の向くままに調べた記事になります。
筆者自体が入門者なので、間違いなどあればご指摘いただけると嬉しいです。
OpenSearch とは
OpenSearchは、Apache Luceneをベースにしたオープンソースの分散型検索・分析エンジンです。
ElasticsearchのフォークとしてAWSが開発しており、JSON形式のドキュメント指向データベースとして、RESTful APIで操作可能です。
OpenSearch の基本
OpenSearch と RDBMS の違いのイメージ
RDBMS に対しての、対応と各 OpenSearch の要素の簡単な説明は次のようになります。
| RDBMS | OpenSearch | 説明 |
|---|---|---|
| データベース | クラスター | 複数のノードで構成されるシステム全体 |
| テーブル | インデックス | データの集合体 |
| 行(レコード) | ドキュメント | 個々のデータ |
| 列(カラム) | フィールド | ドキュメント内の項目 |
| スキーマ | マッピング | データ構造の定義 |
クラスターについては、ちょっと複雑なので、先にインデックスから説明します。
インデックスとは
インデックス は、 OpenSearch におけるデータの集合です。RDBMSの「テーブル」に相当します。
例えば、 EC サイトを構築している場合であれば、次のようなインデックスが作成されることになります。
- products インデックス ... 商品情報を格納
- users インデックス ... ユーザー情報を格納
- orders インデックス ... 注文情報を格納
ただし、 RDBMS と同じように インデックス の設計ができるわけではないので注意が必要です。
RDBMS は、正規化して検索時に join するような設計をするかと思いますが、
OpenSearch の場合は、基本的にはインデックス間の join ができず、非正規化の構成になります。
例えば、 orders インデックスに、 products id と users id が保存されている場合は、 アプリケーション側での join が必要になります。
(orders インデックスをそもそも作成する必要があるのかというのも、インデックス設計の場合は議論となるところなのかもしれません。)
RDBMS は、エンティティ(実体)単位で設計してから、検索方法を考える、
OpenSearch のような全文検索エンジンは、データへの検索方法を設計してから、インデックス設計を考える。のような違いがあると思っています。
ドキュメントとは
ドキュメントとは、 OpenSearch での最小のデータのまとまり、
RDBMS で言うところのレコードになり、 JSON 形式で表現されています。
先ほどの、 EC サイトの例で再度考えると、商品ドキュメントの例は次のようになります。
{
"product_id": "PROD-001",
"product_name": "ワイヤレスイヤホン",
"price": 5980,
"category": "electronics",
"stock": 50,
"is_available": true
}
フィールドとは
フィールドは、ドキュメント内の個々の項目です。 RDBMSのカラムに相当します。
上記の例では、次の項目それぞれが、フィールドとなります。
product_id
product_name
price
category
マッピングとは
マッピングは、 RDBMS でいうところの、スキーマ定義に相当します。
次のように定義を行います。
{
"mappings": {
"properties": {
"product_id": { "type": "keyword" },
"product_name": { "type": "text" },
"price": { "type": "float" },
"category": { "type": "keyword" },
"stock": { "type": "integer" },
"is_available": { "type": "boolean" }
}
}
}
マッピングでは次のようなことを定義します。
()の中は、 mappings に定義するパラメータ名です。
- 検索対象にする?(index)
- どう単語分割する?(analyzer)
- 集計・ソートする?(doc_values)
- 個別保存する?(store)
- データ型は?(type)
それぞれについて簡単に確認していきます。
▫️検索対象にする?(index)
{
"description": {
"type": "text",
"index": true // 検索できる(デフォルト)
},
"internal_memo": {
"type": "text",
"index": false // 検索できない(保存のみ)
}
}
index: true → 検索可能にする
index: false → 検索不可(データは保存されるが検索対象外)
▫️どう単語分割する?(analyzer)
text型の場合、どのように単語分割するかを指定します。
{
"product_name": {
"type": "text",
"analyzer": "japanese" // 日本語形態素解析
},
"english_description": {
"type": "text",
"analyzer": "english" // 英語用アナライザー
}
}
アナライザーの役割:
- "ワイヤレスイヤホン" → ["ワイヤレス", "イヤホン"] に分割
- 検索時にも同じ方法で分割するため、部分一致検索が可能
アナライザーと形態素解析についてよくわからなかったので、ざっと調べてみました。
アナライザーは次の3つの要素で構成されでいるらしいです。
-
Character Filters(文字フィルター)
テキストストリームの前処理を行う
例: HTMLタグの除去、特定文字の置換 -
Tokenizer(トークナイザー)
テキストをトークン(単語)に分割
例: 空白で分割、特定のパターンで分割 -
Token Filters(トークンフィルター)
トークンの変換・追加・削除を行う
例: 小文字化、ストップワード除去、同義語追加
形態素解析は 2 で利用され、次のように文章を分割します。
"私は速く走っています"
↓
["私", "速く", "走る", "て", "いる"]
3 でストップワード(「て」、「いる」などの、検索時にあまり意味を持たない単語)を除外したりしているらしいです。
OpenSearch では、形態素解析に使用する辞書や同義語検索で利用される辞書をユーザーが追加することが可能らしいです。
▫️集計・ソートする?(doc_values)
{
"price": {
"type": "float",
"doc_values": true // 集計・ソート可能(デフォルト)
},
"search_keyword": {
"type": "keyword",
"doc_values": false // 検索のみ、集計不要
}
}
- doc_values: true → 集計、ソート、スクリプトで使用可能
- doc_values: false → メモリ節約(検索のみに使う場合)
OpenSearch では検索以外にも、集計、ソート、スクリプト(商品の金額に消費税をかけて、消費税込みでソートするなど)があります。
そこから高速で参照できるようにする。みたいなイメージをしています。
▫️個別保存(storeパラメータ)
通常、全フィールドは_sourceに保存されますが、store: trueにすると個別にアクセス可能になります。
どういう意味かと言うと。
OpenSearchは、ドキュメントを保存する際に2つの方法でデータを管理しています。
- _sourceフィールド(デフォルト)
ドキュメント全体のオリジナルJSONが、_sourceという特別なフィールドに保存されます。
// 投入したドキュメント
{
"product_id": "PROD-001",
"product_name": "ワイヤレスイヤホン",
"description": "高音質Bluetooth対応のワイヤレスイヤホン。ノイズキャンセリング機能搭載で、クリアな音質を実現...",
"price": 5980
}
// OpenSearch内部
{
"_source": { // ← ここに元のJSON全体が保存される
"product_id": "PROD-001",
"product_name": "ワイヤレスイヤホン",
"description": "高音質Bluetooth対応の...",
"price": 5980
},
// + 検索用の転置インデックス(別領域)
}
- 検索用の転置インデックス(別領域)
検索を高速化するために、各フィールドは「転置インデックス」という別の形式で保存されるらしいです(これは_sourceとは別)。
通常、フィールドの値を取得するには_source全体を読み込む必要があります。しかし、store: trueを設定すると、そのフィールドだけを個別に取り出せるようになります。
{
"mappings": {
"properties": {
"product_name": {
"type": "text",
"store": false // デフォルト:_sourceから取得
},
"large_description": {
"type": "text",
"store": true // このフィールドは個別保存
}
}
}
}
これにより、例えば、長文の説明文があるドキュメントなどで、全てのフィールドを取得するのではなく、
特定のフィールドのみを取得できるというメリットがあります。
▫️データ型(typeパラメータ)
こちらは、想像しやすいかなと思います。
フィールドのデータの型定義になります。
{
"product_name": { "type": "text" }, // 全文検索
"product_id": { "type": "keyword" }, // 完全一致
"price": { "type": "float" }, // 数値
"created_at": { "type": "date" }, // 日付
"is_active": { "type": "boolean" } // 真偽値
}
データ型の詳しい種類はこちら
マッピングで注意しておくべき点
マッピングは、 RDBMS でいうところの、スキーマ定義に相当すると記載しましたが、当然違いはあるようです。
RDBMS とは違う点について簡単に調べました。
▫️text vs keyword の使い分け
同じ文字列型でも、データ型によって、検索方法などに違いがあります。
text はアナライズの対象となる全文検索用の型で、部分一致で検索が可能です。
keyword はアナライズの対象とならず、部分一致の検索ができません。完全一致の検索が可能です。
そのため、 ID などは keyword で、部分一致で検索したい商品名などは text に設定します。
text + keyword のようなマルチタイプ型も定義することもできるようです。
▫️動的マッピングの挙動
動的マッピングが有効になっていると、最初に投入されたデータの値によって、自動的に型が決まります。
明示的にマッピングをしていても、未定義のフィールドがあると自動的にマッピングされてしまいます。
動的マッピングは dynamic パラメータの設定で変更できるらしく、次のように挙動を変更できるようです。
- 未定義フィールドを自動的にマッピングする
- 未定義フィールドを無視する(保存はされるが、検索はできない)
- 未定義フィールドがあるとエラーになる
クラスターについて
クラスターは今までのものとは少し毛色が違ってきます。
今までは OpenSearch の論理的な仕組みについての話でしたが、ここからは具体的なサーバー構成のような話になります。
終わり
急に終わります。
この後は、具体的なサーバー構成についての話を調べて、ローカルに環境構築して触ってみる。みたいなことをしたいのですが、
記事が長くなりそうなのと、アドカレの期日になってしまったので別記事にしたいと思います(モチベが続けば)。
明日は mozumasu さんの記事になります!