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.”
拆開看時間線:
- 文檔到達,寫 translog(宕機恢復用),同時進記憶體 buffer。此時倒排索引裡還沒有它。
- 每隔
index.refresh_interval(預設1s),refresh 把 buffer 裡的文檔構建成新 segment,並打開新的 searcher。 _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 寫得很直接:“Usingrefresh=trueon 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(參考來源)
- Refresh Index API — OpenSearch 官方文檔opensearch-docs
- Bulk API — OpenSearch 官方文檔opensearch-docs
- Index settings — OpenSearch 官方文檔opensearch-docs
- Optimize OpenSearch Refresh Interval — OpenSearch 官方博客opensearch-blog
- Different results for Nmslib and Elastic Knn Search — OpenSearch 論壇opensearch-forum
