修語意搜尋全 0 命中:分數門檻沒跟著 bge-m3 換代(換模型漏掉的第五處)
leo 2026-08-05 實撞:「新上傳的『loop-engine-north-star.md』語義 0 命中,
但舊的『人力媒合系統規劃書』語義 2 命中」。
## 根因不在向量——向量是好的
直打 Vectorize 實測(youlin 實例、bge-m3、1024 維 index):
搜「閉環機」→ loop-engine-north-star.md 穩坐第 1-4 名(0.638/0.603/0.588/0.552)
D1 與 Vectorize 也對得上:659 筆 embeddable 全 is_embedded=1、index vectorCount 659。
真兇是 **min_score 門檻綁在舊模型的分數尺度上**:
舊 bge-base-en-v1.5:中文分數全擠 0.65-0.90(沒區辨力)⇒ t183 取 0.75 砍雜訊,對
新 bge-m3 :尺度整體下移(相關 0.5-0.85、雜訊 0.4 上下)⇒ 0.75 砍掉的是正解
leo 看到的「舊檔中、新檔不中」由此而來——「人力媒合系統規劃書」拿 0.842 僥倖存活,
其餘全被門檻掃掉。08-05 換 bge-m3 的「四處同步」清單(embed.ts/deploy.ts/
deploy-all.mjs/worker.js)**漏了這第五處**,因為它不在 kbdb 而在 portal 呼叫端。
## 改動
· kbdb/src/embed.ts:新增 DEFAULT_MIN_SCORE=0.5,**緊鄰 DEFAULT_EMBED_MODEL**
——門檻是模型的性質,放模型旁邊,下次換模型的人一定會看到
· cypher-executor/src/routes/portal-data.ts:拿掉硬寫的 0.75,
只在使用者顯式指定時才傳 min_score(不再各自持有一份數字=不再漂移)
· console-ui/public/portal/os-split.test.mjs:修好被今天 fe0ee82 弄壞的自測
(結尾標記寫死文案 ⇒ 改文案就炸「抽不到函式區塊」;改成錨定結構)
+斷言同步改成 Mac 給 DMG
## 0.5 怎麼來的(實測分布,不是猜的)
「閉環機」 0.638/0.603/0.588/0.552 全是目標檔 ── 斷崖 ── 0.446 才是雜訊
「火星座標 奧林帕斯山」 0.750…0.500 全對,0.475 以下才是雜訊
「人力媒合系統規劃書」 0.842 對,0.550 起是雜訊
誠實 trade-off:0.5 非每個查詢都乾淨(「AI 上課名冊」0.658 的 ax-academy 會擠進來),
但「偶有雜訊」遠優於現況「什麼都搜不到」。
## 驗
· kbdb 87/87 綠(三筆斷言隨新契約更新:預設不再是「不過濾」)
· cypher-executor 9 failed/301 passed=**與改動前逐數相同**(git stash 前後各跑一次)⇒ 既有債
· portal os-split 自測 10/10 綠
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-2
@@ -37,6 +37,26 @@ import type { Bindings, Entry } from './types';
|
||||
// → 打 backfill 的 `reindex=true`(把 embed=1 的既有 entry 全部重嵌)→ 舊 index 可刪。
|
||||
const DEFAULT_EMBED_MODEL = '@cf/baai/bge-m3'; // 1024-dim,與 Vectorize index dimensions=1024 對齊
|
||||
|
||||
// 🔴 2026-08-05 leo 實撞:換 bge-m3 後**語義搜尋全 0 命中**(新上傳的檔搜不到、舊檔偶爾才中)。
|
||||
// 根因不在向量——實測 Vectorize 端排序完全正確(搜「閉環機」,目標檔穩坐 1-4 名)——
|
||||
// 而在**分數閾值是綁在舊模型的分數尺度上的**:
|
||||
// 舊 bge-base-en-v1.5:中文分數全擠 0.65-0.90(沒區辨力)⇒ t183 取 0.75 砍雜訊,對
|
||||
// 新 bge-m3 :分數尺度整體下移(相關 0.5-0.85、雜訊 0.4 上下)⇒ 0.75 砍掉的是**正解**
|
||||
// youlin 實例實測分布(bge-m3,08-05,直打 Vectorize query):
|
||||
// 「閉環機」 0.638 / 0.603 / 0.588 / 0.552 ← 全是目標檔,**全被 0.75 砍光**
|
||||
// ────── 斷崖 ────── 0.446 以下才是雜訊
|
||||
// 「火星座標 奧林帕斯山」 0.750…0.500 全是火星座標,0.475 以下才是雜訊
|
||||
// 「人力媒合系統規劃書」 0.842 ← 僥倖 >0.75 存活。**這就是 leo 看到「舊檔中、新檔不中」的由來**
|
||||
// ⇒ 斷崖普遍落在 0.5 附近,取 **0.5**:相關的全留、雜訊仍砍。
|
||||
// 誠實 trade-off:0.5 不是每個查詢都乾淨(實測「AI 上課名冊」0.658 的 ax-academy 會擠進來),
|
||||
// 但「偶有雜訊」遠優於「什麼都搜不到」——後者是現在的狀態。
|
||||
//
|
||||
// 🔴 為什麼閾值住在這裡(而不是 portal):它是**模型的性質**,不是頁面的偏好。
|
||||
// 原本硬寫在 `cypher-executor/src/routes/portal-data.ts`,換模型時那裡沒人想到要改
|
||||
// ——這正是 bge-m3 換代「四處同步」清單漏掉的第五處。放在模型常數旁邊,
|
||||
// 下次換模型的人一定會看到它。**別再把數字複製回呼叫端。**
|
||||
const DEFAULT_MIN_SCORE = 0.5;
|
||||
|
||||
/** 實際使用的嵌入模型:env 可覆寫(#59),未設用預設。 */
|
||||
function embedModel(env: Bindings): string {
|
||||
const m = (env.EMBED_MODEL ?? '').trim();
|
||||
@@ -251,7 +271,10 @@ export interface SemanticHit {
|
||||
* 註:向量 metadata 的 library 在寫入端已正規化(未標記='general'),故 $in 不需 NULL 處理;
|
||||
* 但「建 library metadata index 之前」upsert 的既有向量沒有此欄 → 部署清單強制 reindex backfill。
|
||||
* min_score(issue #67):分數閾值——Vectorize 只會硬湊 topK 筆,低分尾全是無關內容;
|
||||
* 過濾放查詢端(非 Vectorize 端,API 無此參數)。預設 0=不過濾(行為與舊版一字不變,向後相容)。
|
||||
* 過濾放查詢端(非 Vectorize 端,API 無此參數)。
|
||||
* 🔴 2026-08-05 起預設=`DEFAULT_MIN_SCORE`(跟著模型走,見該常數的實測分布),
|
||||
* 不再是 0(0=不過濾=#67 要修的病本身,而呼叫端各自硬寫數字則是 08-05 全 0 命中的根因)。
|
||||
* caller 顯式傳值仍優先。
|
||||
*/
|
||||
export async function semanticSearch(
|
||||
env: Bindings,
|
||||
@@ -271,7 +294,7 @@ export async function semanticSearch(
|
||||
returnMetadata: 'indexed',
|
||||
...(Object.keys(filter).length ? { filter } : {}),
|
||||
});
|
||||
const minScore = opts.min_score ?? 0;
|
||||
const minScore = opts.min_score ?? DEFAULT_MIN_SCORE;
|
||||
return (res.matches ?? [])
|
||||
.filter((m) => m.score >= minScore)
|
||||
.map((m) => ({
|
||||
|
||||
Reference in New Issue
Block a user