目ElasticSearch實(shí)戰(zhàn):從LIKE查詢優(yōu)化到完整搜索鏈路搭建)
PHP 項(xiàng)目里做搜索最怕的不是寫不出查詢語句而是數(shù)據(jù)量一上來LIKE %關(guān)鍵詞%直接把數(shù)據(jù)庫拖垮。我見過太多項(xiàng)目在初期用 MySQL 的模糊匹配湊合等到商品表過了百萬行一個搜索請求能把 CPU 打滿頁面轉(zhuǎn)圈轉(zhuǎn)到用戶關(guān)掉。這時候才想起來上 ElasticSearch往往已經(jīng)積了一堆歷史包袱。這篇內(nèi)容就是聊這件事在 PHP 里怎么把 ElasticSearch 用起來從環(huán)境搭建、索引設(shè)計(jì)、數(shù)據(jù)同步到查詢構(gòu)造和踩坑處理把一條完整的鏈路講透。不管你是剛接觸 ES 的新手還是已經(jīng)能跑通 Demo 但總在真實(shí)場景翻車的開發(fā)者下面這些內(nèi)容應(yīng)該都能對上你的痛點(diǎn)。1. 先想清楚PHP 項(xiàng)目為什么需要 ElasticSearch1.1 數(shù)據(jù)庫模糊查詢的天花板在哪很多人對搜索的需求認(rèn)知是滯后的。項(xiàng)目初期一個SELECT * FROM products WHERE title LIKE %手機(jī)%就能滿足需求開發(fā)快、不用額外維護(hù)服務(wù)看起來很美。但這個方案有三個硬傷隨著數(shù)據(jù)增長會逐一暴露。第一是索引失效。前置通配符的LIKE查詢無法利用 B 樹索引MySQL 只能全表掃描。十萬行數(shù)據(jù)可能還能扛百萬行就是災(zāi)難一次查詢幾百毫秒到幾秒不等并發(fā)一上來直接雪崩。第二是相關(guān)性排序缺失。LIKE只能告訴你匹配到了沒法告訴你哪個更匹配。用戶搜無線藍(lán)牙耳機(jī)標(biāo)題里含耳機(jī)的商品全返回但哪個是用戶真正想要的數(shù)據(jù)庫給不出答案。第三是分詞能力為零。中文搜索里蘋果手機(jī)和手機(jī)蘋果在LIKE眼里是兩個完全不同的字符串但在用戶認(rèn)知里是一回事。沒有分詞就沒有真正的語義匹配。ElasticSearch 恰好在這三點(diǎn)上都是強(qiáng)項(xiàng)。它基于倒排索引查詢走的是詞條定位而非全表掃描它內(nèi)置相關(guān)性評分BM25 算法能按匹配度排序它支持中文分詞器能把句子拆成有意義的詞。這就是為什么搜索場景幾乎成了 ES 的標(biāo)配戰(zhàn)場。1.2 ES 在 PHP 技術(shù)棧里的定位需要明確一點(diǎn)ES 不是數(shù)據(jù)庫的替代品而是搜索層的補(bǔ)充。它擅長的是查不擅長事務(wù)性寫。你的訂單、庫存、用戶余額這些強(qiáng)一致性數(shù)據(jù)永遠(yuǎn)應(yīng)該以 MySQL 為準(zhǔn)ES 只存一份用于檢索的副本。這個定位想不清楚后面同步邏輯就會寫歪。在典型的 PHP 架構(gòu)里ES 的位置是這樣的業(yè)務(wù)數(shù)據(jù)寫入 MySQL通過某種同步機(jī)制把需要檢索的字段推送到 ES 建立索引用戶發(fā)起搜索請求時PHP 直接查 ES 拿到文檔 ID 列表再按需回表查 MySQL 補(bǔ)全詳情或者干脆把展示所需字段全冗余進(jìn) ES一次查詢搞定。前者數(shù)據(jù)一致性好但多一次查詢后者性能高但有冗余維護(hù)成本。選哪種取決于你的業(yè)務(wù)對實(shí)時性和字段變更頻率的要求。我個人的經(jīng)驗(yàn)是列表頁展示的字段盡量冗余進(jìn) ES比如商品標(biāo)題、價格、主圖、銷量這樣搜索列表一次 ES 查詢就能渲染不用回表。詳情頁再走 MySQL。這個折中方案在大多數(shù)電商、內(nèi)容類項(xiàng)目里都夠用。1.3 什么規(guī)模才值得引入不是所有項(xiàng)目都該上 ES。一個日活幾百、數(shù)據(jù)量幾萬的小站引入 ES 帶來的運(yùn)維成本可能超過收益。判斷標(biāo)準(zhǔn)可以看這幾條單表數(shù)據(jù)量是否超過五十萬且持續(xù)增長、搜索是否是核心功能而非邊緣需求、是否對搜索的相關(guān)性和響應(yīng)速度有明確要求。三條里中兩條以上就值得考慮。反過來說如果只是后臺管理里偶爾查一下或者數(shù)據(jù)量很小老老實(shí)實(shí)用數(shù)據(jù)庫加索引優(yōu)化別給自己找麻煩。ES 是個 JVM 服務(wù)內(nèi)存占用不小運(yùn)維也需要一定經(jīng)驗(yàn)小項(xiàng)目硬上反而是負(fù)擔(dān)。2. 環(huán)境搭建PHP 側(cè)和 ES 側(cè)怎么對接2.1 ES 服務(wù)的部署與版本選擇ES 的版本選擇有個坑客戶端庫版本必須和服務(wù)端大版本對齊。PHP 官方客戶端elasticsearch-php的 7.x 只能連 7.x 的 ES8.x 連 8.x跨大版本基本連不上或者行為詭異。所以第一步是先定 ES 版本再裝對應(yīng)客戶端。部署方式上開發(fā)環(huán)境用 Docker 最省事一條命令起一個單節(jié)點(diǎn)docker run -d --name es-dev \ -p 9200:9200 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms512m -Xmx512m \ elasticsearch:7.17.0這里discovery.typesingle-node是單節(jié)點(diǎn)模式必須的否則 ES 會嘗試組建集群然后因?yàn)檎也坏狡渌?jié)點(diǎn)而反復(fù)報錯。ES_JAVA_OPTS限制堆內(nèi)存開發(fā)機(jī)內(nèi)存有限不限制的話默認(rèn)可能吃掉一半物理內(nèi)存。生產(chǎn)環(huán)境則要正經(jīng)配置集群、副本、磁盤水位線這些不在本篇展開但心里要有數(shù)。啟動后用curl http://localhost:9200驗(yàn)證返回一段帶版本號的 JSON 就說明活了。注意 ES 7 之后默認(rèn)開啟了安全認(rèn)證如果返回 401說明需要用戶名密碼開發(fā)環(huán)境可以在配置里關(guān)掉安全模塊生產(chǎn)環(huán)境則必須配好證書和賬號。2.2 PHP 客戶端的安裝與初始化用 Composer 裝客戶端版本號要和 ES 對齊composer require elasticsearch/elasticsearch:^7.17初始化客戶端的代碼長這樣use Elasticsearch\ClientBuilder; $client ClientBuilder::create() -setHosts([http://127.0.0.1:9200]) -setRetries(2) -build();setRetries(2)這個參數(shù)值得說一下。ES 在高負(fù)載時偶爾會返回 429請求過多或者連接超時客戶端自動重試能擋掉一部分瞬時抖動。但重試次數(shù)別設(shè)太大否則一個慢請求會拖垮整個 PHP 進(jìn)程。兩次是個比較穩(wěn)妥的值。如果你的 ES 開了認(rèn)證初始化時要加認(rèn)證信息$client ClientBuilder::create() -setHosts([http://127.0.0.1:9200]) -setBasicAuthentication(elastic, your_password) -build();提示客戶端實(shí)例應(yīng)該做成單例或者依賴注入容器里的共享服務(wù)不要每次請求都 new 一個。ClientBuilder 的構(gòu)建過程有一定開銷頻繁創(chuàng)建會拖慢響應(yīng)。2.3 中文分詞的現(xiàn)實(shí)選擇中文搜索繞不開分詞器。ES 自帶的 standard 分詞器對中文是按單字切的蘋果手機(jī)會被切成蘋果手機(jī)搜索手機(jī)能匹配上但相關(guān)性很差而且索引體積膨脹。實(shí)際項(xiàng)目里通常裝 IK 分詞器插件它提供ik_smart粗粒度和ik_max_word細(xì)粒度兩種模式。安裝方式是在 ES 的 plugins 目錄下解壓對應(yīng)版本的 IK 插件包重啟 ES 生效。版本必須和 ES 嚴(yán)格一致7.17 的 ES 就裝 7.17 的 IK差一個小版本都可能加載失敗。建索引時指定分詞器{ mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart } } } }這里analyzer是寫入時的分詞方式用細(xì)粒度保證召回search_analyzer是查詢時的分詞方式用粗粒度保證精度。這個組合是實(shí)踐里比較穩(wěn)的搭配寫入時盡量多切詞查詢時切得粗一點(diǎn)避免把用戶輸入的詞切得太碎導(dǎo)致匹配發(fā)散。3. 索引設(shè)計(jì)字段類型選錯后面全是坑3.1 text 和 keyword 的本質(zhì)區(qū)別這是新手最容易搞混的一對類型也是索引設(shè)計(jì)里最關(guān)鍵的分水嶺。text類型會被分詞適合全文檢索比如商品標(biāo)題、文章正文keyword類型不分詞整段作為一個詞條存儲適合精確匹配、聚合、排序比如商品 ID、分類編碼、狀態(tài)值。一個字段如果既要全文搜索又要精確聚合標(biāo)準(zhǔn)做法是建兩個字段用fields多字段特性{ properties: { brand: { type: text, analyzer: ik_max_word, fields: { raw: { type: keyword } } } } }這樣brand用于全文搜索brand.raw用于聚合和精確過濾。代價是存儲翻倍但對關(guān)鍵字段來說這個代價值得。我見過有項(xiàng)目把品牌字段只建成 text結(jié)果做品牌聚合時發(fā)現(xiàn)聚合出來的是一個個碎詞完全沒法用只能重建索引數(shù)據(jù)量大的時候重建一次要幾個小時。3.2 數(shù)值、日期與布爾字段的取舍數(shù)值字段看似簡單其實(shí)有講究。integer、long、float、double要根據(jù)實(shí)際范圍選別一律用long浪費(fèi)空間。價格這種涉及金額的字段建議用scaled_float配合縮放因子避免浮點(diǎn)精度問題{ price: { type: scaled_float, scaling_factor: 100 } }這樣存 19.99 實(shí)際存的是 1999查詢和聚合都按整數(shù)算精度可控。日期字段用date類型格式建議顯式指定別依賴默認(rèn)的寬松解析{ created_at: { type: date, format: yyyy-MM-dd HH:mm:ss||epoch_millis } }布爾字段用boolean但要注意 ES 對布爾值的解析比較寬松true、1、1 都能識別成 true這在數(shù)據(jù)同步時可能帶來意外最好在 PHP 側(cè)就統(tǒng)一轉(zhuǎn)成真正的布爾值再寫入。3.3 一個可落地的商品索引 mapping 示例把上面的要點(diǎn)串起來一個電商商品索引的 mapping 大概長這樣{ settings: { number_of_shards: 3, number_of_replicas: 1 }, mappings: { properties: { id: {type: long}, title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, brand: { type: text, analyzer: ik_max_word, fields: {raw: {type: keyword}} }, category_id: {type: keyword}, price: {type: scaled_float, scaling_factor: 100}, stock: {type: integer}, status: {type: keyword}, sales: {type: integer}, created_at: {type: date, format: yyyy-MM-dd HH:mm:ss||epoch_millis} } } }分片數(shù)number_of_shards一旦建索引就不能改除非 reindex所以初期要估好。經(jīng)驗(yàn)值是單分片數(shù)據(jù)量控制在幾十 GB 以內(nèi)小項(xiàng)目 1 到 3 個分片足夠。副本數(shù)可以隨時調(diào)整開發(fā)環(huán)境設(shè) 0 省資源生產(chǎn)環(huán)境至少 1。4. 數(shù)據(jù)同步MySQL 到 ES 的橋怎么搭4.1 同步策略的三種流派數(shù)據(jù)從 MySQL 到 ES主流有三種做法各有適用場景。雙寫業(yè)務(wù)代碼里寫完 MySQL 緊接著寫 ES。優(yōu)點(diǎn)是實(shí)時性最好缺點(diǎn)是代碼侵入強(qiáng)而且一旦 ES 掛了或者寫失敗數(shù)據(jù)就不一致了需要補(bǔ)償機(jī)制。適合對實(shí)時性要求極高且能接受復(fù)雜補(bǔ)償邏輯的場景。異步隊(duì)列寫完 MySQL 發(fā)一條消息到隊(duì)列消費(fèi)者負(fù)責(zé)寫 ES。解耦了業(yè)務(wù)和同步ES 抖動不影響主流程失敗可以重試。缺點(diǎn)是有延遲通常幾百毫秒到幾秒。這是大多數(shù)項(xiàng)目的首選。定時全量/增量同步定時任務(wù)掃 MySQL 變更寫 ES。實(shí)現(xiàn)簡單但實(shí)時性差適合數(shù)據(jù)變更不頻繁或者對實(shí)時性要求不高的場景。增量同步通??縰pdated_at字段做游標(biāo)。我的建議是核心搜索數(shù)據(jù)用異步隊(duì)列非核心的用定時增量。別一上來就雙寫除非你的業(yè)務(wù)真的差那幾百毫秒。4.2 用 updated_at 做增量同步的實(shí)操細(xì)節(jié)定時增量同步是最容易落地的方案但細(xì)節(jié)沒處理好會漏數(shù)據(jù)。核心邏輯是按updated_at游標(biāo)拉取變更記錄$lastSyncTime getLastSyncTime(); // 從緩存或配置讀取上次同步時間 $rows $db-query( SELECT * FROM products WHERE updated_at ? ORDER BY updated_at ASC LIMIT 1000, [$lastSyncTime] ); foreach ($rows as $row) { $client-index([ index products, id $row[id], body buildDoc($row), ]); } if (!empty($rows)) { saveLastSyncTime(end($rows)[updated_at]); }這里有幾個坑。第一時間精度問題。MySQL 的updated_at如果是秒級精度同一秒內(nèi)多條更新可能因?yàn)橛螛?biāo)推進(jìn)而漏掉。解決辦法是用配合去重或者把游標(biāo)精確到毫秒。第二刪除的數(shù)據(jù)同步不到。物理刪除后 MySQL 里沒記錄了增量掃不到。要么用軟刪除is_deleted字段要么單獨(dú)記錄刪除日志。第三批量寫入效率。逐條index很慢應(yīng)該用bulk批量提交每批 500 到 1000 條性能能提升一個數(shù)量級。4.3 bulk 批量寫入的正確姿勢bulk的 API 格式比較特殊是 NDJSON換行分隔的 JSONPHP 客戶端封裝好了用起來是這樣$params [body []]; foreach ($rows as $row) { $params[body][] [ index [ _index products, _id $row[id], ] ]; $params[body][] buildDoc($row); } $response $client-bulk($params);注意bulk返回的errors字段只要有任意一條失敗它就是 true需要遍歷items逐條檢查。很多人只看 HTTP 狀態(tài)碼 200 就以為成功了實(shí)際上部分文檔可能因?yàn)?mapping 沖突、字段類型不符等原因?qū)懭胧?。這個檢查邏輯一定要寫if ($response[errors]) { foreach ($response[items] as $item) { if (isset($item[index][error])) { // 記錄失敗原因通常是 mapper_parsing_exception logError($item[index][error]); } } }5. 查詢構(gòu)造把用戶的一句話翻譯成 ES 查詢5.1 match、term、bool 各管什么ES 的查詢 DSL 種類繁多但日常用得最多的就三類搞清楚它們的邊界大部分需求都能拼出來。match是全文檢索會對查詢詞分詞后去匹配 text 字段按相關(guān)性打分。用戶輸入的搜索關(guān)鍵詞走這個。term是精確匹配不分詞直接拿整個值去匹配 keyword 字段用于過濾比如狀態(tài)、分類。bool是組合查詢把多個條件用must必須滿足參與打分、filter必須滿足不參與打分、should滿足其一影響打分、must_not必須不滿足組合起來。關(guān)鍵區(qū)別在于filter 和 mustfilter 不計(jì)算相關(guān)性分?jǐn)?shù)而且結(jié)果可以被緩存性能更好。所以凡是精確過濾性質(zhì)的條件比如狀態(tài)等于上架、價格在區(qū)間內(nèi)、分類屬于某類一律放 filter別放 must。這個習(xí)慣能讓查詢快不少。5.2 一個帶過濾、排序、分頁的完整查詢把用戶搜索場景拼出來大概是這樣$params [ index products, body [ query [ bool [ must [ [match [title $keyword]] ], filter [ [term [status on_sale]], [range [price [gte 100, lte 5000]]], [term [category_id $categoryId]], ], ], ], sort [ [_score [order desc]], [sales [order desc]], ], from ($page - 1) * $size, size $size, ], ]; $response $client-search($params);這個查詢的邏輯是標(biāo)題必須匹配關(guān)鍵詞參與打分同時狀態(tài)、價格、分類作為過濾條件不參與打分排序先按相關(guān)性再按銷量。from和size做分頁。這里有個性能陷阱深度分頁。from size超過一萬時ES 要在每個分片上取from size條再歸并內(nèi)存和 CPU 開銷急劇上升。ES 默認(rèn)max_result_window是 10000超過直接報錯。解決辦法是用search_after游標(biāo)分頁或者限制用戶只能翻到前 N 頁。電商場景里用戶翻到第一百頁的概率極低限制頁數(shù)是最省事的做法。5.3 高亮、聚合與拼寫糾錯搜索列表通常要關(guān)鍵詞高亮ES 原生支持highlight [ fields [ title new \stdClass(), ], pre_tags [em classhl], post_tags [/em], ],返回結(jié)果里highlight.title就是帶標(biāo)簽的片段。注意new \stdClass()是為了在 PHP 里序列化成空對象{}直接寫空數(shù)組[]會被序列化成[]ES 不認(rèn)。聚合用于做篩選面板比如按品牌、分類統(tǒng)計(jì)數(shù)量aggs [ brands [ terms [field brand.raw, size 20], ], ],拼寫糾錯用suggest的term或phrasesuggester適合用戶輸錯字時給你是不是想搜的提示。這個功能不是必須但加上體驗(yàn)會好很多尤其是商品名有生僻詞的時候。6. 踩過的坑與排查鏈路6.1 數(shù)據(jù)寫進(jìn)去了卻搜不到這是最高頻的問題排查要按鏈路走。先確認(rèn)文檔真的寫進(jìn)去了用GET /products/_doc/{id}直接查文檔看_source里字段對不對。如果文檔在但搜不到八成是分詞或字段類型問題。比如標(biāo)題字段建成了keyword那match查詢手機(jī)永遠(yuǎn)匹配不上因?yàn)檎麄€標(biāo)題被當(dāng)成一個詞條。用GET /products/_analyze接口看分詞結(jié)果{ analyzer: ik_max_word, text: 蘋果手機(jī) }如果返回的是單個字而不是詞說明 IK 沒裝好或者 analyzer 名字寫錯了。如果分詞正常但搜不到檢查查詢字段名是否拼錯或者 filter 條件是否把結(jié)果全過濾掉了——這種情況很常見狀態(tài)字段的值和實(shí)際數(shù)據(jù)對不上比如代碼里寫on_sale但數(shù)據(jù)里存的是1。6.2 集群健康狀態(tài)變黃或變紅GET /_cluster/health返回的status有三種green 一切正常yellow 主分片正常但副本未分配red 有主分片未分配。開發(fā)環(huán)境單節(jié)點(diǎn)時副本數(shù)設(shè)了 1 就會一直 yellow因?yàn)楦北緵]法分配到同一個節(jié)點(diǎn)上這是正常的把副本設(shè) 0 就綠了。生產(chǎn)環(huán)境變 yellow 通常是節(jié)點(diǎn)掉線導(dǎo)致副本無處分配變紅則是主分片丟了可能伴隨數(shù)據(jù)丟失。這時候要看GET /_cat/shards?v找出未分配的分片再看GET /_cluster/allocation/explain給出的原因常見的是磁盤水位超限默認(rèn) 85% 觸發(fā)只讀、節(jié)點(diǎn)數(shù)不足等。6.3 內(nèi)存溢出與慢查詢ES 是 JVM 服務(wù)堆內(nèi)存設(shè)置很關(guān)鍵。堆太小頻繁 GC 導(dǎo)致查詢變慢堆太大超過 32GB會失去指針壓縮反而降低性能。經(jīng)驗(yàn)值是物理內(nèi)存的一半且不超過 31GB。慢查詢用GET /_nodes/stats看search相關(guān)的指標(biāo)或者開慢查詢?nèi)罩緖 index.search.slowlog.threshold.query.warn: 2s, index.search.slowlog.threshold.fetch.warn: 1s }慢查詢?nèi)罩灸芏ㄎ坏骄唧w哪條 DSL 慢常見原因是用了腳本查詢、深度分頁、或者 filter 寫成了 must 導(dǎo)致無法緩存。把能轉(zhuǎn) filter 的條件都轉(zhuǎn)過去往往能立竿見影。7. 幾個讓代碼更穩(wěn)的工程習(xí)慣7.1 封裝一個搜索服務(wù)層別在控制器里直接拼 ES 查詢業(yè)務(wù)一復(fù)雜就亂成一鍋粥。抽一個服務(wù)類把索引名、查詢構(gòu)造、結(jié)果解析都收進(jìn)去class ProductSearchService { private $client; private $index products; public function __construct($client) { $this-client $client; } public function search(string $keyword, array $filters, int $page, int $size): array { // 構(gòu)造查詢、執(zhí)行、解析結(jié)果 } }好處是查詢邏輯集中、可測試、換索引名只改一處。而且以后要加緩存、加降級都有地方下手。7.2 降級與容錯ES 不是永遠(yuǎn)可用的。網(wǎng)絡(luò)抖動、集群重啟、查詢超時都可能發(fā)生。搜索接口要有降級方案ES 查失敗時要么返回空結(jié)果加提示要么降級到數(shù)據(jù)庫的簡單查詢雖然慢但能出結(jié)果。關(guān)鍵是別讓 ES 的故障把整個頁面拖死。超時設(shè)置也要合理客戶端默認(rèn)超時可能偏長搜索接口設(shè)個 1 到 2 秒比較合適超時就降級別讓用戶干等。7.3 索引別名與平滑重建mapping 建錯了要改直接改是改不了的只能重建索引。為了不停機(jī)用別名指向真實(shí)索引# 建新索引 products_v2導(dǎo)入數(shù)據(jù) # 然后把別名 products 從 v1 切到 v2 POST /_aliases { actions: [ {remove: {index: products_v1, alias: products}}, {add: {index: products_v2, alias: products}} ] }代碼里永遠(yuǎn)查別名products重建時對業(yè)務(wù)無感。這個習(xí)慣建議從項(xiàng)目第一天就養(yǎng)成后面改結(jié)構(gòu)會輕松很多。7.4 監(jiān)控與日志至少監(jiān)控三個指標(biāo)集群健康狀態(tài)、查詢延遲、寫入失敗率。查詢延遲突然升高往往是數(shù)據(jù)量增長或者慢查詢導(dǎo)致寫入失敗率升高可能是 mapping 沖突或者磁盤滿了。這些指標(biāo)接到現(xiàn)有的監(jiān)控體系里出問題能第一時間發(fā)現(xiàn)而不是等用戶投訴。日志方面把每次 ES 查詢的耗時、DSL脫敏后、結(jié)果數(shù)量記下來排查問題時非常有用。尤其是用戶反饋搜不到的時候能直接復(fù)現(xiàn)當(dāng)時的查詢條件。8. 寫在最后的幾句實(shí)在話ES 這套東西入門不難跑通 Demo 半天就夠難的是在真實(shí)數(shù)據(jù)量和真實(shí)業(yè)務(wù)場景下保持穩(wěn)定和準(zhǔn)確。我踩過的坑里大部分不是 API 用錯而是設(shè)計(jì)階段的疏忽——字段類型選錯、同步邏輯漏了刪除、filter 和 must 混用、深度分頁沒限制。這些問題在數(shù)據(jù)量小的時候都不顯現(xiàn)等顯現(xiàn)的時候往往已經(jīng)要動大手術(shù)。所以如果你正準(zhǔn)備在 PHP 項(xiàng)目里引入 ES我的建議是先把索引 mapping 設(shè)計(jì)好把同步鏈路想清楚把降級方案留好再動手寫查詢。前期多花半天設(shè)計(jì)后期能省幾天排查。至于查詢 DSL 那些細(xì)節(jié)用著用著就熟了官方文檔寫得很全遇到具體問題查一下就行。真正拉開差距的是對業(yè)務(wù)搜索需求的理解和對邊界情況的預(yù)判。