Elasticsearchなしで検索機能を?もちろん可能です!
検索機能を実装する際、多くのエンジニアはすぐにElasticsearch(ES)やAlgoliaを思い浮かべるでしょう。確かにこれらは非常に強力です。しかし、もし2GB RAM程度のVPSで運用している場合、Javaベースの重量級サーバーであるESを動かすのは非常に酷な話です。さらに、メインのデータベースと検索エンジンの間でデータを同期(sync data)させるという厄介な問題にも直面することになります。
実のところ、PostgreSQLは非常に強力な「秘密兵器」です。標準搭載されている tsvector と tsquery を活用すれば、本格的なベトナム語検索エンジンを構築できます。すべてを一つのデータベースで完結させられるため、構成がシンプルになり、余分なメモリを消費することもありません。
クイックスタート:5分で検索を試してみよう
理屈をこねる前に、まずはターミナルやpgAdminを開いてみてください。以下のコマンドを実行して、そのレスポンスの速さを体感してみましょう。
-- 1. デモテーブルの作成
CREATE TABLE blog_posts (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT
);
-- 2. サンプルデータの挿入
INSERT INTO blog_posts (title, content) VALUES
('Pythonプログラミング学習', 'PythonはAIにおいて非常に強力な言語です'),
('PostgreSQLの基本', 'PostgreSQLは全文検索を非常にうまくサポートしています'),
('エンジニアへのロードマップ', 'SQL、データ構造、アルゴリズムを学ぶ必要があります');
-- 3. 検索クエリ
SELECT * FROM blog_posts
WHERE to_tsvector('simple', title || ' ' || content) @@ to_tsquery('simple', 'プログラミング');
@@ 演算子が鍵となります。これは、ベクトル化されたデータの中からキーワードを照合します。少量のデータであれば、結果はほぼ瞬時に返ってきます。
tsvector と tsquery を解読する
全文検索(FTS)を使いこなすには、2つの核心的な概念を理解するだけで十分です。
1. tsvector (Text Search Vector)
tsvector は、クリーニングおよび正規化された単語(語彙素/lexemes)のリストだと考えてください。意味のない単語を除去し、各単語の正確な位置を記憶します。
例:SELECT to_tsvector('simple', 'こんにちは itfromzero の皆さん');
結果:'itfromzero':3 'こんにちは':1 '皆さん':4。この位置情報付きリストのおかげで、Postgresは検索時に一文字ずつスキャンする必要がなくなります。
2. tsquery (Text Search Query)
これはクエリ言語です。柔軟な論理演算子を使用できます。
&(AND): 両方の単語を含む必要がある。|(OR): どちらか一方を含めば十分。!(NOT): その単語を含む結果を除外する。<->(FOLLOWED BY): 特定の順序で並ぶフレーズを検索する(熟語の検索に重要)。
‘simple’ 辞書に関する注意点
なぜベトナム語に ‘simple’ を使うのでしょうか?単語の変形(run/runningなど)がある英語とは異なり、ベトナム語は孤立語です。’simple’ 辞書を使用することで、単語の構造をそのまま維持し、Postgresが勝手に語尾をカットして意味を変えてしまうのを防ぐことができます。
応用:数十万レコードに対するパフォーマンスの最適化
テーブルが大きくなるにつれて問題が発生します。Postgresがテーブル全体をスキャン(Seq Scan)し続けると、データベースはやがて過負荷に陥ります。ここで、インデックスが唯一の解決策となります。
1. GIN インデックス – 圧倒的な高速化
GIN(Generalized Inverted Index)は、全文検索に特化したインデックスです。行単位ではなく、個々の単語単位でインデックスを作成します。
CREATE INDEX idx_fts_post ON blog_posts
USING GIN (to_tsvector('simple', title || ' ' || content));
約10万行のテーブルでも、GINインデックスを使用すればクエリ速度を数秒から10ms以下に短縮できます。非常に印象的な数字です。
2. 声調記号なし(アクセントなし)検索の処理
ベトナムのユーザーは、声調記号を付けずに(アクセントなしで)入力することがよくあります。これに対応するために、unaccent エクステンションを使用します。
CREATE EXTENSION IF NOT EXISTS unaccent;
-- プロフェッショナルな手法:関数とインデックスを同期させて作成
CREATE OR REPLACE FUNCTION f_unaccent_vector(text) RETURNS tsvector AS $$
SELECT to_tsvector('simple', unaccent($1));
$$ LANGUAGE SQL IMMUTABLE;
CREATE INDEX idx_fts_unaccent ON blog_posts USING GIN (f_unaccent_vector(title || ' ' || content));
3. Generated Columns – Postgres 12からの必殺技
クエリのたびにベクトルを再計算するのではなく、専用の列に保存しておくべきです。これにより、CPU使用率を大幅に削減できます。
ALTER TABLE blog_posts
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('simple', unaccent(title) || ' ' || unaccent(content))) STORED;
CREATE INDEX idx_search_vector ON blog_posts USING GIN (search_vector);
この場合、検索クエリは非常にシンプルになります:SELECT * FROM blog_posts WHERE search_vector @@ to_tsquery('simple', 'python');
UI/UXを向上させるための実践的なテクニック
いくつかのプロジェクトを経て得られた、検索機能をよりプロフェッショナルにするための3つのテクニックを紹介します:
- 重み付け (Weighting): 本文(Weight B)よりもタイトル(Weight A)で見つかった結果を優先します。ユーザーは探しているものに素早くアクセスできるようになります。
- ランキング (Ranking):
ts_rank関数を使用して、キーワードの密度が高い記事を上位に表示させます。 - ハイライト (Highlight):
ts_headline関数を使用して、返されたテキスト内のキーワードを自動的に強調(<b>タグなど)します。
現在のデータベースの力を最大限に活用する前に、複雑なツールを急いでインストールしないでください。エンジニアリングにおいてKISS(Keep It Simple, Stupid)の原則は常に正しいです。PostgreSQLのFTSを使いこなすことで、システムは軽量になり、メンテナンスが容易で、非常に安定したものになります。
設定に困ったり、大規模なデータセットに対してさらに最適化したい場合は、以下にコメントを残してください。itfromzeroチームが一緒に解決します!

