商品検索(pg_trgm + Vendure標準検索)実装詳細¶
本プロジェクトでは、PostgreSQL の pg_trgm を活用しつつ、Vendure の
DefaultSearchPlugin(標準検索)をベースに検索を提供します。
検索対象に 商品名を主軸としつつ、補助入力である nameKana(商品名フリガナ)と
keywords(検索キーワード)も含めるため、 DefaultSearchPlugin の拡張点である
searchStrategy を差し替えています。
この実装が「やること / やらないこと」¶
やること(検索の期待挙動)¶
- Vendure の
searchクエリ(標準検索)を利用する - 検索語
termに対して、少なくとも以下を検索対象に含める - 主検索対象:
product_translation.searchNormalizedNameとSearchIndexItem(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_trgmのsimilarity/%演算子などは 現状の検索では未使用- 必要になった場合は、期待挙動(誤字許容度、順位付け、誤検知の許容範囲)を要件化して設計する
仕組み(構成)¶
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 のカスタムフィールド
validate(vendure-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.searchNormalizedKeywordskeywordsの検索専用正規化列- 配列の各要素を
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初期化(リセット + シード)¶
- 依存サービス起動(PostgreSQL / Redis / Mailpit など)
just services- DB リセット + マイグレーション + シード
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.tsがJobQueueService.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\":\"びようえき\"}}' | jqcurl -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登録が前提