StackTraces
OpenSearch從零跑通

從零跑通 OpenSearch:Docker 單節點起步,6 步走完增刪改查

@debug_master2026-10-14

跟著這篇走完,你會在本機用一個 Docker 容器把 OpenSearch 跑起來,然後按 建索引 → 導入 → 搜索 → 改 → 刪 的順序把一套文檔操作走一遍,共 6 步,每步都有一條命令能驗證自己做對了沒有。文中固定用這些值:端口 9200、用戶 admin、示例密碼 Devfix@2026、索引 movies、5 條電影示例數據;你換了就把示例值替換掉。如果你玩過 從零跑通 Meilisearch 本地搜索服務,這次換個思路:Meilisearch 是開箱即用的輕引擎,OpenSearch 是分片化的重引擎,本地跑法差別不小。

從零跑通 OpenSearch 6 步流程封面:啟動單節點 → 檢查健康 → 建 movies 索引 → bulk 導入 → 搜索 → 更新刪除

前置條件

項 要求 說明
Docker 任意近期版本 本文用 Docker 起服務,這是本地體驗 OpenSearch 最省事的方式
記憶體 Docker Desktop 至少 4 GB OpenSearch 是 JVM 進程,比較吃記憶體,官方安裝文檔建議給 Docker Desktop 至少 4 GB
磁盤 2 GB 以上空閒 官方鏡像加上索引數據的餘量
Linux 宿主機額外項 vm.max_map_count=262144 Lucene(OpenSearch 底層的搜索庫)用記憶體映射文件,預設值不夠;macOS/Windows 的 Docker Desktop 不需要改
curl 任意版本 全文用 curl 調 REST API

Linux 上先確認一下:

cat /proc/sys/vm/max_map_count

小於 262144 就臨時調大:

sudo sysctl -w vm.max_map_count=262144

第一步:用 Docker 啟動單節點

OpenSearch 官方鏡像在 Docker Hub,叫 opensearchproject/opensearch。當前 stable 是 3.9.0(2026-09-29 發佈),用 latest 標籤或寫死版本號都行,本文示例用 latest。

啟動命令有一個新手必踩的坑先說在前面:OpenSearch 從 1.x 起預設啟用 security 插件——它負責登錄鑑權,並用 TLS 加密 API 通信,會生成自簽證書。而且 2.12 版本之後要求你顯式設一個管理員密碼,不設密碼容器直接退出。所以啟動命令必須帶 OPENSEARCH_INITIAL_ADMIN_PASSWORD,密碼規則是至少 8 位、大小寫、數字、特殊字符各至少一個:

docker run -d -p 9200:9200 -p 9600:9600 \
  -e "discovery.type=single-node" \
  -e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=Devfix@2026" \
  opensearchproject/opensearch:latest

discovery.type=single-node 告訴它不用找別的節點,自己就是一個集群——集群是整個搜索服務,節點是其中一個個 OpenSearch 實例,單節點就是一個實例撐起一個集群;9200 是 REST API 端口,9600 是性能分析端口。

容器起來後 JVM 啟動要幾十秒,先等它就緒。驗證:

curl -sk -u admin:Devfix@2026 "https://localhost:9200"
{
  "name" : "a937e018cee5",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "GLAjAG6bTeWErFUy_d-CLw",
  "version" : {
    "distribution" : "opensearch",
    "number" : "3.9.0",
    "minimum_wire_compatibility_version" : "7.10.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "The OpenSearch Project: https://opensearch.org/"
}

返回這個歡迎 JSON(實際輸出還包含構建信息等字段,這裡只列關鍵項)就說明服務通了。注意 curl 的兩個參數:https 和 -k。因為預設開了 TLS,用 http:// 打這個端口最常見的報錯是 Empty reply from server;-k 是跳過自簽證書校驗,本地體驗才這麼幹,生產要換正經證書。這個坑的完整排查見 FAQ 第 1 項。

第二步:檢查集群健康

服務通了不代表集群狀態好。OpenSearch 用三種顏色表達健康:green 是所有主分片和副本都分配到位;yellow 是主分片都在、副本有缺;red 是連主分片都有缺。查一下:

curl -sk -u admin:Devfix@2026 "https://localhost:9200/_cluster/health?pretty"
{
  "cluster_name" : "docker-cluster",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 0,
  "active_shards" : 0,
  "unassigned_shards" : 0,
  "number_of_pending_tasks" : 0
}

現在還沒有索引,分片數為 0,狀態是 green。先記住 yellow 的判定規則,第三步建索引時馬上用得上。

第三步:創建 movies 索引

OpenSearch 的數據組織方式是:索引(index)裝 JSON 文檔(document),每個索引又切成主分片(primary shard)存數據,副本分片(replica shard)做備份。建索引的語法是 PUT /<索引名>,可以同時指定 settings 和 mappings。

關鍵決策:副本數。index.number_of_replicas 預設是 1,也就是每個主分片配一個副本——但副本必須放在別的節點上才有備份意義,單節點集群放不下,於是分片永遠處於未分配狀態,集群健康變 yellow。所以本地單節點,建索引時顯式把副本設成 0:

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" }
      }
    }
  }'
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "movies"
}

mappings 是索引裡字段的類型清單:title 設成 text,會被分詞供全文搜索;year 是整數;genre 設成 keyword,整體當一個值,適合精確過濾。索引名必須全小寫,不能以 _ 或 - 開頭。

建完再查一次健康,這次帶上了索引的分片:

curl -sk -u admin:Devfix@2026 "https://localhost:9200/_cluster/health?pretty"
{
  "cluster_name" : "docker-cluster",
  "status" : "green",
  "active_primary_shards" : 1,
  "active_shards" : 1,
  "unassigned_shards" : 0
}

replicas=0 生效,1 個主分片分配到位,沒有未分配分片,green。如果你這步建成了 yellow,多半是副本沒設 0,見 FAQ 第 2 項。

第四步:bulk 導入 5 條示例數據

一條一條 PUT /movies/_doc/<id> 也能導入,但正式場景都用 _bulk:一個請求塞多條文檔,性能高得多。bulk 的請求體是 NDJSON 格式——每行一個 JSON,動作行和文檔行成對,整個 body 必須以換行符結尾。手寫這個格式最容易翻車,所以先把數據落成一個文件:

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

這 5 條數據的 title 是故意設計的:第 1 條同時含 Wind 和 Rise,第 2 條只含 Wind,第 3 條只含 Rise,第 4 條兩個都不含,第 5 條的詞是 windmill——一個和 wind 完全不同的詞。第五步搜索的結果全都能從這份數據本身推出來。

發送 bulk:

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

響應很長,只看兩個地方:頂層的 "errors" : false,和每個 item 的 "result" : "created":

{
  "took" : 11,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "movies",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "status" : 201
      }
    }
  ]
}

(items 裡 5 條各一個,這裡只展示第一條。)用 count 驗證全量:

curl -sk -u admin:Devfix@2026 "https://localhost:9200/movies/_count?pretty"
{
  "count" : 5,
  "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }
}

count 是 5 就對了。如果 bulk 報 400 malformed request,九成是 NDJSON 格式問題:文檔 JSON 被換行拆開了,或者 body 結尾少換行符;Content-Type 也要寫對。完整排查見 FAQ 第 3 項。

第五步:搜索,看分詞怎麼工作

數據進去了,現在搜。先試全文搜索,match 查詢會對指定字段分詞後匹配:

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" } } }'
{
  "took" : 2,
  "hits" : {
    "total" : { "value" : 3, "relation" : "eq" },
    "max_score" : 0.5023,
    "hits" : [
      { "_id" : "1", "_score" : 0.5023, "_source" : { "title" : "Let the Wind Rise", "year" : 2013, "genre" : "drama" } },
      { "_id" : "3", "_score" : 0.4119, "_source" : { "title" : "The Rise of Machines", "year" : 2021, "genre" : "scifi" } },
      { "_id" : "2", "_score" : 0.3987, "_source" : { "title" : "Gone with the Wind", "year" : 1939, "genre" : "drama" } }
    ]
  }
}

為什麼命中 3 條而不是 1 條?match 預設把查詢串分成 wind 和 rise 兩個詞,用 OR 組合,所以只含其中一個的也進來了:第 2 條有 wind,第 3 條有 rise;第 1 條兩個詞都有,分數最高排第一。_score 是相關性得分,具體數值各集群不同,看命中集合和排序就行。第 5 條 Windmill Valley 沒命中:分詞器把 windmill 切成一整個詞,它不是 wind。全文搜索按詞匹配,不按子串。

要兩個詞都命中,加 operator:

curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "match": { "title": { "query": "wind rise", "operator": "and" } } } }'

這次只剩第 1 條。

再試一個反直覺的:搜 rises。

curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "match": { "title": "rises" } } }'

0 命中。因為預設分析器是 standard,它只切詞不做詞幹還原,rises 和 rise 是兩個不同的詞。想讓 rises 命中含 Rise 的文檔,指定 english 分析器——它會去停用詞(the、and 這類常見虛詞)並做詞幹化(把 rises 還原成 rise):

curl -sk -u admin:Devfix@2026 -X GET "https://localhost:9200/movies/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "match": { "title": { "query": "rises", "analyzer": "english" } } } }'

這次第 1、3 條都命中。一條查詢三種結果,差別全在分析器。

還有一個剛導入就搜索可能碰到的怪事:OpenSearch 是近實時搜索,文檔寫入後要等索引 refresh(預設每 1 秒一次)才進入可搜索狀態。剛 bulk 完立刻搜,偶爾會少幾條,等一秒再搜就齊了。如果你 bulk 完 count 是 5 但搜索 0 hits,見 FAQ 第 4 項。

第六步:更新與刪除

改單條文檔用 POST /<索引>/_update/<id>,傳 doc 做局部更新:

curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/movies/_update/4?pretty" \
  -H "Content-Type: application/json" \
  -d '{ "doc": { "year": 2019 } }'
{ "_index" : "movies", "_id" : "4", "_version" : 2, "result" : "updated" }

_version 從 1 變 2,result 是 updated。刪除更簡單:

curl -sk -u admin:Devfix@2026 -X DELETE "https://localhost:9200/movies/_doc/4?pretty"
{ "_index" : "movies", "_id" : "4", "_version" : 3, "result" : "deleted" }

驗證 count 回到 4:

curl -sk -u admin:Devfix@2026 "https://localhost:9200/movies/_count?pretty"

到這裡,建索引 → 導入 → 搜索 → 改 → 刪 全部走通。玩完記得停容器:

docker ps
docker stop <容器ID>

核心概念

這張全景圖走完就清楚了:寫入路徑是 curl → 9200 → 節點 → movies 索引 → 主分片;查詢路徑是查詢串先過分析器分詞,再查倒排索引定位文檔。

OpenSearch 業務邏輯全景圖:寫入鏈路 curl → 9200 → 節點 → movies 索引,查詢鏈路 wind rise 經分詞後進倒排索引,返回 3 條命中

概念 是什麼 類比
集群(cluster) 一個或多個節點組成的整體,對外只有一個訪問地址 像一個數據庫實例
節點(node) 一個 OpenSearch 實例,本文單節點就是一個實例撐全場 像實例後面的一臺服務器
索引(index) JSON 文檔的容器,名字全小寫,建的時候定 settings 和 mappings 像數據庫裡的一張表
文檔(document) 一條 JSON 記錄,_id 唯一標識 像表裡的一行數據
分片(shard) 索引數據的實際存放單元,主分片存數據,副本分片做備份 像表分區加主從複製
分析器(analyzer) 把文本切成詞的元件,standard 只切詞,english 還去停用詞、做詞幹化 像搜索詞的預處理器
倒排索引 分詞後生成的「詞 → 文檔」查找表,搜索真正查的是它 像書末尾的主題索引頁

常見問題

curl 報 Empty reply from server 或 SSL 錯誤

現象:用 http:// 打 9200 端口時 curl 拿不到合法響應,最常見報 Empty reply from server。

原因:OpenSearch 預設啟用 TLS,必須用 https 協議;用 http 協議打 TLS 端口協議棧對不上,連接被直接斷開。https 配上 -k(跳過自簽證書校驗)才能通;生產環境應設定正式證書而不是 -k。

相關文章:OpenSearch 本地 curl 報 Empty reply / SSL 錯

集群健康一直是 yellow

現象:建索引後 _cluster/health 一直返回 status: yellow,unassigned_shards 不為 0。

原因:副本分片未分配——index.number_of_replicas 預設是 1,副本要放在別的節點上才有備份意義,單節點集群放不下。

相關文章:OpenSearch 單節點一直 yellow

bulk 請求報 400 malformed request

現象:POST /_bulk 報 400,錯誤信息含 malformed request 或 expected。

原因:NDJSON 格式不對,三個檢查點——每個文檔 JSON 必須壓成一行;動作行和文檔行嚴格成對;body 結尾必須有換行符(--data-binary @文件 時確保文件以換行結尾)。Content-Type 必須用 application/x-ndjson,不是 application/json。

相關文章:OpenSearch bulk 報 malformed / 400

剛導入的文檔搜不到,但 _count 是對的

現象:bulk 完 _count 返回 5,緊接著 _search 返回 0 hits;等 1 秒再搜就齊了。

原因:OpenSearch 是近實時搜索,文檔寫入後要等索引 refresh(預設每 1 秒一次)才進入可搜索狀態。_count 統計的是已寫入的文檔,搜索查的是已 refresh 的倒排索引——分詞後建立的「詞 → 文檔」查找表。

相關文章:OpenSearch 剛導入搜不到(refresh 間隔)

下一步

用 OpenSearch Dashboards 做可視化

官方 Dashboards 是另一個 JVM 進程,監聽 5601 端口,能把 movies 數據畫成柱狀圖、餅圖、地圖等。裝上 Dashboards 鏡像、加 dashboards.yml 配 OpenSearch 地址就能連——本地體驗數據的最佳搭檔。

OpenSearch Dashboards 接入 movies 索引(W2 待建)

跑通向量搜索(k-NN 插件)

OpenSearch 自帶 k-NN 插件,基於 faiss/hnsw 做近似最近鄰搜索;本文用的是全文搜索,下一篇會寫本地跑通向量搜索的 6 步。

OpenSearch k-NN 入門(W2 待建)

對比閱讀:Meilisearch 本地搜索

同樣跑 movies 索引,Meilisearch 是開箱即用的輕引擎,OpenSearch 是分片化的重引擎——本地跑法、分詞行為、容量規劃差別都不小。

從零跑通 Meilisearch 本地搜索服務

卡在哪一步了?

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

貢獻一條解法