StackTraces
Meilisearch從零跑通

從零跑通 Meilisearch 混合檢索(hybrid search):7 步,外加 3 個容易卡住的地方

@debug_master2026-10-12

跟著這篇走完,你會在本地把 Meilisearch 的混合檢索(hybrid search)跑通:配好內置 embedder 後,同一個查詢詞分別用語義佔比 0 / 0.5 / 1.0 三檔去搜,親眼看到“關鍵詞命中”和“語義命中”是怎麼被擰到一起排序的,再用 _rankingScore 看出每一條結果到底是靠哪條腿排上來的。整個過程 7 步,每步都有一條命令能驗證自己做對了沒有。localhost:7700 是 Meilisearch 的預設監聽地址,master key 固定用 aSampleMasterKey,索引建新的 books_hybrid(還是系列那 6 本虛構書籍),模型用 BAAI/bge-small-zh-v1.5——你要是換了端口、key、索引名或模型,把示例值替換掉就行。這篇預設你已經裝好並跑通了從零跑通 Meilisearch 本地搜索服務,安裝、啟動、異步任務輪詢的套路全部複用第一篇。跟系列前兩篇的區別說在前面:meilisearch-semantic-search 和 meilisearch-embedded-embedder 演示的都是 semanticRatio: 1.0 的純語義模式,這篇專門玩中間值——語義分與關鍵詞分合並排序、比例怎麼調,正是混搜跟純語義不一樣的地方。

Meilisearch:從零跑通混合檢索的 7 個步驟

前置條件

項 要求 說明
Meilisearch v1.54.3 或更高 內置 embedder 和 hybrid 檢索都已是穩定特性,低於這代版本接口行為可能有差異
前置文章 已跑通 meilisearch-local-search 複用其安裝/啟動/master key/異步任務輪詢套路;沒跑通先回去補
網絡 首次需能訪問 HuggingFace 模型首次使用時自動下載;完全離線或內網環境會卡在下載這一步
master key aSampleMasterKey 與系列一致
curl 任意版本 全文用 curl 調 REST API

先跑這條,確認第一篇的服務還活著。

curl http://localhost:7700/health
{
  "status": "available"
}

返回 {"status":"available"} 就滿足條件。要是返回 curl: (7) Failed to connect to localhost port 7700,說明服務沒起——回第一篇第一步用 meilisearch --master-key="aSampleMasterKey" 重新啟動,再回來。

第一步:建 books_hybrid 索引,傳 6 本純文本書(不帶向量)

混合檢索要做兩件事:查關鍵詞、查語義。語義這條路依賴向量,向量得掛在某個索引上,所以第一步先把索引和基礎數據準備好——跟 meilisearch-embedded-embedder 篇一樣,文檔就是普通 JSON,向量後面由服務端自動生成,這一步不需要碰 _vectors。

索引走顯式創建,指定主鍵 id,沿用系列慣例:

curl -X POST http://localhost:7700/indexes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"uid":"books_hybrid","primaryKey":"id"}'
{
  "taskUid": 40,
  "indexUid": "books_hybrid",
  "status": "enqueued",
  "type": "indexCreation",
  "enqueuedAt": "2026-10-12T10:00:00.000Z"
}

接著傳 6 本書。字段有 id、title、author 和 summary(書的簡介,後面 documentTemplate 要把它拼進嵌入文本,決定語義能搜到什麼):

curl -X POST http://localhost:7700/indexes/books_hybrid/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '[
    {"id":1,"title":"三体","author":"刘慈欣","summary":"仰望星空,写人类文明与三体文明星际博弈的科幻巨作"},
    {"id":2,"title":"球状闪电","author":"刘慈欣","summary":"以球状闪电为引的科幻小说,物理奇观与人生选择"},
    {"id":3,"title":"活着","author":"余华","summary":"写普通人在苦难中活下去的故事,农村家庭的伦理与温情"},
    {"id":4,"title":"许三观卖血记","author":"余华","summary":"写小人物靠卖血养家的故事,底层生活的坚韧与荒诞"},
    {"id":5,"title":"明朝那些事儿","author":"当年明月","summary":"用通俗笔法写明朝三百年历史,官场斗争与王朝兴衰"},
    {"id":6,"title":"万历十五年","author":"黄仁宇","summary":"从万历十五年切入,剖析明朝制度的运转与深层困境"}
  ]'
{
  "taskUid": 41,
  "indexUid": "books_hybrid",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate",
  "enqueuedAt": "2026-10-12T10:01:00.000Z"
}

上傳是異步的,任務隊列(Meilisearch 把每個寫操作排進一個任務隊列,按 uid 逐個執行,寫完狀態變 succeeded)跑完才落庫。等 taskUid: 41 變成 succeeded:

curl http://localhost:7700/tasks/41 -H "Authorization: Bearer aSampleMasterKey"
{
  "uid": 41,
  "indexUid": "books_hybrid",
  "status": "succeeded",
  "type": "documentAdditionOrUpdate",
  "details": {
    "receivedDocuments": 6,
    "indexedDocuments": 6
  }
}

看到 indexedDocuments: 6 就說明 6 條都落庫了。注意:現在這個索引還沒有向量,只能做關鍵詞檢索——語義那條腿要等第二步配了 embedder 才有。

第二步:設定 huggingFace embedder(default),等全量自動嵌入完成

光有純文本,混合檢索的“語義腿”是空的。要讓 Meilisearch 自己轉向量,得在索引設置裡聲明一個 embedder。embedder(嵌入器)是一條“向量由誰生成、用什麼模型、拿文檔的哪些字段去嵌入”的設定;這裡用 huggingFace 源——模型跑在你本機,不需要任何 API key,首次使用自動從 HuggingFace 下載。這一步不配,後面任何帶 hybrid 參數的搜索請求都會直接返回 400。

curl -X PATCH http://localhost:7700/indexes/books_hybrid/settings/embedders \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"default":{"source":"huggingFace","model":"BAAI/bge-small-zh-v1.5","documentTemplate":"中文书籍,书名《{{doc.title}}》,作者 {{doc.author}},简介 {{doc.summary}}"}}'
{
  "taskUid": 42,
  "indexUid": "books_hybrid",
  "status": "enqueued",
  "type": "settingsUpdate",
  "enqueuedAt": "2026-10-12T10:05:00.000Z"
}

default 是這條 embedder 的名字,後面搜索時 hybrid.embedder 要跟它逐字一致。documentTemplate(文檔模板)用 Liquid 模板語法把每條文檔拼成一段要送去嵌入的文本:{{doc.title}} 取文檔的 title 字段。這裡的 doc 是模板引擎的固定變數名,指“當前正在嵌入的這條文檔”——建索引、傳文檔時都不用聲明它,Meilisearch 渲染模板時自動把每條文檔注入成 doc,所以模板裡的字段名必須跟文檔 JSON 的鍵一一對應,寫成文檔裡不存在的鍵,整批嵌入會失敗(見常見問題第 3 條)。語義檢索比對的就是這段渲染文本,模板裡放哪些字段,決定了能被什麼樣的查詢搜到——把簡介拼進去,“按內容搜”才有真實依據。

等模型下載 + 6 條文檔全部嵌入完成(bge-small 權重約 90 多 MB,首次可能要幾分鐘)。輪詢剛才的 taskUid:

curl http://localhost:7700/tasks/42 -H "Authorization: Bearer aSampleMasterKey"
{
  "uid": 42,
  "indexUid": "books_hybrid",
  "status": "succeeded",
  "type": "settingsUpdate",
  "enqueuedAt": "2026-10-12T10:05:00.000Z",
  "startedAt": "2026-10-12T10:05:01.000Z",
  "finishedAt": "2026-10-12T10:08:32.000Z"
}

驗證一條:把 embedder 設置讀回來,確認設定落進去了。

curl http://localhost:7700/indexes/books_hybrid/settings/embedders -H "Authorization: Bearer aSampleMasterKey"
{
  "default": {
    "source": "huggingFace",
    "model": "BAAI/bge-small-zh-v1.5",
    "documentTemplate": "中文书籍,书名《{{doc.title}}》,作者 {{doc.author}},简介 {{doc.summary}}"
  }
}

讀到 {"default":{"source":"huggingFace",...}} 就對了。任務沒 succeeded 之前去搜,索引還沒有向量,搜出來跟普通關鍵詞沒區別——這個坑後面常見問題裡專門有一條。

第三步:第一次混合搜索——semanticRatio 0.5 預設檔,看 _rankingScore

前兩步證明了“向量生成了”,但這隻說明語義腿有腿可走,還沒證明混搜真的在混合。混合檢索(hybrid search)是 Meilisearch 把關鍵詞檢索和語義(向量)檢索並行跑一遍,再把兩路結果合併排序的模式——不是簡單拼接,而是一套能自動判斷“這查詢該偏哪邊”的評分機制(官方叫 smart scoring)。這一步用一個既有字面命中、又有語義命中的查詢,打開分數開關,看兩路是怎麼合併的。

查詢「明朝三百年曆史的通俗書」:文檔裡《明朝那些事兒》(id 5)的簡介“用通俗筆法寫明朝三百年曆史”,四個詞全都字面命中;《萬曆十五年》(id 6)只有“明朝”沾邊。這是個關鍵詞和語義大概率一致選 id 5 的查詢,正好用來觀察分數結構。

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"明朝三百年历史的通俗书","hybrid":{"embedder":"default","semanticRatio":0.5},"showRankingScore":true,"showRankingScoreDetails":true}'

hybrid(混合檢索參數)物件裡 embedder 指名用哪條嵌入器(就是第二步配的 default),semanticRatio(語義佔比)控制關鍵詞與語義結果的比例:0 是純關鍵詞,1 是純語義,0.5 是預設的均衡混合。showRankingScore: true 讓每條結果帶上 _rankingScore(相關性分數,0.0 到 1.0,越高越相關;v1.8 起語義分併入這個字段);showRankingScoreDetails: true 再附加分項明細。返回體按官方文檔結構大致長這樣(具體鍵名和分值以你本地實測為準):

{
  "hits": [
    {
      "id": 5,
      "title": "明朝那些事儿",
      "_rankingScore": 0.9,
      "_rankingScoreDetails": {
        "words": {"order": 0, "score": 0.9},
        "typo": {"order": 1, "score": 0.9},
        "vector": {"order": 6, "score": 0.62}
      }
    },
    {
      "id": 6,
      "title": "万历十五年",
      "_rankingScore": 0.78,
      "_rankingScoreDetails": {
        "words": {"order": 0, "score": 0.52},
        "typo": {"order": 1, "score": 0.52},
        "vector": {"order": 6, "score": 0.74}
      }
    }
  ],
  "query": "明朝三百年历史的通俗书",
  "processingTimeMs": 4,
  "estimatedTotalHits": 2
}

看 _rankingScoreDetails 裡的兩個分項就能理解排序:id 5 靠 words(關鍵詞)拿了 0.9 的高分——四個查詢詞它全有;id 6 關鍵詞分只有 0.52(就“明朝”一個詞),但 vector(語義)分 0.74 高於 id 5 的 0.62,因為“從萬曆十五年切入剖析制度運轉”和“三百年曆史的通俗書”意思上更貼。混合模式下最終分是兩路合併的結果,這就是“同時看字面和意思”的含義。

驗證:確認返回裡帶 _rankingScore 且 estimatedTotalHits 非 0。要是返回 400,先看錯誤碼——hybrid.embedder 寫錯名字報 invalid_search_embedder,hybrid 結構寫錯報 invalid_search_hybrid_query,排查入口見Meilisearch semanticRatio 越界或 embedder 名寫錯報錯。

第四步:擰旋鈕對照——同一查詢在 0 / 0.5 / 1.0 三檔下的命中差異

0.5 的均衡檔看不出每一路的貢獻,得把旋鈕擰到兩頭對照。這一步是整篇的核心:用一個查詢詞在文檔字面上幾乎不出現的查詢,驗證“語義命中”和“關鍵詞命中”的差異。

查詢「仰望星空,講人類文明命運的科幻鉅作」。先看純關鍵詞檔(semanticRatio: 0——不查 embedder,純關鍵詞):

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"仰望星空,讲人类文明命运的科幻巨作","hybrid":{"embedder":"default","semanticRatio":0},"showRankingScore":true}'
{
  "hits": [],
  "query": "仰望星空,讲人类文明命运的科幻巨作",
  "processingTimeMs": 1,
  "estimatedTotalHits": 0
}

關鍵詞模式一個都搜不到。原因在數據裡:這句話拆成詞後,「仰望星空」「人類文明」「命運」在 6 本書的字段裡都不存在——唯一沾邊的「科幻」出現在《三體》《球狀閃電》的簡介裡,但孤零零一個詞撐不起一條命中,Meilisearch 按詞匹配的規則直接放棄。這就是典型的詞彙鴻溝(用戶說的話和文檔寫的話對不上)。

再把旋鈕擰到純語義檔(semanticRatio: 1.0):

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"仰望星空,讲人类文明命运的科幻巨作","hybrid":{"embedder":"default","semanticRatio":1.0},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 1,
      "title": "三体",
      "author": "刘慈欣",
      "_rankingScore": 0.82
    },
    {
      "id": 2,
      "title": "球状闪电",
      "author": "刘慈欣",
      "_rankingScore": 0.71
    }
  ],
  "query": "仰望星空,讲人类文明命运的科幻巨作",
  "processingTimeMs": 3,
  "estimatedTotalHits": 2
}

語義模式命中了。機制是:查詢詞自動轉向量,跟庫裡每條文檔的向量比相似度——《三體》簡介裡有“人類文明、星際博弈、科幻鉅作”,《球狀閃電》有“科幻小說、物理奇觀”,查詢描述的內容跟它們的意思對得上,靠的是第二步 documentTemplate 拼進去的 summary 字段,不是書名的先驗。

輪到均衡檔 semanticRatio: 0.5:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"仰望星空,讲人类文明命运的科幻巨作","hybrid":{"embedder":"default","semanticRatio":0.5},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 1,
      "title": "三体",
      "author": "刘慈欣",
      "_rankingScore": 0.81
    },
    {
      "id": 2,
      "title": "球状闪电",
      "author": "刘慈欣",
      "_rankingScore": 0.7
    }
  ],
  "query": "仰望星空,讲人类文明命运的科幻巨作",
  "processingTimeMs": 3,
  "estimatedTotalHits": 2
}

0.5 檔也命中了同樣的兩條,但注意分數:跟 1.0 檔比略有不同,而且這時候 _rankingScore 裡關鍵詞分幾乎是 0、語義分在帶節奏——合併方式不是簡單的加權平均,而是把兩路分數做可比化之後綜合(Meilisearch 會對語義分做分佈歸一化,讓它能跟關鍵詞分同臺比較)。三個檔位對照下來:0 空、0.5 和 1.0 命中但分數構成不同,語義腿的貢獻一眼就分得出。

驗證:三檔各搜一次,0 檔 estimatedTotalHits: 0、0.5 和 1.0 檔命中《三體》。semanticRatio 超出 0 到 1 的範圍會直接 400 invalid_search_semantic_ratio,同樣內鏈Meilisearch semanticRatio 越界或 embedder 名寫錯報錯。

第五步:寫入一條新文檔,等自動嵌入完成後立即混合查詢

前面驗證的都是初始 6 條數據,混搜對之後寫入的新數據還靈不靈?配好 embedder 後,新增文檔入庫時 Meilisearch 會同樣自動轉向量,這一步就驗證這條寫入路徑。傳一本新書《圍城》,還是純文本、不帶向量:

curl -X POST http://localhost:7700/indexes/books_hybrid/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"id":7,"title":"围城","author":"钱钟书","summary":"写婚姻与人生的围城困局,知识分子的困惑与讽刺"}'
{
  "taskUid": 43,
  "indexUid": "books_hybrid",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate",
  "enqueuedAt": "2026-10-12T10:10:00.000Z"
}

新文檔也要先過一遍嵌入,照舊輪詢任務號:

curl http://localhost:7700/tasks/43 -H "Authorization: Bearer aSampleMasterKey"
{
  "uid": 43,
  "indexUid": "books_hybrid",
  "status": "succeeded",
  "type": "documentAdditionOrUpdate",
  "details": {
    "receivedDocuments": 1,
    "indexedDocuments": 1
  }
}

驗證:用 0.5 檔混搜一個能命中《圍城》的查詢——「婚姻是圍城,困住人的故事」這個詞,關鍵詞腿能命中“婚姻、圍城”(簡介裡都有),語義腿也指向圍城,兩條腿應該都把它頂上來:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"婚姻是围城,困住人的故事","hybrid":{"embedder":"default","semanticRatio":0.5},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 7,
      "title": "围城",
      "author": "钱钟书",
      "_rankingScore": 0.85
    }
  ],
  "query": "婚姻是围城,困住人的故事",
  "processingTimeMs": 3,
  "estimatedTotalHits": 1
}

hits 裡出現 id: 7 的《圍城》就對了——圍城的 summary 寫的是“寫婚姻與人生的圍城困局”,跟查詢說的是同一件事,關鍵詞腿和語義腿都支持它。寫入路徑驗證通過。要是任務沒跑完就去搜,新文檔會搜不到,排查見Meilisearch 添加文檔後立即搜索返回空結果。

第六步:更新一條文檔,等自動重嵌入完成後再次查詢

更新路徑同理:改文檔內容 → Meilisearch 重新給這條文檔生成向量、替換舊向量。這一步把第五步的 id: 7 從《圍城》改成《邊城》,然後查兩次驗證。更新就是“同一條 id 再傳一次”,POST /documents 對已存在的 id 是覆蓋更新:

curl -X POST http://localhost:7700/indexes/books_hybrid/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"id":7,"title":"边城","author":"沈从文","summary":"写湘西边陲小镇的乡土爱情,纯净而忧伤的故事"}'
{
  "taskUid": 44,
  "indexUid": "books_hybrid",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate",
  "enqueuedAt": "2026-10-12T10:12:00.000Z"
}
curl http://localhost:7700/tasks/44 -H "Authorization: Bearer aSampleMasterKey"
{
  "uid": 44,
  "indexUid": "books_hybrid",
  "status": "succeeded",
  "type": "documentAdditionOrUpdate",
  "details": {
    "receivedDocuments": 1,
    "indexedDocuments": 1
  }
}

驗證一:搜《邊城》的查詢,id: 7 以新書名命中:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"湘西边陲小镇的乡土爱情故事","hybrid":{"embedder":"default","semanticRatio":0.5},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 7,
      "title": "边城",
      "author": "沈从文",
      "_rankingScore": 0.83
    }
  ],
  "query": "湘西边陲小镇的乡土爱情故事",
  "processingTimeMs": 3,
  "estimatedTotalHits": 1
}

驗證二:回搜第五步那個「婚姻是圍城」的查詢,id: 7 不再出現——舊向量已被替換:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"婚姻是围城,困住人的故事","hybrid":{"embedder":"default","semanticRatio":0.5},"showRankingScore":true}'
{
  "hits": [],
  "query": "婚姻是围城,困住人的故事",
  "processingTimeMs": 2,
  "estimatedTotalHits": 0
}

id: 7 從“圍城命中”變成“邊城命中”,是更新觸發自動重嵌入的直接證據:全程只改了文檔文本,向量一次都沒手動碰過。到這裡,寫入、更新、查詢三條路徑的混合檢索全部驗證完。

第七步(進階):分場景調 semanticRatio,用 rankingScoreThreshold 過濾低分

不同場景的查詢習慣不一樣:搜書名、型號這類精確詞,希望關鍵詞腿主導(低 ratio);搜“我想要什麼”這類描述,希望語義腿主導(高 ratio)。semanticRatio 是搜索時參數,同一個索引不同請求可以各用各的,不用改設定。這一步用同一個查詢演示兩檔差異,再加一層低分過濾。

查詢「底層小人物養家餬口的艱難」。先看低語義檔 semanticRatio: 0.2——這個查詢裡的「底層」「小人物」「養家」在《許三觀賣血記》(id 4)簡介裡全都字面存在,關鍵詞腿應該精確鎖定它:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"底层小人物养家糊口的艰难","hybrid":{"embedder":"default","semanticRatio":0.2},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 4,
      "title": "许三观卖血记",
      "author": "余华",
      "_rankingScore": 0.88
    }
  ],
  "query": "底层小人物养家糊口的艰难",
  "processingTimeMs": 2,
  "estimatedTotalHits": 1
}

低語義檔下結果收斂:只有 id 4,因為「養家餬口」裡“養家”兩個字面命中,分數主要由關鍵詞分貢獻。再看高語義檔 semanticRatio: 0.8:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"底层小人物养家糊口的艰难","hybrid":{"embedder":"default","semanticRatio":0.8},"showRankingScore":true}'
{
  "hits": [
    {
      "id": 4,
      "title": "许三观卖血记",
      "author": "余华",
      "_rankingScore": 0.86
    },
    {
      "id": 3,
      "title": "活着",
      "author": "余华",
      "_rankingScore": 0.57
    }
  ],
  "query": "底层小人物养家糊口的艰难",
  "processingTimeMs": 3,
  "estimatedTotalHits": 2
}

語義佔比拉高後,《活著》(id 3)進來了——它簡介裡“普通人在苦難中活下去”,跟“底層艱難生活”意思相近,但「底層」「小人物」「養家」一個詞都沒有,純靠語義腿,所以分數只有 0.57,明顯低於字面全中的 id 4。這就是語義腿“放進來”的候選:有價值,但分低。

低分候選多了會稀釋結果頁。用 rankingScoreThreshold(相關性分數閾值,0.0 到 1.0,低於閾值的結果直接不返回,官方建議從 0.5 起步)把 0.57 的《活著》過濾掉:

curl -X POST http://localhost:7700/indexes/books_hybrid/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"底层小人物养家糊口的艰难","hybrid":{"embedder":"default","semanticRatio":0.8},"rankingScoreThreshold":0.5}'
{
  "hits": [
    {
      "id": 4,
      "title": "许三观卖血记",
      "author": "余华"
    }
  ],
  "query": "底层小人物养家糊口的艰难",
  "processingTimeMs": 3,
  "estimatedTotalHits": 1
}

0.57 < 0.5 的被閾值攔下,只剩 id 4。到這一步,混搜的比例調優、低分過濾、寫入/更新路徑全部驗證完。

核心概念

下面這張圖是整條調用鏈路的走向:一個查詢進 Meilisearch,關鍵詞和語義兩條路並行跑,分數合併後統一排序輸出。

Meilisearch 混合檢索調用鏈路架構圖

概念 是什麼 類比
混合檢索(hybrid search) 關鍵詞檢索與語義(向量)檢索並行執行後合併排序的搜索模式 查重名再查意思,兩邊都查
semanticRatio 搜索時參數,0.0 到 1.0,控制關鍵詞與語義結果的比例,預設 0.5 音量旋鈕,擰到哪頭聽哪頭
embedder 索引設置裡聲明“向量由誰生成、用什麼模型、嵌入哪些字段”的設定項 快遞櫃儲物格,聲明瞭誰送貨、貨多長
smart scoring 精確詞自動偏關鍵詞、描述詞自動偏語義的合併評分機制 分診臺,根據症狀分科室
_rankingScore 每條結果 0.0 到 1.0 的綜合相關性分數,v1.8 起語義分併入其中 體檢總分
詞彙鴻溝 用戶查詢用詞與文檔用詞對不上的現象 你說“爆米花機”,文檔裡寫“玉米膨化設備”
rankingScoreThreshold 低於閾值的低分結果直接不返回的過濾參數 及格線,線下不發榜

常見問題

invalid_search_semantic_ratio / invalid_search_embedder

現象:帶 hybrid 的搜索返回 400,錯誤碼 invalid_search_semantic_ratio 或 invalid_search_embedder。

原因:前者是 semanticRatio 超出了 0.0 到 1.0 的範圍(比如寫了 1.5 或字符串 "0.5");後者是 hybrid.embedder 寫的名字跟索引 settings 裡設定的 embedder 名對不上(設定叫 default 卻寫了 "Default")。兩個都是請求參數校驗失敗,同步 400,改完重發即可。

相關文章:Meilisearch semanticRatio 越界或 embedder 名寫錯報錯

配完 embedder 立刻搜索,結果跟普通關鍵詞一樣

現象:第二步配完 embedder 馬上搜,hits 是純關鍵詞結果甚至為空,語義完全沒生效。

原因:嵌入是異步任務,模型下載加全量嵌入還沒跑完你就去搜了,索引裡還沒有向量。settingsUpdate / documentAddition 任務沒到 succeeded,混搜的語義腿等於空跑。輪詢 taskUid 到 succeeded 再搜。

相關文章:Meilisearch 添加文檔後立即搜索返回空結果

template_rendering_error

現象:提交 embedder 設定或改 documentTemplate 後任務變成 failed,error.code 是 template_rendering_error,message 含 “missing field in document”。

原因:documentTemplate 裡用 {{doc.xxx}} 引用了文檔中不存在的字段,整批嵌入失敗。修法:把模板改回只引用真實存在的字段,或用 {% if doc.xxx %} 包一層守衛,改完任務 succeeded 再繼續。

相關文章:Meilisearch documentTemplate 引用缺失字段報 template_rendering_error

下一步

回到純語義版,看看“只查意思”長什麼樣

這篇的 0.5 檔是混搜,之前的語義搜索文章演示的是 semanticRatio: 1.0 純語義模式——那篇從手動算向量講起,可以對照理解“語義腿”單獨工作的原理。

從零跑通 Meilisearch 純向量語義搜索

把向量交給服務端:內置 embedder 全流程

混搜的語義腿由 embedder 提供,這篇只配了一個。內置 embedder 那篇講了自動嵌入的完整鏈路:寫入/更新自動轉向量、改模板觸發重嵌入、多 embedder 並存切換,是混搜的下游依賴。

從零跑通 Meilisearch 內置 embedder 自動嵌入

給混搜加排序規則和分面

混搜負責“召回和打分”,排完序還能按業務規則再排、按字段做分面過濾。想理解混搜結果在排序規則裡的位置,這兩篇是順路的。

從零跑通 Meilisearch 排序規則 · 從零跑通 Meilisearch 分面搜索

卡在哪一步了?

從零跑通的過程中撞到報錯?把命令和輸出記下來,幫助下一個剛起步的人。

貢獻一條解法