0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQL vs Elasticsearch:日本語全文検索ベンチマーク大会

0
Last updated at Posted at 2026-07-07

Elasticsearchエンジニア()、RDBを知る

ということで、OSS-DB silverを取ろうと思い、postgreSQLで何かしようってなったら、やっぱ全文検索でしょ!と思い、この記事の執筆にあたりました。

この記事では、PostgreSQLの全文検索がどこまで戦えて、どこからElasticsearchが必要になるのか を、同じ日本語データ(Wikipedia)で実測しました。

対象読者

  • PostgreSQLを勉強中で、全文検索はこれから、という人
  • Elasticsearchの名前は知っているが、全文検索そのものには詳しくない人

結論

速度は「一律どちらが速い」ではなく、クエリの性質で変わる

こういう時 おすすめ ざっくり理由
データが既にPostgreSQLにある/中規模まで/絞り込めるクエリ中心 PostgreSQL(PGroonga) 1システムで完結。少数ヒットのクエリは速い
高頻度語の大量ヒット/前方一致 Elasticsearch 大量マッチのスコアリングと取り出しが最適化
タイポ許容・ハイライト等の検索機能を多用 Elasticsearch 標準機能が手厚い
まず日本語で検索できるか試したい 標準FTSはNG → PGroonga PostgreSQL標準FTSは日本語を語に割れない(後述)

前提知識

📖 そもそも「全文検索」とは?

大量の文書から「この単語を含む文書」を高速に探す仕組みです。

シンプルなやり方
SELECT * FROM docs WHERE body LIKE '%東京%' は、全行を先頭から舐めて部分一致を調べます。件数が増えるほど線形に遅くなり、しかも「どれが一番関連が高いか」の順位づけができません。

全文検索のポイント

  1. 転置インデックス — 「単語 → その単語を含む文書IDの一覧」という対応表を"事前に"作っておく。検索はこの表を引くだけなので速い。本の巻末索引と同じ発想です
  2. トークナイズ(語の切り出し) — 転置インデックスを作るには文章を語に区切る必要があります。英語は空白で切れて楽。日本語は語の間に空白がない(例:「東京都に住む」)ので、ここが最大の難所。方式は2つ:
    • 形態素解析: 辞書を使い「東京都 / に / 住む」と意味のある語に分ける。精度は高いが辞書にない語(新語・固有名詞)に弱い
    • N-gram(bigram): 機械的にN文字ずつ区切る。「東京都」→「東京 / 京都 / 都に…」。取りこぼしは少ないがノイズも増え、インデックスも大きくなりがち
  3. 関連度スコア — ヒットした文書のうち「どれが良い順か」を計算する。代表格が BM25(単語の出現頻度・希少さ・文書長から算出)

PostgreSQLもElasticsearchも、この3点をやっています。違うのは「どこまで作り込めるか」と「運用の形」だけ!

📖 Elasticsearchとは?

検索に特化した全文検索エンジンです。Apache Luceneという検索ライブラリの上に、分散処理・REST API・JSONドキュメント管理を乗せたもの。

  • 転置インデックス・BM25スコアリングを最初から高度に備えている
  • 横にスケールしやすく、大量データの検索・集計が得意
  • ログ解析基盤(いわゆるELK/Elastic Stack)としても広く使われる
  • 日本語は kuromoji(形態素解析)プラグインを入れて対応する

強力な反面、立てて・繋いで・運用する分のコストがかかります。この記事ではそのコストに見合う場面はどこか、も見ます。

📖 PostgreSQLと全文検索の関係は?

PostgreSQLは汎用のリレーショナルDB。多くの場合、アプリのデータはもうここに入っています。だからここで検索まで完結できれば、システムを増やさずに済みます。

PostgreSQLにも標準の全文検索がある
tsvector / tsquery 型とGINインデックスを使う仕組みが標準搭載されています。英語のような空白区切りの言語では、辞書設定を使ってちゃんと動きます。

ただし日本語は標準だと厳しい
PostgreSQL標準には日本語用のトークナイザが入っていません。空白区切り前提の設定(simpleなど)では、空白のない日本語がうまく語に割れず、検索の精度が出ません。※これは後で実際に見せます。

補う拡張が2つ

  • pg_bigm: bigram(N-gram)方式で日本語対応
  • PGroonga: Groonga検索エンジンを組み込む拡張。形態素・N-gramの両対応で機能が豊富。Elasticsearchと殴り合わせるならこちらが本命なので、今回はPGroongaを使います。

PGroongaの魅力は、あくまでPostgreSQLの中で完結すること。SQLの WHERE に検索を書け、JOINやトランザクションもそのまま使えます。


この記事で確かめたい「問い」

比較記事はつい「どっちが優秀か」を競わせがちですが、それはあまり役に立ちません。両者は設計思想が違うので、勝ち負けではなく どこで切り替わるか=境界線 を知りたい。

具体的には、次の軸で「PostgreSQL(PGroonga)で十分な範囲」と「Elasticsearchが効き始める範囲」の境目を実測で探します。

  • 速度: クエリの種類(単語 / フレーズ / AND・OR / 前方一致 / タイポ許容)×データ規模
  • 精度(引き味): 日本語がちゃんと期待どおりに引けるか
  • コスト: インデックスの構築時間・サイズ、そして運用の重さ

検証環境

同じ日本語データを、PGroonga入りPostgreSQLkuromoji入りElasticsearch の両方に投入し、Dockerで並べて計測しました。データは日本語Wikipediaの本文(クリーンなプレーンテキスト)を 1万件 / 10万件 の2スケールで用意し、「規模で挙動がどう変わるか」も見ています。

🔧 バージョン・compose・データの詳細
  • PostgreSQL側: groonga/pgroonga:4.0.6-alpine-18-slim(PostgreSQL 18ベース)。拡張 pgroonga
  • Elasticsearch側: 9.4.3 + analysis-kuromoji プラグイン
  • 実行環境: Docker(WSL2上)
  • データ: Hugging Face の wikimedia/wikipedia(20231101.ja)。wikitext除去済みのクリーンな本文。{id, title, body} のJSONLに整形して両エンジンへ同一投入
# compose.yaml(抜粋)
services:
  postgres:
    image: groonga/pgroonga:4.0.6-alpine-18-slim
    environment:
      POSTGRES_USER: bench
      POSTGRES_PASSWORD: bench
      POSTGRES_DB: bench
    ports: ["5432:5432"]
    shm_size: "1gb"

  elasticsearch:
    build: ./elasticsearch   # FROM elasticsearch:9.4.3 + kuromojiプラグイン
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms2g -Xmx2g
    ports: ["9200:9200"]

PostgreSQL側のインデックス定義:

-- 日本語対応の本命
CREATE INDEX idx_body_pgroonga ON docs USING pgroonga (body);

-- 「素のPostgreSQL標準FTS」比較用(日本語分割不可)
ALTER TABLE docs ADD COLUMN body_tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('simple', body)) STORED;
CREATE INDEX idx_body_tsv ON docs USING gin (body_tsv);

Elasticsearch側のkuromojiアナライザ:

{
  "analyzer": {
    "ja_kuromoji": {
      "type": "custom",
      "tokenizer": "kuromoji_tokenizer",
      "filter": ["kuromoji_baseform", "kuromoji_part_of_speech",
                 "cjk_width", "ja_stop", "kuromoji_stemmer", "lowercase"]
    }
  }
}

検証手順

測る軸はこの4つです。

  • インデックス構築時間(投入後にインデックスを張る時間を計測)
  • インデックスサイズ(ディスク消費)
  • クエリ速度(p50(中央値) / p95 単語・フレーズ・AND・OR・前方一致・タイポの6種)
  • 精度(ヒット件数と中身)

PGroongaは &@(1語)・&@~(クエリ構文)・&^(前方一致)。
Elasticsearchは match / match_phrase / bool / match_phrase_prefix / fuzziness

⚠️ 計測の注意点

フェアに測るために踏んだ工夫と、残る非対称性。

  • 件数は「真の総数」で比較: PostgreSQLは count(*)、ESは track_total_hits: true で全件数える。
  • レイテンシは別計測: 全件カウントは重いので、速度計測はデフォルト設定(上位N件取得)で
  • 非対称①(通信): PostgreSQLはローカルソケット直結、ESはHTTP往復込みで計測している。ESには数msのノイズが乗るため、少数ヒットのクエリでPGが圧勝する絶対値は、エンジン差をやや過大に見せている(ESの10ms前後にはHTTP通信分が含まれる)
  • 非対称②(ソート): ESはBM25でスコア順ソートする、PGroongaの &@ はマッチを返すだけ。ESの方が仕事量が多い
  • なので 絶対値より「クエリ種別ごとの傾向・逆転」に注目してください。桁が変わるような差(前方一致など)は非対称性では説明できない本物の差です

結果と比較

PostgreSQL標準FTSは、日本語で「割れない」

まず一番大事な前提から。PostgreSQL標準のFTS(to_tsvector)は、日本語を語に分割できません。

SELECT to_tsvector('simple', '東京は日本の首都で、多くの企業や大学が集まる。');
-- → '多くの企業や大学が集まる':2 '東京は日本の首都で':1

:1 :2 は「文中の何番目のトークンか」を表す位置情報です。つまりこの文は、たった2つのトークンにしか割れていません。句読点(、。)でしか区切れないので、文節が丸ごと1語になり、「東京」という独立したトークンが存在しないのです。

結果、同じ「東京」で検索しても、標準FTSとPGroongaでこれだけ差が出ます(1万件時点)。

SELECT
  (SELECT count(*) FROM docs WHERE body_tsv @@ to_tsquery('simple','東京')) AS tsvector,
  (SELECT count(*) FROM docs WHERE body &@ '東京') AS pgroonga;
-- → 素のtsvector = 0 / pgroonga = 3395

標準FTSは0件。日本語を語に割れないので「東京」というトークンが索引に無く、何も引けない。一方PGroongaは日本語をちゃんと語に割るので普通に引けます。

つまり、PostgreSQLで日本語の全文検索をやるなら、標準FTSは実質使えず、PGroonga(またはpg_bigm)が前提になります。以降の「PostgreSQL」は基本的にPGroongaを指します。

速度:クエリの性質で勝者が入れ替わる

10万件での計測結果です(各クエリ50回、単位はms)。

クエリ PG p50 PG p95 PGヒット ES p50 ES p95 ESヒット
単語1語(東京) 45.7 48.5 29705 7.8 9.1 28703
フレーズ(機械学習) 0.6 1.2 72 9.7 10.1 72
AND(人工知能 かつ 倫理) 0.9 1.6 42 11.1 12.0 233
OR(犬 または 猫) 3.3 5.4 8530 11.4 12.2 6092
前方一致(プログラ*) 4987.1 5088.4 19 10.8 11.7 4579
タイポ許容(コミニュケーション) 4.1 5.3 0 18.0 18.9 2272

「PGが速い/ESが速い」の一言では語れません。クエリの性質で勝者が変わります

  • 少数ヒットのクエリ(フレーズ・AND)はPGが圧倒的に速い。フレーズ検索は PG 0.6ms vs ES 9.7ms。絞り込めるクエリはPGroongaの圧勝
  • 高頻度語の大量ヒット(単語「東京」=約3万件)はESが速い。PG 45.7ms vs ES 7.8ms で逆転。大量マッチのスコアリングと上位N取得はESが最適化されている。1万件まではPGが速かったですが、10万件で逆転しました
  • 前方一致はESの圧勝(約460倍差)。これは別項で詳しく
  • タイポ許容はESの独擅場。これも別項で

ESの下限が10ms前後なのは前述の通り通信分を含みます。少数ヒットでPGが速い絶対値は割り引いて見てください。それでも桁で速いのは本物です。

インデックスのサイズと構築時間:検索特化ほどコンパクト

インデックスサイズを1万件・10万件で並べると、順序が入れ替わる面白い現象が出ました。

対象 1万件 10万件
PGroonga 251.8 MB 1480 MB
Elasticsearch 155.1 MB 1807 MB
素のtsvector GIN 312 MB 2437 MB
  • 1万件では ES < PGroonga < GIN、10万件では PGroonga < ES < GIN に逆転。ESは固定オーバーヘッド(各種メタ情報)が大きく小規模では有利だが、データが増えるとPGroongaの素の索引の方がコンパクトになりました。
  • どの規模でも tsvector GINが最大。汎用インデックスなので検索特化ほど詰められません。

構築時間(10万件)はさらに象徴的でした。

対象 構築時間
PGroonga(body索引) 98.6秒
tsvector(列生成+GIN索引) 146.5秒 + 228.0秒
Elasticsearch(投入込み) 252.3秒

tsvector GINは構築が最も重く(合計6分超)、サイズも最大、しかも日本語では機能しない。 「コストは最大なのに使えない」という結果です。日本語では最初からPGroongaかESを選ぶべき、と数字が言っています。

前方一致:PGroongaの弱点(issue #96)

表で突出していた PGの前方一致 4987ms。これはPGroonga側の既知の制約です。EXPLAIN ANALYZE で正体が分かります(1万件時点)。

EXPLAIN ANALYZE SELECT id FROM docs WHERE body &^ 'プログラ' LIMIT 20;

 Limit ...
   ->  Seq Scan on docs  (actual time=15.5..2428.2 rows=6)
         Filter: (body &^ 'プログラ'::text)
         Rows Removed by Filter: 9994
 Execution Time: 2428.352 ms

注目は Seq Scan(全表走査)Rows Removed by Filter: 9994。前方一致 &^ なのに PGroongaインデックスが使われず、1万行を総当たりで舐めて9994行を捨てている。だからデータ量に比例して激遅になり、10万件では約5秒に膨れました。

原因はPGroonga公式の issue #96 でメンテナが説明しています。
要約すると、前方一致でも内部で「全文検索用(トークナイズ済み)インデックス」を使ってしまい、前方一致に適した索引が選択されない。トークナイズ済みの索引は語に割れているため、前方一致がうまく当たらないのです。

回避策として「トークナイザーを指定しない索引」を作れば前方一致は効きますが、その索引では今度は全文検索ができなくなります。全文検索用(トークナイズあり)と前方一致用(トークナイズなし)は要件が逆で、1つの索引で両立しにくい、という設計上のトレードオフです。

一方 ESの match_phrase_prefix は10.8msで4579件を正しく返します。前方一致を多用するなら、この一点だけでもESを選ぶ理由になります。

issue自体は古いバージョン(2019年頃)のものですが、本検証の現行 PGroonga 4.0.6 でも Seq Scan が再現しており、素直に使う限りこの制約は生きています。索引を作り込めば改善の余地はあるはずですが、「素の &^ はそのままでは効かない」というのが実測の結論です。

タイポ許容:Elasticsearchのfuzzinessが効く領域

同じタイポ「コミニュケーション」(正しくは「コミュニケーション」)を両エンジンに投げました。

エンジン クエリ ヒット
PostgreSQL body &@~ 'コミニュケーション' 0
Elasticsearch match + fuzziness: AUTO 2272

PGroongaの素の演算子は完全一致ベースなのでタイポを一切拾えず0件。ESは fuzziness で綴りのゆらぎを吸収し、正しい方の文書2272件を拾いました。「打ち間違いに強い検索窓」を素直に作りたいなら、ESの fuzziness は一発で効きます。PGroongaでも曖昧検索はできますが別の設定・構文が必要で、この手軽さの差は実務で効いてきます。

補足:素直なクエリでも、大規模だと結果がズレる

小規模(数千件)では単語・フレーズ・AND・ORでPGとESのヒット件数が一致していたのですが、10万件の実データでは ANDで PG 42件 vs ES 233件 と乖離が出ました。PGroonga(MeCab)とES(kuromoji)の形態素解析の辞書・分割の違いが、多様な実語彙で顕在化したものと考えられます。どちらが「正しい」かは用途次第で、ここは今回は深掘りしていません。ただ 「形態素エンジンが違えば、素直なクエリでも拾う範囲は微妙に変わる」 ことは頭に入れておく必要があります。


考察:境界線はどこか

実測をまとめると、境界線は「規模」だけでなく クエリの性質 で引けます。

PostgreSQL(PGroonga)で十分な領域

  • データが既にPostgreSQLにあり、検索のためだけに別システムを増やしたくない
  • 中規模まで(今回の10万件級なら余裕)
  • 検索が 絞り込める(フレーズ・条件ANDなど、少数ヒット中心)。この帯域はPGが桁違いに速い
  • SQLのJOINやトランザクションと検索を混ぜたい

Elasticsearchが効き始める領域

  • 高頻度語で大量ヒットする検索が多い(大量マッチのスコアリングと取り出しで有利)
  • 前方一致を多用する(PGroongaは索引が効かずSeq Scanに落ちる/issue #96)
  • タイポ許容を手軽に入れたい(fuzziness 一発)
  • ファセット集計・ハイライトなど検索特化機能を使い込む
  • データがさらに大規模で、検索が主役のワークロード

運用コストも忘れずに。PGroongaは「1システムで完結」が最大の武器で、DBを1つ運用するだけ。ESは強力ですが、別システムを立てて・同期して・監視する分の重さが乗ります。「検索の要件がまだ軽いうちはPGroongaで十分、要件が検索特化に振れてきたらESへ」 というのが素直な移行ラインです。


まとめ:使い分け早見表

実測を経て

シーン 選ぶべき 決め手(実測)
日本語で全文検索したい(PostgreSQL標準FTS) 不可 → PGroongaへ 標準FTSは日本語が割れずヒット0
絞り込めるクエリ(フレーズ・AND)中心・中規模まで PostgreSQL(PGroonga) フレーズ 0.6ms vs ES 9.7ms
高頻度語の大量ヒットが多い Elasticsearch 単語「東京」PG 45.7ms vs ES 7.8ms
前方一致を多用 Elasticsearch PG 4987ms(Seq Scan)vs ES 10.8ms
タイポ許容を手軽に Elasticsearch PG 0件 vs ES 2272件
とにかく運用を軽くしたい PostgreSQL(PGroonga) 1システムで完結

「PostgreSQLの全文検索はどこまで戦えるか?」の答えは、「絞り込めるクエリと中規模までなら十分すぎるほど戦える。ただし高頻度語の大量ヒット・前方一致・タイポ許容の3つはElasticsearchの土俵」 でした。


あとがき

OSSDBの勉強だ!と意気込んで環境作って遊んでいましたが、この辺の知識って試験に出るの?
タイトルでvsと銘打ったのに、結局適材適所、に逃げてしまいました。すみません。
てか、全文検索に詳しくなっただけでは。。。

🛠 ハマったところ・構築Tips

検証中に踏んだ地雷をまとめておきます。

  • PGroongaのインデックスサイズが pg_relation_size で0になる: PGroongaは実データをGroonga独自ファイル(pgrn*)に持つため、pg_relation_size では測れない。du で実ファイルを合算する
  • Wikipediaデータの入手: 以前使えた abstract ダンプは2025年2月に廃止、旧CirrusSearchダンプも2026年に非推奨化。配置がよく変わるので、安定して入手できる Hugging Faceの wikimedia/wikipedia を使うのが楽(wikitext除去済み・streaming対応)

参考リンク


計測はあくまで単一ノード・ローカル環境での一例です。PostgreSQL・ES双方ともチューニング余地があり、数字は環境で変わります。ワークロードで実測して判断するのが確実です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?