Elasticsearch は、JSON の文書を保存して、全文検索と集計を速く行うための検索エンジンです。 操作はすべて HTTP の REST API で、curl から JSON を送って行います。RDB にたとえると、インデックスが表、ドキュメントが行、フィールドが列、マッピングが表の定義にあたります。

# インデックス(表)にドキュメント(行)を1件入れて、全文検索する
curl -X PUT "$ES/products/_doc/1" -H 'Content-Type: application/json' \
  -d '{"name": "ワイヤレスキーボード", "category": "pc", "price": 4980}'
curl -X GET "$ES/products/_search" -H 'Content-Type: application/json' \
  -d '{"query": {"match": {"name": "キーボード"}}}'
RDB Elasticsearch
データベースの表(テーブル) インデックス
行 ドキュメント(JSON)
列 フィールド
表の定義(CREATE TABLE) マッピング
主キー _id
SQL Query DSL(JSON で書く検索の言語)
GROUP BY・AVG 集計(aggregations)

確認環境:API の仕様、パラメータの既定値、kuromoji の部品と出力例は、Elasticsearch 9 系の公式ドキュメント(elastic.co/docs)で確かめました。Python のコードは、公式の Python クライアント elasticsearch 9.5.1(Python 3.12、Ubuntu 24.04)で実行し、送られた HTTP リクエスト(メソッド・URL・JSON 本文)を、リクエストを記録する擬似サーバーで受けて、記事の curl の例と同じ内容になることを確かめています。動的マッピングの ignore_above の値は Elasticsearch のソース(GitHub の main ブランチ)で確かめました(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Elasticsearch入門|インデックスと検索クエリの書き方・日本語検索」

準備:起動と接続

ローカルで動かす

公式ドキュメントのローカル開発用の手順では、Docker(Docker Compose)がある環境で次の1行を実行すると、Elasticsearch(ポート 9200)と Kibana(ポート 5601)が起動します。ドキュメントでは、この構成は開発・試験用で、本番には使わないこととされています。

curl -fsSL https://elastic.co/start-local | sh

この記事の curl の例は、接続先と認証を環境変数にまとめて書いています。

export ES=http://localhost:9200
export ES_API_KEY='(発行した API キー)'
# 認証ヘッダーを付けて curl を呼ぶ関数(以下の例で使う)
escurl() { curl -s -H "Authorization: ApiKey $ES_API_KEY" -H "Content-Type: application/json" "$@"; }

escurl "$ES/"                # バージョンなどが返れば接続できている
接続の形 curl での書き方
API キー -H "Authorization: ApiKey $ES_API_KEY"
ユーザー・パスワード -u elastic:$ELASTIC_PASSWORD
自己署名の証明書(HTTPS) --cacert http_ca.crt(-k は検証を止めるので試験用のみ)

Kibana の Dev Tools を使うときは、curl -X GET "$ES/products/_search" -d '...' を GET products/_search と JSON の本文の形で書きます。

Python クライアント

pip install elasticsearch       # 9.5.1 で確認。サーバーが 8 系なら 'elasticsearch>=8,<9'

Python クライアント 9.5.1 は、Accept: application/vnd.elasticsearch+json; compatible-with=9 というヘッダーを付けて送っていました。クライアントの大きな版(9 など)は、サーバーの版に合わせます。

インデックスとマッピングを作る

動的マッピング(自動で型が決まる)

マッピングを書かずにドキュメントを入れると、Elasticsearch が JSON の値から型を決めます(動的マッピング)。

JSON の値 決まる型
true / false boolean
整数 long
小数 float
日付の形の文字列 date
それ以外の文字列 text と、その下に .keyword(keyword 型、256文字を超える値は索引に入れない)
オブジェクト object
null フィールドを追加しない

文字列は「全文検索用の text」と「完全一致用の .keyword」の2つが自動で作られます。 便利な反面、カテゴリのように完全一致しか使わない列にも text ができ、日本語の文章には日本語向けでないアナライザーが使われます。本番では、次のように明示的にマッピングを書きます。

明示的なマッピング

escurl -X PUT "$ES/products" -d '{
  "settings": {
    "number_of_shards": 1,
    "analysis": {
      "analyzer": {
        "ja": {
          "type": "custom",
          "char_filter": ["icu_normalizer"],
          "tokenizer": "kuromoji_tokenizer",
          "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "ja_stop", "kuromoji_stemmer", "lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name":        { "type": "text", "analyzer": "ja", "fields": { "raw": { "type": "keyword" } } },
      "description": { "type": "text", "analyzer": "ja" },
      "category":    { "type": "keyword" },
      "price":       { "type": "integer" },
      "in_stock":    { "type": "boolean" },
      "created_at":  { "type": "date" }
    }
  }
}'

ja アナライザーは、後の「日本語検索」で説明します(analysis-kuromoji と analysis-icu のプラグインが必要)。

型 使いどころ 検索
text 文章・商品名など、言葉で探すもの match(分割した語で探す)
keyword ID・カテゴリ・タグ・状態など term(完全一致)、並べ替え・集計
integer / long / float / scaled_float 数値 range、集計
date 日時 range、date_histogram
boolean 真偽 term
nested 配列の中のオブジェクトを1件ずつ扱う nested クエリ
dense_vector ベクトル(意味検索) knn

1つの列を全文検索と並べ替え・集計の両方に使うときは、fields で text と keyword を両方持たせます(マルチフィールド)。 上の例では name で検索し、name.raw で並べ替え・集計できます。

マッピングの確認と変更

escurl "$ES/products/_mapping?pretty"                     # マッピングを見る
escurl -X PUT "$ES/products/_mapping" -d '{
  "properties": { "maker": { "type": "keyword" } }
}'                                                         # 新しいフィールドの追加はできる
escurl -X DELETE "$ES/products"                            # インデックスを消す(データも消える)

作ったフィールドの型やアナライザーは、あとから変えられません。 変えるときは、新しいマッピングでインデックスを作り、_reindex でデータを移します。

escurl -X POST "$ES/_reindex" -d '{
  "source": { "index": "products" },
  "dest":   { "index": "products_v2" }
}'

アプリからは別名(エイリアス)で参照しておくと、移したあとに別名を付け替えるだけで切り替えられます。

ドキュメントの CRUD

操作 API SQL でいうと
ID を指定して登録・上書き PUT /products/_doc/1 INSERT(あれば置き換え)
ID を自動で付けて登録 POST /products/_doc INSERT
無いときだけ登録 PUT /products/_create/1 INSERT(重複はエラー)
1件取得 GET /products/_doc/1 SELECT ... WHERE id = 1
一部の更新 POST /products/_update/1 UPDATE ... SET
1件削除 DELETE /products/_doc/1 DELETE ... WHERE id = 1
条件で更新・削除 _update_by_query / _delete_by_query UPDATE / DELETE ... WHERE
まとめて登録 POST /_bulk 複数行の INSERT
# 登録・上書き
escurl -X PUT "$ES/products/_doc/1" -d '{
  "name": "ワイヤレスキーボード", "description": "静かに打てる薄型のキーボード",
  "category": "pc", "price": 4980, "in_stock": true, "created_at": "2026-09-01"
}'

# 1件取得
escurl "$ES/products/_doc/1?pretty"

# 一部の更新(指定したフィールドだけ変わる)
escurl -X POST "$ES/products/_update/1" -d '{ "doc": { "price": 4480 } }'

# 削除
escurl -X DELETE "$ES/products/_doc/1"

bulk でまとめて登録

_bulk は、「操作の行」と「データの行」を交互に並べた NDJSON(1行1つの JSON)を送ります。 最後の行にも改行が必要です。

cat > products.ndjson <<'EOF'
{"index":{"_index":"products","_id":"1"}}
{"name":"ワイヤレスキーボード","description":"静かに打てる薄型のキーボード","category":"pc","price":4980,"in_stock":true,"created_at":"2026-09-01"}
{"index":{"_index":"products","_id":"2"}}
{"name":"メカニカルキーボード","description":"打ち心地の良い青軸","category":"pc","price":12800,"in_stock":false,"created_at":"2026-09-15"}
{"index":{"_index":"products","_id":"3"}}
{"name":"ワイヤレスマウス","description":"静音クリックで静かに使える","category":"pc","price":2980,"in_stock":true,"created_at":"2026-10-01"}
{"index":{"_index":"products","_id":"4"}}
{"name":"USB-Cハブ","description":"サーバー室でも使える7ポート","category":"accessory","price":3980,"in_stock":true,"created_at":"2026-10-05"}
EOF

curl -s -H "Authorization: ApiKey $ES_API_KEY" -H 'Content-Type: application/x-ndjson' \
  -X POST "$ES/_bulk?refresh=wait_for" --data-binary @products.ndjson

-d ではなく --data-binary を使います。-d は改行を取り除くので、NDJSON の形が崩れます。応答の "errors": true は、どれか1件でも失敗したという意味で、HTTP のステータスは 200 のままです。bulk の結果は errors と items の中の status で確かめます。

登録した直後に検索に出ない

Elasticsearch は、登録したドキュメントを一定間隔の「リフレッシュ」で検索できるようにします。 既定では1秒ごとです(index.refresh_interval)。試験やバッチで「入れてすぐ検索」したいときは、?refresh=wait_for を付けるか、POST /products/_refresh を呼びます。GET /products/_doc/1 の1件取得は、リフレッシュを待たずに読めます。

検索クエリの書き方

検索の基本形

escurl -X GET "$ES/products/_search?pretty" -d '{
  "query":   { "match": { "description": "静かなキーボード" } },
  "_source": ["name", "price"],
  "sort":    [ { "price": "asc" } ],
  "from": 0,
  "size": 10
}'

応答の形は次のとおりです(値は省略しています)。

{
  "took": 3,
  "timed_out": false,
  "hits": {
    "total": { "value": 2, "relation": "eq" },
    "max_score": null,
    "hits": [
      { "_index": "products", "_id": "3", "_score": null, "_source": { "name": "…", "price": 2980 }, "sort": [2980] }
    ]
  }
}
部分 意味
hits.total.value 一致した件数。既定では 10,000 件までは正確(track_total_hits)
hits.total.relation eq は正確な件数、gte は「それ以上」
hits.hits[]._score 関連度のスコア。sort を指定すると null
hits.hits[]._source 登録した JSON(_source で列を絞れる)

match:全文検索

match は、検索語をフィールドと同じアナライザーで語に分けてから探します。 既定では、分けた語のどれか1つが含まれれば一致(OR)です。

# どれかの語を含む(OR、既定)
escurl -X GET "$ES/products/_search" -d '{
  "query": { "match": { "description": "静かな キーボード" } }
}'

# すべての語を含む(AND)
escurl -X GET "$ES/products/_search" -d '{
  "query": { "match": { "description": { "query": "静かな キーボード", "operator": "and" } } }
}'

# 語の順番どおりに並んでいるもの(フレーズ)
escurl -X GET "$ES/products/_search" -d '{
  "query": { "match_phrase": { "description": "薄型のキーボード" } }
}'

# 複数のフィールドから探す(name を2倍重く)
escurl -X GET "$ES/products/_search" -d '{
  "query": { "multi_match": { "query": "キーボード", "fields": ["name^2", "description"] } }
}'

term:完全一致

term は、検索語を分けずに、そのままの値で探します。 keyword・数値・日付・真偽のフィールドに使います。

escurl -X GET "$ES/products/_search" -d '{
  "query": { "term": { "category": "pc" } }
}'

# IN (...) にあたる
escurl -X GET "$ES/products/_search" -d '{
  "query": { "terms": { "category": ["pc", "accessory"] } }
}'

text のフィールドに term を使うと、ほとんど一致しません。 ドキュメントでは、text のフィールドは登録時に分析されて値が変わるので、term は使わないようにと注意されています。例として、Quick Brown Foxes! は [quick, brown, fox] に変わるので、term で Quick Brown Foxes! を探しても何も返らないと説明されています。完全一致で探したい文字列は keyword 型(または .keyword のサブフィールド)にします。

探し方 使うクエリ フィールドの型
言葉で探す match / match_phrase / multi_match text
値が一致 term / terms keyword・数値・真偽
範囲 range 数値・日付
値があるか exists すべて
前方一致 prefix keyword
ワイルドカード wildcard keyword(遅くなりやすい)
あいまい(打ち間違い) match の fuzziness / fuzzy text / keyword

range:範囲

escurl -X GET "$ES/products/_search" -d '{
  "query": { "range": { "price": { "gte": 1000, "lte": 5000 } } }
}'

escurl -X GET "$ES/products/_search" -d '{
  "query": { "range": { "created_at": { "gte": "now-30d/d" } } }
}'

gte(以上)・gt(より大きい)・lte(以下)・lt(より小さい)を組み合わせます。日付は now-30d/d(30日前の0時)のような書き方ができます。

bool:条件の組み合わせ

bool は、複数のクエリを AND・OR・NOT で組み合わせます。

句 意味 スコア
must 一致しなければならない(AND) 加算する
filter 一致しなければならない(AND) 使わない。結果がキャッシュされうる
should 一致するとよい(OR) 加算する
must_not 一致してはならない(NOT) 使わない。キャッシュされうる
escurl -X GET "$ES/products/_search" -d '{
  "query": {
    "bool": {
      "must":     [ { "match": { "name": "キーボード" } } ],
      "filter":   [ { "term":  { "category": "pc" } },
                    { "range": { "price": { "gte": 1000, "lte": 10000 } } } ],
      "must_not": [ { "term":  { "in_stock": false } } ]
    }
  },
  "sort": [ { "price": "asc" } ]
}'

関連度に関係しない条件(カテゴリ・価格・在庫など)は must ではなく filter に書きます。 スコアの計算をしないので軽く、キャッシュも効きます。

should の扱いには決まりがあります。ドキュメントでは、should が1つ以上あり、must も filter も無いときは minimum_should_match の既定が 1(どれか1つは一致が必要)、それ以外の場合は 0(一致しなくてもよく、スコアを上げるだけ)とされています。

# 「pc か accessory」かつ「在庫あり」:should を OR として使うなら minimum_should_match を書く
escurl -X GET "$ES/products/_search" -d '{
  "query": {
    "bool": {
      "filter": [ { "term": { "in_stock": true } } ],
      "should": [ { "term": { "category": "pc" } }, { "term": { "category": "accessory" } } ],
      "minimum_should_match": 1
    }
  }
}'

ハイライトとページング

escurl -X GET "$ES/products/_search" -d '{
  "query": { "match": { "description": "キーボード" } },
  "highlight": { "fields": { "description": {} } }
}'

from + size で取れるのは、既定で 10,000 件目まで(index.max_result_window)です。それより深くたどるときは、前のページの最後の sort の値を渡す search_after を使います。

escurl -X GET "$ES/products/_search" -d '{
  "size": 100,
  "sort": [ { "created_at": "desc" }, { "name.raw": "asc" } ],
  "search_after": ["2026-10-01T00:00:00.000Z", "ワイヤレスマウス"]
}'

集計(aggregations)

集計は aggs に書きます。 件数のバケツ(グループ)を作る「バケット集計」と、合計・平均などを出す「メトリクス集計」を入れ子にできます。ヒットしたドキュメント自体が要らなければ "size": 0 にします。

# SELECT category, COUNT(*), AVG(price) FROM products GROUP BY category
escurl -X GET "$ES/products/_search" -d '{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 10 },
      "aggs":  { "avg_price": { "avg": { "field": "price" } } }
    }
  }
}'

応答では、aggregations.by_category.buckets に {"key": "pc", "doc_count": …, "avg_price": {"value": …}} の形で並びます。

集計 種類 SQL でいうと
terms バケット GROUP BY 列(上位 size 件、既定 10)
range バケット CASE WHEN price < 1000 ... で区分
date_histogram バケット GROUP BY date_trunc('month', created_at)
filter / filters バケット COUNT(*) FILTER (WHERE ...)
avg / sum / min / max メトリクス AVG / SUM / MIN / MAX
stats メトリクス 件数・最小・最大・平均・合計をまとめて
cardinality メトリクス COUNT(DISTINCT 列)(近似値)
percentiles メトリクス パーセンタイル(近似値)
bucket_selector パイプライン HAVING
# 月ごとの件数と売上
escurl -X GET "$ES/products/_search" -d '{
  "size": 0,
  "query": { "range": { "created_at": { "gte": "2026-01-01" } } },
  "aggs": {
    "per_month": {
      "date_histogram": { "field": "created_at", "calendar_interval": "month" },
      "aggs": { "total": { "sum": { "field": "price" } } }
    }
  }
}'

# 価格帯ごとの件数
escurl -X GET "$ES/products/_search" -d '{
  "size": 0,
  "aggs": {
    "price_band": {
      "range": { "field": "price", "ranges": [ { "to": 3000 }, { "from": 3000, "to": 10000 }, { "from": 10000 } ] }
    }
  }
}'

集計できるのは keyword・数値・日付などで、text のフィールドはそのままでは terms 集計に使えません。 商品名で集計したいときは、マルチフィールドの name.raw(keyword)を指定します。

日本語検索(kuromoji)

なぜ日本語用のアナライザーが要るのか

アナライザーは、文章を検索用の語(トークン)に分ける部品です。 既定の standard アナライザーは、空白や記号で語を区切る仕組みで、空白の無い日本語の漢字・ひらがなは1文字ずつに分かれます(ドキュメントの cjk_bigram の例は、standard で1文字ずつになった「東京都は、日本の首都であり」を「東京 / 京都 / 都は …」と2文字ずつに組み直すものです)。1文字ずつでも検索はできますが、「京都」で「東京都」が当たるような、意図しない一致が増えます。

kuromoji は、辞書(MeCab-IPADIC)を使って日本語を単語に分ける、形態素解析のプラグインです。

# すべてのノードに入れて、ノードを再起動する
sudo bin/elasticsearch-plugin install analysis-kuromoji
sudo bin/elasticsearch-plugin install analysis-icu      # icu_normalizer を使う場合

ドキュメントでは、プラグインはクラスターのすべてのノードに入れ、入れたあとに各ノードを再起動する必要があるとされています。

kuromoji の部品

組み込みの kuromoji アナライザーは、次の順に処理する組み合わせです。

順 部品 すること
1 文字フィルター(CJKWidthCharFilter) 全角英数字・半角カナの幅をそろえる
2 kuromoji_tokenizer 辞書を使って単語に分ける
3 kuromoji_baseform 活用した語を基本形にする(飲み → 飲む)
4 kuromoji_part_of_speech 助詞などの品詞を取り除く
5 ja_stop 「これ」「の」などのよく出る語を取り除く
6 kuromoji_stemmer カタカナ語の末尾の長音を取る(サーバー → サーバ)
7 lowercase 英字を小文字にする

ドキュメントにある各部品の例です。

部品 入力 出力
kuromoji_tokenizer(search モード) 関西国際空港 関西 / 関西国際空港 / 国際 / 空港
kuromoji_tokenizer(normal モード) 関西国際空港 関西国際空港
kuromoji_baseform 飲み 飲む
ja_stop(「ストップ」を追加) ストップは消える 消える
kuromoji_stemmer(minimum_length 4) コピー / サーバー コピー / サーバ
ユーザー辞書を使った分割 東京スカイツリー 東京 / スカイツリー

kuromoji_stemmer は、4文字(minimum_length の既定)未満のカタカナ語は変えません。 そのため「コピー」はそのまま、「サーバー」は「サーバ」になり、「サーバ」と「サーバー」のどちらで探しても一致します。

tokenizer のモード

mode 長い複合語 例:関西国際空港
normal 分けない 関西国際空港
search 分けて、元の語も残す 関西 / 関西国際空港 / 国際 / 空港
extended search に加え、辞書に無い語を1文字ずつにする (未知語「アブラカダブラ」→ ア / ブ / ラ / カ / ダ / ブ / ラ)

search モードなら、「空港」で探しても「関西国際空港」が当たります。 組み込みの kuromoji アナライザーは mode と user_dictionary を設定できます。

全角・半角をそろえる(icu_normalizer)

ドキュメントでは、辞書に全角の文字が含まれているために、全角の文字で思わぬトークンが出ることがあり、それを避けるために icu_normalizer の文字フィルターを加えたアナライザーを作る方法が示されています。この記事のマッピングの ja アナライザーは、その形にしています。

{
  "analyzer": {
    "ja": {
      "type": "custom",
      "char_filter": ["icu_normalizer"],
      "tokenizer": "kuromoji_tokenizer",
      "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "ja_stop", "kuromoji_stemmer", "lowercase"]
    }
  }
}

analysis-icu を入れない場合は、char_filter の行を除き、filter の先頭に組み込みの cjk_width(幅をそろえるトークンフィルター)を入れる形もあります。

ユーザー辞書(固有名詞・社内用語)

辞書に無い商品名や社内用語は、意図どおりに分かれないことがあります。ユーザー辞書で分け方を指定できます。

escurl -X PUT "$ES/products_ja" -d '{
  "settings": {
    "analysis": {
      "tokenizer": {
        "ja_user_dict": {
          "type": "kuromoji_tokenizer",
          "mode": "search",
          "user_dictionary_rules": [ "東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞" ]
        }
      },
      "analyzer": {
        "ja_dict": { "type": "custom", "tokenizer": "ja_user_dict", "filter": ["kuromoji_baseform", "lowercase"] }
      }
    }
  }
}'

1行の形は 元の語,分けた語(空白区切り),読み(空白区切り),品詞 です。ファイルにするときは config/userdict_ja.txt に置いて "user_dictionary": "userdict_ja.txt" と指定します。

分け方を確かめる(_analyze)

検索が当たらないときは、まず _analyze で、文章と検索語がどう分かれるかを見ます。 両方が同じトークンを含んでいなければ、match は当たりません。

escurl -X GET "$ES/products/_analyze?pretty" -d '{
  "analyzer": "ja",
  "text": "静かに打てるキーボード"
}'

# アナライザーを作らずに、部品を組み合わせて試す
escurl -X GET "$ES/_analyze?pretty" -d '{
  "tokenizer": { "type": "kuromoji_tokenizer", "mode": "search" },
  "filter": ["kuromoji_baseform"],
  "text": "関西国際空港に飲みに行く"
}'

形態素解析で取りこぼすときは N-gram と組み合わせる

辞書で分けると、「ーボード」のような単語の途中の文字列では当たりません。部分一致も拾いたいときは、同じ列を N-gram(2文字ずつなど)でも索引に入れ、両方で探してスコアを足す方法があります。

escurl -X PUT "$ES/products_ng" -d '{
  "settings": {
    "analysis": {
      "tokenizer": { "bigram": { "type": "ngram", "min_gram": 2, "max_gram": 2 } },
      "analyzer": {
        "ja":        { "type": "custom", "tokenizer": "kuromoji_tokenizer",
                       "filter": ["cjk_width", "kuromoji_baseform", "kuromoji_part_of_speech", "ja_stop", "kuromoji_stemmer", "lowercase"] },
        "ja_bigram": { "type": "custom", "tokenizer": "bigram", "filter": ["cjk_width", "lowercase"] }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": { "type": "text", "analyzer": "ja", "fields": { "ngram": { "type": "text", "analyzer": "ja_bigram" } } }
    }
  }
}'

escurl -X GET "$ES/products_ng/_search" -d '{
  "query": { "multi_match": { "query": "ボード", "fields": ["name^2", "name.ngram"] } }
}'

name が kuromoji、name.ngram が2文字ずつの分割です。name^2 で kuromoji の一致を重くしているので、kuromoji で当たるものが上に、N-gram だけで当たるものが下に並びます。この例の ja は、analysis-icu を使わず、組み込みの cjk_width で幅をそろえる形にしています。PostgreSQL での日本語の部分一致検索は、関連記事「PostgreSQL日本語全文検索|pg_trgmとpg_bigmの速度比較と選び方」で扱っています。

SQL との対応表

SQL Elasticsearch
CREATE TABLE PUT /products(mappings)
ALTER TABLE ADD COLUMN PUT /products/_mapping(型の変更は不可)
INSERT PUT /products/_doc/1・POST /_bulk
SELECT * WHERE id = 1 GET /products/_doc/1
UPDATE SET price = 4480 WHERE id = 1 POST /products/_update/1 + {"doc": {...}}
DELETE WHERE id = 1 DELETE /products/_doc/1
WHERE category = 'pc' term(filter の中)
WHERE category IN ('pc', 'acc') terms
WHERE price BETWEEN 1000 AND 5000 range(gte・lte)
WHERE maker IS NOT NULL exists
WHERE name LIKE 'キー%' prefix(keyword 型)
WHERE description LIKE '%静か%' match(意味は違う:語単位の一致)
AND / OR / NOT bool の must・filter / should / must_not
ORDER BY price sort
LIMIT 10 OFFSET 20 size: 10, from: 20
COUNT(*) GET /products/_count・hits.total
GROUP BY category terms 集計
COUNT(DISTINCT maker) cardinality 集計(近似値)
HAVING bucket_selector
JOIN 基本は無い。1つのドキュメントにまとめて入れる(非正規化)か nested
トランザクション 無い(1ドキュメント単位の操作)

LIKE '%静か%' と match は同じではありません。 match は「アナライザーで分けた語が一致するか」で探し、関連度の順に並べます。文字列のどこかに含まれるかを調べる LIKE とは考え方が違います。ドキュメント型のデータベースと SQL の対応は、関連記事「MongoDB入門|mongoshのコマンド一覧とSQLとの対応表」で扱っています。

Elasticsearch には SQL で問い合わせる機能もあり、POST /_sql/translate で SQL を Query DSL に変換した結果を見られます。DSL の書き方を調べるときに使えます。

escurl -X POST "$ES/_sql/translate" -d '{
  "query": "SELECT name, price FROM products WHERE category = '\''pc'\'' ORDER BY price DESC LIMIT 10"
}'

Python のコード(全文)

公式の Python クライアントで、ここまでの操作を1つにまとめたコードです。

import os
from elasticsearch import Elasticsearch, helpers

es = Elasticsearch(
    os.environ.get("ES_URL", "https://localhost:9200"),
    api_key=os.environ.get("ES_API_KEY"),          # または basic_auth=("elastic", "パスワード")
    ca_certs=os.environ.get("ES_CA_CERTS"),        # 自己署名の証明書なら http_ca.crt のパス
)

INDEX = "products"

# 1. インデックスを作り直す(マッピングと日本語のアナライザー)
es.indices.delete(index=INDEX, ignore_unavailable=True)
es.indices.create(
    index=INDEX,
    settings={
        "number_of_shards": 1,
        "analysis": {
            "analyzer": {
                "ja": {
                    "type": "custom",
                    "char_filter": ["icu_normalizer"],
                    "tokenizer": "kuromoji_tokenizer",
                    "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "ja_stop",
                               "kuromoji_stemmer", "lowercase"],
                }
            }
        },
    },
    mappings={
        "properties": {
            "name":        {"type": "text", "analyzer": "ja", "fields": {"raw": {"type": "keyword"}}},
            "description": {"type": "text", "analyzer": "ja"},
            "category":    {"type": "keyword"},
            "price":       {"type": "integer"},
            "in_stock":    {"type": "boolean"},
            "created_at":  {"type": "date"},
        }
    },
)

# 2. まとめて登録(bulk)
docs = [
    {"_id": "1", "name": "ワイヤレスキーボード", "description": "静かに打てる薄型のキーボード", "category": "pc", "price": 4980, "in_stock": True,  "created_at": "2026-09-01"},
    {"_id": "2", "name": "メカニカルキーボード", "description": "打ち心地の良い青軸",           "category": "pc", "price": 12800, "in_stock": False, "created_at": "2026-09-15"},
    {"_id": "3", "name": "ワイヤレスマウス",     "description": "静音クリックで静かに使える",     "category": "pc", "price": 2980, "in_stock": True,  "created_at": "2026-10-01"},
    {"_id": "4", "name": "USB-Cハブ",            "description": "サーバー室でも使える7ポート",     "category": "accessory", "price": 3980, "in_stock": True, "created_at": "2026-10-05"},
]
ok, errors = helpers.bulk(es, ({"_index": INDEX, **d} for d in docs), refresh="wait_for")
print("bulk:", ok, errors)

# 3. 1件の取得・更新・削除
print(es.get(index=INDEX, id="1")["_source"])
es.update(index=INDEX, id="1", doc={"price": 4480})
es.delete(index=INDEX, id="4")

# 4. 全文検索(match)
r = es.search(index=INDEX, query={"match": {"description": "静かなキーボード"}})
for h in r["hits"]["hits"]:
    print(h["_score"], h["_source"]["name"])

# 5. 条件の組み合わせ(bool)とページング
r = es.search(
    index=INDEX,
    query={
        "bool": {
            "must":     [{"match": {"name": "キーボード"}}],
            "filter":   [{"term": {"category": "pc"}},
                         {"range": {"price": {"gte": 1000, "lte": 10000}}}],
            "must_not": [{"term": {"in_stock": False}}],
        }
    },
    sort=[{"price": "asc"}],
    from_=0, size=10,
    source=["name", "price"],
)
print("total:", r["hits"]["total"]["value"])

# 6. 集計(GROUP BY category, AVG(price))
r = es.search(
    index=INDEX, size=0,
    aggs={"by_category": {"terms": {"field": "category"},
                          "aggs": {"avg_price": {"avg": {"field": "price"}}}}},
)
for b in r["aggregations"].get("by_category", {}).get("buckets", []):
    print(b["key"], b["doc_count"], b["avg_price"]["value"])

# 7. アナライザーの確認
r = es.indices.analyze(index=INDEX, analyzer="ja", text="静かに打てるキーボード")
print([t["token"] for t in r["tokens"]])

Python のコードが送る HTTP リクエスト

このコードを実行し、擬似サーバーで受けたリクエストです。Python の引数が、curl の例と同じ URL・JSON になっていることが分かります。

Python メソッドと URL 本文(抜粋)
indices.delete(..., ignore_unavailable=True) DELETE /products?ignore_unavailable=true なし
indices.create(settings=..., mappings=...) PUT /products {"mappings":{...},"settings":{...}}
helpers.bulk(..., refresh="wait_for") PUT /_bulk?refresh=wait_for {"index":{"_id":"1","_index":"products"}} と JSON の行が交互
get(index=..., id="1") GET /products/_doc/1 なし
update(..., doc={"price": 4480}) POST /products/_update/1 {"doc":{"price":4480}}
delete(index=..., id="4") DELETE /products/_doc/4 なし
search(query=..., sort=..., from_=0, size=10, source=[...]) POST /products/_search {"from":0,"query":{...},"size":10,"sort":[...],"_source":["name","price"]}
search(size=0, aggs=...) POST /products/_search {"aggs":{...},"size":0}
indices.analyze(...) POST /products/_analyze {"analyzer":"ja","text":"静かに打てるキーボード"}

Python では from が予約語なので from_ と書き、_source は source と書きます。 送られる JSON では from・_source に変わっていました。bulk のリクエストの Content-Type は application/vnd.elasticsearch+x-ndjson; compatible-with=9 で、すべてのリクエストに Authorization ヘッダーが付いていました。

よくあるつまずき

症状 原因 直し方
入れたのに検索で出ない リフレッシュ前(既定 1秒間隔) ?refresh=wait_for、_refresh
term で文字列が当たらない text 型は分析されている keyword 型か .keyword で term
terms 集計で text のフィールドがエラー text は集計に使えない keyword のマルチフィールドを指定
日本語で関係ない結果が多い standard アナライザーで1文字ずつ kuromoji のアナライザーをマッピングに設定
型を変えたい 既存のフィールドの型は変更できない 新しいインデックスを作って _reindex
10,000 件目より後が取れない index.max_result_window(既定 10,000) search_after
hits.total.value が 10000 で止まる track_total_hits の既定 "track_total_hits": true(重くなる)
bulk が 200 なのに一部入っていない 1件ごとの失敗は items に入る errors と各 status を確かめる

よくある質問

Q. Elasticsearch と OpenSearch のどちらを使えばよいですか。 A. OpenSearch は、Elasticsearch 7.10 をもとに分かれたプロジェクトで、この記事の基本の API(インデックス・CRUD・match・term・bool・集計)は同じ形で書けます。日本語の解析も、OpenSearch では analysis-kuromoji のプラグインで行います。AWS の OpenSearch Service を使うか、Elastic のサービスを使うかで決まることが多く、Python では OpenSearch 用のクライアント(opensearch-py)を使います。

Q. RDB の代わりに使えますか。 A. 向いていません。トランザクションや JOIN が無く、登録してから検索できるまで少し間があります。RDB をデータの正本にして、検索に必要な列を Elasticsearch にコピーする構成が一般的です。

Q. シャードの数はいくつにすればよいですか。 A. シャードは、インデックスを分けて持つ単位です。主シャードの数はあとから変えられない(分割・縮小の API はある)ので、データ量とノードの数から決めます。小さなインデックス(この記事の例など)なら number_of_shards: 1 で始め、データが増えてから見直す方法があります。

まとめ

  • インデックス = 表、ドキュメント = 行、マッピング = 表の定義。操作はすべて REST API の JSON
  • 文字列は text(全文検索)と keyword(完全一致・並べ替え・集計) を使い分ける。両方要るならマルチフィールド
  • 検索は match(言葉で探す)・term(値が一致)・range・bool。スコアに関係ない条件は filter に書く
  • text のフィールドに term を使わない
  • 集計は aggs。terms が GROUP BY、avg・sum などを入れ子にする
  • 日本語は kuromoji(analysis-kuromoji プラグイン)。当たらないときは _analyze で分け方を見る
  • 型は変えられないので、変えるときは 新しいインデックス + _reindex
  • Python クライアントでは from_・source と書く

参考・出典

確認日はいずれも 2026年10月13日です。

  • Elastic Docs「Run Elasticsearch locally」:https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart
  • Elastic Docs「Dynamic field mapping」:https://www.elastic.co/docs/manage-data/data-store/mapping/dynamic-field-mapping
  • Elastic Docs「Match query」:https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query
  • Elastic Docs「Term query」:https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-term-query
  • Elastic Docs「Boolean query」:https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query
  • Elastic Docs「SQL Translate API」:https://www.elastic.co/docs/reference/query-languages/sql/sql-translate
  • Elastic Docs「Japanese (kuromoji) analysis plugin」:https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji
  • Elastic Docs「kuromoji analyzer」:https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji-analyzer
  • Elastic Docs「kuromoji_tokenizer」:https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji-tokenizer
  • Elastic Docs「kuromoji_baseform」「ja_stop」「kuromoji_stemmer」:https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji-baseform ・ https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji-stop ・ https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-kuromoji-stemmer
  • Elastic Docs「Python client examples」:https://www.elastic.co/docs/reference/elasticsearch/clients/python/examples
  • Elasticsearch のソース「DynamicFieldsBuilder.java」:https://github.com/elastic/elasticsearch/blob/main/server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Elasticsearch入門|インデックスと検索クエリの書き方・日本語検索」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?