Elasticsearchへの一括投入は、送信側で改行を1つ落としただけで全体が通らなくなり、逆に応答が正常に返ってきても中身の一部だけが黙って落ちていることがあります。書式そのものは単純ですが、実装で詰まる箇所は書式の外側に集中する構造です。9.5系(2026年8月時点・最新は8月20日公開の9.5.2)を前提に、2行1組の組み立て方、1リクエストを何で切るか、応答から再送対象を絞る受信処理、そして初期ロードと差分同期を分ける設計までを順に見ていきます。
まとめ:データ投入の設計で先に決める5つの値と結論
実装に入る前に決めておく値は5つです。第1に、1リクエストの上限バイト数(数MBから数十MB)。第2に、送信ワーカの並列数。第3に、ドキュメントの_idを業務側の主キーから決めるか自動採番に任せるか。第4に、初期ロードの経路(既存索引への追記か、新索引へ洗い替えてエイリアスを切り替えるか)。第5に、差分同期の方式(二重書き・更新時刻ポーリング・変更データ取得のいずれか)。
bulk APIで失敗する実装のほとんどは、書式ではなく再送設計で壊れます。応答は1件の失敗が残りへ影響しない仕様なので、通信層が成功でもerrorsが真なら中身は欠けています。再送で消えるのは429と503だけ。400と409は何度送っても同じ結果になるため、設計側で潰すしかありません。再送を安全にする条件はただ1つ、同じドキュメントが同じ_idに着地することです。索引そのものの設計はElasticsearchの転置インデックスとシャード設計の側の前提を引き継ぎます。
bulk APIの要求形式|2行1組のNDJSONと改行で決まる送信可否
bulk APIの本文は通常のJSONではありません。公式リファレンスが示す構造は「アクションとメタデータの行、改行、任意の本文行、改行」の繰り返しで、改行そのものが区切り文字として機能します。JSON配列だと思って組めば、最初の1回で弾かれます。
メタ行と本文行の組み方|index・create・updateで変わる行数
1つの動作は原則2行で表現します。1行目が何をするかを示すメタ行、2行目が対象のドキュメント本体です。動作はindex・create・update・deleteの4種類で、公式の説明ではindexは既存ドキュメントがあれば置換してバージョンを増やし、createは存在しない場合のみ書き込みます。
行数が動作ごとに変わる点が、自前で組み立てるときの最初の落とし穴です。deleteはメタ行だけの1行で完結し、本文行を付けると次の動作のメタ行として解釈されてずれていきます。updateの本文行は部分ドキュメントを包んだ形で、indexとは構造が別物。4動作を1リクエストに混在させられる仕様のため、混ぜたときほどこのずれが起きます。
Content-Typeと最終行の改行|整形出力で弾かれる境界
ヘッダはapplication/jsonかapplication/x-ndjsonのどちらかを指定します。そのうえで公式が明示しているのが最終行の扱いで、データの最後の行は改行文字で終わらなければなりません。改行の直前にキャリッジリターンが入る形式は許容されるため、Windows環境で生成したファイルをそのまま送っても書式としては通ります。
逆に確実に落ちるのが整形出力です。この形式は文字どおりの改行を区切りとして使うため、JSONを整形して出力してはならないと公式が名指しで注意しました。ログを読みやすくする目的で整形オプションを付けたまま本番へ出すと、書式エラーだけが返って原因が見えません。ヘッダ設定はElasticsearchのREST APIのエンドポイント体系と認証の側の整理と合わせて確認してください。
索引名の指定場所|URLパスとメタ行の_indexの優先順位と分岐
投入先の索引は、URLパスに書く方法とメタ行の_indexに書く方法の2つがあります。パスに書けば全動作の既定になり、メタ行に書けばその動作だけを別の索引へ向けられる仕組みです。両方に書いた場合はメタ行が勝ちます。
1回の送信が単一の索引で完結するならパスに書いて本文を軽くし、テナントや日付で宛先が変わるならメタ行だけで指定して、宛先の決定を送信側へ一本化します。エイリアス以外への書き込みを禁じたい場合はrequire_aliasが使えます。
bulk 1リクエストの粒度|件数ではなくバイト数で区切る理由
「1回に何件送ればよいか」という問いは、実は問いの立て方が違います。制約はすべてバイト数で効いてくるため、件数で切る実装は文書長のばらつきをそのまま事故に変えます。
http.max_content_lengthの既定100mbと413が返る境界
まず前提となるのが、硬い上限です。ElasticsearchはHTTPリクエストの最大サイズを既定で100mbに制限しており、クライアント側でこれを超えないようにする責任があると公式が明記しています。設定名はhttp.max_content_lengthで、超えたリクエストは413で拒否されます。
ここで設定値を引き上げる判断は勧められません。100MBの本文を1回で送るということは、失敗したときに100MB分をまるごと再送するということです。公式のネットワーク設定の説明でも、100MBを超える場合の対処として挙げられているのは設定変更ではなく1リクエストあたりの件数削減でした。413が出たら上限を上げず、分割の粒度を見直す側へ倒します。
数十MBを上限に置く目安|文書長のばらつきを吸収する分割方法
実際に置くべき上限は、硬い上限よりはるかに小さい値です。投入速度の調整に関する公式ガイドは、大きなリクエストのほうが速く見えたとしても1リクエストあたり数十MBを超えないようにと述べています。100mbという上限と、数十MBという推奨の間には意図的な余白があります。
そのうえで、切り方を件数からバイト数へ変えます。実装は、シリアライズ済みの行を1本ずつバッファへ足しながら累計バイト数を数え、閾値を超えたら送信して次のバッファへ移る形です。件数で切ると、平均2KBの文書に混ざった200KBの文書が数十件入っただけで想定の10倍を送ってしまいます。全文フィールドを持つ業務データでは、このばらつきが常態です。何MBに置くかを実測で詰める手順はElasticsearchのチューニングで扱うバルクサイズの実測の側にまとめています。
並列度の決め方|writeスレッドプールのキュー長から逆算する
並列数についても、公式は特定の数値を示していません。最も良い数は検証でしか分からないとしたうえで、入出力かCPUのどちらかが飽和するまでワーカ数を段階的に増やす進め方を勧めています。上から数字を決めるのではなく、下から上げて頭打ちを探す方式です。
受け側の容量は数値で示されています。writeスレッドプールの既定サイズは割り当てプロセッサ数、最大でも1+割り当てプロセッサ数で、待ち行列の長さは10000と割り当てプロセッサ数×750の大きいほうです。8コアのノードなら同時に処理されるのは8本前後で、超えた分はキューへ積まれ、キューが埋まれば429が返ります。並列度を上げても、この2つの数字より先には進めません。
部分失敗の扱い|要求が通っても中身が落ちる前提で書く受信処理
bulk APIの応答設計は、他のAPIとは前提が違います。公式は「1つの動作の失敗は残りの動作へ影響しない」と明記しました。リクエスト自体は成立したまま中身の一部だけが失敗した状態が、正常な返り値として存在します。
errorsフラグとitems配列|失敗した1件だけを取り出す読み方
応答に含まれるのはtook・errors・itemsの3つです。判定に使うのはerrorsで、これが偽なら全件成功、真なら1件以上が失敗しています。通信層のステータスコードだけを見て成功と記録する実装は、ここで静かに壊れます。
itemsは送信した動作と同じ順序・同じ件数で返り、各要素に個別のステータスが入ります。したがって、失敗行を特定する手掛かりは添字です。送信バッファをすぐ捨てず、添字と業務キーの対応表を持ったまま応答を受け取る設計にしておくと、失敗した1件だけを再送用のバッファへ移せる仕組みになります。ここを持たない実装は、部分失敗を検知できても「どの行か」が分からず、結局バッチ全体の再送に戻ります。
再送してよい429と503、設計で潰す400と409の線引き
失敗した動作は、再送で消えるものと消えないものに分かれます。429は投入速度に受け側が追随できていない信号で、公式は少し止めてから、できればランダム化した指数バックオフで再試行するよう述べています。503も同様に一時的な状態です。一方400と409は、何度送っても同じ結果になります。
| ステータス | 典型的な原因 | 取るべき対処 |
|---|---|---|
| 429 | キューが埋まった | 待ってから再送する |
| 503 | シャードが未割当 | 待ってから再送する |
| 400 | 型やフィールドの不整合 | 投入前の検証で弾く |
| 409 | createの重複や版の衝突 | 再送せず記録する |
この分岐を1箇所にまとめておくと、リトライ処理が単純になります。再送キューへ入れてよいのは429と503だけ。400は投入前のスキーマ検証で減らす対象で、リトライ回数を増やしても改善しません。受け側のスキーマをどう決めるかはElasticsearchのマッピング設計を参照してください。409は重複投入が正しく止められた合図なので、失敗として数えるかどうかを業務側で決めます。updateの版衝突についてはretry_on_conflictで再試行回数を指定できますが、既定は0のため、明示しない限り1回で諦めます。
再送時の重複を止める|_idの決め方とcreateの使いどころ
再送設計で最も壊れやすいのは、タイムアウトした送信の扱いです。応答が返らなかっただけで、受け側は書き込みを終えているかもしれません。この状態で同じバッチを送り直すと、_idを指定していない限り自動採番で同じ内容の別ドキュメントが増えます。検索結果に同じレコードが2件並ぶ障害は、ほぼこの経路です。
止め方は1つです。業務側の主キー、あるいは主キーから決定的に導ける文字列を_idにして、同じ入力が必ず同じ宛先へ着地するようにします。こうすればindexでの再送は単なる上書きになり、何度送っても結果は変わりません。既存を絶対に上書きしたくない初回ロードではcreateへ寄せ、返ってきた409を「既に入っている」という情報として扱います。外部システムが版番号を持つなら、version_typeを外部版に切り替えて古いデータでの巻き戻しを拒否させる手もあります。
初期ロードと差分同期|一度きりの流し込みと継続反映を分ける設計
同じbulk APIを使っていても、初期ロードと差分同期は別の設計課題です。前者は失敗したらやり直せばよく、後者はやり直せません。1つのバッチ処理にまとめると、どちらの要件も満たさない実装になります。
初期ロードの経路|洗い替えとエイリアス切り替えで無停止にする手順
既存の索引へ直接流し込む方式は、途中で失敗したときに中途半端な状態が検索へ露出します。避けるなら、新しい索引を作ってそこへ投入し、完了後にエイリアスの向き先を差し替えます。アプリケーション側はエイリアス名だけを見ているので、切り替えの瞬間に停止時間は発生しません。
- 新しい索引を作り、マッピングと設定を明示的に指定する
- 投入用の設定へ退避したうえでbulk APIで全件を流し込む
- 件数と代表クエリの結果を旧索引と突き合わせて検証する(検証用クエリの書き方)
- エイリアスの削除と追加を1回の操作でまとめて実行する
- 旧索引を数日残してから削除する
この手順の要点は4番目です。エイリアスの付け替えは削除と追加を同一の操作にまとめられるため、どちらの索引も指していない瞬間が生まれません。既にElasticsearch内にあるデータを作り直すなら、外部から再送するより_reindexで内部的に流したほうが速く終わります。時系列データで索引を定期的に切り替える運用は、ElasticsearchのILMによる5フェーズの索引ライフサイクル設計の側の仕組みへ寄せられます。
差分同期の三方式|二重書き・更新時刻ポーリング・変更データ取得
継続的な反映には3つの方式があります。業務アプリの書き込み時にElasticsearchへも同時に書く二重書き、更新時刻の列を定期的に読んで前回以降の行だけを送るポーリング、データベースのトランザクションログを読む変更データ取得です。
- 二重書き:実装は最も短いが、片方だけ成功した状態が残る
- 更新時刻ポーリング:既存の表に手を入れずに始められる
- 変更データ取得:削除まで拾えるが構成要素が増える
受託の現場で最初に検討するのはポーリングです。更新時刻の列があれば追加開発がほぼ不要で、遅延も分単位に収まります。二重書きは、同期のずれを検知する仕組みを別途持たない限り勧めません。発生順へ積まれるだけで更新されないログ由来のデータなら差分同期の議論自体が不要で、Elasticsearchのログ収集における収集経路と索引分割の側の構成が当てはまります。
削除の反映|論理削除の列を持たない表で消し漏れが出る理由と対処
ポーリング方式には構造的な穴があります。更新時刻で「変わった行」を拾う仕組みは、消えた行を拾えません。削除された行はもう表に存在しないので、次の巡回では何も返らず、検索側には消したはずのデータが残り続けます。
対処は3つのうちどれかです。第1に、業務側を論理削除に変えて削除フラグの立った行を差分として拾い、bulk APIのdeleteへ変換する。第2に、変更データ取得へ切り替えて削除イベントを直接受け取る。第3に、定期的に全件のキー一覧を突き合わせ、検索側にしか存在しないキーを消す照合バッチを回す。第1案が取れないレガシーな表では、第3案を夜間に置く構成が現実的でしょう。個人情報の削除要求が絡む案件で照合を持たずに本番へ出すと、後で作り直しになります。
投入中の設定退避|戻し忘れが事故になる仕組みと復帰の確認手順
大量投入の前に一時的な設定変更を入れる手順は広く知られていますが、戻す側はあまり書かれていません。事故はほぼ戻す側で起きます。
退避してよい設定と、退避してはいけない設定を分ける実務上の判断基準
公式が大規模な初期ロードで挙げているのは2つです。リフレッシュ間隔を-1にして定期的な索引の可視化を止めること、index.number_of_replicasを0にして複製の書き込みを省くこと。どちらも完了後に元へ戻す前提で、戻し先には5秒といった値が例示されています。
判断の線引きは「検索できない時間」と「壊れたときの被害」で引きます。リフレッシュ間隔の退避は、投入中に検索結果へ反映されなくなるだけで、データそのものは失われません。レプリカ0は違います。この状態でノードが1台落ちれば、そのシャードのデータは復旧できず、既に本番で検索を受けている索引にレプリカ0を当ててはいけません。新索引を作って洗い替える経路なら、切り替え前の索引はまだ誰も見ていないので安全に退避できます。これが、前節で洗い替えを勧めた理由でもあります。
復帰の順序|レプリカを戻してからリフレッシュ間隔を戻す理由と確認
戻す順序の根拠は、復帰時の処理負荷です。レプリカ数を戻した瞬間から複製の割り当てと同期が始まり、その間はディスクとネットワークが使われます。リフレッシュ間隔を先に戻すと、複製の同期と定期リフレッシュが同じ時間帯に重なり、投入直後で負荷の高いノードをさらに押します。先にレプリカを戻し、割り当てが正常になったことを確認してからリフレッシュ間隔を戻すのが、この重なりを避ける順序です。
確認は索引の設定を読み出して2つの値を見るだけです。index.number_of_replicasが想定の本数に戻っているか、refresh_intervalが-1のまま残っていないか。戻し忘れの症状は「投入は終わっているのに検索結果に出てこない」という形で現れ、投入処理自体は正常終了しているため切り分けに時間がかかります。復帰処理は正常終了時だけでなく、異常終了時にも必ず走る位置へ置いてください。
bulk APIを自前で組む条件と、公式クライアントへ寄せる場面
ここまでの内容を踏まえると、bulk APIをHTTPで直接組むべきかどうかは条件で決まります。両方あり得るという結論では実装が始まらないので、線を引きます。
採用条件|bulkを自前実装で足りる三つの前提と確認の当て方
自前で組んでよいのは、次の3つがすべて成り立つときです。第1に、投入元が単一で、動作がindexかdeleteに固定されていること。第2に、投入件数の上限が読めていて、バイト数での分割ロジックが数十行で済むこと。第3に、429を受けたら投入全体を止めて待てる運用であること。夜間バッチで数十万件を流す構成なら、この3つはたいてい満たされます。
確認は実装前に当てます。動作が2種類に収まるかは業務側の更新パターンで分かり、件数の上限は既存テーブルの行数と1行あたりのバイト数から見積もれます。3つ目だけは運用側の合意が要るため、要件定義の段階で「投入が遅れたときに待てるか」を確認しておきましょう。
見送る場面|再送と分割をライブラリへ寄せたほうが短く終わる条件
逆に、公式クライアントのヘルパへ寄せたほうが早いのは次の場合です。4動作を混在させる、投入元が複数あって並列度の制御が要る、部分失敗の再送を自動で回したい。自前で書けば分割・並列送信・失敗行の抽出・バックオフ再送の4つを自作することになり、その4つは公式クライアントのヘルパが既に持っています。ここは自前実装を勧めません。
判断が割れやすいのは、Elastic系ではない互換実装へ投入する場合です。bulk APIの書式が共通でも、クライアントライブラリの接続確認で弾かれることがあり、その差はElasticsearchとOpenSearchのライセンスとAPI互換の違いの側で整理しました。投入経路の設計から差分同期の実装、運用への引き継ぎまでを外部に任せたい場合は、データ分析基盤構築・MLOps構築支援で要件定義の段階から引き受けています。
よくある質問
bulk APIの実装で実際に問い合わせの多い点を5つ挙げます。
1回のbulkリクエストに何件入れるのが良いですか?
判断軸は件数ではなくバイト数です。公式の投入速度ガイドは、大きいほうが速く見えても1リクエストあたり数十MBを超えないよう述べています。硬い上限はHTTPリクエストの既定100mbですが、そこまで大きくすると失敗時の再送が重くなります。実装では累計バイト数が閾値を超えたところで区切り、閾値は数MBから始めて429が出ない範囲まで上げてください。
bulkのレスポンスが200なら全件登録できていますか?
いいえ。1つの動作の失敗が残りへ影響しない仕様のため、一部だけが失敗した状態でも応答が正常に返る仕組みです。判定に使うのは応答本文のerrorsで、真なら1件以上が失敗しています。itemsは送信順と同じ並びでステータスを返すので、添字で失敗行を特定できます。通信層のステータスだけで成功と記録する実装は、データ欠損に気づけません。
429が返ったとき、どのくらい待って再送すればよいですか?
固定間隔ではなく、ランダム化した指数バックオフが公式の推奨です。待ち時間を倍々に伸ばしつつ揺らぎを加え、複数ワーカの再送が同時刻へ集中しないようにします。429は受け側のキューが埋まった信号なので、急いでも状況は改善しません。同じバッチで繰り返し429が出る場合は、送信の並列度を落とすか投入時間帯をずらす側で対処します。
データベースの更新をリアルタイムで反映できますか?
反映速度は方式次第です。二重書きなら書き込みと同時に反映できる代わりに、片方だけ成功した状態の検知と復旧を別途作る必要があります。更新時刻ポーリングは実装が軽い一方、巡回間隔ぶんの遅延が残ります。トランザクションログを読む変更データ取得なら秒単位まで詰められますが、構成要素が増える点には注意が必要です。検索結果が数分遅れても業務が回るなら、ポーリングで始めて必要になってから切り替えるのが安全です。
削除したデータが検索結果に残ってしまうのはなぜですか?
更新時刻を基準にした差分同期では、削除を差分として拾えないためです。行が消えた事実はどこにも残らず、次の巡回では何も返りません。対処は、論理削除へ変えて削除フラグを差分として拾う、変更データ取得で削除イベントを受け取る、全件のキーを突き合わせる照合バッチを回す、のいずれかです。既存の表に手を入れられない案件なら3つ目を夜間に置きます。
関連記事
- Elasticsearchとは?転置インデックスとシャード設計・ライセンス系列で決める採用可否:投入先の索引設計の前提
- ElasticsearchのREST API|エンドポイント体系とPUT・POSTの分岐、APIキーとエラー処理:bulk以外の呼び出しと認証
- Elasticsearchのチューニング|メモリ配分とシャード設計・遅いクエリの特定手順:バルクサイズの実測と429の根本対処
- ElasticsearchのILM|5フェーズの設計とmin_ageの起点・データストリームとの使い分け:時系列索引の切り替えの自動化
- ElasticsearchとOpenSearchの違い|ライセンス・API互換・機能差から移行先を決める:互換実装へ投入するときの差分