ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Elasticsearch完整知识体系:从核心概念到集群部署与查询调优

Elasticsearch完整知识体系:从核心概念到集群部署与查询调优 我记得自己刚接触 Elasticsearch 那阵子最大的感受就是资料多得吓人但脑子里的知识全是碎片。今天装了个单机版会搜了明天碰到集群部署又懵了昨天刚搞懂 match 查询今天看到聚合一脸茫然。直到后来我把整套 Elasticsearch 完整知识体系按照概念—部署—建模—查询—集成—调优的顺序重新捋了一遍才真正觉得自己入了门。这篇文章就是把整套体系掰开揉碎讲清楚适合刚准备上手 ES 的同学也适合学了一段时间但总觉得不成体系的开发。我尽量少谈虚的。每一步用到的东西都给出能直接抄的配置、命令和代码顺便把我踩过的坑同步给你。1. 体系化学习Elasticsearch先搞清楚一张知识地图该长什么样1.1 为什么零散学ES总感觉会了但又不会很多人在学 ES 时走的路径是先搜一篇《Elasticsearch 安装教程》把单机跑起来再搜《Spring Data Elasticsearch 增删改查》照着敲一遍。运气好能跑通 demo但一旦换一个场景比如要自定义分词、要改分片数、要处理线上字段类型选错就完全抓瞎。我自己的体会是ES 的知识点之间耦合太紧。你光知道索引这个词不够你得知道它对应 MySQL 里的数据库还是表你光知道有分片这个概念不够你得知道分片数是创建索引时一次性定死、之后改不了的。这些知识不串成一张网就会反复踩同一个坑。这就是我为什么坚持用知识地图的方式来学 ES。一张完整的地图能让你在最开始就看到全貌然后把每一步的学习都放在正确的坐标上。1.2 我划分的六个知识域在整理知识体系时我把 Elasticsearch 拆成了六个知识域。这六个域不是随便分的而是按照一条真实数据从进到出、再被运维和调优所覆盖的路径来拆的核心概念索引、文档、映射、分片、副本、倒排索引。这是理解一切的地基。部署环境单机安装、集群搭建、配置项、安全验证、跨版本兼容。数据建模索引设计、字段类型选择、分词器尤其 IK、Mapping 管理。数据操作文档 CRUD、批量操作、更新策略、重建索引。查询分析词条查询、全文查询、复合查询、聚合统计。代码集成与性能Spring Data ES、Java High Level REST Client、慢查询、写入调优、面试高频点。每个知识域内部有依赖关系但它们又共同服务于一个目标让 Elasticsearch 稳定、高效地支撑搜索和分析业务。1.3 这张知识地图怎么用我不建议你把这篇文章当成通读手册从第一个字看到最后一个字。更好的用法是先花半小时把六个知识域的名称记在脑子里然后对照你手头的任务按需深入。比如你现在要做一个商品搜索那就重点看映射设计和查询 DSL你打算搭测试环境就重点看部署集群那一节你被线上写入性能问题折磨直接跳到调优部分。最后我建议你一边看一边画自己的脑图不用追求和别人一样。知识地图的作用是让大脑先有一个放置知识的抽屉后面遇到任何细节你都能立刻知道该放进哪个抽屉。2. 核心概念用MySQL的思维方式理解ES的数据模型2.1 索引、文档、映射和关系型数据库的一一对应刚接触 ES 的人最容易犯的毛病是把索引直接理解成 MySQL 的索引也就是为了加速查询而建立的 B 树结构。其实 ES 里的 index 更接近 MySQL 里的 database。我习惯用下面这张对照表来帮助记忆ElasticsearchMySQL说明索引index数据库database一个索引包含一类业务数据类型type7.x后废弃表table旧版本一个索引下可以有多个type文档document行row一条完整 JSON 记录字段field列column文档中某个具体的键值对映射mapping表结构DDL定义字段名称、类型、分词规则查询 DSLSQLES 自己的查询语言注意7.0 之后 ES 已经移除了多 type 的概念一个索引对应一类文档不能再像以前那样在一个索引里放订单和用户两种完全不同的数据。设计索引时就要按业务域来拆。2.2 分片与副本数据分布和容灾的底层逻辑分片shard是 ES 实现分布式存储的核心。一个索引的数据会被切分成多个分片分散到集群的不同节点上。分片数有两个特性你必须刻在脑子里创建索引时指定之后不能修改。每个分片是一个 Lucene 索引底层由倒排索引和各类数据结构组成。分片设置过大会带来问题每个分片都有自己的开销分片太多会浪费资源分片设置太小则限制集群横向扩展能力。我一般建议单个分片数据量控制在 30GB 到 50GB 左右这是一个在性能和查询开销之间比较平衡的经验值。副本replica是分片的拷贝作用有两个一是容灾主分片所在的节点挂了副本可以顶上二是分摊查询压力ES 默认会将查询请求同时分发到主分片和副本分片。副本数量随时可以调整不像分片数那样被锁死。2.3 倒排索引为什么ES搜索比MySQL快传统关系型数据库做模糊查询时常用LIKE %关键词%它只能全表扫描数据量一上来就非常吃力。ES 的解决方案是构建倒排索引它把每个字段的内容先分词再记录哪个词出现在哪些文档里。可以把它类比成一本书末尾的索引页。你想找分布式这个词出现在哪些页直接查索引页就行不需要从第一页翻到最后一页。ES 在写入文档时会实时维护这份索引页查询时通过词条直接定位到文档列表再结合相关性打分排序返回。这也是为什么 ES 在全文检索场景下几乎是首选不是因为它有什么神秘魔法而是用空间换时间提前把词 → 文档的映射关系建好了。2.4 分词器在概念体系中的位置分词器Analyzer负责把一段文本切成词条。它并不只是按空格切这么简单完整流程包括字符过滤、分词、Token Filter 三个步骤比如转小写、去除停用词、同义词替换等。分词器工作在写入和查询两个阶段写入时决定文档被切成哪些词存进倒排索引查询时决定用户的查询语句被切成哪些词去匹配。所以同一个字段写入和查询若使用了不同的分词器结果往往对不上。这个点我在第 4 节讲 IK 分词时会详细展开。3. 安装、部署与集群环境环节最容易踩的坑3.1 本机三件套Windows / Linux / Docker 的安装差异Elasticsearch 依赖 JDK从 7.0 开始ES 自带捆绑了 OpenJDK所以你本地即使没有单独安装 Java也能跑起来。但如果你要用 Spring Data ES 做客户端调用还是建议单独装 JDK 8 或 17具体看 ES 版本对应的推荐 JDK 版本。Windows 上启动 ES我建议不要直接双击 elasticsearch.bat而是先确认两件事不能用管理员身份解压到C:\Program Files这种带空格的目录否则后续容易出现路径解析问题。默认监听127.0.0.1如果要从局域网访问必须改config/elasticsearch.yml里的network.host。Linux 上有个经典问题ES 不能用 root 用户启动并且默认要求vm.max_map_count至少为 262144。很多新人启动时报max virtual memory areas vm.max_map_count [65530] is too low执行这行命令就行sysctl -w vm.max_map_count262144想永久生效记得写入/etc/sysctl.conf。Docker 方式最省心但要注意数据卷挂载否则容器删掉数据就全没了docker run -d --name es \ -p 9200:9200 -p 9300:9300 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms512m -Xmx512m \ -v es_data:/usr/share/elasticsearch/data \ docker.elastic.co/elasticsearch/elasticsearch:8.14.0启动后访问http://localhost:9200能看到 JSON 信息就说明环境 OK。3.2 集群搭建要点与节点角色真实项目里几乎不会用单机 ES至少要三节点起步。集群配置里最重要的三个参数分别是cluster.name、node.name和discovery.seed_hosts。三个节点只要cluster.name一致并且能通过discovery.seed_hosts相互发现就会自动组成集群。ES 的节点角色是可以拆分的不要把三个节点都设置成全能角色。我一般会把主节点和数据节点分开# 主节点 node.roles: [ master ] # 数据节点 node.roles: [ data, content ] # 协调节点负责接收请求和分发 node.roles: [ remote_cluster_client ]视频软件、数据库这类系统最常见的问题就是所有节点都既当主又当数据节点集群规模一大主节点被数据读写拖垮整个集群脑裂风险直线上升。这里没那么多玄学节点角色划分干净集群稳定率会高非常多。3.3 安全验证从http到账号体系从 8.0 开始ES 默认开启安全认证第一次启动时会在控制台输出elastic用户的初始密码还会生成证书。很多人照着老教程操作发现curl localhost:9200返回 401就是没搞清这个变化。如果你想快速体验功能可以使用xpack.security.enabled: false关掉认证。但生产环境千万别这么干。生产环境建议走 HTTPS 加用户名密码或者用反向代理做一层认证。至少把elastic这个超级管理员的密码改掉并为不同业务创建独立的用户和角色。3.4 OpenSearch与Elasticsearch的关系近几年总有人问 OpenSearch 和 Elasticsearch 是不是同一个东西。这个问题的背景是Elasticsearch 在 7.11 之后把协议许可证从 Apache 2.0 换成了 SSPL 和 Elastic License社区一部分人基于最后的 Apache 2.0 版本分支诞生了 OpenSearch。对普通使用者来说OpenSearch 的 API 在绝大多数场景下和 Elasticsearch 7.10 保持兼容很多基于 ES 7.x 写的代码可以直接迁移到 OpenSearch。区别主要体现在版本演进速度、部分高级特性如机器学习、向量检索以及各自生态的更新节奏。如果你的项目不需要 Elastic 后来推出的付费高级特性OpenSearch 可以作为一个开源替代方案来评估如果团队一直在用 Elastic Stack 全家桶继续留在 Elasticsearch 生态里更省心。4. IK分词器中文搜索体验的隐形门槛4.1 为什么默认分词器处理中文会翻车ES 自带的标准分词器对英文友好因为它可以按空格和标点切分词条。但中文词与词之间没有天然分隔比如中华人民共和国成立了标准分词器很可能把整句话当成一个词或者切出中华人民共和国这种偏差结果。如果你搜中国就匹配不到中华人民共和国用户体验直接崩了。这就是中文搜索必须引入 IK 分词器的根本原因。IK 分词器内置了中文词库能根据词典把文本切成更合理的词语也支持自定义词典扩展领域词汇。4.2 IK分词器的安装与配置安装 IK 分词器最稳妥的方式是先去 GitHub 找到和你 ES 主版本完全一致的 IK 版本然后执行./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.14.0/elasticsearch-analysis-ik-8.14.0.zip装完到plugins/analysis-ik目录下确认存在config/IKAnalyzer.cfg.xml然后重启 ES。用下面的请求测试分词效果POST /_analyze { analyzer: ik_max_word, text: 中华人民共和国成立了 }ik_max_word会尽可能切出最多词语适合索引ik_smart只会切出最粗粒度的词适合搜索时使用。两者配合是常见做法索引用ik_max_word查询用ik_smart。4.3 扩展词典和停用词让领域词被正确识别分词器做得再好也覆盖不了你所在行业的专有名词。比如尤克里里大促价全域兴趣电商这类词IK 默认词库里根本没有会被切成莫名其妙的碎片。解决办法是在IKAnalyzer.cfg.xml里配置扩展词典entry keyext_dictcustom/my_ext.dic/entry然后在my_ext.dic文件里一行一个词尤克里里 大促价 全域兴趣电商保存后要重启 ES 才能生效。我自己踩过一次坑改完文件没重启分词结果纹丝不动还以为是缓存问题。其实 IK 的词典在启动时加载线上环境改词典最稳妥的做法是通过 IK 的远程词库热更新不过那需要预留 HTTP 服务我这里不展开了。如果某些词在搜索时需要被过滤比如的了吧这类字符就在stopword.dic里配置停用词避免它们参与倒排索引占用空间。5. 索引、映射与文档操作数据如何进入ES5.1 创建索引与Mapping设计创建索引时最忌讳的就是拿到 JSON 就直接往里写让 ES 自动推断映射。ES 的自动映射在开发阶段挺方便但线上很容易埋雷比如某个数字字段被推断成long某个应该分词的文本被推断成keyword后面再想改就很痛苦。推荐的方式是显式定义 mappingPUT /products { settings: { number_of_shards: 3, number_of_replicas: 1 }, mappings: { properties: { title: { type: text, analyzer: ik_max_word }, category: { type: keyword }, price: { type: double }, stock: { type: integer }, createTime: { type: date, format: yyyy-MM-dd HH:mm:ss } } } }5.2 字段类型选择text vs keyword 别再选错这是面试和日常开发里最容易出问题的点。一句话总结text类型会分词支持全文搜索和模糊匹配但不能用于精确匹配、排序和聚合。keyword类型不分词支持精确匹配、排序和聚合但不能做全文检索。打个比方商品标题戴森吸尘器 V12必须用text因为用户会搜吸尘器或者戴森而商品货号DS-V12-BLACK必须用keyword因为用户搜索它是精确匹配你不能让 ES 把货号给拆了。如果你既要全文搜索又要精确过滤可以对同一个字段使用多字段语法title: { type: text, analyzer: ik_max_word, fields: { keyword: { type: keyword } } }查询时用title做全文搜索用title.keyword做精确匹配和排序。5.3 文档CRUD幂等更新与版本机制文档操作的四个核心 API 分别是# 新增/覆盖 PUT /products/_doc/1 { title: 戴森吸尘器 } # 根据 id 查询 GET /products/_doc/1 # 更新指定字段 POST /products/_update/1 { doc: { price: 3999 } } # 删除 DELETE /products/_doc/1要注意POST /products/_update/1和PUT /products/_doc/1的区别前者是局部更新只改传入字段后者是整体覆盖如果没写 title原 title 就没了。如果更新一个不存在的 idupdate会报错index会直接新增。这个细节很容易在测试时误判。ES 的每次文档更新都会生成新版本号。乐观锁更新可以带上外部版本号或if_seq_no参数防止并发更新互相覆盖。电商库存这类高频更新场景需要注意这个机制。5.4 重建映射线上字段类型错了怎么办这是每个 ES 开发者迟早会遇到的问题字段类型已经定了、数据已经写了很多但发现应该用keyword的字段变成了text想直接改 mapping 是做不到的。ES 不允许你修改已有字段的类型。解决思路是索引重建先创建新索引定义好正确映射用reindex把旧索引的数据迁移过去最后把旧索引删除或者通过别名切换访问入口。POST /_reindex { source: { index: products_old }, dest: { index: products_new } }注意reindex是服务端操作数据量大的时候尽量避开业务高峰文档数量多时可以加上slices参数提升迁移并发度。我用这个方案处理过几次线上索引迁移只要提前验证好映射和别名切换顺序风险完全可控。6. 查询DSL与聚合从精确匹配到多维统计6.1 词条查询与全文查询的区别ES 查询 DSL 中最容易混淆的一组概念就是term和match。term是词条查询它不会对查询关键字做分词直接拿着完整词去倒排索引里找精确匹配。match是全文查询会对查询关键字做分词然后用分出来的多个词分别去匹配。举个例子用match查询红色连衣裙ES 会先分词成红色连衣裙然后匹配包含任一词语的文档用term查询红色连衣裙则只会匹配包含完整词条红色连衣裙的文档。这也是为什么很多新手用term查text字段总是查不到数据——因为text字段写入时已经被切碎了倒排索引里根本没有红色连衣裙这个完整词条。记住这个实战结论精确查询用keyword字段 term全文检索用text字段 match。6.2 复合查询bool 过滤上下文日常业务几乎不会只用单条查询更多是用bool组合多个条件。四个关键字的作用分别是must必须满足并参与相关性打分。filter必须满足但不参与打分性能更好。should至少满足一个配合minimum_should_match控制数量。must_not必须不满足也不参与打分。一个典型的电商筛选查询可以写成GET /products/_search { query: { bool: { must: [ { match: { title: 连衣裙 } } ], filter: [ { term: { category: 女装 } }, { range: { price: { gte: 100, lte: 500 } } } ], must_not: [ { term: { status: 下架 } } ] } } }性能差距最明显的地方就在filter和mustfilter的结果会被 ES 缓存反复执行同样的过滤条件时速度极快。所以像类目价格区间状态这类固定约束一律丢进filtermust只留真正影响相关性的搜索词。6.3 聚合分析metric与bucket聚合是 ES 在搜索之外的看家本领。两种最核心的聚合类型指标聚合metric计算最大值、最小值、平均值、求和、计数等。桶聚合bucket按条件分桶类似于 SQL 里的GROUP BY。按类目统计商品数量的写法GET /products/_search { size: 0, aggs: { by_category: { terms: { field: category } } } }size: 0的意思是只返回聚合结果不返回命中的文档。很多人第一次跑聚合发现返回了一堆搜索文档就是因为忘了设置这个参数。在桶聚合下再嵌套指标聚合能实现每个类目的平均价格、最高价格这类多维分析GET /products/_search { size: 0, aggs: { by_category: { terms: { field: category }, aggs: { avg_price: { avg: { field: price } }, max_price: { max: { field: price } } } } } }这里还有一个坑对text字段做聚合会报错必须先使用keyword类型字段。所以第 5 节讲的字段类型选择直接影响你是否能顺利做聚合。6.4 查询层面的几个实战建议第一先设计filter再设计must。把过滤型条件全部用filter包裹可以有效利用缓存。第二深度分页别用from size。ES 默认最多查到第 10000 条一旦翻页过深每次都要把所有分片的排序结果拉到协调节点再截断性能会急速下降。业务上需要下一页逐页翻的用search_after需要一次性导出大量数据的用scroll。第三能用term就别用match_phrase。短语匹配的开销明显更大如果业务不要求词语前后顺序用match配合operator: and效果接近但性能更好。7. Spring Data Elasticsearch 与 Java High Level REST Client代码落地的两种姿势7.1 用Spring Data ES做基础CRUDSpring Data Elasticsearch 把 ES 的索引映射和 CRUD 封装成了类似 JPA 的体验。定义一个实体类加注解就行了Document(indexName products) public class Product { Id private String id; Field(type FieldType.Text, analyzer ik_max_word) private String title; Field(type FieldType.Keyword) private String category; Field(type FieldType.Double) private Double price; }继承接口后就能获得一整套现成的增删改查public interface ProductRepository extends ElasticsearchRepositoryProduct, String { ListProduct findByTitle(String title); ListProduct findByCategoryAndPriceBetween(String category, Double min, Double max); }方法名解析规则和 Spring Data JPA 很像基本的基础查询不用写一行 DSL。适合快速交付 CRUD 和管理后台场景。7.2 用High Level REST Client做复杂查询一旦遇到聚合、嵌套查询、自定义排序Spring Data 的方法名派生的查询就有点不够用了。我更倾向用 High Level REST Client 直接构造请求。在工程里引入依赖后可以这样写聚合查询SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); sourceBuilder.size(0); TermsAggregationBuilder categoryAgg AggregationBuilders.terms(by_category) .field(category); AvgAggregationBuilder avgPrice AggregationBuilders.avg(avg_price).field(price); categoryAgg.subAggregation(avgPrice); sourceBuilder.aggregation(categoryAgg); SearchRequest request new SearchRequest(products); request.source(sourceBuilder); SearchResponse response client.search(request, RequestOptions.DEFAULT);High Level REST Client 的优势之一是和原生 DSL 结构几乎一一对应。你在 Kibana 里调通的查询几乎可以逐行翻译成 Java 代码排错时两边对照非常方便。7.3 两种方式的选型建议我的建议很简单新增、修改、简单查询用 Spring Data Repository 那一套涉及聚合、复杂 bool 查询、性能敏感的搜索接口用 High Level REST Client 直查。实际项目中通常两种方式共存并不互斥。另外从 7.17 开始官方把 High Level REST Client 标记为维护模式更推荐的是 Elasticsearch Java Client。不过大量存量项目仍在使用 RestHighLevelClient如果你维护的是老项目不必急着迁移新项目可以直接从新客户端开始API 风格更现代。8. 性能优化与高频面试题构建知识体系的最后一块拼图8.1 写入性能调优的几个思路大批量导入数据时写入性能常常是第一个瓶颈。我总结出几个有效且常用的调优手段批量写入使用_bulkAPI每批 5000 到 10000 条文档实测吞吐量比逐条插入提升好几倍。调整refresh_interval默认每秒刷新一次让写入立即可搜。如果做离线导入可以临时设为-1导完再改回来能显著减少段合并开销。副本设为 0导数据阶段可以先关掉副本导入完成后再恢复。但生产环境做在线导入时不要随便用这招会有数据丢失风险。合理设置number_of_shards分片太少写不散分片太多则每个分片都要维持自身开销。前面提到单分片 30GB 到 50GB 的经验值在这个范围内相对稳妥。8.2 查询性能与硬件配置查询慢先看两件事慢日志和profile。ES 支持通过慢日志配置打印超过阈值时间的查询index.search.slowlog.threshold.query.warn: 10s index.search.slowlog.threshold.query.info: 1s找到慢查询后再用profile: true查看分片内具体哪个环节耗时高是分词、倒排索引还是收集器耗时。硬件方面ES 是内存吃紧的系统。JVM 堆内存建议设置为物理内存的一半且不超过 32GB。为什么是 32GB因为 JVM 启用压缩指针的上限大概是 32GB超过后对象指针不再压缩内存反而浪费。剩下的一半留给操作系统页缓存Lucene 的倒排索引大量依赖 OS 文件缓存加速。存储上首选 SSD。机械盘跑 ES 在数据量小的时候可能感觉不明显一旦索引数据量上 GB查询延迟会明显恶化。8.3 面试高频考点速查这套完整知识体系也能用来准备 ES 相关的技术面试我把高频考点整理成一张速查表问题核心要点ES 为什么快倒排索引 分布式分片并行 内存页缓存 近实时刷新text 和 keyword 区别是否分词能否精确匹配、排序、聚合分片数为什么不能改创建索引时决定路由改后数据无法定位深度分页方案search_after、scroll、避免 fromsize 过深写副本还是写主分片默认写入主分片再同步副本返回成功可配置 wait_for_active_shards如何设计 mapping先分析查询和聚合需求再定字段类型和分词器IK 分词器原理词典匹配 歧义处理支持扩展词典和停用词集群节点角色master、data、ingest、coordinating 各司其职聚合类型metricavg/sum/min/max与 bucketterms/range/histogram索引生命周期按时间分索引、冷热分离、定期删除或归档哪怕你当前不面试把这十道题目里对应的知识点吃透ES 的使用水平也会比大多数同龄开发者高出一截。我在实际带人和做项目的时候越来越觉得 ES 这套完整知识体系的真正价值不在于收藏一份文档而在于你遇到问题的时候能准确判断这个问题属于哪一层。是数据建模问题就不要去调查询参数是写入瓶颈就不要整天调堆内存。这种分层定位的能力比记住任何一条 API 都重要。最后分享一个对我很有用的小习惯每次处理完一个线上问题我都会在脑图对应知识域下新增一个小节点写上问题现象和解决方案。半年下来那张知识地图就成了我自己的排错手册比任何教程都顺手。
返回列表