DAY 139 · 2026-05-19

朋友找不到我寫過的舊文,所以我幫網站加上語意搜尋

SLIDES · 6
Day 139 slide 1Day 139 slide 2Day 139 slide 3Day 139 slide 4Day 139 slide 5Day 139 slide 6
1 / 6

朋友問我:「你之前那篇講 Raycast 本地模型的,是哪一天?」

我自己想了一下也答不出來。將近 140 天的文章散在 Threads、Facebook、LinkedIn 三個地方,平台內建的搜尋只能找一模一樣的字串,找不到「概念」。朋友要嘛翻到天荒地老、要嘛打開 Google 賭一把運氣。

於是 Day 137 剛蓋好的 dawsonwang.com,今天加上兩種搜尋:關鍵字 + 語意。

下面整套做法分享,零月費、不用資料庫,三步就能跟著做。

打開 https://dawsonwang.com/search,輸入「AI 焦慮」、切到語意模式,會把任何在講「擔心被取代」、「跟 AI 共存」、「人類的價值」這類主題的篇章都撈出來——就算文章裡根本沒有「焦慮」兩個字也找得到。

兩種搜尋差在哪裡

關鍵字搜尋(keyword search):你打什麼字,它就找文章裡有沒有「一模一樣」的那串字。打「AI 焦慮」就只找出現「AI 焦慮」這四個字的文章。

語意搜尋(semantic search):把你的查詢句和每一篇文章都先變成一串數字(叫做向量 / embedding),再比兩串數字有多接近。「AI 焦慮」跟「擔心被 AI 取代」字面完全不一樣,但概念上靠近,語意搜尋會把兩種說法都當成同一件事。

兩種模式各自的長處:

面向 關鍵字 語意
找到的條件 字串對得起來 意思對得起來
速度 即時 首次查約 1 秒(要呼叫 API)
漏網 同義詞、換句話說會漏掉 比較不會漏
誤判 幾乎不會 偶爾撈出意外相近的篇
適合什麼 我知道我要找哪個字 我只知道我要找哪個概念

兩個都做,使用者預設停在關鍵字(快、準),找不到再切到語意。

整套花了多少成本

免費。

  • 嵌入模型:Cloudflare Workers AI 上的 @cf/baai/bge-m3(每天 10,000 neurons 免費,對 bge-m3 來說一天大概可以跑 900 萬 input tokens,個人站一年都用不完)
  • Hosting:Vercel hobby 方案,serverless function(雲端裡需要時才被叫起來的小段程式,沒用就不收錢)在免費額度內
  • 向量資料庫:沒有。將近 140 篇文章 × 1024 維 × 4 bytes ≈ 560 KB 的 binary 檔(就是一個壓得很小的數字檔),直接當靜態資源(網站打包好就附帶給瀏覽器下載的檔,不用後端伺服器來回算)放網站

聽到「向量搜尋」第一直覺會想到 Pinecone、Weaviate、Qdrant 這些專門的 vector database,要開 server、要付月費。但個人網站根本用不到——一個 Float32Array 加暴力 cosine 比對就夠快,在瀏覽器跑都不會頓。

做法分三步

Step 1:build 時把每篇文章都嵌入成向量

寫一支腳本,跟 yarn build 一起跑。每篇文章(subtitle + body,截 4000 字以內留安全邊界)一次 10 篇送給 Cloudflare Workers AI,拿回 1024 維向量、normalize 後寫進 binary 檔。

normalize 是把每個向量的長度拉成 1。這樣之後算「兩個向量有多像」就只是把兩串數字一對一相乘再加總,數字越大代表越像——快非常多。

最後輸出兩個檔到 public/search/

  • semantic-meta.json:每篇的 day 編號、標題、摘要(人類看的)
  • semantic-vectors.bin:全部文章的向量壓成一個檔(機器看的)

Step 2:把使用者的查詢也變成向量

不能讓瀏覽器直接呼叫 Cloudflare API,因為這樣 API token(呼叫付費 API 的通行密碼,被人拿到就會幫你刷帳單)會曝光在前端。所以開一支 serverless function /api/embed 當代理:使用者在前端輸入的字,先送到伺服器端的小程式,由它代為呼叫付費 API、再把結果回傳——token 永遠不會被前端看到。

Step 3:瀏覽器算相似度

把預先算好的兩個檔載進來(一次下載,之後每次查詢都用同一份)。拿到 query 的向量後,逐篇算「兩串數字有多像」、由高到低排序,前幾名就是搜尋結果。

將近 140 篇 × 1024 次乘加在 JavaScript 跑大約 5ms,比一次 API 往返還快。

為什麼是 bge-m3,不是 OpenAI 的 embedding

我同時試過 OpenAI 的 text-embedding-3-small,效果不錯但要付錢,而且自己網站要長期跑,預算不好估。

bge-m3(北京智源研究院 BAAI 做的多語言模型)三個理由勝出:

  1. 支援繁中:bge-m3 訓練資料涵蓋 100+ 語言,中文混英文丟下去能用
  2. Cloudflare Workers AI 有免費額度:每天 10k neurons,個人站撐爆都用不完
  3. 1024 維夠小:放在 client 端的 binary 檔不會肥到要分批載

如果是公司產品要服務上萬篇文章、要做正經的 RAG(拿自己的文件餵給 AI 來回答問題的做法),那就該認真選 Pinecone + 大模型 embedding。但將近 140 篇個人文章,bge-m3 + 一個 binary 檔就是甜蜜點。

小細節:build-time 預先算 vs runtime 即時算

整個架構最關鍵的選擇是「文章的向量在 build 時算好,不在使用者查詢時算」。

文章不會每分鐘變動,但搜尋會。所以:

  • 文章向量 build 一次:將近 140 篇文章嵌入只發 14 次 API call(一批 10 篇),花不到一分鐘
  • 查詢向量每次算:使用者打字、按 Enter,才呼叫一次 API
  • 比對在 client 端:瀏覽器拿到 query 向量後,跟靜態檔做點積,沒有任何後端負載

這樣資料庫不用、後端不用、月費不用。多寫一篇就是 build 時多嵌入一次而已。

還能再壓的兩個地方

實測每次 deploy 跑了多少 token:將近 140 篇文章、每篇截到 4000 字元上限,總共送出大約 26 萬字元。換成 token(bge-m3 對中文大約 0.7–1 token / 字元)落在 20–30 萬,每次 deploy 用掉每天免費額度的 2–3%,一天 deploy 30 次以上才會碰到天花板。

但這 26 萬字元裡,每次 deploy 都有大半是重複的——改了一篇,其他 100 多篇還是被重新嵌入一次。兩個方向可以再省:

  1. 加 content-hash 快取:build 前先讀現有的 semantic-meta.json,每篇文章算個 hash,對得上就跳過、只嵌入動過的。改一篇 deploy 一次的成本從 25 萬 token 掉到幾千 token,差兩個數量級。
  2. 放寬 4000 字元的截斷:目前有 8 篇文章被切掉後半段沒進向量。bge-m3 支援到 8192 token,cap 拉到 7000–7500 字元就能完整嵌入這 8 篇,總 token 量還是遠在免費額度內。

兩個都是「能省但現階段不必省」的優化——免費額度本來就用不完,等寫到第 500 篇再回來補也來得及。但這篇本身就是在記錄做法,把帳算清楚比較誠實。


朋友從滑社群滑到放棄,到打一個概念就能找到對應文章。順手在搜尋框輸入「Raycast 本地模型」,Day 107 就排在第一個——一開始那個朋友的問題,現在打字按 Enter 就有答案。

語意搜尋的概念在學術界很多年前就有了,現在只是因為 embedding 模型變強、變便宜、API 有免費額度,所以個人網站也能用得起。

兩天前剛把 dawsonwang.com 蓋起來,今天就接上語意搜尋——不會寫程式的話,把這篇丟給工程師朋友、或直接餵給 Claude Code,照著三步走也能複製。

「我這個工具夠不夠便宜?」越來越不是問題。剩下的問題是「我有沒有想到要做」。

延伸閱讀
看完整 165 篇 →