
做搜索引擎的都知道Elasticsearch基本是繞不開的那一個。不管是給站內做商品搜索、日志檢索還是給業務系統做數據聚合分析ES靠一套HTTP JSON接口就能把海量數據的存儲、索引和檢索全部包攬下來。很多人第一次接觸ES時容易被一堆名詞弄暈——索引、文檔、分片、副本、mapping、分詞器看著好像都能理解真到自己上手建索引、寫數據就各種抓瞎為什么我的中文搜不出來為什么字段類型選錯了改都改不掉為什么寫入的數據查不到這篇文章我就從最基礎的“索引創建、數據插入、請求示例”講起把ES日常使用中最常碰到的那幾個環節完整過一遍。適合剛入門的開發者也適合部署完ES之后還沒理清讀寫流程的運維同學。我會把每個請求都貼出來并解釋清楚保證你可以照著敲一遍就通。1. 先把關鍵概念搞明白索引、文檔、倒排索引1.1 ES的“索引”和MySQL的“索引”根本不是一回事很多從MySQL轉過來的用戶第一次用ES都會在這里栽跟頭。MySQL里的索引是表結構上的一個輔助對象用來加速查詢的B樹而ES里的索引是一個完整的邏輯存儲空間它包含了一系列的配置信息settings、字段結構定義mapping以及真正落盤的數據shard也就是分片。打個比方如果你把MySQL里的“數據庫”和“表”合并成一個東西那大概就是ES里的索引。在7.0版本之前ES里還有個type概念一個索引下面可以分多個type結果把數據結構搞得不清不楚官方后來干脆把type給廢掉了。現在一個索引基本就等價于一張二維表但它存的不是行而是JSON文檔。每個文檔都有一個_id相當于主鍵。文檔里的字段類型靠mapping來定義比如字符串字段要選text還是keyword數字字段用什么精度日期字段的格式是什么這些都直接影響檢索和聚合的結果。所以創建索引這件事的本質其實就是先規劃好數據的存儲結構再加上分片和副本策略。1.2 倒排索引為什么ES搜索能這么快ES能成為搜索引擎的老大哥核心武器就是倒排索引。傳統的正排索引是按“文檔→關鍵詞”的方向存儲比如一篇文檔里有哪些詞查詢時得從頭掃描所有文檔才能知道誰命中了關鍵詞。倒排索引反過來它維護的是“關鍵詞→文檔ID列表”的映射幾乎每個詞都對應一串包含它的文檔ID查詢時只要找到這個詞就能瞬間定位到所有相關文檔。具體到實現上ES會對每個text字段做分詞再把分詞結果寫進倒排表。你搜“搜索引擎”的時候ES會先把“搜索引擎”拆成“搜索”“引擎”具體拆法取決于分詞器再拿著這些詞去倒排表里找。這也是為什么ES對中文搜索的效果高度依賴分詞器——標準分詞器處理中文時基本是一整句話當做一個詞搜起來非常痛苦生產環境基本都會換用IK分詞器或者其他的中文分詞方案。理解了這點你就能明白為什么ES適合搜索而不適合做事務型存儲它為了查詢效率犧牲了復雜事務能力和強一致性但反過來你給ES丟幾千萬條數據進去它依然能在幾百毫秒內把結果給你撈出來這種能力在傳統關系型數據庫里是做不到的。2. 動手之前環境準備與啟動2.1 Windows和Linux下的啟動方式Elasticsearch是Java寫的東西所以環境里得有JDK。好消息是ES在8.x版本之后內置了捆綁的JDK你不需要自己再折騰JAVA_HOME只要把安裝包解壓出來就能用。Windows下直接雙擊bin/elasticsearch.batLinux下執行bin/elasticsearch等幾秒鐘看到started字樣就代表啟動成功了。Linux上有一個必須注意的坑ES拒絕用root賬號啟動。你如果用root執行啟動命令控制臺上會直接報can not run elasticsearch as root。這不是bug是安全策略ES要求使用非root用戶運行。生產環境建議單獨建一個用戶比如adduser esuser然后把ES目錄的屬主改成這個用戶再切過去啟動。另外如果你準備部署集群或多節點還需要把elasticsearch.yml里的network.host從默認的127.0.0.1改成內網地址同時配置discovery.seed_hosts和cluster.initial_master_nodes否則節點之間互相找不到。啟動完成后驗證方式很簡單瀏覽器或者curl訪問http://localhost:9200返回一段包含cluster_name、version等信息的JSON就說明ES已經起來了。2.2 安裝IK分詞器和準備好用的調試工具中文場景下IK分詞器幾乎成了標配。安裝它沒有復雜的流程直接從GitHub release頁下載和ES版本嚴格對應的zip包解壓后放到ES安裝目錄的plugins/ik文件夾下重啟ES就生效了。注意版本必須嚴格一致ES 8.15的版本就對應IK 8.15.x的插件包裝錯版本的話ES會因為插件校驗失敗直接拒絕啟動。調試ES請求我最推薦Kibana的Dev Tools控制臺。它會自動補全DSL語法還帶歷史記錄比在終端里敲curl舒服太多了。如果你不想裝KibanaPostman也可以但注意ES部分接口對Content-Type有嚴格要求比如application/json沒設對ES會直接返回Content-Type header [application/x-www-form-urlencoded] is not supported之類的錯誤。還有一點8.x版本默認開啟了安全認證首次啟動會生成一個隨機密碼讓你配置。如果你只是在本地學習使用想省去這層麻煩可以在elasticsearch.yml里關閉安全模塊把xpack.security.enabled設為false同時把自帶的TLS加密也關掉也就是把xpack.security.transport.ssl.enabled設為false然后重啟。當然生產環境千萬別這么干這是自己學習的偷懶辦法。3. 索引創建一次完整的索引設計3.1 先設計字段類型再寫創建請求我見過一句話總結得很準確ES的mapping是設計寫在前面后悔留給后面。因為字段類型一旦寫入是不能直接修改的想改只能重建索引。所以創建索引之前把業務字段捋清楚特別重要。假設我現在要做一個簡單的博客文章搜索功能包含文章標題、正文內容、發布時間、作者ID、標簽列表、瀏覽量這幾個字段。跟MySQL建表類似ES每個字段都要選一個類型核心的對應關系如下標題和正文需要被全文檢索用text類型并配置中文分詞器發布時間用date類型同時指定格式比如yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis多個格式用雙豎線分隔作者ID不需要分詞用keyword標簽列表也是keyword的數組ES原生支持數組存儲瀏覽量用integer或long后續可能要排序和聚合字段類型選錯的典型后果是你把一個keyword字段硬拿來全文檢索結果發現怎么搜都搜不到或者你把數字存成了text聚合統計時報Text fields are not optimised for operations that require per-document field data。3.2 創建索引的請求示例與參數說明創建索引用的是PUT請求路徑直接寫索引名請求體里帶上settings和mappings兩塊配置。看一個具體示例PUT /blog_article { settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 5s }, mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, publish_time: { type: date, format: yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis }, author_id: { type: keyword }, tags: { type: keyword }, views: { type: integer } } } }這部分有幾個關鍵點要解釋清楚。第一number_of_shards是分片數它決定了索引的數據在物理上被切成了幾塊。分片數在索引創建后就沒法修改了因為ES是根據_id的哈希值來路由文檔到分片的改分片數意味著所有數據的存放位置都要重算。所以規劃分片數時不要太隨意也絕不要為了圖以后擴容方便就上來設個幾百個分片。ES官方建議單個分片容量控制在30GB到50GB之間比如預估數據總量300GB設置6到10個分片是合理的。單分片性能有上限分片過多又會導致集群元數據膨脹、查詢聚合的開銷變大。第二number_of_replicas是副本數默認1。副本既保證數據高可用又分擔讀請求壓力。和分片數不同副本數在索引創建后可以動態調整不需要重建索引。第三refresh_interval決定了索引的“可見性”刷新周期。這后面講數據插入的時候會細說但配置成5s或10s對寫入頻繁的場景是更穩的選擇。analyzer和search_analyzer這里我分別配置了ik_max_word和ik_smart。ik_max_word做最細粒度拆分“中華人民共和國”會被拆成“中華人民共和國”“中華人民”“中華”“華人”“人民共和國”“人民”“共和國”等多個詞適合索引階段提高召回率ik_smart做粗粒度拆分適合查詢階段提高精準率。這是比較經典的組合方式。3.3 mapping設計中的常見坑第一不要對keyword字段做全文檢索。keyword不會分詞存儲時是一個完整字符串你拿一個長句子去match搜索怎么也匹配不上只有用term做精確匹配才會命中。第二text字段默認不能用于聚合、排序和腳本操作。ES在聚合時對分詞后的text字段無能為力會直接報Fielddata is disabled on text fields by default。你需要聚合的字段一定要用keyword類型或者建一個keyword類型的子字段。第三動態映射是個雙刃劍。ES默認開著動態映射你插入一個索引里沒有定義過的字段時它會根據JSON值自動推斷字段類型并寫進mapping。這在開發期很省事但在生產環境特別容易出問題。比如你第一次插入的age字段是個數字ES建成了long后來有個文檔把age傳成了字符串寫入直接報錯。另一個更隱蔽的問題是日志類數據字段特別多動態映射會產生大量字段導致mapping膨脹、內存飆升。所以生產環境我建議要么關掉dynamic要么把dynamic設為strict只允許顯式定義的字段寫入至少也要對關鍵索引做嚴格的mapping管控。4. 數據插入從單條到批量4.1 單條插入的兩種寫法索引建好了接下來就是往里面塞數據。ES插入單條文檔有兩條路。第一條是不指定ID讓ES自動生成隨機ID用POST請求POST /blog_article/_doc { title: Elasticsearch入門指南, content: 本文介紹Elasticsearch的基礎概念和使用方法……, publish_time: 2024-06-01 10:30:00, author_id: a1024, tags: [Elasticsearch, 搜索], views: 321 }第二條是指定業務ID用PUT請求或POST請求加在路徑上PUT /blog_article/_doc/1001 { title: Elasticsearch入門指南, content: 本文介紹Elasticsearch的基礎概念和使用方法……, publish_time: 2024-06-01 10:30:00, author_id: a1024, tags: [Elasticsearch, 搜索], views: 321 }兩種寫法怎么選如果你有自己的數據庫主鍵強烈建議把它作為ES的_id。這樣后續做增量同步時用同樣的ID重復寫入不會產生重復文檔而是覆蓋更新。同時文檔路由也依賴_id固定的ID會讓數據分布更可控。讓ES自動生成隨機ID更適合大量導入且沒有業務主鍵的日志型數據字符串ID比自增數字ID的分布更均勻能避免熱點分片。插入成功后的返回結果大概是{ _index: blog_article, _id: 1001, _version: 1, result: created, _shards: { total: 2, successful: 1, failed: 0 } }注意看_shardstotal是副本加主分片的總數successful表示成功寫入的分片數。這里total為2、successful為1是因為我設置了1個副本但單機環境下副本無法分配到其他節點所以副本分片是未分配的unassigned狀態只有主分片成功寫入。這在單機學習中很正常無需緊張。但如果你在集群中看到successful長期小于total就要排查副本是否分配失敗了。4.2 批量插入bulk接口的使用姿勢單條插入方便理解但生產環境往ES灌數據肯定不能一條一條地發請求。每發一次HTTP請求都有網絡開銷而ES寫入單個文檔的耗時其實非常短真正慢的是請求傳輸。批量寫入能把這些開銷攤薄寫入吞吐量能差出幾個數量級。ES的批量接口是_bulk請求格式比較特殊要求每兩行一組一行是操作元信息一行是文檔數據。POST /blog_article/_bulk {index:{_id:1002}} {title:ES批量寫入實踐,content:bulk接口使用示例……,publish_time:2024-06-02 09:00:00,author_id:a1026,tags:[ES],views:120} {index:{_id:1003}} {title:搜索質量優化,content:查詢調優與排序策略……,publish_time:2024-06-03 11:20:00,author_id:a1024,tags:[搜索],views:88}這個格式如果手寫很容易多一個逗號或者少一個換行報錯又不明顯。實際項目中一般由客戶端SDK在內存中拼裝好NDJSON數據再一次性提交日志采集工具比如Logstash、Filebeat也是用這種方式推送的。有人會問bulk一次到底該提交多少條太多會占用過大內存太少又起不到批量效果。常規經驗值是單次bulk請求體控制在5MB到15MB之間文檔數在1000到5000條左右。具體最優值得靠壓測去找你可以用curl反復調整這個參數觀察ES監控面板里寫入延遲和拒絕率的變化。除了index操作bulk里還能混用create、update、delete。區別在于create在文檔已存在時會報版本沖突index則直接覆蓋適合冪等寫入的場景。4.3 寫入后為什么可能查不到refresh機制詳解很多初學者第一次往ES寫數據緊接著就去搜索剛才的內容結果搜不到第一反應就是出bug了。其實這是ES的refresh機制在起作用。ES寫入數據時文檔不會立刻落盤到不可變的磁盤段segment中而是先進入內存緩沖區和translog日志。refresh操作會把內存緩沖區的數據生成一個新的segment讓這部分數據變得可搜索。默認的refresh_interval是1秒也就是說寫入成功后最多等1秒就能搜到。你可以在搜索請求上加refreshtrue參數強制先刷新再查詢POST /blog_article/_doc/1004?refreshtrue { title: 強制刷新測試, content: 這個文檔寫入后立即可見, publish_time: 2024-06-04 00:00:00, author_id: a1030, tags: [測試], views: 1 }但請注意refreshtrue并不適合生產環境高頻寫入時使用。每次寫入都立即刷新會讓內存中的小segment數量暴漲觸發頻繁的segment合并寫性能和磁盤IO都會很難看。常規做法是實時性要求高的業務保持默認的1秒刷新后臺周期性批量導數據的場景反而可以把refresh_interval調大甚至導入期間臨時設為-1關閉刷新等數據全部導入完再恢復刷新這樣能大幅加快寫入速度。批量導入期間關閉刷新后內存緩沖區的數據不會被刷成可搜索段但也不會丟數據在translog里有完整記錄。導入完成后再執行一次POST /blog_article/_refresh強制刷新所有數據就能被搜到了。4.4 更新與刪除文檔的原理更新和刪除看著是兩類操作底層其實是同一個機制。ES的segment是不可變的所以不存在“改某條數據”這種物理操作。更新一個文檔真實發生的事情是新版本文檔先寫入內存緩沖區舊版本文檔不會被立刻物理刪除而是被打上一個刪除標記等后臺segment合并時才會真正清理。POST /blog_article/_update/1001 { doc: { views: 999 } }上面這個請求會做一次部分字段更新只修改views字段其他字段不動。如果字段不存在就新增如果文檔本身不存在會報document_missing_exception。如果想在文檔不存在時自動創建可以在請求體里把doc_as_upsert設為true。刪除就更直接了DELETE /blog_article/_doc/1001刪除后你會看到result字段變成deleted。如果刪除一個不存在的文檔result會是not_found但HTTP狀態碼依然返回200這點和MySQL里DELETE影響行數為0不同ES不會把“沒刪到”當作錯誤處理代碼判斷時要留意。因為更新刪除都是標記位機制頻繁更新刪除后再大量寫入會導致磁盤上存在很多“虛”數據查詢時會掃描到這些帶刪除標記的文檔再過濾掉影響查詢性能。常規維護手段是定期執行POST /blog_article/_forcemerge強制合并把segment里的刪除標記徹底清掉。5. 用一次搜索驗證索引和數據5.1 最常用的search請求示例數據插進去之后最直接的事就是搜一下。看最典型的match查詢POST /blog_article/_search { query: { match: { title: Elasticsearch } } }返回結果的hits.total會告訴你命中了多少文檔hits.hits里是命中的文檔列表每個文檔都帶著_score相關度分數。相關度分數是ES按照詞頻、逆文檔頻率等算法算出來的默認按分數倒序排列。另一個常用查詢是term它不會對搜索詞做分詞而是拿整個詞去倒排索引里精確匹配。所以term查詢適合keyword字段match查詢適合text字段。很多人一開始搞混這兩個記住一條經驗查keyword用term查text用match基本不會出錯。5.2 查看索引的mapping、settings和健康狀態有時候你需要確認之前建的索引結構或者排查為什么某個字段行為不對。幾個最常用的只讀接口要記住。查看索引字段定義GET /blog_article/_mapping查看索引配置GET /blog_article/_settings一次性查看集群里所有索引的健康狀態、分片數和文檔數可以用cat接口GET /_cat/indices?v返回結果里會包含health列可能是green、yellow或red。單機環境下因為你只跑了1個ES節點副本分片沒有可用的第二個節點來分配所以狀態通常是yellow這不影響正常讀寫。但你需要在心里清楚yellow意味著副本處于不完整狀態此時如果主分片所在節點宕機數據就存在丟失風險。想要真正達到green狀態至少需要起兩個ES節點讓副本分片分配到別的機器上。另外查看所有分片落在哪些節點、為什么未分配可以用GET /_cat/shards?v每行會顯示索引名、分片編號、是主分片還是副本、落在哪個節點、存儲了多少文檔。6. 常見問題與排查技巧實錄6.1 集群狀態變紅索引顯示不可讀寫集群變紅表示有主分片缺失這意味著部分數據徹底不可用了。最普遍的原因是節點重啟后磁盤空間不足分片無法重新分配。排查順序建議這樣走第一步GET /_cat/indices?v看哪個索引是red第二步GET /_cat/shards?v看缺失的是哪個分片第三步GET /_cluster/allocation/explain?pretty這個接口會直接用大白話告訴你為什么分片分配不上去比如the node is above the disk water mark之類的提示。如果是磁盤水位問題導致的變紅處理和防范策略有這么幾條及時清理數據或擴盤調大節點磁盤水位閾值不推薦屬于臨時手段同時給索引配置只讀閾值以內的數據生命周期策略。還有一類比較隱蔽的情況同一集群里有不同版本的ES節點老節點無法讀取新節點寫入的segment格式分配也會失敗這種情況需要統一集群版本。6.2 連接拒絕9200端口通不通ES的HTTP服務默認監聽9200端口節點間通信走9300端口。你在瀏覽器訪問9200覺得沒問題但Java客戶端或者應用容器連不上優先確認幾件事ES所在機器的防火墻是否放行了9200ES的network.host是否還是localhost只有本機能連外部服務器當然連不上客戶端和ES版本是否匹配Java客戶端Maven坐標的版本必須和ES服務端主版本一致比如服務端8.15客戶端必須用8.15.x用7.x連8.x會直接報版本不兼容異常。針對自身學習場景如果不想管太多網絡安全配置可以保持network.host: 127.0.0.1只在本機curl訪問即可。想讓自己項目遠程調試時再改同時配上ES自帶的認證模塊或者單獨限制訪問IP。6.3 寫入性能明明不高到底卡在哪ES寫入慢最常見的原因第一個是refresh太頻繁第二個是段合并太頻繁第三個是bulk批次太小第四個是磁盤性能不夠。如果你的寫入目標量級是每秒幾萬條那么把index.refresh_interval調成10s到30s甚至大批量導入時暫時設成-1一次bulk的條數調大到幾千條把請求體撐到10MB左右觀察監控中的段合并指標如果merges線程長期繁忙適當調大index.merge.scheduler.max_thread_count機械硬盤建議調低到1避免IO飽和SSD可以調高到4日志型數據寫入前設置index.number_of_replicas: 0等數據寫完后再把副本調回來整套組合拳打下來寫入吞吐量翻幾倍很正常。6.4 text字段聚合報錯怎么辦直接抄一個常見報錯場景你寫了這樣的請求POST /blog_article/_search { size: 0, aggs: { by_tags: { terms: { field: tags } } } }如果tags被定義成了text類型聚合時會報錯Fielddata is disabled on text fields by default。原因前面講過text字段是分詞后的詞條數組拿它做terms聚合得到的是拆分后的詞頻不是原始字段的完整值。解決方案有三個一是改mapping字段類型為keyword二是保留text主字段的同時增加一個keyword子字段比如tags: { type: text, fields: { keyword: { type: keyword } } }聚合時用tags.keyword三是對已有text字段動態開啟fielddata但這個是官方明確不建議的會大量占用堆內存屬于臨時救火方案。6.5 分片數設置不合理索引卡死最后提醒一個比較典型的新手陷阱——把分片數設得過多。比如數據量比較小的業務索引上來就設了30個分片每個分片只有幾百MB數據。最終表現是集群里到處是分片讀寫慢、內存緊張、集群狀態不穩定。分片數量規劃的經驗公式可以這樣估算一個分片容量極限大約50GB但達到30GB后查詢性能就開始下降。目標分片數 預計總數據量 / 30GB再乘以1.5左右的冗余系數。分片副本數默認1即可。如果規劃后發現分片太少或太多因為創建后不能改分片數常用的解決方案是新建一個分片數合理的新索引用reindex接口把數據遷移過去再把索引別名切換到新索引上對業務做到平滑替換。提示索引別名是ES里非常實用的功能通過別名讀寫索引可以做到零停機切換。平時操作索引先用別名生產環境維護時就方便多了。寫在后面的一點點建議這套流程走下來你其實已經有能力自己搭一套基礎搜索服務了建索引、寫數據、搜數據以及處理常見的讀寫問題。ES的入門曲線確實陡但最難的永遠是前面這段概念混淆期。我的建議是不要急著記語法先把索引、映射、分片、倒排索引這幾個底層概念吃透后面學查詢DSL和集群運維都會順很多。我個人在實際操作中還有一個體會學習ES一定要搭配Kibana一起用。它自帶的Dev Tools能邊敲邊看響應還能自動補全比在終端里反復拼curl高效很多。另外新建索引的時候哪怕只是測試也養成寫mapping的習慣別完全依賴動態映射。等數據多了再想回頭改字段類型代價可不是一點半點。