iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
佛心分享-SideProject30

營養師想做一個飲食建議產品系列 第 21 篇

Day21 - Food Database 要長什麼樣子?

  • 分享至 

  • xImage
  •  

同樣是「食物資料」,超商跟速食店的分類方式完全不一樣,怎麼統一?

Day20 決定要建 Food DB。但要動手之前,第一個要解決的其實不是「去哪裡抓資料」,而是「資料要長什麼樣子」。這篇講 FoodItem 這個資料型別的設計過程。

🧱 地基概念

Schema:先決定每一筆資料有哪些欄位

Schema 可以想成一張表格的「表頭」:先決定每一筆資料要有哪些欄位、每個欄位放什麼型別的內容,之後所有資料都照同一個格式存放,程式才能穩定地讀取與篩選。這個專案用 TypeScript 的型別 FoodItem 來描述這份表頭。

為什麼「分類」是最難的部分

基本欄位(品名、熱量)很好決定,真正花時間討論的是分類。因為每個品牌的分類邏輯差很多:MOS 有「元氣早餐」、全家有「壽司手卷飯糰」、Subway 有「潛艇堡」。如果程式只靠各品牌自己的分類字串比對,每加一個新品牌就要重新寫一套規則,完全沒辦法擴充。

🔧 實際操作

欄位不是憑空想的:從「用途」反推回來

在決定每個欄位之前,我先問的是:後面的程式,拿這份資料要做什麼? 每一個欄位都對應一個實際的需求:

後面的程式要做的事 所以需要的欄位
把食物換算成六大類份數(用食物代換表公式) 熱量與三大營養素(caloriesKcal、proteinG、fatG、carbohydrateG)
AI 只回報編號,程式再查回真實資料(Day18) 唯一的識別碼 foodId
依餐次篩選候選(例如早餐不要出現水餃) mealSlot
貼近使用者真實的外食習慣 cuisineStyle
知道每筆資料可不可信、出自哪裡 sourceType、confidence、validationStatus、sourceUrl、retrievedAt

另一方面,現實世界的資料也會反過來影響設計:品牌官網公布的資訊不一樣(例如 Subway 沒有公布鈉含量),所以 sodiumMg 要設成選填;品牌分類各自不同,所以原樣保留在 category,另外加統一分類。這個「需求決定要哪些欄位、現實決定哪些要選填」的來回,就是 Schema 設計的日常。

基本欄位

欄位 放什麼
foodId、name、brand 識別碼、品名、品牌
category 品牌自己的原始分類(例如「6吋潛艇堡」),原樣保留當補充資訊
servingSize 份量描述
caloriesKcal、proteinG、fatG、carbohydrateG 熱量與三大營養素
sodiumMg(選填) 鈉含量。有些品牌官網沒有公布,沒資料就留空,不填 0,因為 0 會被誤讀成「真的零鈉」

foodId 怎麼命名?

foodId 像每一筆食物的「身分證」。Day18 講過,AI 只回報編號,程式再用編號查回真實資料,所以編號必須唯一、穩定。命名規則是「品牌名-該品牌自己的識別」,三個品牌因為官網提供的資訊不同,組法略有不同:

品牌 foodId 範例 怎麼組出來
MOS MOS Burger-M000203 品牌 + 官網商品頁網址裡的商品編號
全家 全家-0558422 品牌 + 全家官網的商品編號
Subway Subway-6吋潛艇堡-嫩切雞肉 官網沒有單品編號,只好用「品牌 + 分類 + 品名」組合,把分類也編進去才不會撞號

設計原則是優先沿用官網既有的編號,而不是自己發流水號。這樣同一個商品,不管腳本重跑幾次,算出來的 foodId 都一樣;全家的匯入腳本也靠這一點,判斷某個商品「已經抓過」就直接跳過,不用重打 API。目前全部 1915 筆資料的 foodId 沒有重複。

跨品牌的統一分類:兩個互相獨立的欄位

最後定案的做法,是額外加兩個欄位,用「統一的說法」替換各品牌各自的分類:

  • mealSlot(角色):這個東西在一餐裡扮演什麼角色。有 main(主餐)、breakfast(早餐)、side(附餐小菜)、soup(湯品)、drink(飲料)、dessert(甜點)
  • cuisineStyle(風格):這是哪種風格的店家做的。有 fastfood、western、convenience、chineseMeal、japanese、breakfastShop、beverageShop

兩個欄位互不相關,可以自由組合。舉例來說(示意),MOS 的元氣早餐可以標成 mealSlot=breakfast、cuisineStyle=fastfood;Subway 的潛艇堡可以標成 mealSlot=main、cuisineStyle=western。這樣不管之後加入哪個新品牌,只要能歸到這兩個維度,候選篩選的程式邏輯就完全不用改。

分類系統的附帶好處:用「早餐從 mealSlot=breakfast 挑、午晚餐一定要有一個 mealSlot=main」這種結構化規則,比維護一個範例庫更精確,也更好維護。

記錄「這筆資料有多可信」的欄位

不同的蒐集方式,精確度不一樣,所以每筆資料也記錄自己的可信度:

  • sourceType:資料來源類型(official 官方、government 政府、estimated 估算)
  • confidence:可信度(high、medium、low)。用正則表達式解析官方營養標示表格的標 high,用 AI 語意抽取的標 medium
  • validationStatus:檢查結果(ok 或 needs_review),以及 sourceUrl、retrievedAt 記錄出處與抓取時間

這樣使用資料時,就知道每一筆的可信程度不是一樣的。

// 範例
 {
    "foodId": "MOS Burger-M000032",
    "brand": "MOS Burger",
    "category": "主餐",
    "mealSlot": "main",
    "cuisineStyle": "fastfood",
    "sourceUrl": "https://www.mos.com.tw/menu/set_detail.aspx?id=M000032",
    "sourceType": "official",
    "confidence": "high",
    "retrievedAt": "2026-08-26T03:58:57.532Z",
    "name": "摩斯鱈魚堡",
    "servingSize": "1份",
    "caloriesKcal": 508,
    "proteinG": 20.5,
    "fatG": 28.7,
    "carbohydrateG": 39.6,
    "sodiumMg": 608,
    "validationStatus": "ok"
  }

小結

Schema 看起來只是「幾個欄位」,但真正的設計重點是用兩個獨立的統一分類,把各品牌的差異吸收掉,讓後面的程式不需要知道資料來自哪個品牌。表頭定好了,下一篇 Day22 就要開始真正花時間的部分:把各品牌官網的營養資料一筆一筆蒐集回來。


上一篇
Day20 - AI 最大的問題其實不是 AI,而是資料
下一篇
Day22 - 資料怎麼蒐集?爬蟲、Firecrawl、官方 API 該怎麼選
系列文
營養師想做一個飲食建議產品 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言