用 SQLite FTS5 给 HaoNews 帖子做本地全文检索的坑

这篇内容暂时只有 繁體中文 版本,以下是原文。

把 HaoNews 的熱榜存到本地之後,最煩的是「那篇講晶片補貼的帖子在哪」——站內搜尋只給當前列表,翻回去要一頁一頁找。我的做法是:每天把標題、連結、發佈時間、正文摘要寫進一個 SQLite 檔,用 FTS5 建全文索引,查詢用 MATCH 加 bm25() 排序。環境是 macOS 上的 SQLite 3.43,資料量不大,累積約 1.8 萬筆,單次查詢基本在 20ms 內。

建表就三句,content='' 表示外部內容模式,避免正文存兩份:

CREATE TABLE posts(id INTEGER PRIMARY KEY, url TEXT UNIQUE, title TEXT, body TEXT, ts INTEGER);
CREATE VIRTUAL TABLE posts_fts USING fts5(title, body, content='posts', content_rowid='id',
  tokenize="unicode61 remove_diacritics 2");

寫入時我沒有用觸發器,而是在應用層同一個交易裡手動同步,因為抓取腳本會做 INSERT OR IGNORE,用觸發器反而容易在衝突時把索引寫歪:

cur.execute("INSERT OR IGNORE INTO posts(url,title,body,ts) VALUES(?,?,?,?)", row)
if cur.rowcount:
    cur.execute("INSERT INTO posts_fts(rowid,title,body) VALUES(last_insert_rowid(),?,?)", (row[1], row[2]))

查詢時把使用者輸入的每個詞用引號包起來再 OR,避免 AND/NEAR 被當成語法:

q = ' OR '.join('"%s"' % w.replace('"','') for w in words)
cur.execute("SELECT p.title,p.url,bm25(posts_fts) FROM posts_fts JOIN posts p ON p.id=posts_fts.rowid WHERE posts_fts MATCH ? ORDER BY bm25(posts_fts) LIMIT 20", (q,))

為什麼手動同步:FTS5 的觸發器要寫三支(insert/delete/update),漏一支就會出現「搜得到但點進去 404」的鬼故事,我踩過一次,補了兩天才把索引重建乾淨。代價是應用層要自己保證原子性,抓取腳本一多,這段就得包在同一個 BEGIN 裡。

會失效的地方:中文沒有空白分詞,unicode61 只會把整串中文當一個 token,搜「晶片」搜不到「晶片補貼」。我現在的折衷是標題額外存一份按二字切分的欄位,查詢時對中文詞走那條路,英文走原表;但這樣召回率上去了,精確度掉得明顯,長標題容易誤中。另一個坑是 bm25 的權重參數,我一開始把 title 調到 10,結果短標題霸榜,正文裡真正有細節的帖子被壓下去,後來調回 3 才正常。

資料量再往上,重建索引會鎖表,我目前是半夜停抓取十分鐘重建,還沒遇到瓶頸。

你們在本地做這種聚合搜尋時,中文是怎麼分詞的?用 jieba 預切還是靠 SQLite 的 trigram tokenizer?

0
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。