<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>インデックス on 怠惰技術ブログ</title>
    <link>/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/</link>
    <description>Recent content in インデックス on 怠惰技術ブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Sun, 21 Dec 2025 12:00:00 +0900</lastBuildDate>
    <atom:link href="/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PostgreSQLのpg_trgmで中間一致検索を高速化する仕組みを学ぶ</title>
      <link>/posts/2025-12-21-pgsql-pg-trigm/</link>
      <pubDate>Sun, 21 Dec 2025 12:00:00 +0900</pubDate>
      <guid>/posts/2025-12-21-pgsql-pg-trigm/</guid>
      <description>&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;この記事は、以下の記事を読んで疑問に思ったことを調べた学習記録である。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zenn.dev/team_zenn/articles/zenn-search-tuning-story&#34;&gt;Zennの検索スピードを5倍に高速化した話&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;記事では、Zennのサイト内検索をpg_trgm拡張を使って平均6倍、95パーセンタイルで4.25倍高速化した事例が紹介されている。&lt;/p&gt;
&lt;h2 id=&#34;なぜ中間一致検索は遅いのか&#34;&gt;なぜ中間一致検索は遅いのか&lt;/h2&gt;
&lt;p&gt;通常、PostgreSQLで&lt;code&gt;LIKE &#39;%keyword%&#39;&lt;/code&gt;のような中間一致検索を実行すると、BTreeインデックスが使えずフルスキャンが発生する。BTreeインデックスは文字列の前方一致には有効だが、中間一致では活用できない構造になっているためである。&lt;/p&gt;
&lt;p&gt;データ量が増えると、このフルスキャンが深刻なパフォーマンスボトルネックになる。参考記事では、検索に1秒〜数秒かかる状態だったとのことだ。&lt;/p&gt;
&lt;h2 id=&#34;n-gramインデックスの仕組み&#34;&gt;n-gramインデックスの仕組み&lt;/h2&gt;
&lt;p&gt;n-gramインデックスは、文字列をn文字ずつに分割してインデックス化することで、中間一致検索でもインデックスを効かせる仕組みである。&lt;/p&gt;
&lt;h3 id=&#34;3-gramの例&#34;&gt;3-gramの例&lt;/h3&gt;
&lt;p&gt;「PostgreSQL」という文字列を3-gram（トライグラム）で分割すると以下のようになる。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;__P, _Po, Pos, ost, stg, tgr, gre, reS, eSQL, QL_, L__
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;先頭と末尾にはパディング文字（&lt;code&gt;_&lt;/code&gt;）が付与される。&lt;/p&gt;
&lt;h3 id=&#34;検索時の動作&#34;&gt;検索時の動作&lt;/h3&gt;
&lt;p&gt;「stgre」というキーワードで検索する場合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;検索キーワードを3-gramで分割: &lt;code&gt;stg&lt;/code&gt;, &lt;code&gt;tgr&lt;/code&gt;, &lt;code&gt;gre&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;インデックスから&lt;strong&gt;これらすべてのトライグラムを含む&lt;/strong&gt;文書を抽出&lt;/li&gt;
&lt;li&gt;抽出された候補に対してRecheck処理を実行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重要なのは「いずれか」ではなく「&lt;strong&gt;すべて&lt;/strong&gt;」のトライグラムが存在する文書が候補になる点である。もし「いずれか」だと、無関係な文書が大量に候補に含まれてしまう。&lt;/p&gt;
&lt;h2 id=&#34;recheck処理が必要な理由&#34;&gt;Recheck処理が必要な理由&lt;/h2&gt;
&lt;p&gt;n-gramインデックスでは、インデックスレベルでの検索後に必ずRecheck処理が必要になる。&lt;/p&gt;
&lt;h3 id=&#34;具体例&#34;&gt;具体例&lt;/h3&gt;
&lt;p&gt;以下のような状況を考える。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本文: 「小学校校長」&lt;/li&gt;
&lt;li&gt;クエリ: 「小学校長」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3-gramで分割すると：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「小学校校長」→ &lt;code&gt;小学校&lt;/code&gt;, &lt;code&gt;学校校&lt;/code&gt;, &lt;code&gt;校校長&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;「小学校長」→ &lt;code&gt;小学校&lt;/code&gt;, &lt;code&gt;学校長&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;「小学校」が共通しているため、n-gramレベルでは「小学校校長」が候補として抽出される。しかし実際には「小学校長」という文字列は含まれていない。&lt;/p&gt;
&lt;p&gt;このような&lt;strong&gt;false positive（誤検出）を除外するため&lt;/strong&gt;、インデックスで絞り込んだ候補に対して、実際に検索キーワードが含まれているかを厳密にチェックする必要がある。これがRecheck処理である。&lt;/p&gt;
&lt;h2 id=&#34;pg_trgmとpg_bigmの選択&#34;&gt;pg_trgmとpg_bigmの選択&lt;/h2&gt;
&lt;p&gt;PostgreSQLには2つの主要なn-gram拡張がある。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pg_trgm&lt;/strong&gt;: 3-gram方式、PostgreSQL本体にcontribとして付属&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg_bigm&lt;/strong&gt;: 2-gram方式、サードパーティ製（NECが開発）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;比較表&#34;&gt;比較表&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;機能&lt;/th&gt;
          &lt;th&gt;pg_trgm&lt;/th&gt;
          &lt;th&gt;pg_bigm&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;エコシステム&lt;/td&gt;
          &lt;td&gt;PostgreSQLコミュニティ&lt;/td&gt;
          &lt;td&gt;サードパーティ&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ILIKE対応&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;2文字以下の検索&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Recheck無効化&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;インデックスサイズ&lt;/td&gt;
          &lt;td&gt;小&lt;/td&gt;
          &lt;td&gt;大（約2倍）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;なぜpg_trgmが選ばれたか&#34;&gt;なぜpg_trgmが選ばれたか&lt;/h3&gt;
&lt;p&gt;参考記事では、以下の理由でpg_trgmのみを採用している。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
