ponshu-room-lite/RECOMMENDATION_EXPANSION_PL...

407 lines
12 KiB
Markdown
Raw Normal View 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`
**リクエスト**:
```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<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** (現在):
```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<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`**
```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
**レビュー**: 開発者(必要に応じて修正してください)