1. 为什么我们要把数据从Solr搬到Elasticsearch
搜索这块,很多团队早期选型的时候都是Solr先上。原因很简单:那会儿Solr的文档全、上手快、跟Lucene贴得近,运维也相对直观。但业务跑个两三年之后,问题就慢慢冒出来了——分片再平衡不够灵活、聚合分析能力偏弱、生态工具链跟不上、团队里新人更熟悉Elasticsearch那一套。于是“Solr集群数据迁移至Elasticsearch”就成了一个绕不过去的工程任务。
我先说清楚这个项目到底在做什么:它指的是把一个正在线上服务的Solr集群里的全量索引数据,加上迁移期间产生的增量数据,完整、准确、尽量不停机地搬到Elasticsearch集群里,并且保证迁移后搜索结果的召回率、排序逻辑、字段语义基本一致。这件事听起来就是“导数据”,但真正做过的人都知道,坑几乎全在细节里——字段类型映射、分词器差异、深分页、唯一键冲突、增量追平、双写切换,每一步都能让你加班到凌晨。
这篇文章适合谁看?如果你正面临Solr到Elasticsearch的迁移,或者你是个搜索方向的工程师、后端开发、运维,需要独立扛下这个迁移任务,那这篇内容基本可以当成一份实操手册来用。我会把方案选型、字段映射、全量+增量同步、数据校验、灰度切换这几个核心环节全部拆开讲,并且把我自己踩过的坑和验证过的参数都摆出来。全文基于常见的工程实践来写,具体数值你还要结合自己的数据量和集群规格去调。
先说一个总的原则:迁移不是复制粘贴,而是一次数据模型和查询语义的重新对齐。你如果只盯着“把文档数量对上”,那上线后一定会被业务方追着问“为什么搜出来的东西不一样了”。
2. 迁移方案的整体设计与选型思路
2.1 三种主流迁移路线的取舍
做Solr到Elasticsearch的迁移,市面上常见的路线无非三种,我把它们的适用场景和代价列出来,你可以直接对号入座。
| 迁移路线 | 核心做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 离线导出导入 | Solr导出JSON/CSV,再用Logstash或Bulk API灌入ES | 实现简单,可控性强 | 停机时间长,增量难处理 | 数据量小、可接受停机的内部系统 |
| 双写过渡 | 应用层同时写Solr和ES,历史数据离线迁 | 切换平滑,可回滚 | 改造成本高,需处理写失败 | 线上核心业务,不能停机 |
| 变更捕获同步 | 通过数据源binlog或消息队列追增量 | 对应用侵入小 | 链路长,延迟和顺序要处理 | 有统一数据管道的中大型系统 |
我个人的建议是:只要你的业务不能停,就走“全量离线迁移 + 增量追平 + 双写灰度切换”这条组合路线。纯离线方案适合做验证和预演,但真正上线必须解决增量问题,否则你全量导完的那一刻,数据就已经落后了。
这里有个关键判断点:你的增量数据从哪来?如果Solr的数据源头是MySQL这类关系库,那最稳的是从binlog或者业务消息队列里拿变更,而不是去监听Solr本身。因为Solr的增量能力(比如DataImportHandler的delta)在复杂场景下并不可靠,尤其是删除操作经常追不到。这一点我在后面增量章节会详细展开。
2.2 为什么不能直接照搬Solr的Schema
很多人第一反应是“我把Solr的schema.xml翻译成ES的mapping不就行了”。理论上可以,实际上会出大问题。Solr和Elasticsearch虽然都基于Lucene,但两者的字段模型、动态映射、分词链、多值字段处理方式差异很大。
举个最典型的例子:Solr里一个字段可以同时是indexed=true、stored=true、docValues=true,还能挂多个copyField。到了ES里,你要把它拆成text类型(用于全文检索)加keyword类型(用于聚合排序)的多字段(multi-field),或者干脆用fields子字段。如果你直接把Solr的string字段映射成ES的text,那聚合就会报fielddata相关的错误;反过来把需要分词的字段映射成keyword,那搜索就变成了精确匹配,召回率直接崩。
所以我的做法是:先梳理Solr的字段清单,按“用途”重新分类,而不是按“类型”翻译。分类维度就三个——全文检索字段、精确匹配/聚合字段、仅存储展示字段。分完类再决定ES的mapping,这样出来的结构才是干净的。
2.3 迁移成功的验收标准要先定死
在动手之前,一定要和业务方把验收标准白纸黑字定下来,否则最后扯皮没完。我一般会定这几条:
- 文档总数一致:允许有极小的误差,但要能解释清楚每一条差异的来源。
- 核心查询召回一致:抽取Top N高频查询词,对比迁移前后Top 100结果的交集比例,一般要求95%以上。
- 关键字段值一致:抽样比对文档的字段内容,尤其是ID、标题、时间、状态这类。
- 排序逻辑一致:如果原来有boost、function query,要确认ES里的等价实现。
- 性能不劣化:P99查询延迟不能比原来差太多,这个要压测。
提示:验收标准里最容易被忽略的是“删除数据”。Solr里已经删掉的文档,如果迁移时没同步删除,ES里就会多出来,导致搜索结果出现“幽灵数据”。这个必须在增量阶段专门处理。
3. 核心细节解析:字段映射与数据模型对齐
3.1 字段类型映射的对照关系
这一块是迁移的地基,我整理了一份常用的对照表,你可以直接拿去改。
| Solr字段定义 | Elasticsearch映射 | 说明 |
|---|---|---|
| string (indexed, stored) | text + keyword子字段 | 全文检索用text,聚合排序用keyword |
| string (indexed=false) | keyword (index=false) | 仅存储,不参与检索 |
| int / long / float | integer / long / float | 数值类型直接对应 |
| date | date | 注意format要显式指定 |
| boolean | boolean | 直接对应 |
| multivalued=true | 数组类型 | ES天然支持数组,无需特殊声明 |
| copyField | copy_to | 语义接近,但要注意目标字段类型 |
这里有个细节:Solr的date字段默认格式和ES不完全一样。ES的date默认能识别ISO 8601,但如果你的Solr里存的是时间戳或者自定义格式,一定要在mapping里写清楚format,否则导入时会大量报mapper_parsing_exception。我见过一次因为时间格式没对齐,导致整个批次导入失败,排查了半天才发现是Solr存的是yyyy-MM-dd HH:mm:ss而ES默认不认这个。
3.2 分词器的对齐是召回率的关键
Solr常用的分词器有StandardTokenizer、IKAnalyzer、SmartChineseAnalyzer等,ES这边对应的是standard、ik_max_word/ik_smart等。如果你两边用的分词器不一样,那同样的查询词切出来的term就不同,召回结果必然有差异。
我的经验是:迁移时尽量让ES的分词器和Solr保持一致。如果Solr用的是IK,那ES也装IK插件,并且把analyzer和search_analyzer都配成对应的模式。如果实在无法一致,那就要做召回对比测试,把差异大的查询词挑出来,看是否需要调整查询侧的改写逻辑。
还有一个容易忽略的点:Solr的omitNorms、termVectors这些参数在ES里没有直接对应,如果你原来依赖这些做打分,迁移后排序会变。这种情况要么在ES里用similarity自定义,要么在查询层用function score补回来。
3.3 唯一键与去重策略
Solr里通常用uniqueKey来标识文档,ES里对应的是_id。迁移时一定要把Solr的uniqueKey映射成ES的_id,这样才能保证重复导入时是覆盖而不是新增。
但这里有个坑:如果你的Solr uniqueKey是复合字段拼出来的,那ES的_id也得用同样的拼接规则,否则去重就失效了。我一般会在迁移脚本里显式构造_id,而不是让ES自动生成。自动生成的ID虽然省事,但一旦重跑迁移任务,就会产生大量重复文档,清理起来非常痛苦。
注意:ES的
_id长度是有限制的,虽然官方说最大512字节,但实际使用中建议控制在几十字节以内。如果你的uniqueKey很长,考虑做一次哈希,但要保证哈希不冲突。
4. 全量迁移的实操过程与关键环节
4.1 数据导出的方式选择
从Solr导出全量数据,常见的有几种方式:Solr的/select接口分页拉取、/export接口(走docValues,速度快)、或者直接读Solr的索引文件。我推荐用/export接口,前提是你的字段都开了docValues,它的吞吐比普通分页查询高一个数量级。
如果字段没开docValues,那就只能用游标(cursorMark)分页。这里千万不能用start+rows做深分页,Solr在深分页时性能会急剧下降,而且可能因为索引变化导致漏数据或重复。游标分页是官方推荐的稳定方案,用法是每次带上上一次返回的nextCursorMark。
导出的时候我一般会写成流式处理:一边从Solr读,一边批量往ES写,中间不落盘。这样内存占用可控,也不会有中间文件清理的问题。批大小我一般设500到1000条,太大容易触发ES的bulk超时,太小则网络往返开销大。
4.2 批量写入ES的参数调优
往ES灌数据,核心就是用Bulk API。但Bulk的参数不是随便设的,我列几个关键点:
- 批量大小:单次bulk建议控制在5MB到15MB之间。太小吞吐上不去,太大容易导致节点内存压力。
- 并发度:用多线程并发写,但线程数不要超过ES数据节点的数量太多,一般设成数据节点数的2到4倍比较稳。
- 刷新间隔:迁移期间把
refresh_interval设成-1,迁完再改回来。这样能大幅提升写入速度,因为避免了频繁的segment刷新。 - 副本数:迁移期间可以把副本数临时设为0,迁完再调回。这能减少写入时的复制开销。
- translog:如果对迁移期间的持久性要求不高,可以把translog的durability设成async,进一步提升写入性能。
这些参数我在一次千万级文档的迁移里实测过,调整前后写入速度差了将近3倍。但要注意,这些优化是以牺牲部分实时性和容错性为代价的,迁移完成后必须全部改回生产配置。
4.3 迁移过程中的进度监控
全量迁移动辄几个小时甚至几天,没有监控就是盲人摸象。我一般会做三层监控:
第一层是总量进度,记录已迁移文档数和总文档数的比例,这个可以从Solr的/select?q=*:*&rows=0拿到总数,再和ES的_count对比。
第二层是速率监控,看每秒写入多少条,如果速率突然掉下来,说明可能遇到了热点或者ES在合并segment。
第三层是错误监控,Bulk请求会返回每个文档的成功失败状态,失败的文档要单独收集起来,分析原因后重试。常见的失败原因有字段类型不匹配、_id冲突、mapping未定义等。
提示:Bulk API返回的
errors: true不代表整批失败,而是部分文档失败。一定要逐条解析返回结果,把失败的文档挑出来,否则你会以为迁移成功了,实际上丢了一批数据。
5. 增量同步与双写切换的实现
5.1 增量数据的捕获方式
全量迁移只是第一步,真正难的是增量。前面说过,增量最好从数据源头拿,而不是从Solr拿。如果你的数据源是MySQL,那用binlog是最干净的方案,因为增删改都能捕获到,而且顺序有保证。
如果数据源本身就是通过消息队列写入Solr的,那更简单,直接让ES也订阅同一个topic就行。但要注意消息的顺序问题,如果同一个文档的多次变更被乱序消费,最终状态可能不对。解决办法是用文档ID做分区键,保证同一文档的变更进同一个分区,这样顺序就有保障了。
如果实在只能从Solr侧拿增量,那就要用Solr的last_modified字段做时间窗口轮询。但这种方式有两个硬伤:一是删除操作捕获不到,二是时间窗口边界容易漏数据。所以用这种方式的话,必须配合定期的全量对账来兜底。
5.2 双写阶段的流量控制
双写不是简单地在代码里加一行写ES就完事了。你要考虑几个问题:
- 写失败怎么办:如果ES写失败,是阻塞主流程还是降级?我的建议是降级,记录失败日志,后续补偿,不能因为ES的问题影响Solr的正常写入。
- 写顺序:先写Solr还是先写ES?一般先写Solr,因为它是当前的主服务,ES是备选。等切换完成后反过来。
- 性能影响:双写会让写入延迟增加,要评估这个增量是否可接受。如果ES写入慢,可以考虑异步化,用队列解耦。
双写阶段一般持续一到两周,期间要持续做数据对账,确认ES的数据和Solr基本一致,才能进入切换。
5.3 灰度切换与回滚预案
切换不是一刀切,而是灰度。我的做法是:
- 先切读流量的1%,观察错误率和延迟。
- 没问题后逐步放大到10%、50%、100%。
- 每一步都保留快速回滚的能力,回滚就是把读流量切回Solr。
回滚预案一定要提前演练,不能等到出问题才临时想。我见过一个团队切换时没准备回滚方案,结果ES查询延迟飙升,业务方投诉,最后只能紧急停服回滚,影响很大。
注意:灰度期间Solr和ES的数据必须保持双写,否则回滚后Solr的数据是旧的,会出现数据不一致。所以双写要一直持续到灰度完全结束、确认稳定后再停。
6. 数据校验与常见问题排查
6.1 数据一致性校验的三种手段
迁移做完,怎么证明数据是对的?我一般用三种手段交叉验证:
第一种是数量校验,对比Solr和ES的文档总数。这个最快,但只能发现大问题。
第二种是抽样校验,随机抽一批文档,逐字段对比内容。这个能发现字段映射和值转换的问题。
第三种是查询校验,用真实的高频查询词,对比两边返回的Top N结果。这个最能反映业务体感。
三种手段结合,基本能把问题覆盖住。数量校验发现漏数据,抽样校验发现字段错误,查询校验发现分词和排序问题。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 导入报mapper_parsing_exception | 字段类型或格式不匹配 | 看报错字段名和值 | 调整mapping或转换数据格式 |
| 文档数量对不上 | 漏导、重复、删除未同步 | 对比ID集合 | 补导差异、处理删除 |
| 搜索结果变少 | 分词器不一致 | 对比analyze结果 | 统一分词器或改写查询 |
| 聚合报fielddata错误 | text字段做聚合 | 看字段类型 | 改用keyword子字段 |
| 写入速度慢 | 刷新频繁、副本多 | 看ES监控 | 临时调refresh_interval和副本数 |
| 深分页超时 | 用了from+size | 看查询语句 | 改用search_after |
| 增量延迟大 | 消费慢、批量小 | 看队列积压 | 提高并发和批量大小 |
6.3 我踩过的几个坑
第一个坑是时间字段的时区问题。Solr存的是UTC,ES默认也按UTC解析,但展示层如果按本地时区处理,就会出现时间偏移。这个要在迁移前就统一约定好,别等到上线才发现。
第二个坑是多值字段的顺序。Solr的多值字段是有序的,ES的数组虽然也保留顺序,但在某些查询和聚合场景下顺序不保证。如果你的业务依赖多值字段的顺序,要特别小心。
第三个坑是删除操作的同步。前面提过,从Solr侧拿增量捕获不到删除。我的解决办法是在业务层做软删除,用一个is_deleted字段标记,迁移时把这个字段也带过去,查询时过滤掉。这样即使物理删除捕获不到,逻辑删除也能同步。
第四个坑是ID冲突。如果Solr的uniqueKey在ES里不是唯一的,导入时就会互相覆盖。这个要在迁移前做一次唯一性校验,把冲突的ID找出来处理掉。
7. 迁移后的性能调优与长期维护
7.1 索引层面的调优
迁移完成后,ES的索引配置要针对查询场景重新调优。几个关键点:
- 分片数:根据数据量和节点数来定,一般单分片控制在30GB到50GB。分片太多会增加集群管理开销,太少则无法充分利用并行能力。
- 副本数:生产环境至少1个副本,保证高可用。
- refresh_interval:根据实时性要求设置,一般1秒到30秒。如果对实时性要求不高,可以设长一点,减少segment合并压力。
- merge策略:默认的TieredMergePolicy对大多数场景够用,如果写入量大可以调大
max_merged_segment。
7.2 查询层面的适配
Solr的查询语法和ES的DSL差异不小,迁移后查询层基本都要改。常见的转换包括:
- Solr的
q=field:value对应ES的match或term查询。 - Solr的
fq对应ES的filter上下文。 - Solr的
boost对应ES的boost或function_score。 - Solr的
facet对应ES的aggregation。
这些转换不是一对一的,尤其是复杂的facet和function query,需要仔细对照。我建议在迁移前就把核心查询列出来,逐个做等价改写和结果对比。
7.3 长期对账机制
迁移不是一锤子买卖,上线后还要有长期的对账机制。我一般会保留一个定时任务,每天抽样对比Solr和ES的数据,发现不一致就告警。等确认ES完全稳定、Solr下线后,这个任务再撤掉。
这个对账机制在灰度期间尤其重要,它能帮你及时发现双写遗漏或者增量延迟的问题。我见过一个案例,双写时因为某个异常分支没写ES,导致部分文档缺失,靠对账任务才发现的。
8. 一些实操心得和收尾建议
整个迁移做下来,我最大的体会是:技术方案本身不难,难的是细节的完整性和过程的严谨性。Solr到ES的迁移,本质上是一次数据管道的重建,任何一个环节的疏忽都会在最终结果上放大。
如果让我给正在做这件事的人几条建议,我会说:第一,先做小规模预演,拿一个子集跑通全流程,把坑都踩一遍再上全量;第二,验收标准一定要量化,别用“差不多”这种词;第三,回滚预案必须提前准备好并且演练过;第四,双写和对账要持续到确认稳定为止,不要急着下线Solr。
还有一个小技巧:迁移期间可以给ES的索引加一个migration_batch字段,记录每条数据是哪一批迁过来的。这样一旦发现某批数据有问题,可以精准定位和重跑,不用全量重来。这个字段在迁移完成后可以删掉,但在迁移期间非常有用。
最后说一句,迁移完成后别急着庆祝,先观察一周的查询监控和错误日志。很多问题不是迁移当下暴露的,而是业务流量上来之后才显现。稳一周,再考虑把Solr集群缩容或者下线。