iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0

我們的 PokeThreads 走到現在,已經是一個具備 Load Balancer、Stateless Server、Sharded Database、Cache、CDN、Message Queue 的系統了,但有一個使用者天天在用、我們卻從來沒認真設計過的功能:搜尋

Pikachu 想找某篇提到「寶可夢」的貼文,或是想直接搜尋 Charmander 這個帳號,這種需求聽起來理所當然,但要做好一點都不簡單。

最直覺的做法:SQL 查詢

先看最陽春的寫法:

SELECT *
FROM posts
WHERE content LIKE '%寶可夢%';

看起來簡單又完全合理,反正資料都在 Database 裡,查一下不就好了?

在資料量很小的時候,這樣寫確實沒問題,但隨著 PokeThreads 一路走到今天的規模,這支 Query 會出大問題。

為什麼 LIKE 查詢撐不住

索引救不了前後都有萬用字元的查詢

Database 的 Index(索引)本質上是排序過的資料結構,適合用來加速「等於」「範圍」「前綴比對」這類查詢,例如 WHERE content LIKE '寶可夢%'(只有結尾是萬用字元)還可能用得上索引。

LIKE '%寶可夢%' 這種前面也有萬用字元的寫法,Database 沒辦法用索引快速定位起點,只能整張表逐筆比對,這就是 Full Table Scan(全表掃描)

Sharding 後問題被放大

回想 Day 18 我們把 Posts 表 Sharding 成好幾個獨立的 Database Node,這對一般依照 Shard Key 查詢的操作很有幫助,但對「搜尋」這種需求完全是反效果,因為我們不知道符合關鍵字的貼文分散在哪些 Shard,只能對每一個 Shard 都做一次 Full Table Scan,再把結果合併起來。

Shard 越多,一次搜尋要付出的代價反而越高。

本來就不是為了「搜尋」而生

就算不考慮效能,LIKE 查詢在語意上也做不到真正的搜尋引擎會做的事:

  • 沒辦法按照「相關程度」排序結果
  • 沒辦法處理同義詞、模糊比對、錯字容錯
  • 沒辦法做斷詞(例如把「寶可夢訓練家」拆成「寶可夢」「訓練家」分別比對)

換個角度:Inverted Index

真正的搜尋引擎不會每次現場掃描全部文件,而是預先建立一份反過來的索引

一般的資料表是「每一列存一篇文章」,而 Inverted Index 反過來,是「每一個字,對應一份出現過這個字的文章清單」:

一般索引:Post ID -> 文章內容

Inverted Index:
"寶可夢" -> [Post 1, Post 5, Post 42, ...]
"訓練家" -> [Post 5, Post 9, ...]
"神奇"   -> [Post 3, Post 42, ...]

當使用者搜尋「寶可夢」,系統不需要掃過所有文章,只要直接去 Inverted Index 查「寶可夢」對應的清單,就能立刻拿到候選結果,這個查詢複雜度跟資料總量幾乎無關,只跟這個字出現的次數有關。

AI 如何補上關鍵字搜尋的盲點

前面提到,LIKE 查詢有個天生的限制:沒辦法處理同義詞、模糊比對

Inverted Index 解決的是效能問題,但這個語意上的限制其實還在,如果 Pikachu 搜尋「跑得很快的寶可夢」,貼文裡寫的卻是「敏捷」「速度型」,關鍵字比對完全抓不到這種意思相近、字面卻不同的內容。

這正是這幾年業界普遍導入 AI 的地方:把每一篇貼文丟給一個 Embedding Model,轉換成一組能代表「語意」的向量(Vector),意思相近的內容,向量在空間中的距離也會相近。搜尋時把使用者輸入的關鍵字也轉成向量,直接找出向量空間裡最接近的貼文,這種做法叫 Semantic Search(語意搜尋),也稱 Vector Search。

實務上很少整個換掉 Inverted Index,而是兩者混用,稱為 Hybrid Search:關鍵字比對負責精準比對(例如帳號名稱、Hashtag 這類需要完全命中的內容),Vector Search 負責語意相關的內容,兩邊的結果再一起排序合併。

資料怎麼進到 Search Index?

選項一:發文時同步寫進

這代表 POST /posts 除了寫 Database,還要多等 Search Index 也寫入成功才能回應,等於在寫入路徑上多綁了一個依賴。

如果 Search Index 那端變慢或暫時掛掉,連帶拖累使用者發文這個核心操作,這正是 Day 11 我們在同步 Notification 上踩過的坑。

選項二:透過 Message Queue 非同步更新

這才是實務上更常見的做法:

透過 Message Queue 非同步更新搜尋索引的架構圖

發文流程維持原本的樣子,Database 依然是 Source of Truth,額外多發布一個 post_created 事件到 Message Queue,由一個獨立的 Search Indexer Worker 消費這個事件,把新貼文的內容寫進 Search Index。

Trade-off:搜尋結果可能慢半拍

把索引更新變成非同步之後,會有一段時間差:Pikachu 剛發的文章,可能要過幾百毫秒甚至幾秒鐘,才會真正出現在搜尋結果裡。

這其實就是 Day 17 談過的 Eventual Consistency,Search Index 不追求跟 Database 完全即時同步,而是接受「最終會同步」這件事。

對搜尋功能來說,這個延遲通常完全可以接受:沒有人會期待自己剛發的文章,一秒鐘內就出現在全站搜尋結果的第一名。

小結

搜尋看起來只是加一個搜尋框,但背後其實是完全不同的資料結構與查詢方式:

  • SQL LIKE 查詢
    • Full Table Scan,Sharding 後更糟
  • Inverted Index
    • 字 -> 文件清單,查詢與資料量脫鉤
  • Vector Search
    • 向量距離找語意相近的內容,補上關鍵字比對的盲點

PokeThreads 現在多了一條資料流:Database 負責寫入與讀取真相,Search Index 透過 Message Queue 非同步更新、專門負責回答「哪些文章符合這個關鍵字」。

這也再次印證一件事:不是所有查詢需求都該塞給同一個 Database 解決,該用什麼工具取決於問題本身。


上一篇
Day 21 一個人讓系統爆炸:Hot User 與 Hot Post
系列文
系統設計就像九頭蛇:打造社群網站的 30 天22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
lin1015
iT邦新手 5 級 ‧ 2026-09-20 12:18:42

從 LIKE 全表掃描一路接到 inverted index、vector 與 hybrid search,選型脈絡很順。既然資料庫是 Source of Truth、索引又非同步更新,漏事件、重複事件或文章刪除時,你會怎麼補償與定期 reindex?

我要留言

立即登入留言