StackTraces
OpenSearch已驗證

OpenSearch 剛導入的文檔 _count 正常但 _search 返回 0 hits(refresh 間隔)

@debug_master今天更新閱讀 6 min00

bulk 導入 OpenSearch 後立刻 _search 返回 0 hits,但 GET /movies/_count 已經是 5。根因是 OpenSearch 近實時搜索:文檔先寫 translog 和記憶體 buffer,refresh(預設每 1 秒)後才生成可搜索的 segment。解法是等一秒重搜,或導入時加 refresh=wait_for;?refresh=true 只適合本地體驗。適用 OpenSearch 全版本,本文實測 3.9.0。

Issue & Env(案發現場與環境)

  • 業務場景:跟著 從零跑通 OpenSearch 用 _bulk 導入 5 條電影文檔,響應裡 "errors" : false,每條 item 都是 "result" : "created"。緊接著搜 title: "wind rise",返回 0 hits;同一條查詢等一秒再跑,命中 3 條。
  • 環境:OpenSearch 3.9.0,Docker 單節點,索引 movies(1 主分片、0 副本),security 插件開啟(HTTPS + Basic Auth)。
  • 關鍵區分:不是寫入失敗,也不是查詢語法問題。_count 和 _search 查的數據源不同——前者統計已寫入的文檔,後者查已 refresh 的倒排索引。兩者之間有最多一個 refresh 間隔的窗口期。

StackTrace(堆疊日誌)

Bulk API 官方文檔把這套行為講得很直白,原話是:“OpenSearch uses a near-real-time search model where documents are not immediately searchable after indexing.”

bulk 返回後立刻執行 GET /movies/_count?pretty:

{
  "count" : 5,
  "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }
}

緊接著執行 _search(查 title: "wind rise",按示例數據應命中 3 條):

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 },
  "hits" : {
    "total" : { "value" : 0, "relation" : "eq" },
    "max_score" : null,
    "hits" : []
  }
}

count 是 5,hits.total.value 是 0,兩條命令間隔不到一秒。等一秒重跑 _search,value 變回 3。

這個“矛盾”在社區裡出現過。OpenSearch 論壇有個 k-NN 召回率例外低的帖子(#3142,2020-06),維護者 jmazane 排查的第一步就是問寫入可見性:“After indexing, the documents may not be immediately searchable.”——先確認文檔真的能搜到,再懷疑算法。剛導入的文檔,_search 查不到是常態。

Reproduce(復現程式碼)

前提:本地 OpenSearch 已啟動,示例值沿用 quickstart(端口 9200、用戶 admin、密碼 Devfix@2026)。

建索引:

curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies?pretty" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": { "index.number_of_shards": 1, "index.number_of_replicas": 0 },
    "mappings": {
      "properties": {
        "title": { "type": "text" },
        "year":  { "type": "integer" },
        "genre": { "type": "keyword" }
      }
    }
  }'

導入 5 條文檔:

cat > movies.ndjson <<'EOF'
{ "index": { "_index": "movies", "_id": "1" } }
{ "title": "Let the Wind Rise", "year": 2013, "genre": "drama" }
{ "index": { "_index": "movies", "_id": "2" } }
{ "title": "Gone with the Wind", "year": 1939, "genre": "drama" }
{ "index": { "_index": "movies", "_id": "3" } }
{ "title": "The Rise of Machines", "year": 2021, "genre": "scifi" }
{ "index": { "_index": "movies", "_id": "4" } }
{ "title": "Deep Sea Chronicles", "year": 2018, "genre": "documentary" }
{ "index": { "_index": "movies", "_id": "5" } }
{ "title": "Windmill Valley", "year": 2015, "genre": "drama" }
EOF

curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?pretty" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @movies.ndjson

連查兩次,結果矛盾:

# 立刻查 _count:5 条都在
curl -sk -u admin:Devfix@2026 "https://localhost:9200/movies/_count?pretty"

# 紧接着 _search:0 hits
curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "match": { "title": "wind rise" } } }'

# 等一秒重跑上一条 _search:3 条命中
sleep 1

判定標準:_count 返回 "count" : 5 且首次 _search 返回 "value" : 0,即復現成功。

Root Cause(根因分析)

寫入鏈路:translog + buffer → refresh → segment

Refresh Index API 文檔給了寫入鏈路的原話:“When you index a document, it is first written to a translog and added to in-memory buffers. The document is not searchable until a refresh operation converts these in-memory structures into searchable segments on disk.”

拆開看時間線:

  1. 文檔到達,寫 translog(宕機恢復用),同時進記憶體 buffer。此時倒排索引裡還沒有它。
  2. 每隔 index.refresh_interval(預設 1s),refresh 把 buffer 裡的文檔構建成新 segment,並打開新的 searcher。
  3. _search 只查 searcher 可見的 segment。refresh 沒跑,文檔就不在搜索的視野裡。

_count 統計的是已寫入的文檔,走的是實時寫入側;_search 查的是已 refresh 的倒排索引。數據源不同,“count 對、search 空”就是窗口期內的正常狀態。

近實時是設計取捨,不是 bug

為什麼不每寫一條就 refresh?官方博客《Optimize OpenSearch Refresh Interval》說得很清楚:“Refresh operations are resource intensive. The procedure of transferring data into new segments and rendering them searchable demands CPU, memory, and input/output (I/O) resources.”

每次 refresh 都生成一個新 segment。高頻寫入配高頻 refresh,會產生大量小 segment,後臺 merge 得花更多資源把它們合掉。所以 Lucene 系引擎(OpenSearch、Elasticsearch、Solr)統一選了“近實時”:攢一秒的寫入,一次 refresh 生成一個 segment,用一秒的可見性延遲換索引吞吐。一秒延遲是特性,不是缺陷。

預設 1s 只對“活躍”分片生效

自動 refresh 還有個前提條件。文檔裡寫的是:未顯式設置 index.refresh_interval 時,分片每秒 refresh,直到連續 30 秒(index.search.idle.after,預設 30s)沒有搜索請求,分片進入 idle 狀態。idle 後不再自動 refresh,直到下一次搜索請求或手動調用 Refresh API——首個到達 idle 分片的搜索請求會等 refresh 完成再返回。

The Fix(解決方案)

方案一:等一秒重搜(預設行為,零改動)

本地體驗什麼都不用改,sleep 1 後重搜即可。預設 refresh_interval: 1s 保證了新文檔最多一秒後可見。

方案二:導入時用 refresh 參數

Bulk、Index、Update、Delete 這些文檔 API 都支持 refresh 查詢參數,三個取值:

  • false(預設):不主動 refresh,按索引的 refresh 間隔(通常 1 秒)變可見。
  • true:強制立即 refresh 受影響的分片,文檔馬上可搜,但有性能代價。
  • wait_for:等下一次計劃中的 refresh 完成再返回,兼顧可見性和性能。

本地演示,導入完立刻要搜:

curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?refresh=true&pretty" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @movies.ndjson

生產程式碼裡“寫完馬上讀”的場景,官方推薦 wait_for 而不是 true——它不強制立即 refresh,只是把請求掛到下一次週期性 refresh,不會打亂 segment 節奏:

curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?refresh=wait_for&pretty" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @movies.ndjson

方案三:批量導入前關自動 refresh,導完手動補

全量導入場景,refresh 一秒一次純屬浪費。先把間隔設成 -1 關掉自動 refresh(Index settings 文檔原話:“Can be set to -1 to disable refreshing. Default is 1s.”),導完手動 refresh 一次,再恢復預設:

# 导入前关闭
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies/_settings?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "index": { "refresh_interval": "-1" } }'

# ……执行 bulk 导入……

# 导入完成,手动 refresh 一次
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/movies/_refresh?pretty"

# 恢复默认间隔
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/movies/_settings?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "index": { "refresh_interval": "1s" } }'

官方博客的建議與此一致:在已知的寫入密集任務前關閉自動 refresh,結束後手動觸發一次。

注意事項

  • 生產別用 refresh=true。Refresh API 文檔的 Best practices 寫得很直接:“Using refresh=true on index, update, or delete operations forces an immediate refresh and creates inefficient index structures (small segments) that must later be merged, impacting both indexing and search performance.”
  • refresh_interval 是動態設置,_settings 直接改,不用重建索引。
  • 驗證“寫沒寫進去”用 _count,驗證“能不能搜到”用 _search。兩者結果不一致時,先看時間差有沒有超過一個 refresh 間隔。
  • 這個現象容易和查詢語法問題混淆,分詞行為見 從零跑通 OpenSearch 第五步。

References(參考來源)

最後更新於 2026-10-17

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法