開発・技術選定

全文検索を実装する — LIKE から始めて、どこで限界が来るか

検索機能は、最初から検索エンジンを立てる必要はありません。
段階的に上げていくのが現実的です。それぞれの限界を整理します。

段階1:LIKE(〜数千件)

qs = Post.objects.filter(
    Q(title__icontains=q) | Q(body__icontains=q)
)

数千件までは体感で問題ありません。まずこれで出すのが正解です。

限界は次の3点です。

  • 件数が増えると遅くなる(インデックスが効かない中間一致)
  • 表記ゆれに対応できない(「サーバ」と「サーバー」)
  • 関連度順に並べられない

段階2:PostgreSQL の trigram(〜数十万件)

中間一致にインデックスを効かせられます。

CREATE EXTENSION pg_trgm;
CREATE INDEX idx_post_title_trgm ON post USING gin (title gin_trgm_ops);
from django.contrib.postgres.search import TrigramSimilarity

qs = (Post.objects
      .annotate(sim=TrigramSimilarity("title", q))
      .filter(sim__gt=0.1)
      .order_by("-sim"))

あいまい一致と関連度順が同時に手に入るのが利点です。
タイプミスにもある程度耐えます。

段階3:全文検索インデックス

PostgreSQL の tsvector を使う方法もありますが、
日本語では単語の区切りが判定できないという根本的な問題があります。

標準の設定では「東京都」を1語として扱えず、期待した検索結果になりません。
形態素解析の拡張を入れるか、次の段階へ進むことになります。

段階4:専用の検索エンジン

日本語をきちんと扱うなら、形態素解析器を組み込んだ検索エンジンが必要です。
ただし運用対象が1つ増えるので、本当に必要になってから導入します。

判断の目安はこうしています。

状況 選択
〜数千件、完全一致で足りる LIKE
〜数十万件、あいまい検索が要る trigram
関連度・絞り込み・件数が本格的 検索エンジン

日本語で共通して必要な正規化

どの段階でも、検索前に文字列を揃えておくと精度が上がります。

import unicodedata

def normalize(s: str) -> str:
    s = unicodedata.normalize("NFKC", s)   # 全角英数→半角、半角カナ→全角
    return s.casefold().strip()

NFKC 正規化を入れるだけで、「Django」と「django」が一致するようになります。
これはどの段階でも効くので、最初から入れておきます。

検索ログを残す

何が検索されているかを記録すると、改善点が見えます。

SearchLog.objects.create(query=q, hits=qs.count())

ヒット0件の検索語が、そのまま「足りないコンテンツ」の一覧になります。

まとめ

  • まず LIKE で出す。数千件までは十分
  • 中間一致とあいまい検索が要るなら trigram
  • 日本語の形態素解析が要る段階で初めて検索エンジンを検討する
  • NFKC 正規化はどの段階でも効くので最初から入れる
  • ヒット0件のログが改善のヒントになる