很多人在Spring Boot项目里接入Elasticsearch时,最容易卡住的不是写代码,而是卡在“到底该怎么启动一个能用的ES环境”和“索引结构该怎么设计”这两步。网上教程要么只讲单机部署、要么只贴CRUD代码,真正把部署、索引设计、客户端选型、查询写法串起来讲的确实不多。这篇文章我把实际项目中从零接入ES的完整过程拆开揉碎讲一遍,包括我怎么部署、怎么建模、怎么写查询、以及上线后踩过的那些坑。
1. 为什么我最终选了ES而不是MySQL模糊查询
先聊一个最基础的问题:什么时候你才真正需要Elasticsearch?
我在某个数据检索类项目里,早期用的是MySQL的 LIKE 模糊查询,数据量在几十万条的时候勉强能扛,但一旦到了百万级、并发一高,LIKE '%关键词%' 这种写法会导致全表扫描,性能直线下降。而且业务方还提了需求:要支持多字段权重排序、要按价格区间过滤、要支持分页深翻页、要对搜索结果里匹配到的关键词高亮显示。这些需求用MySQL实现起来非常别扭,哪怕能实现,索引和SQL的复杂度也会高到让人绝望。
后来我把搜索这块迁移到ES之后,整条查询链路干净了很多。ES倒排索引天然适合全文检索,分词、过滤、聚合、排序都是内置能力;而且它自带分布式能力,数据量上来以后横向扩容就行。
但ES不是什么银弹,它对事务支持弱、更新性能比MySQL差,所以我在项目里把它定位成“搜索专用层”,MySQL负责业务数据存储,ES负责检索和聚合,两者通过业务侧做数据同步。这个定位很重要,它决定了你后面整个架构的走向。建议读者在动手前也先想清楚:你的场景是真的需要ES,还是普通的MySQL查询加个缓存就能解决?
2. 本地环境部署:从Docker到Linux的完整对比
2.1 我选Docker部署的原因和完整步骤
ES的部署方式大概分三种:直接下载压缩包解压运行、用Docker跑容器、用云厂商托管服务。我本地开发阶段用的是Docker,原因很简单:快速、干净、不污染系统环境,想换版本就改个镜像标签,删了重来成本极低。
我部署单机版ES的Docker命令大致如下:
docker network create es-network docker run -d \ --name es-node1 \ --network es-network \ -p 9200:9200 \ -p 9300:9300 \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "xpack.security.enabled=false" \ docker.elastic.co/elasticsearch/elasticsearch:7.14.2这里的discovery.type=single-node非常关键,它是单机模式必需的配置,否则ES会默认走集群发现流程,找不到其他节点就会一直报错。ES_JAVA_OPTS里我给ES分配了512MB堆内存,这个值不能随便填,有个经验原则是不要超过物理内存的50%,而且不要超过31GB,因为超过之后JVM的压缩指针会失效、内存利用率反而下降。
启动之后用curl http://localhost:9200验证,返回带 cluster_name 的JSON就说明成功了。第一次跑起来如果发现容器不断重启,大概率是vm.max_map_count不够,Linux下需要执行:
sudo sysctl -w vm.max_map_count=262144这个参数控制的是进程能映射的最大内存映射区域数量,ES底层用了大量内存映射文件,默认值65530根本不够用。这是新手最容易踩的坑,而且Docker日志里提示非常隐晦,只会报“max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]”。
2.2 如果不用Docker,压缩包部署有哪些注意事项
如果是在内网服务器上部署,Docker有时候并不被运维允许,那就得用压缩包方式部署。解压 tar.gz 之后,需要单独创建一个非root用户来启动ES,因为ES出于安全考虑不允许root直接运行,这也是好多人在生产环境部署时直接懵掉的地方。
具体步骤大概是这样:创建esuser用户,把解压后的目录属主改成这个用户,然后切换到该用户去执行bin/elasticsearch。配置文件里要改两个路径:
path.data: /data/es/data path.logs: /data/es/logs这两个目录必须给 esuser 写权限。启动时加-d参数后台运行,日志在logs/elasticsearch.log里。如果你要跨机器部署集群,还需要在config/elasticsearch.yml里配置network.host为具体内网IP,并配置discovery.seed_hosts指向其他节点地址。
2.3 通过Kibana快速验证和调试索引
ES没有可视化界面,你光靠curl做增删改查太吃力了,所以我强烈建议把Kibana也一起部署起来。Docker方式下一行命令就搞定:
docker run -d \ --name kibana \ --network es-network \ -p 5601:5601 \ -e "ELASTICSEARCH_HOSTS=http://es-node1:9200" \ docker.elastic.co/kibana/kibana:7.14.2Kibana里的Dev Tools控制台是我日常调试ES的利器,它相当于ES的“命令行客户端”,可以直接写ES的RESTful API语句,还能看到返回结果的格式化输出。后文涉及查询语句的地方,我强烈建议你先在Kibana里跑通了再搬到Java代码里,这样能极大减少排错时间。
3. Spring Boot集成前的设计:索引建模决定查询成败
3.1 索引映射的核心概念与设计思路
ES里的“索引”大概相当于MySQL里的“数据库 + 表”的复合概念。索引里的“映射”(Mapping)定义字段类型和分词规则,相当于MySQL的表结构设计。很多人一上来直接往ES里灌JSON数据,让ES做动态映射,看上去很方便,实际上给后面埋了巨大的坑。
动态映射的问题在于:ES会根据第一条数据的字段值自动推断类型。比如某条数据的price字段如果第一次写入是整数类型,ES就把它映射成long;后续如果写入带小数的值,就会报类型冲突。更麻烦的是字符串字段默认会同时生成 text 类型和 keyword 类型,很多人在检索时发现“等值匹配都没结果”,就是因为用了text类型去做term查询。
我在实际项目里设计的商品索引映射长这样(缩减版):
PUT /goods { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "analysis": { "analyzer": { "ik_pinyin_analyzer": { "type": "custom", "tokenizer": "ik_max_word", "filter": ["my_pinyin"] } }, "filter": { "my_pinyin": { "type": "pinyin", "keep_full_pinyin": false, "keep_first_letter": true, "keep_separate_first_letter": false } } } }, "mappings": { "properties": { "goodsId": {"type": "keyword"}, "goodsName": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "pinyin": {"type": "text", "analyzer": "ik_pinyin_analyzer"} } }, "categoryId": {"type": "keyword"}, "price": {"type": "double"}, "status": {"type": "integer"}, "createTime": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} } } }3.2 分词器的选择:ik分词与拼音搜索的实现
这里重点讲讲分词器。ES自带的标准分词器对中文支持很差,它会把一句话切成单个汉字,搜索结果惨不忍睹。所以中文场景必定要引入IK分词器插件,ik_max_word 是尽量切分出最多的词组合(用于索引),ik_smart 是切出最合理的粗粒度词语(用于搜索),这一对搭配是网上公认的最佳组合。
拼音分词器则是我为了实现“输入拼音首字母也能搜到商品”而加的。我用的拼音过滤器配置如上,它会在索引阶段生成字段的拼音版本,搜索时用户输入lx就能命中“联想”相关商品。这个功能用户体验提升非常明显,但也要付出索引体积变大的代价,一个字会同时存多个版本。
要注意的是,IK分词器在ES 7.14.2版本需要下载对应版本的插件包,直接用低版本插件会导致启动报错。安装方式是解压到plugins/ik目录,然后重启ES。IK还支持自定义词典,比如行业黑话、品牌名拆词不准确的问题,可以编辑IKAnalyzer.cfg.xml配置扩展词典。
3.3 为什么我坚持不用动态映射
在我的项目里,动态映射功能我是彻底关闭的,Mapping里每个字段的用途都是事先规划好的。关闭方法是:
PUT /goods/_mapping { "dynamic": false }或者在新索引创建时直接写成"dynamic": "strict"。为什么要这么干?举个例子:商品描述字段我希望存原文但不需要全文检索,那就可以设置"index": false,省空间省评分时间;但如果让动态映射自动处理,它会给每个字符串都生成text+keyword两套倒排索引,空间浪费极其严重。
ES的映射设计至少要回答四个问题:这个字段将来要全文检索吗?要做精确匹配吗?要参与排序或聚合吗?要存原始值吗?每回答一个“不”,你就可以关掉一套存储或索引结构,成本和性能都会优化不少。这条经验是我在压测时用惨痛教训换来的,一个索引里塞了十几个冗余的keyword字段,写性能直接掉了一半以上。
4. Spring Boot集成ES:客户端选型和基础配置
4.1 选对客户端:RestHighLevelClient还是ElasticsearchClient
Spring Boot接入ES的方式,网上最常见的无非两种:RestHighLevelClient和 Spring Data Elasticsearch。我两个都用过,要说体验和可控性,我更倾向于直接在代码里用官方的RestHighLevelClient,而Spring Data Elasticsearch只适合做极简单的CRUD。
两者的核心区别在于:Spring Data Elasticsearch 把ES的查询封装成了类似Spring Data JPA的Repository风格,你只要定义接口方法名就能自动生成查询。听起来很爽,但它的封装模型太“重”了,一旦涉及聚合、复杂查询、高亮、排序权重调整,你就得冲破一层又一层封装,写起来反而更痛苦。RestHighLevelClient 则是直接对应ES底层的REST API,每个方法都对应一个明确的请求语义,ES文档里怎么写,代码里就怎么调,排查问题特别顺手。
我在pom里用的依赖如下(7.14.2版本):
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.14.2</version> </dependency>注意版本号必须和你部署的ES服务端版本保持一致,主版本不同会有兼容性问题,ES客户端7.x访问6.x服务端虽然部分兼容,但我在实战中遇到过字段类型解析报错的情况,劝大家别省这个事儿。
4.2 配置类的完整写法和连接池调优
创建一个ElasticsearchConfig,核心就是构建RestHighLevelClient对象。我在配置文件里用的是自定义配置项:
elasticsearch: hosts: 127.0.0.1:9200 connectTimeout: 5000 socketTimeout: 30000 connectionRequestTimeout: 1000 maxConnectNum: 100 maxConnectPerRoute: 50对应的配置类代码如下:
@Configuration @ConfigurationProperties(prefix = "elasticsearch") public class ElasticsearchConfig { private List<String> hosts; private int connectTimeout; private int socketTimeout; private int connectionRequestTimeout; private int maxConnectNum; private int maxConnectPerRoute; @Bean(destroyMethod = "close") public RestHighLevelClient restHighLevelClient() { List<HttpHost> httpHosts = hosts.stream() .map(host -> { String[] parts = host.split(":"); return new HttpHost(parts[0], Integer.parseInt(parts[1]), "http"); }) .collect(Collectors.toList()); RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[0])); builder.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder .setConnectTimeout(connectTimeout) .setSocketTimeout(socketTimeout) .setConnectionRequestTimeout(connectionRequestTimeout)); builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder .setMaxConnTotal(maxConnectNum) .setMaxConnPerRoute(maxConnectPerRoute)); return new RestHighLevelClient(builder); } }连接池参数里,maxConnectTotal是总连接数上限,maxConnectPerRoute是单个路由的最大连接数。并发量高的查询服务,这两个值太小会导致大量请求排队等待连接释放,表现就是接口RT突然飙升;但设得太大会占用文件描述符,操作系统默认1024很快会被打满。我一般按并发预估量来设置,先给总连接100、每路由50,压测后再做调整。
4.3 数据同步:从MySQL到ES的双写策略对比
索引建好了、客户端也配置好了,下一个问题是数据从哪来。我在项目里刚开始做的是“业务代码双写”,即每次MySQL写操作成功后,顺手往ES里再写一份。这个方案在数据量小的时候没问题,但一旦业务逻辑复杂、调用链路长,双写会导致代码侵入性极强,改个业务还得惦记着ES的一致性。
后来我换成了基于日志订阅的异步同步方案:用MySQL的binlog监听工具把数据变更事件发到消息队列,再由消费端更新ES索引。这个方案的好处是业务代码完全无侵入,ES的写入压力也能通过消费端做削峰限流。唯一要注意的是初始化阶段需要全量数据灌入,一般写个一次性脚本从MySQL批量查出数据后BulkRequest批量写入ES,速度比单条写入快几个量级。
同步方案的选择直接影响架构复杂度,我最后给出的建议是:业务简单项目用双写+定期补偿,业务复杂项目必须上消息队列异步同步,千万不要在核心业务代码里同步去调ES的写接口,那样ES抖动会直接拖垮业务接口。
5. 核心查询实战:从简单查询到复杂聚合
5.1 关键词搜索和高亮显示的实现
搜索接口是项目里最核心的功能,我用MultiMatchQueryBuilder实现多字段联合搜索。比如我要按照商品名称来搜,同时给名称匹配度高的商品加权排序,代码核心部分是这样写的:
SearchRequest searchRequest = new SearchRequest("goods"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery("联想笔记本", "goodsName", "goodsName.pinyin"); multiMatchQuery.type(MultiMatchQueryBuilder.Type.BEST_FIELDS); multiMatchQuery.boost("goodsName", 2.0f); searchSourceBuilder.query(multiMatchQuery); searchSourceBuilder.from(0).size(10);这里我用了multiMatchQuery的 BEST_FIELDS 类型,它会在多个字段中选取匹配分最高的那个作为该文档的最终得分,更适合“一条文档里多个字段分别匹配”的场景。而且我给goodsName设了更高的boost权重,意味着商品名称匹配要比拼音匹配更“值钱”,这样搜索结果排序会更符合业务直觉。
高亮显示的需求,初创搜索项目基本都会遇到。ES返回的命中结果里默认只给原文,要在前后端展示出红色的“联想笔记本”,就得让ES在查询时预处理高亮片段:
HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("goodsName"); highlightBuilder.preTags("<span style='color:red'>"); highlightBuilder.postTags("</span>"); searchSourceBuilder.highlighter(highlightBuilder);对应查询结果里,高亮内容在SearchHit#getHighlightFields()里,前端直接渲染带HTML标签的字符串即可。这里有个经验:高亮字段如果设置了"index" : false,高亮就取不到,因为高亮是基于倒排索引词项来定位的。
5.2 BoolQuery组合查询:过滤、范围与排序
实际业务场景里几乎没有“只搜一个关键词”的查询,大概率伴随着分类过滤、价格区间、上下架状态过滤等条件。ES的BoolQuery就是干这个的,它的核心结构是 must、should、filter 三个子句:
- must:必须匹配,参与评分
- filter:必须匹配,但不参与评分,缓存友好
- should:条件满足则加分,不满足也不排除
为什么把过滤条件放到filter而不是must?因为用户筛分类、筛价格区间这类条件,根本不该影响相关性排序,而且filter子句的查询结果会被ES自动缓存,下一笔相同条件的查询直接从缓存里出结果,性能远优于must。这个细节很多人不知道,导致查询性能白白差了几倍。
典型代码:
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.multiMatchQuery("联想笔记本", "goodsName", "goodsName.pinyin")); boolQuery.filter(QueryBuilders.termQuery("categoryId", "123456")); boolQuery.filter(QueryBuilders.rangeQuery("price").gte(4000).lte(8000)); boolQuery.filter(QueryBuilders.termQuery("status", 1)); searchSourceBuilder.query(boolQuery); searchSourceBuilder.sort("price", SortOrder.ASC);范围查询rangeQuery对数值和日期类型都适用,字段类型错了也会直接报查询解析异常,这也是为什么要提前建Mapping的原因。
5.3 聚合查询:统计分类数量与平均价格的写法
除了检索,ES还常被用来做聚合统计。比如电商后台需要统计当前筛选条件下每个分类的商品数量、价格平均值和最高价,这在MySQL里是一条含GROUP BY的长SQL,在ES里用聚合查询几行代码搞定:
TermsAggregationBuilder categoryAgg = AggregationBuilders.terms("category_agg") .field("categoryId") .size(20); AggregationBuilder avgPriceAgg = AggregationBuilders.avg("avg_price").field("price"); categoryAgg.subAggregation(avgPriceAgg); searchSourceBuilder.aggregation(categoryAgg);返回结果里嵌套着每个分类的bucket以及对应的平均价。聚合字段在ES里有一个硬性要求:参与聚合的字段类型必须是keyword或开启了fielddata的text字段。如果映射里类型是纯text,聚合会直接报错。我在前面Mapping设计里把categoryId定义为keyword,就是为了服务于聚合场景。这里顺便强调一遍:规范化Mapping规划真的能在后面省下大把排错的工夫。
5.4 深分页问题:用search_after替代from+size
ES传统分页方式from + size在浅分页时没问题,但数据量大、翻页深就完了。比如100万条数据,你要取第9990页的10条,ES会把前面9万多个文档的评分值和排序信息全部拉出来排序,然后才取第9990页的内容,内存开销巨大,还容易导致节点OOM。
我在后台管理系统的翻页场景里,最终用search_after方案解决了深分页问题。它的核心逻辑是记住上一页最后一条数据的排序值,下一页从这个值之后开始查:
searchSourceBuilder.query(boolQuery); searchSourceBuilder.sort("goodsId", SortOrder.DESC); searchSourceBuilder.sort("_doc"); // 稳定排序 if (lastGoodsId != null) { searchSourceBuilder.searchAfter(new Object[]{lastGoodsId}); }用search_after时有一个前置条件:排序字段必须包含一个唯一值,比如goodsId,否则相同排序值的文档在翻页时可能被跳过或重复。如果你需要用户随意跳转到第任意页(像百度搜索那样),那就只能用scroll或者付费版游标,但大多数业务场景里“只支持下一页/上一页”完全够用,深翻页通常是很低频的需求。
6. 上线后的性能优化与常见坑
6.1 磁盘和内存:ES节点配置的最佳实践
ES是个很“吃”机器配置的中间件,上线前没规划好资源,线上出问题几乎是必然的。
首先是内存:ES对CPU要求不高,但内存极其敏感。堆内存同时包含索引缓存、查询缓存、分片元数据等数据。千万不要把堆内存设得太大,我给一个双节点集群设置的是每节点16GB堆内存,剩余内存留给操作系统做文件缓存,因为ES查询热数据其实大量依赖系统文件缓存,操作系统层面的cache比JVM堆更高效。
然后是磁盘:ES数据目录必须是本地磁盘,千万别放在NFS共享存储或者云盘这种IO能力弱的地方。分片副本也需要额外磁盘空间,建议至少留出数据用量50%的余量。我曾经遇到过磁盘用量到85%后ES自动变为只读的情况,当时业务方的写入全部失败,排查半天才发现是磁盘水位线触发了机制。生产环境建议把cluster.routing.allocation.disk.watermark阈值调高,提前干预。
6.2 慢查询日志:排查线上搜索慢的必备工具
ES检索变慢的时候,第一件事就是看慢查询日志。要开启慢日志只需要在索引配置里设置阈值,比如:
PUT /goods/_settings { "index.search.slowlog.threshold.query.warn": "1s", "index.search.slowlog.threshold.query.info": "500ms", "index.search.slowlog.threshold.fetch.info": "800ms" }这样一旦查询超过阈值,ES就会在logs/index_search_slowlog.log里打印出慢查询对应的原始请求体。通过慢日志你能看到完全展开的DSL语句,再一步步排查是哪个查询条件导致慢。我记得有一次线上搜索慢,慢日志里显示一大半时间花在了一个复杂聚合上,后来改成近似基数统计才解决。
慢日志必须在集群稳定、索引量大的情况下才有统计意义,本地一两个索引看不出明显效果。如果你们ES集群压测阶段就慢,先检查索引分片是不是分配得过于不均,再查查询里是否有script或者wildcard这类开销极大的操作。
6.3 分片数和副本数:别盲目照抄默认值
很多人在创建索引时数字随便填,或者干脆用默认一个主分片一个副本。默认值对demo没问题,但生产环境的索引分片数建议和经验如下:
分片数主要受数据量影响。单分片能承载的数据量一般在20GB到50GB之间,全量数据100GB,那至少得3个主分片。但分片数设太多也有副作用,每个分片都要消耗句柄和内存,查询时需要合并所有分片返回的结果,分片数量多了查询性能反而下降。经验公式是:节点数乘以每个节点能支撑的分片数为标准,单节点控制在5个分片以内。
副本数的意义不只是容灾,还能分摊查询压力。查询QPS高的时候,把副本数从1提高到2,相当于把查询并发能力翻了将近一倍。但副本数量增多,对应写入时需要同步的数据份数也增多,写入性能会下降,这个要按业务读写比例来平衡。我项目里的商品索引是典型读多写少场景,所以我设置的副本数量为2,而你如果做日志写入场景,副本可以设0,靠分片机制和备份策略兜底,写入速度提升非常明显。
6.4 重启之后数据全丢?说说安全配置和快照备份
我见过不少朋友在本地用ES做开发,某天重启电脑后发现自己好不容易灌进去的测试数据全没了,一脸懵。这通常是因为他以为Docker容器加了-v参数做了数据卷映射,实际上没映射到正确的ES数据目录。Docker官方ES镜像默认的数据目录在/usr/share/elasticsearch/data,挂载时卷必须指向这个目录:
-v /your/es/data:/usr/share/elasticsearch/data我自己的本地方案里,数据目录就是持久化在宿主机上的:
docker run -d \ --name es-node1 \ --network es-network \ -p 9200:9200 \ -p 9300:9300 \ -v /opt/es/data:/usr/share/elasticsearch/data \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ docker.elastic.co/elasticsearch/elasticsearch:7.14.2生产环境除了数据卷持久化,还要考虑ES本身的快照备份机制。ES支持把索引快照备份到本地磁盘或云存储,定期执行快照创建。我现在的习惯是每天晚上自动执行一次全量索引快照,快照保留7份,这样就算误删索引或者节点彻底损坏,也能恢复到最近一个快照点的状态。快照API非常简单:
PUT /_snapshot/my_backup/snapshot_20250101 { "indices": "goods", "ignore_unavailable": true }这个习惯一定要养成,很多人在开发阶段觉得ES就是个缓存工具,丢了就丢了,真到生产环境出现误删索引事故,那时候后悔已经晚了。
6.5 生产环境必须开启的安全认证配置
本地开发我用的是xpack.security.enabled=false关闭认证,这样调试方便。但生产环境如果ES裸奔在公网,等于把你的数据直接暴露给全网随便查,是非常严重的安全隐患。如果你用云服务商的托管ES,一般默认开了访问控制;如果是自建集群,至少要做到以下三步:
第一,在网络层面限制访问来源。防火墙只放开应用服务器和运维跳板机的IP到ES的9200端口,不要用0.0.0.0来源。
第二,开启ES内置安全认证。设置内置用户密码:
bin/elasticsearch-setup-passwords interactive然后Java客户端请求里带上用户名密码:
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "your_password"));第三,在ES配置里把xpack.security.enabled设置成 true,并且给Kibana的访问也配置认证,否则Kibana连不上自带认证的ES集群。
我在某个项目就因为图省事,内网ES只做了防火墙限制,没开启认证,后来安全扫描发现ES的9200端口能被外网直接访问,敏感业务数据直接通过search接口被拉走,好在数据量规模不大、没有造成实质性恶果,但这件事之后我对ES网络安全敏感度拉满,只要不是本地开发环境,一律强制开启认证。
7. 我在项目里沉淀下来的几点心得
从零开始搭这套ES搜索链路,前后踩坑踩了大半个月,最后把自己实际使用中的几条判断标准分享出来。
第一个心得是:ES的版本选择和时间点选择极其重要。我用的是7.14.2版本,这个版本兼顾了稳定性和新特性。8.x版本虽然升级了客户端实现,但对旧API做了大量调整,如果你们团队没有专门的搜索维护人员,升级成本不小。一开始就定死版本,然后所有依赖和插件都锁版本,不要去追新。
第二个心得是:能用Kibana Dev Tools解决的事,绝不用代码去试错。我在写Java代码之前,所有的查询DSL都是先在Kibana里敲一遍,确认返回结果和性能符合预期之后,才翻译成Java代码。这样一次成型率极高,几乎不会出现“代码写完查不到数据”的尴尬。
第三个心得是:做搜索功能,不要等到全部设计好了再开发。ES的Mapping虽然要提前规划,但业务需求变化很快,你永远无法保证第一版设计就是最合理的。我的做法是Mapping先定义核心字段,其他字段允许动态映射兜底上线,等搜索功能跑通后再逐步收紧。
第四个心得是关于“一致性”的。ES的索引更新是近实时的,默认1秒延迟,所以写完MySQL马上查ES,查不到是正常现象,不要认为是Bug。做数据同步时,我给ES的写入加上了一层缓冲重试机制,因为任何消息队列或者binlog方案都可能出现短暂延迟,ES短暂不可用时重试能让整个同步流程稳定不少。
最后说一下这个项目的演进方向。我现在已经在这个商品搜索的基础上增加了“搜索热词统计”和“个性化推荐权重”两个模块,底层用的还是ES的聚合能力。如果你的项目刚起步,照着这条链路做一套搜索功能是完全够用的;如果未来数据量真的到了需要大集群的程度,再考虑节点扩容和索引生命周期管理,那就是另一个阶段的故事了。