はじめに
香水コレクション管理アプリ fragfolio の開発ログ、第3回です。
今回は「AIを使って香水マスタを正規化する」というテーマに挑戦しました。
香水というジャンルは、アプリ開発においてかなり扱いづらいデータのひとつです。
ブランド名や製品名は膨大で、しかも「Bleu de Chanel」「ブルードゥシャネル」「BLEU DE CHANEL」のように表記ゆれが当たり前。さらに Dior Sauvage のように濃度違い(EDT/EDP/Parfum/Elixir)があり、毎年のように新しいフランカー(派生版)が追加されていきます。
つまり「マスターデータを最初から完璧に整備する」のは不可能。ユーザー入力をそのまま保存すればデータベースは一瞬でぐちゃぐちゃになります。
そこで fragfolio では AIを使って入力を正しいブランド名+香水名に正規化し、必要に応じて候補をサジェストするというアプローチを試すことにしました。
正規化の難しさ
改めて課題を整理すると以下の通りです。
- 表記ゆれ:言語・カタカナ・大文字小文字の違い
- バリエーション:濃度やフランカーで名前が細分化
- 誤字や略称:「ソバージュ」「シャネル5」のような入力
- 新作の追加:毎年新しい香水が発売され、更新が追いつかない
どれか1つでも厄介ですが、香水の場合は全部が重なります。
この領域での「正規化」は、アプリの根幹を左右する課題と言っていいでしょう。
実装の工夫
構造化出力で壊れJSON問題を解決
初期は「AIにJSONを出力させて、そのままパース」していました。しかし実際にはカンマ抜けや余計な文章が混じり、壊れJSONで落ちることが多発。
そこで現在は 構造化出力(JSON Schema)+Function Calling に全面移行しました。
- OpenAI →
response_format: { type: "json_schema" } - Gemini 2.5 Flash →
responseSchema - Claude 3.5 Haiku → Tool Use
これにより「壊れJSONとの戦い」はほぼ解消。安定して処理できるようになりました。
候補数は exact 指定しない
「必ず10件返せ」と指定すると、モデルが9件しか返さないだけでエラー扱いになります。
そこで min/max を設定して幅を持たせる → 多めに返してサーバ側で丸める 方式に変更。
これで安定性が大きく改善しました。
Few-shot プロンプトで精度向上
AIは「例を見せると学習する」性質があります。そこで few-shot 的に入力例と正規化例を数件提示してから本番入力を渡しています。
例えば:
入力: ソバージュ
出力: Dior / Sauvage Eau de Parfum
といったサンプルを数件与えるだけで、誤字や略称の扱いが格段に改善しました。
Chain of Thought(思考の分解)
曖昧な入力に強くするため、モデルに「考え方のステップを踏ませてからJSONを返す」よう指示しています。
プロンプトの中で「誤字修正 → ブランド推定 → 香水名特定 → 濃度抽出」という流れを暗示させ、最終的には JSONだけ返すように制約。これが精度向上に役立っています。
モデルの使い分け
fragfolio では以下のようにモデルを役割分担しています。
- GPT-4o-mini
精度・速度・コストのバランスが最も良い。メインで利用。 - Gemini 2.5 Flash
JSON Schema対応。候補を大量生成させたいときに便利。 - Claude 3.5 Haiku
日本語の曖昧入力を補正するのが得意。補助的に利用。
コード例(OpenAI Provider 抜粋)
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->apiKey,
'Content-Type' => 'application/json',
])->post('https://api.openai.com/v1/chat/completions', [
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'user', 'content' => $prompt],
],
'tools' => $tools,
'tool_choice' => 'required',
'temperature' => 0.1,
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'name' => 'SuggestFragrancesSchema',
'schema' => [
'type' => 'object',
'properties' => [
'suggestions' => [
'type' => 'array',
'items' => [...],
'minItems' => 10,
'maxItems' => 30,
],
],
'required' => ['suggestions'],
],
'strict' => true,
],
],
]);
まとめ
今回の実験を通じて、香水マスタの正規化におけるAI活用の手応えを強く感じました。
- 構造化出力で壊れJSON問題を解消
- 候補数は幅を持たせて安定化
- few-shot や Chain of Thought で曖昧入力への耐性を強化
- モデルを使い分けて精度とコストを両立
「香水」というニッチかつ難しいドメインでも、工夫次第でAIを現実的なソリューションとして組み込めることが分かりました。fragfolioにとって、これは大きな一歩です。




コメント