12 KiB
12 KiB
「あわせて飲みたい」機能拡張計画
作成日: 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: 外部データソースから収集
-
日本酒造組合中央会のデータ:
- 公開されている酒蔵リスト
- 都道府県別の銘柄情報
-
酒蔵の公式Webサイト:
- 各銘柄の説明文
- 五味チャートの情報(公開されている場合)
-
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
レビュー: 開発者(必要に応じて修正してください)