# 「あわせて飲みたい」機能拡張計画 **作成日**: 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` **リクエスト**: ```json { "target": { "prefecture": "新潟県", "type": "純米吟醸", "taste_stats": { "aroma": 4, "sweetness": 3, "acidity": 3, "bitterness": 2, "body": 4 }, "flavor_tags": ["フルーティー", "すっきり"] }, "exclude_ids": ["abc123", "def456"], // 既に登録済みの銘柄 "limit": 5 } ``` **レスポンス**: ```json { "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. ハイブリッドレコメンド実装 ```dart // lib/services/sake_recommendation_service.dart class SakeRecommendationService { /// ハイブリッドレコメンド(既存 + 未知の銘柄) static Future> getHybridRecommendations({ required SakeItem target, required List ownItems, int limit = 10, }) async { final recommendations = []; // 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> _fetchUnknownRecommendations({ required SakeItem target, required List 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** (現在): ```dart Text('五味チャート・タグ・酒蔵・産地から自動選出\n※現在は登録済みの銘柄からおすすめを表示') ``` **After** (Phase 2.0): ```dart Text('五味チャート・タグ・酒蔵・産地から自動選出\n💡 あなたにおすすめの未知の銘柄も表示中') ``` **未知の銘柄のカードにバッジ表示**: ```dart // 未知の銘柄には「🆕 未登録」バッジを表示 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をハッシュ化) ```dart // lib/services/data_contribution_service.dart class DataContributionService { static Future uploadSakeData() async { final userProfile = ref.read(userProfileProvider); // ユーザーが同意していない場合は送信しない if (!userProfile.hasConsentedToDataSharing) return; final box = Hive.box('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`** ```sql 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`** ```sql 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 **レビュー**: 開発者(必要に応じて修正してください)