コンテンツにスキップ

商品検索(pg_trgm + Vendure標準検索)実装詳細

本プロジェクトでは、PostgreSQL の pg_trgm を活用しつつ、Vendure の DefaultSearchPlugin(標準検索)をベースに検索を提供します。

検索対象に 商品名を主軸としつつ、補助入力である nameKana(商品名フリガナ)と keywords(検索キーワード)も含めるため、 DefaultSearchPlugin の拡張点である searchStrategy を差し替えています。

この実装が「やること / やらないこと」

やること(検索の期待挙動)

  • Vendure の search クエリ(標準検索)を利用する
  • 検索語 term に対して、少なくとも以下を検索対象に含める
  • 主検索対象: product_translation.searchNormalizedNameSearchIndexItem(Vendure標準検索インデックス)のバリアント名 / 説明 / SKU(商品番号)
  • 補助検索対象: Product のカスタムフィールド nameKana と検索専用列 searchNormalizedKeywords
  • かな/カナ(ひらがな/全角カタカナ/半角カナ)の揺れは、クエリ側で NFKC 正規化のうえ「そのまま / ひらがな→カタカナ / カタカナ→ひらがな / 全角カタカナ→半角カナ」を展開して OR で吸収する
  • ILIKE を用いた部分一致検索を行う(pg_trgm は主に性能改善のために利用)
  • 結果順位は pg_trgm を維持したまま、完全一致 > 前方一致 > 部分一致商品名 > SKU > nameKana > keywords > その他 の重み付けで安定化する

pg_trgm による簡易曖昧検索(概要)

本プロジェクトの「簡易曖昧検索」は、pg_trgm の trigram インデックス + ILIKE を前提とした部分一致検索です。検索語は NFKC 正規化で半角カナなどを寄せたうえで、かな/カナ揺れを クエリ展開して OR 条件で吸収し、product_translation.searchNormalizedName / product.searchNormalizedKeywords / search_index_item / product.customFieldsNamekana に対して一致判定を行います。

できること / できないこと(差分を明示)

区分 内容
できること 部分一致(ILIKE)での商品名/説明/SKU検索
できること かな/カナ揺れの吸収(クエリ展開による OR 条件)
できること 商品名主検索 + nameKana / keywords の補助検索
できること trigram インデックスによる ILIKE の高速化
できないこと 類義語(synonym)展開
できないこと 検索語のハイライト表示
できないこと 誤字耐性(similarity / % 演算子)

できること

  • 部分一致ILIKE)で商品名 / 説明 / SKU(商品番号)にヒットさせる
  • かな/カナ揺れは クエリ展開で吸収する
  • まず NFKC 正規化で半角カナを全角へ寄せる
  • そのうえで そのまま / ひらがな→カタカナ / カタカナ→ひらがな / 全角カタカナ→半角カナ
  • pg_trgm により ILIKE 検索の速度を改善する
  • pg_trgm を維持したまま SQL の score 計算で順位品質を改善する

できないこと(別途実装が必要)

  • 類義語(synonym)展開
  • ハイライト(検索語の強調表示)
  • 誤字耐性(similarity / % 演算子による曖昧一致)

これらが必要になった場合は、要件として切り出して別途設計してください。

やらないこと(別対応が必要)

  • 漢字↔読みの自動変換(例: 美容液ビヨウエキ
  • 読みでヒットさせたい場合は、nameKana に読み(全角カタカナ)を登録する運用とする
  • 同義語(synonym)自動展開
  • 必要になった場合は、要件として切り出して別途設計する(標準検索 + pg_trgm だけでは解決しない)
  • typo(誤字)を吸収する曖昧一致(類似度検索)
  • pg_trgmsimilarity / % 演算子などは 現状の検索では未使用
  • 必要になった場合は、期待挙動(誤字許容度、順位付け、誤検知の許容範囲)を要件化して設計する

仕組み(構成)

1. 検索戦略(SearchStrategy)

DefaultSearchPlugin.init({ searchStrategy }) により、Postgres向けの検索戦略を差し替えています。

  • 実装: apps/vendure-server/src/search/pg-trgm-search-strategy.ts
  • 方式: ILIKE を中心に検索し、pg_trgm の trigram インデックスが有効な環境では高速化を期待できる
  • nameKana / keywords を検索対象に含めるため、SearchIndexItem に加えて Product / ProductTranslation を join する
  • 商品名と keywords は検索専用の正規化列へ同期し、SMILE 由来で表記揺れが起きうる半角カナ を表示値と切り離して吸収する
  • 正規化列が未同期の一時状態でも、既存の productName / customFieldsKeywords へフォールバックして検索停止を避ける

2. nameKana(商品名フリガナ)

商品 (Product) のカスタムフィールド nameKana に、商品名の読みを 全角カタカナ で登録します。

  • 目的: 漢字↔読み の揺れを検索で吸収するための検索用データ
  • 位置づけ: 運用ユーザーが登録する補助 override。商品名検索の主軸ではなく、辞書なしでも読み検索を成立させるための補助手段
  • 入力ルール: 全角カタカナのみ(半角カナ・ひらがなはバリデーションで拒否)
  • Vendure のカスタムフィールド validatevendure-config.shared.ts)で検証する
  • 定義: apps/vendure-server/src/vendure-config.shared.ts
  • 既存DB反映用マイグレーション: apps/vendure-server/src/migrations/20260106124000_add_product_name_kana_custom_field.ts

3. keywords(検索キーワード)

商品 (Product) のカスタムフィールド keywords に、検索でヒットさせたい語を登録します。

  • 目的: 商品名や説明文に含まれない検索用キーワードを運用で補う
  • 想定流入元: 運用入力に加え、SMILE 由来の表記揺れを含む補助語句
  • 保存方針: 検索時は product.searchNormalizedKeywords に同期した NFKC 正規化済み値を参照する
  • 定義: apps/vendure-server/src/vendure-config.shared.ts

4. 検索専用正規化列

元の表示値や運用値を壊さずに半角カナ混入だけを検索面で吸収するため、次の列を別保持する。

  • product_translation.searchNormalizedName
  • 商品名の検索専用正規化列
  • ProductEvent を契機に、商品翻訳名を NFKC 正規化して同期する
  • product.searchNormalizedKeywords
  • keywords の検索専用正規化列
  • 配列の各要素を NFKC 正規化し、空白区切りの単一テキストとして同期する

データベース(pg_trgm)

pg_trgm の導入とインデックス付与はマイグレーションで行います。

  • マイグレーション: apps/vendure-server/src/migrations/20260108193000_enable_pg_trgm_and_indexes.ts
  • CREATE EXTENSION IF NOT EXISTS pg_trgm;
  • search_index_item(標準検索インデックス)と product / product_translation(nameKana / 正規化済み商品名 / 正規化済み keywords)に GIN trigram インデックスを作成

運用手順

事前条件(ビルド運用)

Vendure プラグイン群(@ritsubi/plugins)は ビルド前提です。(検索戦略自体は apps/vendure-server 側ですが、起動前にリポジトリ標準のビルド運用を守ってください)

  • pnpm -C packages/plugins build

ローカルDB初期化(リセット + シード)

  1. 依存サービス起動(PostgreSQL / Redis / Mailpit など)
  2. just services
  3. DB リセット + マイグレーション + シード
  4. DATABASE_URL=postgres://vendure:vendure_dev_password@localhost:5433/vendure_dev \\ STOREFRONT_URL=http://localhost:3001 \\ EMAIL_FROM=no-reply@ritsubi.local \\ SUPERADMIN_USERNAME=ec-admin@ritsubi.local \\ SUPERADMIN_PASSWORD=SuperAdmin123! \\ just vendure-db-reset-clean

補足:

  • 本リポジトリでは src/seed-data.tsJobQueueService.start() を行い、search の reindex ジョブが 完了するまで待機してから終了します。(ジョブキューが動いていないと search_index_item が生成されず、検索結果が 0 件になります)

keywords の入力

  • Vendure Dashboard の商品編集画面にある「検索キーワード」へ入力します。
  • 複数指定する場合は、UI の入力仕様に従って(複数要素として)登録します。
  • シードCSVでは product:keywords| 区切りで指定しています(リスト化される前提)。

nameKana の入力

  • Vendure Dashboard の商品編集画面にある「商品名フリガナ」へ入力します。
  • 入力は 全角カタカナのみ(半角カナ・ひらがなはバリデーションで拒否)です。

検索インデックス再構築(必要時)

標準検索のインデックス再構築は、Admin API の reindex(Job)で実行できます。

ローカル確認(Shop API)

検索結果が返ること(0件にならないこと)を確認します。

  • かな/カナ揺れ(例: びようえき / ビヨウエキビヨウエキ
  • curl -sS http://localhost:3030/shop-api -H 'content-type: application/json' --data-binary '{\"query\":\"query($term:String!){search(input:{term:$term,take:10}){totalItems items{productName slug sku}}}\",\"variables\":{\"term\":\"びようえき\"}}' | jq
  • curl -sS http://localhost:3030/shop-api -H 'content-type: application/json' --data-binary '{\"query\":\"query($term:String!){search(input:{term:$term,take:10}){totalItems items{productName slug sku}}}\",\"variables\":{\"term\":\"ビヨウエキ\"}}' | jq
  • SKU(商品番号)
  • curl -sS http://localhost:3030/shop-api -H 'content-type: application/json' --data-binary '{\"query\":\"query($term:String!){search(input:{term:$term,take:10}){totalItems items{productName slug sku}}}\",\"variables\":{\"term\":\"EXV-CL-001\"}}' | jq

期待挙動の例

  • 商品名が 美容液 で、nameKanaビヨウエキ の場合
  • びようえき / ビヨウエキ / ビヨウエキ で検索するとヒットしやすい(かな/カナ揺れはクエリ展開で吸収)
  • 商品名が ビヨウエキ美容液 のように SMILE 由来で半角カナを含む場合
  • びようえき / ビヨウエキ / ビヨウエキ のいずれでも searchNormalizedName 経由の商品名主検索でヒットするようにする
  • keywordsビヨウエキ のような半角カナが登録されている場合
  • びようえき / ビヨウエキ / ビヨウエキ のいずれでも searchNormalizedKeywords 経由の補助検索でヒットするようにする
  • 美容液ビヨウエキ のような自動変換は行わないため、読みでヒットさせるには nameKana 登録が前提