ponshu-room-lite/RECOMMENDATION_EXPANSION_PL...

12 KiB
Raw Blame History

「あわせて飲みたい」機能拡張計画

作成日: 2026-01-22
ステータス: Phase 2.0リリース後1ヶ月で実装予定


📊 現状分析

実装済み機能

ローカルレコメンドエンジン:

  • 五味チャートのコサイン類似度計算
  • 酒蔵・都道府県・タグによる類似度スコアリング
  • スコア順にソート最大10件

制限事項

登録済みの銘柄のみ:

  • ユーザーが登録した日本酒からのみレコメンド
  • 未知の銘柄(まだ登録していない日本酒)は推薦されない

🎯 Phase 2.0: 未知の銘柄のレコメンド(拡張計画)

1. Synology NAS上の共有データベース構築

アーキテクチャ:

┌─────────────────────────────────────┐
│  Synology NAS (PostgreSQL)          │
│  posimai-nas.ts.net (Tailscale)     │
│                                      │
│  ┌──────────────────────────────┐  │
│  │  日本酒マスターDB             │  │
│  │                               │  │
│  │  - 銘柄名                     │  │
│  │  - 蔵元                       │  │
│  │  - 都道府県                   │  │
│  │  - 五味チャート(平均値)     │  │
│  │  - タグ                       │  │
│  │  - 人気度(登録回数)         │  │
│  └──────────────────────────────┘  │
│         ↓                            │
│  ┌──────────────────────────────┐  │
│  │  ユーザー登録DB               │  │
│  │                               │  │
│  │  - user_id (匿名化)          │  │
│  │  - sake_id                    │  │
│  │  - 評価・レビュー             │  │
│  │  - 五味チャート              │  │
│  └──────────────────────────────┘  │
└─────────────────────────────────────┘
         ↑ HTTPS (Tailscale)
┌─────────────────────────────────────┐
│  Flutter App                        │
│                                      │
│  - 自分のカードHive            │
│  - 未知の銘柄API経由           │
└─────────────────────────────────────┘

2. API設計

エンドポイント: /api/v1/recommendations

リクエスト:

{
  "target": {
    "prefecture": "新潟県",
    "type": "純米吟醸",
    "taste_stats": {
      "aroma": 4,
      "sweetness": 3,
      "acidity": 3,
      "bitterness": 2,
      "body": 4
    },
    "flavor_tags": ["フルーティー", "すっきり"]
  },
  "exclude_ids": ["abc123", "def456"], // 既に登録済みの銘柄
  "limit": 5
}

レスポンス:

{
  "recommendations": [
    {
      "id": "sake_12345",
      "name": "八海山 純米吟醸",
      "brewery": "八海醸造",
      "prefecture": "新潟県",
      "type": "純米吟醸",
      "taste_stats": {
        "aroma": 4,
        "sweetness": 3,
        "acidity": 3,
        "bitterness": 2,
        "body": 4
      },
      "flavor_tags": ["フルーティー", "すっきり"],
      "similarity_score": 0.92,
      "reason": "新潟県つながり / すっきり / 似た味わい",
      "popularity": 1523 // 何人が登録したか
    },
    // ... 最大5件
  ]
}

3. ハイブリッドレコメンド実装

// lib/services/sake_recommendation_service.dart
class SakeRecommendationService {
  /// ハイブリッドレコメンド(既存 + 未知の銘柄)
  static Future<List<RecommendedSake>> getHybridRecommendations({
    required SakeItem target,
    required List<SakeItem> ownItems,
    int limit = 10,
  }) async {
    final recommendations = <RecommendedSake>[];
    
    // 1. 既存の銘柄から類似を検索(ローカル)
    final ownRecs = getRecommendations(
      target: target,
      allItems: ownItems,
      limit: 5, // 半分
    );
    recommendations.addAll(ownRecs);
    
    // 2. 未知の銘柄を検索API経由
    try {
      final unknownRecs = await _fetchUnknownRecommendations(
        target: target,
        excludeIds: ownItems.map((item) => item.id).toList(),
        limit: 5, // 残り半分
      );
      recommendations.addAll(unknownRecs);
    } catch (e) {
      debugPrint('⚠️ Failed to fetch unknown recommendations: $e');
      // エラー時は既存のみ表示
    }
    
    // 3. スコア順にソート
    recommendations.sort((a, b) => b.score.compareTo(a.score));
    
    return recommendations.take(limit).toList();
  }
  
  /// 未知の銘柄をAPIから取得
  static Future<List<RecommendedSake>> _fetchUnknownRecommendations({
    required SakeItem target,
    required List<String> excludeIds,
    int limit = 5,
  }) async {
    final response = await http.post(
      Uri.parse('https://posimai-nas.ts.net/api/v1/recommendations'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'target': {
          'prefecture': target.displayData.prefecture,
          'type': target.displayData.type,
          'taste_stats': target.hiddenSpecs.tasteStats,
          'flavor_tags': target.hiddenSpecs.flavorTags,
        },
        'exclude_ids': excludeIds,
        'limit': limit,
      }),
    );
    
    if (response.statusCode != 200) {
      throw Exception('API Error: ${response.statusCode}');
    }
    
    final data = jsonDecode(response.body);
    return (data['recommendations'] as List)
        .map((json) => RecommendedSake.fromJson(json))
        .toList();
  }
}

4. UI改善

Before (現在):

Text('五味チャート・タグ・酒蔵・産地から自動選出\n※現在は登録済みの銘柄からおすすめを表示')

After (Phase 2.0):

Text('五味チャート・タグ・酒蔵・産地から自動選出\n💡 あなたにおすすめの未知の銘柄も表示中')

未知の銘柄のカードにバッジ表示:

// 未知の銘柄には「🆕 未登録」バッジを表示
Stack(
  children: [
    Image.network(unknownSake.imageUrl),
    if (unknownSake.isUnknown)
      Positioned(
        top: 8,
        right: 8,
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
          decoration: BoxDecoration(
            color: Colors.orange,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Text('🆕 未登録', style: TextStyle(color: Colors.white, fontSize: 10)),
        ),
      ),
  ],
)

5. データ収集戦略

ステップ1: 既存ユーザーのデータを匿名化して収集

実装:

  • アプリ初回起動時に「データ提供の同意」を取得
  • ユーザーが登録した日本酒のデータをNASに送信
  • user_idは匿名化(device_info_plusでデバイスIDをハッシュ化
// lib/services/data_contribution_service.dart
class DataContributionService {
  static Future<void> uploadSakeData() async {
    final userProfile = ref.read(userProfileProvider);
    
    // ユーザーが同意していない場合は送信しない
    if (!userProfile.hasConsentedToDataSharing) return;
    
    final box = Hive.box<SakeItem>('sake_items');
    final allItems = box.values.toList();
    
    final payload = allItems.map((item) => {
      'name': item.displayData.name,
      'brewery': item.displayData.brewery,
      'prefecture': item.displayData.prefecture,
      'type': item.displayData.type,
      'taste_stats': item.hiddenSpecs.tasteStats,
      'flavor_tags': item.hiddenSpecs.flavorTags,
      'smv': item.hiddenSpecs.sakeMeterValue,
    }).toList();
    
    await http.post(
      Uri.parse('https://posimai-nas.ts.net/api/v1/data/upload'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'user_id': await _getAnonymizedUserId(),
        'sake_items': payload,
      }),
    );
  }
}

ステップ2: 外部データソースから収集

  1. 日本酒造組合中央会のデータ:

    • 公開されている酒蔵リスト
    • 都道府県別の銘柄情報
  2. 酒蔵の公式Webサイト:

    • 各銘柄の説明文
    • 五味チャートの情報(公開されている場合)
  3. Gemini APIによる自動収集:

    • 日本酒の名前と蔵元から五味チャートを推定
    • コスト: 約5円/銘柄(画像なし、テキストのみ)

6. データベーススキーマ

テーブル: sake_master

CREATE TABLE sake_master (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  brewery VARCHAR(255) NOT NULL,
  prefecture VARCHAR(50) NOT NULL,
  type VARCHAR(50), -- 純米吟醸, 大吟醸, etc.
  taste_aroma INT DEFAULT 3,
  taste_sweetness INT DEFAULT 3,
  taste_acidity INT DEFAULT 3,
  taste_bitterness INT DEFAULT 3,
  taste_body INT DEFAULT 3,
  flavor_tags TEXT[], -- {フルーティー, すっきり}
  smv DECIMAL(3,1), -- 日本酒度
  popularity INT DEFAULT 0, -- 何人が登録したか
  image_url TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_sake_prefecture ON sake_master(prefecture);
CREATE INDEX idx_sake_type ON sake_master(type);
CREATE INDEX idx_sake_popularity ON sake_master(popularity DESC);

テーブル: user_sake_data

CREATE TABLE user_sake_data (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id_hash VARCHAR(64) NOT NULL, -- 匿名化されたユーザーID
  sake_id UUID REFERENCES sake_master(id),
  taste_aroma INT,
  taste_sweetness INT,
  taste_acidity INT,
  taste_bitterness INT,
  taste_body INT,
  rating INT, -- 1-5
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_user_sake_user ON user_sake_data(user_id_hash);
CREATE INDEX idx_user_sake_sake ON user_sake_data(sake_id);

7. 実装工数見積もり

項目 工数 担当
PostgreSQLセットアップ 4時間 Synology NAS
APIサーバー構築FastAPI 8時間 Python
レコメンドアルゴリズム(サーバー側) 6時間 Python
Flutter側の統合 6時間 Dart
データ収集機能 4時間 Dart + Python
テスト・調整 4時間 総合
合計 32時間 約4日

8. リリーススケジュール

Phase 1.0(現在)

  • ローカルレコメンドエンジン実装済み
  • 既存の銘柄からの推薦

Phase 2.0リリース後1ヶ月

  • 🔄 Synology NAS環境構築
  • 🔄 日本酒マスターDB構築初期データ: 100銘柄
  • 🔄 ハイブリッドレコメンド実装
  • 🔄 データ収集機能実装

Phase 3.0リリース後3ヶ月

  • 🔮 ソーシャル機能統合
  • 🔮 「この銘柄を登録している人はこれも登録しています」
  • 🔮 コミュニティの人気ランキング

9. ユーザーへの説明

現在の表示:

「あわせて飲みたい」
五味チャート・タグ・酒蔵・産地から自動選出
※現在は登録済みの銘柄からおすすめを表示

Phase 2.0での表示:

「あわせて飲みたい」
五味チャート・タグ・酒蔵・産地から自動選出
💡 あなたにおすすめの未知の銘柄も表示中

バッジ:

  • 🆕 未登録: まだ登録していない銘柄
  • 📚 登録済み: 自分のカードから推薦

📝 まとめ

現状: 既に優秀なローカルレコメンドエンジンが実装済み

Phase 2.0での拡張: 未知の銘柄を含めたハイブリッドレコメンドを実装予定

実装工数: 約32時間4日

リリース時期: リリース後1ヶ月2026年3月頃


作成日: 2026-01-22
作成者: Cursor AI
レビュー: 開発者(必要に応じて修正してください)