StackTraces
Meilisearch從零跑通

從零跑通 Meilisearch 多索引聯查(multi-search / federation):6 步,外加 3 個容易卡住的地方

@debug_master2026-10-29

跟著這篇走完,你會在本地把 Meilisearch 的多索引聯查跑通:movies 索引已經在(複用第一篇的),再補建 movie_ratings 和 movie_people 兩個索引——這樣手上就有三個獨立索引了。然後你用同一個端點 /multi-search 演示兩種模式:不傳 federation 就是各歸各的 results 陣列(適合電商搜索欄的分區塊 UI),傳了 federation: {} 就是合併重排的單條 hits 列表(適合全局搜索框),再調 weight 讓某個索引排靠前、用 distinct 跨索引去重、換兩套分頁體系。整個過程 6 步,每步一條驗證命令。localhost:7700 + aSampleMasterKey 跟系列前幾篇一致,索引名固定用 movies / movie_ratings / movie_people——你換了端口、key 或索引名,把示例值替換掉就行。

這篇預設你已經跑通了從零跑通 Meilisearch 本地搜索服務——Meilisearch 啟動、異步任務輪詢、movies.json 下載導入的套路全部複用,不再重複。跟系列前 8 篇的區別說在前面:前 8 篇搜的都是單索引內部的數據(中文分詞、分面過濾、排序排名、向量語義……),這篇第一次跨出去——一次 HTTP 請求,搜多個索引,要麼各歸各展示,要麼合併重排後一起返回。這也正是 Meilisearch multi-search 官方文檔裡兩種模式(multi-index / federated)的分野。

Meilisearch:從零跑通多索引聯查的 6 個步驟

前置條件

項 要求 說明
Meilisearch v1.54.3 或更高 multi-search 和 federated search 都是穩定特性,低於這代版本 federation.distinct 等細節可能缺失
movies.json 已下載並導入 movies 索引 沒導回第一篇第三步;本文不重複下載導入
master key aSampleMasterKey 與系列一致
curl 任意版本 全文 curl 調 REST API

先跑這條確認服務活著且 movies 索引已在。

curl 'http://localhost:7700/indexes/movies/stats' \
  -H 'Authorization: Bearer aSampleMasterKey'
{
  "numberOfDocuments": 31944,
  "isIndexing": false,
  "fieldDistribution": { "id": 31944, "title": 31944, "genres": 31944, "release_date": 31944 }
}

返回 "isIndexing": false 且 numberOfDocuments 為 31944 左右就滿足條件。報 index_not_found 說明 movies 索引沒建,回第一篇補。

第一步:補建 movie_ratings 和 movie_people 兩個索引

movies 已經有了,但 multi-search 需要至少兩個不同索引才能展示跨索引效果。這一步補建 movie_ratings(評分數據)和 movie_people(演職人員),讓三個索引都能搜到 “batman” 相關的內容——movies 裡有 Batman 系列電影,movie_ratings 裡有這些電影的評分,movie_people 裡有出演這些電影的演員和導演。

先說明一個本文刻意做的設計:三個索引統一含 title 字段。因為後面第四步要用 federation.distinct 按 title 跨索引去重,而去重的硬前提是去重屬性名在各索引裡完全一致——如果 movies 叫 title、movie_ratings 叫 movie_title,distinct 用哪個名字都會漏掉另一個。這是跨索引搜索建模時最容易埋的坑,本文用統一的 title 把它避開,也順帶講清楚這條約束。

先顯式建兩個索引:

# movie_ratings 索引:字段 id / title / rating / votes
curl -X POST http://localhost:7700/indexes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"uid":"movie_ratings","primaryKey":"id"}'

# movie_people 索引:字段 id / title / name / role
curl -X POST http://localhost:7700/indexes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"uid":"movie_people","primaryKey":"id"}'

兩條命令都返回 taskUid 和 "status": "enqueued"。往兩個索引裡塞數據,都選 Batman 相關的,這樣後面搜 “batman” 時三個索引都能命中:

# movie_ratings:Batman 系列的评分数据(title 与 movies 里的电影名一致)
curl -X POST http://localhost:7700/indexes/movie_ratings/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '[
    {"id":1,"title":"Batman","rating":7.8,"votes":320000},
    {"id":2,"title":"Batman Returns","rating":7.0,"votes":210000},
    {"id":3,"title":"Batman Begins","rating":8.2,"votes":580000},
    {"id":4,"title":"The Dark Knight","rating":9.0,"votes":1800000},
    {"id":5,"title":"The Dark Knight Rises","rating":8.4,"votes":900000},
    {"id":6,"title":"The Batman","rating":7.8,"votes":650000},
    {"id":7,"title":"Batman v Superman","rating":6.4,"votes":730000}
  ]'

# movie_people:Batman 系列的演员和导演
curl -X POST http://localhost:7700/indexes/movie_people/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '[
    {"id":1,"title":"Batman","name":"Michael Keaton","role":"Actor"},
    {"id":2,"title":"Batman","name":"Jack Nicholson","role":"Actor"},
    {"id":3,"title":"Batman","name":"Tim Burton","role":"Director"},
    {"id":4,"title":"Batman Begins","name":"Christian Bale","role":"Actor"},
    {"id":5,"title":"The Dark Knight","name":"Heath Ledger","role":"Actor"},
    {"id":6,"title":"The Dark Knight","name":"Christopher Nolan","role":"Director"},
    {"id":7,"title":"The Batman","name":"Robert Pattinson","role":"Actor"},
    {"id":8,"title":"The Batman","name":"Zoë Kravitz","role":"Actor"}
  ]'

說明一下:官方 movies.json 只有 title / genres / release_date / overview 四個字段,沒有評分和演職人員——那是另一個 IMDb 數據集才有的。所以這兩個索引是手動造的,故意選了 Batman 系列,讓三個索引搜同一個詞都能命中。

導入是異步任務,輪詢到 succeeded 後用 stats 確認:

curl 'http://localhost:7700/indexes/movie_ratings/stats' -H 'Authorization: Bearer aSampleMasterKey'
curl 'http://localhost:7700/indexes/movie_people/stats' -H 'Authorization: Bearer aSampleMasterKey'

返回體裡 numberOfDocuments 分別是 7 和 8 就說明兩個索引都導入完成。報 index_not_found 是索引沒建成功,先查建索引的任務狀態;isIndexing: true 是還在處理,再等等。

第二步:multi-index search——分開返回,各歸各

multi-search 端點是 POST /multi-search,請求體必須有 queries 陣列。不傳 federation 字段(或顯式傳 null)就是 multi-index 模式——Meilisearch 對每條 query 獨立執行搜索,返回一個 results 陣列,陣列裡每個元素對應一條 query 的結果集。官方文檔見 Multi-index search。

先跑一遍三個索引各搜 “batman”:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "queries": [
      { "indexUid": "movies", "q": "batman", "limit": 3 },
      { "indexUid": "movie_ratings", "q": "batman", "limit": 3 },
      { "indexUid": "movie_people", "q": "batman", "limit": 3 }
    ]
  }'

返回體是 {"results": [...]} 陣列,三條 query 各一個:

{
  "results": [
    {
      "indexUid": "movies",
      "hits": [
        { "id": 268, "title": "Batman", "genres": ["Fantasy", "Action"] },
        { "id": 364, "title": "Batman Returns", "genres": ["Action", "Fantasy"] },
        { "id": 1735, "title": "Batman & Robin", "genres": ["Action", "Science Fiction"] }
      ],
      "query": "batman",
      "processingTimeMs": 2,
      "limit": 3,
      "offset": 0,
      "estimatedTotalHits": 8
    },
    {
      "indexUid": "movie_ratings",
      "hits": [
        { "id": 1, "title": "Batman", "rating": 7.8, "votes": 320000 },
        { "id": 3, "title": "Batman Begins", "rating": 8.2, "votes": 580000 }
      ],
      "query": "batman",
      "processingTimeMs": 0,
      "limit": 3,
      "offset": 0,
      "estimatedTotalHits": 6
    },
    {
      "indexUid": "movie_people",
      "hits": [
        { "id": 1, "title": "Batman", "name": "Michael Keaton", "role": "Actor" },
        { "id": 3, "title": "Batman", "name": "Tim Burton", "role": "Director" }
      ],
      "query": "batman",
      "processingTimeMs": 0,
      "limit": 3,
      "offset": 0,
      "estimatedTotalHits": 2
    }
  ]
}

讀一下這個響應體的結構——這是 multi-index 模式的簽名:

  • 頂層是 results 陣列,長度等於 queries 陣列長度,順序也一一對應——results[0] 是 movies 的結果、results[1] 是 movie_ratings、results[2] 是 movie_people。
  • 每條 query 可以帶完全獨立的參數——上例裡三個 limit 都是 3,但你可以給 movies 寫 limit: 10、給 movie_ratings 寫 limit: 2、給 movie_people 加個 filter: "role = Director"——都獨立生效。
  • 每條 results 的結構跟單個 POST /indexes/{uid}/search 的響應完全一致——hits / estimatedTotalHits / processingTimeMs 這些字段一個不差。意味著你現有的單索引搜索處理邏輯幾乎不用改,把輸入從單條換成陣列就行。

驗證命令:

curl -s -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{"queries":[{"indexUid":"movies","q":"batman","limit":1},{"indexUid":"movie_people","q":"batman","limit":1}]}' \
  | jq '.results | length'

返回 2——results 陣列長度等於 queries 長度就通了。返回 400 是某條 query 的 indexUid 拼錯了,multi-search 的 fail-fast 行為會讓整個請求直接失敗,哪怕其他 query 是合法的——這個坑見 FAQ 第 2 項。

第三步:federated search——合併重排,一條 hits 列表

剛才的 multi-index 模式把三個索引的結果分開返回——如果你的 UI 是搜索欄下面分三個區塊顯示(“電影:”、“評分:”、“相關人員:”),這正合適。但如果你想要的是一條統一的結果流——不管結果來自哪個索引,按相關度排在一條列表裡——那就要用 federated search。

切換條件很簡單:請求體里加一個 federation 字段,哪怕是空物件 {}。

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": {},
    "queries": [
      { "indexUid": "movies", "q": "batman" },
      { "indexUid": "movie_ratings", "q": "batman" },
      { "indexUid": "movie_people", "q": "batman" }
    ]
  }'

返回體結構完全變了——從 {"results": [...]} 陣列變成了 {"hits": [...]} 單物件:

{
  "hits": [
    {
      "id": 268, "title": "Batman", "genres": ["Fantasy", "Action"],
      "_federation": { "indexUid": "movies", "queriesPosition": 0 }
    },
    {
      "id": 1, "title": "Batman", "name": "Michael Keaton", "role": "Actor",
      "_federation": { "indexUid": "movie_people", "queriesPosition": 2 }
    },
    {
      "id": 1, "title": "Batman", "rating": 7.8, "votes": 320000,
      "_federation": { "indexUid": "movie_ratings", "queriesPosition": 1 }
    },
    {
      "id": 364, "title": "Batman Returns", "genres": ["Action", "Fantasy"],
      "_federation": { "indexUid": "movies", "queriesPosition": 0 }
    }
  ],
  "processingTimeMs": 3,
  "limit": 20,
  "offset": 0,
  "estimatedTotalHits": 16
}

這就是 federated search 的“啊哈時刻”——請求體加一行 "federation": {},返回體就從陣列變成物件,結果從各歸各的三段變成一條合併流,按 merge 算法重排,每條 hit 多出一個 _federation 字段標識它來自哪個索引。這個字段的有無,就是兩種模式的唯一開關。

讀一下 _federation 裡的字段:

字段 含義
indexUid 這條文檔來自哪個索引
queriesPosition 它在 queries 陣列裡的下標(0-based),同一個 indexUid 在 queries 裡出現兩次時能靠它區分
weightedRankingScore 可選,配了 federationOptions.weight 時出現,值是 _rankingScore × weight

Meilisearch 內部怎麼合併的?核心邏輯是歸一化 ranking score 後按字典序比較:先比 relevancy score(words / typo / attribute / exactness / vector 合併值),tie 再看 sort/geosort,再 tie 看全局 ranking score,完美 tie 按 queries 陣列順序優先。詳細規則見 federated search 官方文檔——本文不展開,你只需要知道「結果按多級 tiebreak 排成一條降序流」就夠用了。

驗證命令:

curl -s -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{"federation":{},"queries":[{"indexUid":"movies","q":"batman"}]}' \
  | jq 'has("hits") and (has("results") | not)'

返回 true——有 hits 沒有 results,就是 federated 模式。

第四步:weight 權重 + distinct 跨索引去重

federation 預設所有 query 等權合併——movies 的結果、movie_ratings 的結果、movie_people 的結果在排名上“公平競爭”,誰更相關誰排前面。但你大概率想要電影本身排在前面(用戶搜 “batman” 最可能是找電影),評分和演職人員作為補充。這就用到 weight。

weight:讓某個索引的結果排靠前

weight 是 federationOptions 裡的一個浮點乘數,預設 1.0:

  • weight > 1.0:該 query 的所有結果 ranking score 被乘大,更容易排在前面
  • weight < 1.0:結果 ranking score 被乘小,靠後
  • weight = 0.0:等效於排除該 query 的全部結果(負數非法)

讓 movies 權重拉到 1.5,另外兩個壓在 1.0 以下:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": {},
    "queries": [
      { "indexUid": "movies", "q": "batman",
        "federationOptions": { "weight": 1.5 } },
      { "indexUid": "movie_ratings", "q": "batman",
        "federationOptions": { "weight": 0.6 } },
      { "indexUid": "movie_people", "q": "batman",
        "federationOptions": { "weight": 0.6 } }
    ]
  }'

返回體裡 movies 的結果會集中排在前面。想看每條 hit 的加權後分數,在 query 里加 "showRankingScore": true,這時 _federation.weightedRankingScore 字段會出現。

weight 調優建議:先用小步長試(1.2 / 0.8),不要一上來拉 2.0 vs 0.1。因為 weight 是乘法——如果 movies 裡有一條 highly relevant 的 batman 結果(原始 rankingScore 0.95),乘 1.5 後 1.425,確實穩在前;但 movie_people 裡一條特別精確的命中(直接精確匹配 title 字段),哪怕 weight 只有 0.6,乘出來的分也可能超過 movies 裡一條弱相關的。weight 是「偏置」不是「開關」。

distinct:跨索引去重

注意第三步的返回體——同一個 “Batman” 出現了三次:movies(電影)裡一條、movie_ratings(評分)裡一條、movie_people(演職員)裡兩條。federated 合併後這些會散落在列表裡。如果你想讓每個電影只佔一個位置,用 federation.distinct。

federation.distinct 做的是跨所有參與索引的去重,按指定屬性值去——三個索引都有 title,所以 "distinct": "title" 會讓 title 值相同的文檔只留一條。前提是:去重屬性必須在每個參與索引的 filterableAttributes 裡聲明過。

先給三個索引都聲明 title 可過濾:

curl -X PATCH 'http://localhost:7700/indexes/movies/settings/filterable-attributes' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -d '["title","genres"]'

curl -X PATCH 'http://localhost:7700/indexes/movie_ratings/settings/filterable-attributes' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -d '["title","rating"]'

curl -X PATCH 'http://localhost:7700/indexes/movie_people/settings/filterable-attributes' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -d '["title"]'

等三個 PATCH 任務都 succeeded,再發帶 federation.distinct 的請求:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": { "distinct": "title" },
    "queries": [
      { "indexUid": "movies", "q": "batman" },
      { "indexUid": "movie_ratings", "q": "batman" },
      { "indexUid": "movie_people", "q": "batman" }
    ]
  }'

現在 “Batman” 這個 title 值只留一條,而不是 movies / movie_ratings / movie_people 各佔一條。這就是跨索引去重的效果。

兩個不能忘的邊界:第一,去重屬性必須在每個參與索引都聲明 filterable,任一索引漏了整體 400——見 FAQ 第 1 項;第二,同一個請求裡同時寫 "federation": {"distinct": "title"} 和某條 query 裡寫 "distinct": "title",會直接 400(invalid_multi_search_distinct),兩個 distinct 不兼容。

第五步:分頁的兩套體系

multi-search 的分頁有兩個變數:在哪層分頁(query 層還是 federation 層)和用哪種模式(offset/limit estimation 還是 page/hitsPerPage exhaustive)。

先講一個硬性限制:federated 模式下,queries 裡的 offset/limit/page/hitsPerPage 全部被忽略——引擎先跑完所有 query,合併完結果,再在 federation 層分頁。queries 裡寫了 limit 不報錯但不生效,這是個靜默坑,見 FAQ 第 3 項。

estimation 分頁(預設)

不傳任何分頁參數時 federation 預設 offset=0 / limit=20。顯式寫:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": { "offset": 10, "limit": 5 },
    "queries": [
      { "indexUid": "movies", "q": "batman" },
      { "indexUid": "movie_ratings", "q": "batman" }
    ]
  }'

返回體裡 offset: 10, limit: 5,跳過前 10 條返回接下來 5 條。這個模式性能好但 estimatedTotalHits 是估算值,做頁碼導航不夠精確。

exhaustive 分頁(精確總數)

需要精確知道總命中數(做“共 N 條結果、第 x/y 頁”這種 UI)時,用 page + hitsPerPage:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": { "page": 2, "hitsPerPage": 5 },
    "queries": [
      { "indexUid": "movies", "q": "batman" },
      { "indexUid": "movie_ratings", "q": "batman" }
    ]
  }'

返回體字段變了:

{
  "hits": [ "..." ],
  "processingTimeMs": 4,
  "page": 2,
  "hitsPerPage": 5,
  "totalHits": 14,
  "totalPages": 3
}

從 estimatedTotalHits 變成了 totalHits(精確值),多了 totalPages(總頁數)。代價是引擎要遍歷所有命中文檔來精確計數,大數據集下比 estimation 慢。

兩種分頁模式互斥——同一個 federation 物件裡同時寫 offset 和 page,行為未定義,實踐中避免。

multi-index 模式下的分頁

不傳 federation 時,每條 query 的 offset/limit/page/hitsPerPage 獨立生效——這跟 federated 模式相反。給 movies 寫 "limit": 5、給 movie_ratings 寫 "limit": 10,返回體裡 results[0].limit = 5、results[1].limit = 10,各自截斷各自的結果。

第六步:每條 query 帶獨立參數 + facetsByIndex

multi-search 每條 query 本質就是一個完整的 search 請求——所有 POST /indexes/{uid}/search 的參數都能用:filter / sort / attributesToRetrieve / facets / hybrid / vector 等。你可以在同一個請求裡,讓 movies 只搜 Action 片、讓 movie_ratings 只返回評分 ≥ 8.0 的記錄。

上一步已經給三個索引聲明瞭 filterableAttributes(movies 有 title+genres,movie_ratings 有 title+rating,movie_people 有 title),現在髮帶獨立 filter 的 federated search:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": {},
    "queries": [
      { "indexUid": "movies", "q": "batman",
        "filter": "genres = Action",
        "attributesToRetrieve": ["id","title","genres"] },
      { "indexUid": "movie_ratings", "q": "batman",
        "filter": "rating >= 8.0",
        "attributesToRetrieve": ["id","title","rating"] },
      { "indexUid": "movie_people", "q": "batman",
        "attributesToRetrieve": ["id","title","name","role"] }
    ]
  }'

返回體裡 movies 的 hits 都帶 genres: ["Action", ...],movie_ratings 的 hits 都滿足 rating >= 8.0。每條 query 的 filter 互相獨立、互不干擾——movies 用 genres 過濾、movie_ratings 用 rating 過濾,這正是 multi-search 比“分別發三個 HTTP 請求”更方便的地方:網絡只走一次,但每條 query 保留獨立參數。

你可能注意到中文字段名的問題——這些數據是英文的,filter 用英文;如果你的數據是中文,filter 表達式裡的屬性名和值要用你自己的字段,跟這篇中文搜索對對口徑。

facetsByIndex:按索引分別返回分面分布

federation 物件還可以帶 facetsByIndex——指定哪些索引的哪些屬性要返回 facet 分佈:

curl -X POST 'http://localhost:7700/multi-search' \
  -H 'Authorization: Bearer aSampleMasterKey' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "federation": {
      "facetsByIndex": {
        "movies": ["genres"],
        "movie_ratings": ["rating"]
      }
    },
    "queries": [
      { "indexUid": "movies", "q": "batman" },
      { "indexUid": "movie_ratings", "q": "batman" }
    ]
  }'

返回體多了一個 facetsByIndex 字段:

{
  "hits": [ "..." ],
  "facetsByIndex": {
    "movies": {
      "distribution": {
        "genres": { "Action": 8, "Fantasy": 2, "Science Fiction": 1 }
      }
    },
    "movie_ratings": {
      "stats": {
        "rating": { "min": 6.4, "max": 9.0 }
      }
    }
  }
}

facetsByIndex 讓你能一次請求同時拿到 movies 的 genres 分佈和 movie_ratings 的 rating 數值範圍,不用分別在兩條 query 裡寫 facets 參數再在應用層拼回來。

核心概念

這張架構圖就是整條調用鏈路,從左到右看兩種模式怎麼分叉:

Meilisearch:多索引聯查調用鏈路架構圖

概念 是什麼 類比
multi-search 一個端點一次請求搜多個索引的能力,入口是 POST /multi-search 像搜索引擎的“同時搜網頁+圖片+視頻”按鈕
federation multi-search 請求體裡的一個字段——有則合併重排返回單列表,無則各歸各返回陣列 像一個“合併開關”:關掉是多路返回,打開是一條統一結果流
federation.weight federationOptions 裡的浮點乘數,乘到 ranking score 上控制某 query 結果的優先級 像 DJ 臺的音量旋鈕:權重 1.5 的 query 聲音大一點,更容易被聽到
federation.distinct 跨所有參與索引按指定屬性值去重 像合併多個 Excel 表後按電影名去重——不管數據在哪個表,同名的只留一條
fail-fast multi-search 的錯誤處理策略:任一 query 參數非法,整個請求立即返回 400 像一條流水線:任一工位報錯,整條線停下,不會讓好的工位繼續出活

要點回顧

  • 一個端點,兩種模式——POST /multi-search 的返回形態由 federation 字段決定:不傳返回 results[](各歸各,適合分區塊 UI),傳了返回 hits(合併單列,適合全局搜索框)。
  • weight 是偏置不是開關——federationOptions.weight 是乘在 ranking score 上的浮點乘數(預設 1.0);用 1.2/0.8 這樣的小步長調,別一上來拉 2.0 vs 0.1。
  • distinct 跨索引去重——federation.distinct 按屬性值跨所有參與索引去重,硬前提是去重屬性在每個參與索引都聲明過 filterable,任一漏了整體 400。
  • 分頁分兩層兩套——federated 模式下 queries 裡的 offset/limit 被靜默忽略,分頁要寫在 federation 物件裡:offset/limit(估算)或 page/hitsPerPage(精確)二選一;multi-index 模式下各 query 獨立分頁。
  • fail-fast 是原子語義——任一條 query 參數非法,整個 multi-search 立即 400,不返回部分成功;需要部分成功就在應用層併發發獨立 search。

常見問題

federation.distinct 報 Attribute “xxx” is not filterable

現象:髮帶 "federation": {"distinct": "title"} 的 multi-search 返回 400,消息說 distinct 的屬性名不是 filterable。

原因:federation.distinct 做的是跨索引去重,本質是對每個參與索引執行 filter 操作。去重屬性必須在每個參與索引的 filterableAttributes 設置裡顯式聲明過,任一索引漏了就整體失敗。這不是 bug 是設計——去重依賴 filter 能力。

相關文章:Meilisearch federation.distinct 要求每個索引聲明 filterableAttributes

multi-search 一條 query 報錯,整個請求全掛

現象:multi-search 裡三條 query,第二條的 indexUid 拼錯了(比如 movie_rating 少了 s),整個請求返回 400——第一條和第三條的結果也拿不到,雖然它們完全合法。

原因:multi-search 是原子請求——引擎先做一輪全請求參數校驗,任何一條 query 不通過就直接返回第一個錯誤,不進入執行階段。這就是 fail-fast 策略。如果你需要部分成功的語義(某條 query 掛了其他照樣返回),別用 multi-search,在應用層併發發獨立的 search 請求。

相關文章:Meilisearch multi-search fail-fast:一條 query 報錯整個請求失敗

federated 模式下 queries 裡寫 offset/limit 不生效

現象:給 federated search 的某條 query 寫了 "limit": 5,但返回體裡還是預設 20 條,limit 參數被靜默忽略了——不報錯也不生效。

原因:federated search 的分頁是合併結果層面的——引擎先跑完所有 query(不做單 query 分頁),合併重排後再分頁。queries 裡的 offset/limit/page/hitsPerPage 在 federation 模式下被完全忽略。分頁參數必須寫在 federation 物件裡:federation.offset/limit(estimation)或 federation.page/hitsPerPage(exhaustive),二選一。

相關文章:Meilisearch federated search 在 queries 裡寫 offset/limit 被忽略

下一步

給 multi-search 裡的 query 加向量搜索

multi-search 的每條 query 完整支持 hybrid / vector / semantic 等所有單索引 search 參數——你可以讓 movies 用 hybrid(關鍵詞+語義混合)、讓 movie_people 用純關鍵詞、讓 movie_ratings 只 filter。先給參與索引配好 embedder,再在 query 里加 "hybrid": {"embedder": "default", "semanticRatio": 0.5}。向量嵌入和 hybrid 參數的細節回混合檢索篇。

從零跑通 Meilisearch 混合檢索(hybrid search):7 步

統一各索引的 filterable / sortable 設定

前面用了 title / genres / rating 做 filter,每次都 PATCH settings 再等異步任務,比較碎。建議用 PATCH /settings 一次性把 filterableAttributes / sortableAttributes / searchableAttributes 配好,每個索引一份設定清單。屬性配好之後寫 filter / sort 就不會再遇到 Attribute is not filterable 這類 400。

Meilisearch filter 報 Attribute is not filterable

把 federation 接進前端 unified search bar

後端 multi-search 跑通後,前端用 instant-meilisearch(JS / React / Vue 元件庫)接搜索框。傳一個帶 federation 的 multi-search request,前端按 _federation.indexUid 分組渲染,或直接按合併流渲染。instant-meilisearch 的 <InstantSearch> 元件原生支持 multi-search。

卡在哪一步了?

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

貢獻一條解法