personalization-engine について
personalization-engine は、EC チーム向けの商品レコメンド導入ガイドです。プラットフォーム別のツール選定、推薦方式、除外条件、効果測定に加え、カスタム構築やヘッドレスストアの方法も扱います。
主な機能
- 01プラットフォーム別のツール選定
- 02協調フィルタリングと類似商品推薦の設定
- 03在庫切れ・購入済み・閲覧中商品の除外
- 04クリックやカート追加による効果測定
活用シーン
- 01商品ページに「よく一緒に購入される商品」を追加
- 02再訪顧客向けホームページのパーソナライズ
- 03ヘッドレスストア向けレコメンド API の構築
- 04売上やカート追加率への影響をテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill personalization-engineインストール後、Claude Code で/personalization-engineを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/browsing-history-recommendations-with-co/criteria.jsontask.mdevals/co-purchase-matrix-and-fbt-recommendatio/criteria.jsontask.mdevals/unified-recommendation-api-with-redis-ca/criteria.jsontask.md詳細
パーソナライゼーションエンジン
概要
パーソナライズされた商品レコメンデーションを表示すると、各顧客に最も関連性の高い商品を提示できるため、平均注文額とセッションの閲覧深度が向上します。「よく一緒に購入されている商品」、「こちらもおすすめ」、パーソナライズされたホームページのセクションは、いずれもレコメンデーションの一種です。主要なプラットフォームには、カスタムコードなしで協調フィルタリングやレコメンデーションアルゴリズムを扱えるアプリがあります。カタログの規模、トラフィック量、またはレコメンデーションのロジックがアプリベースのソリューションで対応できる範囲を超える場合にのみ、カスタムのレコメンデーションエンジンを構築してください。
このスキルを使用する場面
- 商品ページに「よく一緒に購入されている商品」や「こちらもおすすめ」のカルーセルを追加する場合
- リピーター向けのパーソナライズされたホームページを実装する場合
- モバイルアプリやヘッドレスストアフロント向けのレコメンデーション API を構築する場合
- コールドスタート時(履歴がない場合)のレコメンデーションで、関連性の低い商品が返される場合
- パーソナライゼーションが AOV とセッションあたりの収益に与える影響を A/B テストする場合
基本的な手順
ステップ 1:プラットフォームを特定し、適切なレコメンデーションツールを選択する
| プラットフォーム | 推奨ツール | 理由 |
|---|---|---|
| Shopify | LimeSpot または Code Black Belt の Frequently Bought Together | LimeSpot は ML を活用した、パーソナライズされたホームページ、PDP、カート、購入後のレコメンデーションを提供します。Frequently Bought Together は PDP 専用に設計されています |
| WooCommerce | YITH WooCommerce Frequently Bought Together または LimeSpot | YITH は最も人気があり、LimeSpot は ML ベースのレコメンデーションで WooCommerce をサポートしています |
| BigCommerce | LimeSpot または Boost AI Search & Discovery | どちらもパーソナライズされたレコメンデーションを提供し、BigCommerce App Marketplace から利用できます |
| カスタム/ヘッドレス | 同時購入マトリックスとコサイン類似度を使って構築 | アルゴリズム、除外ロジック、API レスポンス形式を完全に制御するには、この方法が必要です |
ステップ 2:プラットフォーム別のセットアップ
Shopify
オプション A:LimeSpot(推奨 — パーソナライゼーションを網羅したスイート)
- Shopify App Store から LimeSpot Personalizer をインストールします
- LimeSpot は注文履歴と閲覧データを自動的に分析し、レコメンデーションモデルを構築します
- レコメンデーションウィジェットの配置を設定します:
- LimeSpot → Recommendations → Configure placements に移動します
- 次の項目を有効にします:PDP(「よく一緒に購入されている商品」)、カート(「こちらの商品も購入されています」)、ホームページ(「あなたへのおすすめ」)
- 配置ごとにレコメンデーションアルゴリズムを選択します:
- よく一緒に購入されている商品:アイテム間協調フィルタリング(注文内での共起に基づく)
- あなたへのおすすめ:ユーザーとアイテムの協調フィルタリング(閲覧履歴に基づく)
- 類似商品:コンテンツベースフィルタリング(同じカテゴリ、近い価格、類似した属性)
- 除外項目を設定します。在庫切れの商品と、最近購入された商品は必ず除外してください
オプション B:Code Black Belt の Frequently Bought Together(PDP に特化)
- App Store からインストールします
- アプリが過去の注文を分析し、一緒に購入されることが最も多い商品を自動的に特定します
- PDP に「よく一緒に購入されている商品」セクションを表示し、セット割引オプションを提供します
- 手動での設定は不要です。モデルは注文データに基づいて自動的に更新されます
テストと測定:
- LimeSpot では、Analytics → A/B Tests に移動し、レコメンデーションアルゴリズムを比較します
- レコメンデーション枠ごとにクリックスルー率とカート追加率を追跡します
- 結論を出す前に、十分なトラフィックを確保したうえで、各テストを少なくとも 2 週間実施します
WooCommerce
YITH WooCommerce Frequently Bought Together(無料/プレミアム):
- WordPress プラグインディレクトリからインストールします
- YITH → Frequently Bought Together → Settings に移動します
- レコメンデーション方法を選択します:自動(注文履歴に基づく)または手動(商品ごとに指定)
- 表示を設定します(商品詳細ページでの表示位置、表示する商品数、割引タイプなど)
- 無料版では商品を手動で割り当てられます。プレミアム版では注文履歴を利用して自動的に提案を生成します
LimeSpot for WooCommerce:
- LimeSpot Personalizer for WooCommerce をインストールします
- Shopify 版と同じ方法で設定します。ホームページ、商品詳細ページ、カートでのレコメンデーションをすべて利用できます
WooCommerce の組み込みクロスセルとアップセル:
- 任意の商品で、Linked Products タブに移動します
- アップセル商品(商品詳細ページに表示)とクロスセル商品(カートに表示)を手動で追加します
- 手動での設定ですが、商品同士の関係をよく把握している小規模なカタログでは効果的です
BigCommerce
LimeSpot for BigCommerce:
- BigCommerce App Marketplace からインストールします
- Shopify/WooCommerce 版と同じ設定および機能を利用できます
Boost AI Search & Discovery:
- App Marketplace からインストールします
- 検索機能に加えて、レコメンデーションウィジェットも含まれています
- 「Frequently Bought Together」と「Similar Products」のセクションを設定します
カスタム/ヘッドレス
ヘッドレスストアフロントでは、応答を高速化するために、事前計算した類似度行列を使ってレコメンデーションパイプラインを構築します:
// lib/recommendations.ts
// Step 1: Pre-compute co-purchase matrix nightly (run as a cron job)
// PostgreSQL: count how often each product pair appears in the same order
export const coPurchaseMatrixSQL = `
INSERT INTO product_co_purchases (product_a_id, product_b_id, co_purchase_count, updated_at)
SELECT
a.product_id AS product_a_id,
b.product_id AS product_b_id,
COUNT(DISTINCT a.order_id) AS co_purchase_count,
NOW() AS updated_at
FROM order_items a
JOIN order_items b ON a.order_id = b.order_id AND a.product_id < b.product_id
GROUP BY a.product_id, b.product_id
HAVING COUNT(DISTINCT a.order_id) >= 3 -- Minimum confidence threshold
ON CONFLICT (product_a_id, product_b_id)
DO UPDATE SET co_purchase_count = EXCLUDED.co_purchase_count, updated_at = NOW();
`;
// Step 2: Serve "Frequently Bought Together" from the pre-computed matrix
export async function getFrequentlyBoughtTogether(
productId: string, limit = 6, excludeProductIds: string[] = []
): Promise<Product[]> {
const pairs = await db.productCoPurchases.findMany({
where: {
OR: [{ productAId: productId }, { productBId: productId }],
NOT: { OR: [{ productAId: { in: excludeProductIds } }, { productBId: { in: excludeProductIds } }] },
},
orderBy: { coPurchaseCount: 'desc' },
take: limit * 2, // Fetch extra to filter out-of-stock
});
const relatedIds = pairs.map(p => p.productAId === productId ? p.productBId : p.productAId);
const products = await db.products.findMany({ where: { id: { in: relatedIds }, status: 'active', inventoryQuantity: { gt: 0 } } });
return products.slice(0, limit);
}
// Step 3: Browsing history recommendations using product vectors
function buildProductVector(product: Product, allCategoryIds: string[], allTags: string[]): number[] {
const categoryEncoding = allCategoryIds.map(id => product.categoryId === id ? 1 : 0);
const priceNormalized = product.priceInCents / 100000; // Normalize to 0-1
const tagEncoding = allTags.map(tag => product.tags.includes(tag) ? 1 : 0);
return [...categoryEncoding, priceNormalized, ...tagEncoding];
}
export async function getRecommendationsFromBrowsingHistory(
sessionProductIds: string[], limit = 8
): Promise<Product[]> {
if (sessionProductIds.length === 0) return getBestSellers(limit);
const [browsedProducts, allProducts, allCategoryIds, allTags] = await Promise.all([
db.products.findMany({ where: { id: { in: sessionProductIds } } }),
db.products.findMany({ where: { status: 'active', inventoryQuantity: { gt: 0 }, id: { notIn: sessionProductIds } }, take: 500 }),
db.categories.findMany({ select: { id: true } }).then(cats => cats.map(c => c.id)),
db.productTags.findMany({ distinct: ['tag'] }).then(tags => tags.map(t => t.tag)),
]);
// Build "taste vector" by averaging vectors of browsed products
const vectors = browsedProducts.map(p => buildProductVector(p, allCategoryIds, allTags));
const tasteVector = vectors[0].map((_, i) => vectors.reduce((sum, v) => sum + v[i], 0) / vectors.length);
// Rank all products by cosine similarity to taste vector
const ranked = allProducts
.map(p => {
const vec = buildProductVector(p, allCategoryIds, allTags);
const dot = tasteVector.reduce((sum, val, i) => sum + val * vec[i], 0);
const mag = Math.sqrt(tasteVector.reduce((s, v) => s + v * v, 0)) * Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
return { product: p, score: mag > 0 ? dot / mag : 0 };
})
.sort((a, b) => b.score - a.score);
return ranked.slice(0, limit).map(r => r.product);
}
// Step 4: Unified recommendation API with caching
export async function getRecommendations(context: 'pdp' | 'homepage' | 'cart', productId?: string, sessionProductIds: string[] = []) {
const cacheKey = `recs:${context}:${productId ?? 'none'}:${sessionProductIds.slice(0, 3).join('-')}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
let products: Product[];
switch (context) {
case 'pdp':
products = await getFrequentlyBoughtTogether(productId!, 6, [productId!]);
if (products.length < 4) {
const extra = await getRecommendationsFromBrowsingHistory(sessionProductIds, 4 - products.length);
products = [...products, ...extra];
}
break;
case 'homepage':
products = sessionProductIds.length > 0
? await getRecommendationsFromBrowsingHistory(sessionProductIds, 12)
: await getBestSellers(12);
break;
default:
products = await getBestSellers(8);
}
await redis.setex(cacheKey, 300, JSON.stringify(products)); // 5-minute cache
return products;
}
顧客数が 10k 以上のストアの場合: BG/NBD + ALS 協調フィルタリング(Python の implicit ライブラリ)を使用すると、コサイン類似度を使う方法よりも、ユーザーと商品のレコメンデーション精度が大幅に向上します。
ステップ 3:レコメンデーション対象から除外する項目を設定する
すべてのレコメンデーションエンジンで、次の項目を除外する必要があります:
- 在庫切れの商品 — 顧客が購入できない商品は決しておすすめしないでください
- 現在表示中の商品 — 商品詳細ページの「Related Products」から、現在表示中の商品を除外します
- 最近購入された商品 — 先週購入した商品を表示すると、顧客に悪い体験を与えていることが伝わってしまいます
LimeSpot の場合: Settings → Exclusions で除外項目を設定します。在庫切れの商品は自動的に除外されます。
Frequently Bought Together の場合: アプリが在庫切れのバリエーションをバンドルの提案から自動的に非表示にします。
カスタム実装の場合: 現在の商品と顧客が最近購入した商品を含む excludeProductIds を、すべてのレコメンデーション関数に渡します。
ステップ 4:レコメンデーションのパフォーマンスを測定する
以下の指標を毎週確認します:
| 指標 | 目安 | 確認場所 |
|---|---|---|
| 「Frequently Bought Together」のクリック率 | 5~15% | LimeSpot Analytics またはカスタムトラッキング |
| レコメンデーション経由のカート追加率 | 3~8% | LimeSpot Analytics |
| レコメンデーション経由の売上 | 総売上の 5~20% | LimeSpot/Tidio のアトリビューションレポート |
ベストプラクティス
- ゼロから構築する前にアプリを使う — LimeSpot と Frequently Bought Together は十分に調整されており、正しく実装するのに数週間かかるエッジケース(コールドスタート、在庫切れ、最近購入された商品の除外)にも対応しています
- 併買行列を毎晩更新する — 新しい注文によって、一緒によく購入される商品が変わります。データが古くなると、レコメンデーションの品質が低下します
- 在庫切れの商品はモデル構築時ではなく、クエリ実行時に除外する — 在庫はレコメンデーションモデルの更新よりも速く変化します
- レコメンデーションカルーセルの商品数は 6~8 点に制限する — 8 点を超えると選択肢が多すぎて決められなくなり、クリック率が低下します
- フィードバックループを実装する — レコメンデーション枠ごとのクリック率とカート追加率を追跡し、アルゴリズムのバリエーションを継続的に比較します
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| おすすめに毎回同じ人気商品が表示される | 多様性の制約を追加します。おすすめ枠のうち、特定のカテゴリーが占める割合を30%までに抑え、価格帯にも幅を持たせます |
| コールドスタートのユーザーに関連性の低いベストセラーが表示される | ページビューが1回でもあればシグナルとして収集します。最初に閲覧した商品のカテゴリーを使って、ベストセラーのフォールバックを絞り込みます |
| 閲覧中の商品がおすすめに含まれる | おすすめ関数には、除外対象として常に現在の商品IDを渡します |
| 同時購入マトリックスがバンドル販売のプロモーションに偏っている | すべての商品が同じプロモーション用バンドルに含まれていた注文を除外します。そのような注文は、実際の同時購入の親和性を反映しません |
関連スキル
- @customer-segmentation
- @customer-lifetime-value
- @product-reviews-ratings
- @user-generated-content