同样 15,000 条重规则,Percolate Query 比 Easysearch 慢 21.8 倍 —— Heavy-OR 场景实测
INFINI Labs 小助手 发表了文章 • 0 个评论 • 4136 次浏览 • 6 天前
15,000 条 heavy-OR 规则,200,000 条文档,同一台机器:Easysearch 在线规则引擎全流程 11.68 秒,Percolate Query 仅搜索阶段就跑了 254.30 秒——慢了 21.8 倍。
在"规则先存、文档后到"这类场景下,Percolate Query 的延迟会随规则数量和复杂度的增长快速恶化。规则涨到数千条后,每批文档匹配的耗时可以从秒级攀升至几分钟。这类问题换索引参数、调批次大小、精简 DSL,都治标不治本,根子在执行模型本身。
本文通过一组 heavy-OR 基准测试,量化两种方案的实际差距。
测试配置
测试在同一台主机上运行,使用同一套规则文本和文档样本。Percolate Query 的查询条件由相同规则翻译而来,保证两侧规则语义一致。
| 参数 | 值 |
| :------------- | ------------------------: |
| 规则总数 | 15,000 |
| 文档总数 | 200,000 |
| 批次大小 | 10,000 / 批 |
| 重规则数量 | 2,500 条大 OR 热点规则 |
| 单条大 OR 规模 | 随机 50 ~ 500 个 OR 条件 |
测试结果
| 路径 | 用时 |
| :------------------------- | ------------: |
| 纯写入 plain_bulk | 6.025535s |
| 在线规则引擎 rules_only | 11.684568s |
| Percolate Query 搜索阶段 | 254.304583s |
同样 15,000 条规则 + 200,000 条文档
具体指标:
- Easysearch 在线规则引擎全流程:`11.68s`
- Percolate Query 搜索阶段:`254.30s`
- 差值:`242.62s`
- 倍数:`21.76 倍`
- 每批(10,000 文档)平均耗时:Easysearch 约 `0.49s`,Percolate Query 约 `12.69s`
## 开启规则引擎的增量成本
规则匹配会对写入链路产生多少额外开销,是评估在线规则引擎可行性的重要指标之一。
开启规则引擎的写入增量
与之对比,Percolate Query 仅搜索阶段就需要 `254.30s`。换言之,Easysearch 在线规则引擎把规则匹配叠加进写入流程,新增成本约为 Percolate Query 搜索耗时的 **1/44.9**。
## 只看匹配引擎本体
上一组数据(11.68s vs 254.30s)包含了 Easysearch 的在线写入、bulk 解析和索引处理等通用开销。为了单独衡量规则匹配引擎自身的性能,我们用 Java 直调 JNI 做了一次离线 match,绕过写入链路,只跑规则匹配逻辑。
| 路径 | 用时 |
| :---------------------------- | ------------: |
| Easysearch 纯匹配(JNI 离线) | `5.046934s` |
| Percolate Query 搜索阶段 | `254.304583s` |
只比匹配本身
这组数据说明两点:Easysearch 的性能优势并非来自写入链路的整合效率,即便剔除通用写入成本,规则匹配引擎本体与 Percolate Query 之间依然存在约 50 倍的差距。
## 为什么 Percolate Query 会慢
根因在执行模型,OR 条件多只是放大器。
每批文档到达时,Percolate Query 都要走完这套流程:
1. 把文档放进临时内存索引
2. 基于规则中的 terms 筛选候选规则
3. 对候选规则逐条验证
以本次测试为例,各阶段耗时分布如下:
- 规则翻译:`9.560294s`
- 规则导入:`7.451857s`
- percolate 搜索:`254.304583s`
搜索阶段是每批文档都必须重新支付的代价。
Heavy-OR 规则在这套流程里两头放大:规则覆盖面广,候选集更难剪掉;单条规则条件多,逐条验证也更重。
Easysearch 规则引擎把规则提前编译好,文档到达后直接匹配,不走这套每批重建的流程,差距就在这里。
---
## 适用场景
以下场景对规则匹配的吞吐和延迟要求较高,是 Easysearch 在线规则引擎的典型适用范围:
- **内容审核**:规则持续增长且复杂度高,需要稳定的处理吞吐,对单批延迟敏感。
- **舆情监测**:热点词、别名、邻近词组合多,规则天然形成大 OR 结构,是 Percolate Query 最容易触及性能瓶颈的场景。
- **广告定向**:人群包条件不断叠加,文档流量高,规则匹配需要足够轻量,避免影响整条投放链路。
- **告警规则**:延迟直接影响告警有效性,规则命中需要尽量贴近文档写入时刻。
- **实时反欺诈**:规则复杂、变更频繁、吞吐高,要求文档到达后立即完成判断。
## 小结
在本次 heavy-OR 基准测试中:
- 相同规则集(15,000 条)和文档量(200,000 条),Easysearch 在线规则引擎全流程耗时 **11.68s**,Percolate Query 仅搜索阶段耗时 **254.30s**,相差 **21.8 倍**。
- 开启规则引擎带来的写入链路增量成本为 **5.66s**,约为 Percolate Query 搜索阶段耗时的 **1/44.9**。
- 剔除写入通用开销后,规则匹配引擎本体的差距约为 **50 倍**。
如果你的业务已经有 Percolate Query 延迟随规则增长持续上升的问题,不用看 demo 数据——把你线上最重的那批规则拿出来,跑一次就知道差距在哪。
规则引擎功能当前需要试用 License。你可以先下载 Easysearch:<https://infinilabs.cn/download>,再联系售前申请试用 License 并获取开通指引。
## 关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
官网文档:<https://docs.infinilabs.com/easysearch>
> 作者:张磊,极限科技(INFINI Labs)搜索引擎研发负责人,对 Elasticsearch 和 Lucene 源码比较熟悉,目前主要负责公司的 Easysearch 产品的研发以及客户服务工作。
---
相关文章:
- [Easysearch ZSTD 基准测试:高压缩率下实现近 5 倍查询吞吐](https://infinilabs.cn/blog/202 ... ntage/)
- [Easysearch 2.0.0 性能测试](https://infinilabs.cn/blog/202 ... ments/)
- [Easysearch 时序数据的基于时间范围的合并策略](https://infinilabs.cn/blog/202 ... earch/)
- [Easysearch Rollup 相比 OpenSearch Rollup 的优势分析](https://infinilabs.cn/blog/202 ... ollup/)
- [Easysearch Rollup 使用指南](https://infinilabs.cn/blog/202 ... ollup/)
Easysearch BKD Merge 异常排查实录:最终定位到旧版 GraalVM JIT 运行时
INFINI Labs 小助手 发表了文章 • 0 个评论 • 7055 次浏览 • 2026-04-10 17:51
最近一次高并发写入压测中,我们遇到了一个非常诡异的 BKD merge 崩溃。从报错看,很像 Easysearch 2.1.2 在 merge 阶段把 segment 读成了错误状态。典型错误是这样的:
text<br /> java.lang.ArrayIndexOutOfBoundsException: Index -3 out of bounds for length 8<br /> java.lang.ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8<br />
异常栈最终落在 Lucene BKD 相关路径上:
BKDReader.readNodeData()BKDWriter.merge()Lucene90PointsWriter.merge()
如果只看栈,很容易把问题归到 Easysearch 的 BKD merge 逻辑。但排查到最后,结论恰恰相反。
问题不在 Easysearch 的代码,而在 JDK 运行时。 更精确地说,是某个特定Oracle GraalVM 21构建中的JVMCI/Graal JIT路径,把 Lucene BKD 的热点代码执行错了。
1、为什么这个问题难查
它有几个特别迷惑人的特征:
- 只在高并发写入压测下触发
- 服务重启后的前几轮最容易复现
- 同一进程里,删了索引重新压,后面复现率反而下降
- 不是固定字段,多个数字类型字段都中过招
ZSTD和best_compression两种 codec 下都能复现
实际命中过的字段包括@timestamp、size、status、_seq_no。所以这不是某个字段、某种 codec 或某个 mapping 的偶发问题。
2、第一层排除:merge reader 不是第一现场
一开始我们确实怀疑 merge reader,毕竟异常直接出现在 merge 路径上。但日志顺序很快给出了相反的证据。在 merge 真正崩溃之前,source segment 已经先出现了这些异常信号:
point-sort-restore-multiple-zero-ordssource-write-point-doc-mismatchpointCount > docCountpack-index-negative-codereader-invalid-start-pos- 最后才是
ArrayIndexOutOfBoundsException
这意味着两件事:merge reader 不是第一现场,source segment 在写出阶段就已经坏了。merge reader 只是读到了已经损坏的 BKD index,并在那个阶段暴露了异常。
3、第二层排除:Easysearch 自己的 BKD 写入逻辑也没有先出错
继续往前追溯,我们发现问题比OneDimensionBKDWriter.add()还要早。真正的异常出现在排序/回填链路上:
PointValuesWriterMutablePointTreeReaderUtils.sort()StableMSBRadixSorter
关键证据来自两个探针:
point-sort-restore-multiple-zero-ordsunwrittenSlotCount == source-write-point-doc-mismatch delta
这说明在某次排序/回填过程中,有一部分槽位根本没有被写入,默认值0被 restore 回填到ords[],再通过docIDs[0]放大成大量docID=0,最终导致pointCount > docCount,source segment 进入错误状态。
到这一步,排查重点已经不是“Easysearch 的 BKD merge 逻辑存在缺陷”,而是 Lucene points 排序链路的执行结果和源码语义不一致。
4、真正的转折点:抓到了
reorder()自身的 coverage 异常
真正把方向扭转过来的,不是又一次复现,而是一个更早的探针:
point-sort-reorder-coverage-mismatch
这个探针验证的是:StableMSBRadixSorter.reorder()是否真的按源码应有的次数完整执行。
我们抓到的典型样本之一如下:
targetSegment=_xfield=statusk=7expectedLoopCount=9800actualIterationCount=8204firstCoverageMismatchBucket=201firstCoverageExpected=9788firstCoverageActual=8192
更关键的是,同一条日志里还带出了这个信息:
text<br /> skippedSourceSamples=[201:[{ord=8192,bucket=201,docID=9090,byteAtK=200}, ...]]<br />
这条信息非常重要,因为它说明:bucket201理论上应该处理9788条,实际只处理了前8192条,但从ord=8192往后的样本,读出来仍然还是bucket=201。这直接推翻了“后半段数据被污染后改桶”的旧解释,指向了一个更直接的结论:reorder()自己的 coverage 被截断了。
另一个样本中出现了同类边界:firstCoverageExpected=31822,firstCoverageActual=16384。
到这里,一个很不自然的特征浮现出来:8192、16384——这些明显的 2 的幂边界,更像是运行时或 JIT 执行异常,而不是普通业务逻辑 bug。
5、哪段代码最可疑
此时怀疑对象已经不是泛泛的“BKD 整体有问题”,而是 Lucene 中的这段热点循环:
java<br /> for (int i = 0; i < HISTOGRAM_SIZE; ++i) {<br /> final int limit = endOffsets[i];<br /> for (int h1 = fixedStartOffsets[i]; h1 < limit; h1++) {<br /> final int b = getBucket(from + h1, k);<br /> final int h2 = startOffsets[b]++;<br /> save(from + h1, from + h2);<br /> }<br /> }<br /> restore(from, to);<br />
代码位于org.apache.lucene.util.StableMSBRadixSorter#reorder(...)。
按源码语义,这段代码应该完整扫描每个 bucket 的范围,并最终把全部结果 restore 回去。但我们抓到的事实是:expectedLoopCount != actualIterationCount,某些 bucket 只跑到8192/16384就停了,随后出现未写槽位,restore 把默认0回填,最终 source segment 进入错误状态。
如果这是 Java 源码本身的稳定逻辑 bug,它在解释执行时也应该稳定触发,而不应该强烈依赖某个 JDK/JIT 组合。后面的 JVM 对照实验基本排除了这个可能性。
6、最强证据:只换 JDK / JIT 路径,结果就变了
这次排查中最有说服力的,不是某一条日志,而是对照实验。
基线组:旧版 Oracle GraalVM 21,默认 JVMCI/Graal JIT
环境:
Oracle GraalVM 21+35.121+35-jvmci-23.1-b15Linux aarch64 / ARM64UseJVMCICompiler = true
结果:很快复现,命中了point-sort-reorder-coverage-mismatch、point-sort-reorder-underfilled、point-sort-restore-multiple-zero-ords,随后 merge 报ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8。
对照组:关闭 JVMCI/Graal JIT 或纯解释执行
只改 JVM 参数,不改代码和压测口径:
-XX:-UseJVMCICompiler-Xint
结果一致:都没有再出现上述探针和异常。
这三组对照的意义很直接:如果这是 Easysearch 或 Lucene 的纯 Java 逻辑 bug,解释执行也应该能稳定复现。但现实是基线组复现,关闭 JVMCI 和纯解释执行都不复现。问题显然高度依赖 JIT 路径。
版本对照:较新的 GraalVM 21 构建在当前测试中未复现
这里需要补充一条重要的边界条件。我们后来又测试了一个较新的 GraalVM 版本:
text<br /> java version "21.0.9" 2025-10-21 LTS<br /> Java(TM) SE Runtime Environment Oracle GraalVM 21.0.9+7.1 (build 21.0.9+7-LTS-jvmci-23.1-b79)<br />
在当前压测中,这个版本没有再出现 merge 错误。
因此结论必须写得更精确:已知会复现的是较早的21+35-jvmci-23.1-b15,已知在当前测试中未复现的是较新的21.0.9+7-LTS-jvmci-23.1-b79。更准确的工程判断不是“GraalVM 21 整体都有问题”,而是某个特定 GraalVM 21 构建有问题,较新的构建很可能已经修复或规避了该问题。这里仍需保持严谨:只能说“在当前压测中未复现”,还不能直接说“已经被完整证明没有问题”。
平台边界:不能写成 ARM 专属
除了前面详细展开的Linux aarch64 / ARM64主要实验环境外,有用户反馈在以下环境中也出现过同类问题:
- 操作系统:
openEuler - 内核:
4.19.90-2112.8.0.0131.oe1.x86_64 - 架构:
x86_64
这是用户的测试环境,不是我们能够独立完整复现并逐项展开的。但这条信息已经足够说明:当前不能把问题简单写成“ARM 平台专属”。更准确的说法是:我们在ARM64上系统性复现并完成了主要对照实验,另外也有openEuler x86_64测试环境的同类现象反馈,因此平台边界目前还没有被完全钉死。
7、更强的同机对照:换成 Oracle HotSpot 21.0.10 后,全量写入跑完也没有问题
为了进一步排除“是不是所有 Java 21 都会这样”,我们在同一台服务器上把/infini/easysearch/jdk从Oracle GraalVM 21换成普通Oracle HotSpot 21.0.10,恢复默认 JVM 参数,用同样的写入压测继续验证。
其中一轮的结果很有说服力:
- 索引:
nginx_zstd3_40mt4 - codec:
ZSTD threads=16bulk_size=1000target_docs=181463624
最终after_count=181463624,delta_written=181463624,全量文档写入完成,服务端没有出现任何 BKD merge 错误。
这条结果至少说明:同一台机器、同一套 Easysearch、同样的数据规模和写入模型,只要把 JDK 从Oracle GraalVM 21换成Oracle HotSpot 21.0.10,问题就不再出现。
到这一步,工程判断已经比较清晰了:不是 Easysearch 自身逻辑导致,也不是所有 Oracle JDK 21 都会出错,更像是特定Oracle GraalVM 21构建相关的 JVMCI/Graal JIT 路径问题。
8、最关键的外部对照:Elasticsearch 8.19.5 也复现了
如果说前面的结论还能被质疑为“Easysearch 某些实现差异触发的”,那么后面的外部对照基本排除了这个方向。
我们在同一台服务器上部署了Elasticsearch 8.19.5(Lucene 9.12.2),JDK 也切到相同的Oracle GraalVM 21,执行同类写入压测。结果 Elasticsearch 也复现了同样的 BKD merge 崩溃。
关键异常完全一致:
text<br /> java.lang.ArrayIndexOutOfBoundsException: Index -4 out of bounds for length 8<br />
栈也一样落在BKDReader.readNodeData、BKDWriter$MergeReader.collectNextLeaf、BKDWriter$MergeReader.next。
这条证据的力度很强:不是 Easysearch 独有的问题,不是当前这套 Lucene 代码路径独有的问题,Elasticsearch 8.19.5 + Lucene 9.12.2在同类 GraalVM 21 环境下也会出现同类异常。到这一步,再把问题归因于 Easysearch 本身的代码逻辑,已经缺乏依据了。
9、这次排查最终说明了什么
把整条证据链串起来,当前阶段的结论已经比较清楚。
已验证的事实:
- 问题不是 merge reader 先制造坏数据,source segment 在更早阶段就已经进入错误状态
- 不是单字段问题,也不是
ZSTD或best_compression专属 - 已抓到
StableMSBRadixSorter.reorder()自身的 coverage 异常 - 关闭
UseJVMCICompiler后问题不复现,-Xint下也不复现 - 同机切到
Oracle HotSpot 21.0.10后,Easysearch 全量写入跑完未见 BKD merge 异常 Elasticsearch 8.19.5 + Lucene 9.12.2在同类 GraalVM 21 环境下也复现- 较新的
21.0.9+7-LTS-jvmci-23.1-b79在当前压测中未复现 - 某用户的
openEuler x86_64测试环境中也出现过同类错误,因此不能写成 ARM 专属
工程结论:
从工程证据来看,Easysearch 本身的代码逻辑没有问题。
当前最符合事实的结论是:问题高度相关于特定Oracle GraalVM 21构建,更具体地,是该构建相关的 JVMCI/Graal JIT 路径。它把 Lucene BKD 相关热点代码执行到了错误状态。已知较早构建21+35-jvmci-23.1-b15可复现,已知较新的21.0.9+7-LTS-jvmci-23.1-b79在当前测试中未复现。平台边界目前尚未完全钉死,不能再简单写成仅限ARM64。
换句话说,这不是“Easysearch 的 BKD merge 实现有 bug”,而是特定 JDK/JIT 运行时把本来正确的 Lucene BKD 代码执行错了。
10、建议版本与规避方案
如果你在生产或测试环境中运行 Easysearch 或 Elasticsearch,并且使用的是某些Oracle GraalVM 21构建,且启用了默认的 JVMCI/Graal JIT,那么在高并发写入、频繁 merge、BKD 热点路径被充分打热的场景下,需要特别警惕这类问题。
现阶段比较明确的建议是:
- 避免继续使用已经验证可复现的旧版构建:
Oracle GraalVM 21+35.1或21+35-jvmci-23.1-b15 - 优先升级到当前测试中未复现的版本:
Oracle GraalVM 21.0.9+7.1(即21.0.9+7-LTS-jvmci-23.1-b79) - 如果短期内不方便升级 GraalVM,直接切换到普通
Oracle HotSpot 21.0.10
直接落到版本号上会更清晰:
- 避免继续使用已经验证可复现的旧版构建:
- 已确认应避开:
21+35-jvmci-23.1-b15 - 当前更推荐:
21.0.9+7-LTS-jvmci-23.1-b79
原因很简单:前者我们已经复现了,后者在当前压测中没有复现。当然,这里的“推荐”是基于当前测试结果,不代表上游已经正式确认该问题已被修复。
11、最后
这次排查最大的价值,不是“又复现了一次 BKD merge 崩溃”,而是把一个看起来像 Easysearch 代码 bug 的现象,收敛成了一个有明确边界的运行时问题。
它至少说明两件事:
- 栈顶报错的位置不一定是真正的第一现场
- 真正有说服力的不是猜测,而是对照实验
这次结论之所以成立,不是因为主观判断,而是因为我们已经拿到了足够强的工程证据:同机 HotSpot 不复现,关闭 JVMCI 不复现,解释执行不复现,Elasticsearch 也复现,较新的 GraalVM 21.0.9+7.1 在当前测试中未复现,且某用户的 openEuler x86_64 测试环境也出现过同类错误。
所以,这一次,问题确实不在 Easysearch,而在特定版本的 JDK/JIT 运行时。
---
关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
官网文档:<https://docs.infinilabs.com/easysearch>
作者:张磊,极限科技(INFINI Labs)搜索引擎研发负责人,对 Elasticsearch 和 Lucene 源码比较熟悉,目前主要负责公司的 Easysearch 产品的研发以及客户服务工作。
---
相关文章:
- [Easysearch 国产替代 Elasticsearch:8 大核心问题解读](https://infinilabs.cn/blog/202 ... -8-qa/)
- [Elasticsearch VS Easysearch 性能测试](https://infinilabs.cn/blog/202 ... sting/)
- [从 Elastic 迁移到 Easysearch 指引](https://infinilabs.cn/blog/202 ... earch/)
- [Elasticsearch 磁盘空间异常:一次成功的故障排除案例分享](https://infinilabs.cn/blog/202 ... ormal/)
- [Easysearch、Elasticsearch、Amazon OpenSearch 快照兼容对比](https://infinilabs.cn/blog/202 ... earch/)
INFINI Labs 产品更新 | Easysearch 2.1.0 新增高性能 Rules 规则引擎插件,数据探索 Discover 等
INFINI Labs 小助手 发表了文章 • 0 个评论 • 8951 次浏览 • 2026-03-17 16:02

INFINI Easysearch v2.1.0 发布:新增 Rules 规则引擎(百万级规则、复杂表达式、自动同步恢复)与 形态学分析插件(俄语/英语词形还原,提升搜索召回率);审计日志支持动态用户审计,UI 新增日志查看、配置及数据探索页面,运维更高效。INFINI Console、Gateway、Agent、Loadgen v1.30.3 统一基于 [Framework](https://docs.infinilabs.com/framework/) 升级,优化本地磁盘队列数据消费。详情见 Release Notes。
Easysearch v2.1.0
INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
Easysearch 本次更新如下:
🚀 功能特性 (Features)
- 新增 Rules 规则引擎插件,提供高性能的规则匹配能力
- 支持 linux-x64 和 linux-aarch64 架构
- 支持 Ingest Pipeline 集成,数据写入时自动匹配规则并添加标签
- 支持复杂的规则表达式(AND/OR/NOT、near、正则、数值范围等)
- 支持百万级规则库,匹配性能是传统方案的上百倍。
- 支持多节点集群自动同步和广播编译规则
- 节点启动时自动同步缺失的规则库
- 规则库同步期间自动保护写入,确保规则完整性
- 本地元数据文件持久化记录编译历史,支持规则库文件丢失后的自动恢复
- 支持 linux-x64 和 linux-aarch64 架构
- 新增形态学分析插件(analysis-morphology),支持俄语和英语的形态分析
- 精准还原:基于词典将动词时态、名词格位等还原为标准原型(如 went → go)
- 词元扩展:同时索引原词与关联词根(如runner → runner, run),实现智能搜索匹配
- 高召回率:解决俄语复杂的变格与变位搜索难题,确保不同语法形式下均能精准检索
- 精准还原:基于词典将动词时态、名词格位等还原为标准原型(如 went → go)
- 审计日志新增动态指定用户进行审计的功能
- UI 插件新增如下能力
- 支持审计日志在线查看
- 支持审计日志模块动态配置
- 新增数据探索页面


✈️ 改进优化 (Improvements)
- 支持审计日志在线查看
- 将“结巴”分词插件日志迁移至 Log4J,并降低周期性任务的日志级别以减少冗余
🐛 问题修复(Bug Fixes)
- 修复少量 UI 界面操作问题



Console v1.30.3
INFINI Console 是一款开源的非常轻量级的多集群、跨版本的搜索基础设施统一管控平台。通过对流行的搜索引擎基础设施进行跨版本、多集群的集中纳管,企业可以快速方便的统一管理企业内部的不同版本的多套搜索集群。
Console 本次详细更新记录如下:
✈️ 改进优化 (Improvements)
- 此版本包含了底层 [Framework](https://docs.infinilabs.com/framework/) 的更新,解决了一些常见问题,并增强了整体稳定性和性能。虽然 Console 本身没有直接的变更,但从 Framework 中继承的改进间接地使 Console 受益。
🐛 问题修复(Bug Fixes)
- 修复 Agent 关联不成功问题
Gateway v1.30.3
INFINI Gateway 是一个开源的面向搜索场景的高性能数据网关,所有请求都经过网关处理后再转发到后端的搜索业务集群。基于 INFINI Gateway 可以实现索引级别的限速限流、常见查询的缓存加速、查询请求的审计、查询结果的动态修改等等。
Gateway 本次更新如下:
✈️ 改进优化 (Improvements)
- 此版本包含了底层 [Framework](https://docs.infinilabs.com/framework/) 的更新,解决了一些常见问题,并增强了整体稳定性和性能。虽然 Gateway 本身没有直接的变更,但从 Framework 中继承的改进间接地使 Gateway 受益。
Agent v1.30.3
INFINI Agent 负责采集和上传 Elasticsearch, Easysearch, Opensearch 集群的日志和指标信息,通过 INFINI Console 管理,支持主流操作系统和平台,安装包轻量且无任何外部依赖,可以快速方便地安装。
Agent 本次更新如下:
🚀 功能特性 (Features)
- 在 Kubernetes 环境下通过环境变量 http.port 探测 Easysearch 的 HTTP 端口
✈️ 改进优化 (Improvements)
- 此版本包含了底层 [Framework](https://docs.infinilabs.com/framework/) 的更新,解决了一些常见问题,并增强了整体稳定性和性能。虽然 Agent 本身没有直接的变更,但从 Framework 中继承的改进间接地使 Agent 受益。
Loadgen v1.30.3
INFINI Loadgen 是一款开源的专为 Easysearch、Elasticsearch、OpenSearch 设计的轻量级性能测试工具。
Loadgen 本次更新如下:
✈️ 改进优化 (Improvements)
- 此版本包含了底层 [Framework](https://docs.infinilabs.com/framework/) 的更新,解决了一些常见问题,并增强了整体稳定性和性能。虽然 Loadgen 本身没有直接的变更,但从 Framework 中继承的改进间接地使 Loadgen 受益。
更多详情请查看以下各产品的 Release Notes 或联系我们的技术支持团队!
- [Coco AI App](https://docs.infinilabs.com/co ... notes/)
- [Coco AI Server](https://docs.infinilabs.com/co ... notes/)
- [INFINI Easysearch](https://docs.infinilabs.com/ea ... earch/)
- [INFINI Gateway](https://docs.infinilabs.com/ga ... notes/)
- [INFINI Console](https://docs.infinilabs.com/co ... notes/)
- [INFINI Agent](https://docs.infinilabs.com/ag ... notes/)
- [INFINI Loadgen](https://docs.infinilabs.com/lo ... notes/)
- [INFINI Framework](https://docs.infinilabs.com/fr ... notes/)
期待反馈
欢迎下载体验使用,如果您在使用过程中遇到如何疑问或者问题,欢迎前往 INFINI Labs Github(<https://github.com/infinilabs>) 中的对应项目中提交 Feature Request 或提交 Bug。
下载地址: <https://infinilabs.cn/download>
邮件:hello@infini.ltd
电话:(+86) 400-139-9200
Discord:<https://discord.gg/4tKTMkkvVX>
也欢迎大家微信扫码添加小助手(INFINI-Labs),加入用户群一起讨论交流。

关于极限科技(INFINI Labs)

极限科技,全称极限数据(北京)科技有限公司,是一家专注于实时搜索与数据分析的软件公司。旗下品牌极限实验室(INFINI Labs)致力于打造极致易用的数据探索与分析体验。
极限科技是一支年轻的团队,采用天然分布式的方式来进行远程协作,员工分布在全球各地,希望通过努力成为中国乃至全球企业大数据实时搜索分析产品的首选,为中国技术品牌输出添砖加瓦。
官网:<https://infinilabs.cn>
Easysearch ZSTD 基准测试:高压缩率下实现近 5 倍查询吞吐
INFINI Labs 小助手 发表了文章 • 0 个评论 • 4607 次浏览 • 2026-03-17 12:41
在搜索引擎领域,压缩算法的选择一直是一个经典的权衡难题:
- 选择高压缩率(如
best_compression/ DEFLATE),磁盘省了,但查询解压慢; - 选择高速编码(如默认 LZ4),查询快了,但磁盘占用大。
Easysearch 引入了基于 JDK 21 FFM(Foreign Function & Memory API) 直连本地 ZSTD 动态库的加速方案,试图打破这一困局。为了验证效果,我们在完全对等的环境下,对 Easysearch(ZSTD)和 Elasticsearch 7.10.2(best_compression)进行了一次严格的查询吞吐对比测试。
结果令人振奋——即使在系统明显背景负载下,Easysearch 也没有因为高压缩而变慢,反而在查询吞吐上实现了近 5 倍提升。
---
测试环境
为确保对比公平,两套集群的硬件资源、JVM 配置、数据规模、索引结构完全对齐:
| 配置项 | Easysearch | Elasticsearch 7.10.2 |
| :--------------------- | :----------------------------------- | :----------------------------- |
| 节点数 | 3 | 3 |
| JVM 堆内存 | 12GB × 3 | 12GB × 3 |
| node.processors | 16 × 3 | 16 × 3 |
| 文档数 | 10,000,000 | 10,000,000 |
| 主分片 / 副本 | 3 / 0 | 3 / 0 |
| 数据类型 | nginx 访问日志 | nginx 访问日志 |
| 字段数 | 17 | 17 |
| mapping | 完全一致(MD5 校验) | 完全一致(MD5 校验) |
| Stored fields 压缩模式 | ZSTD (JDK21 FFM/native, level=3) | best_compression (DEFLATE) |
压缩机制对比:
best_compression映射到 LuceneBEST_COMPRESSION;在 stored fields 路径上,压缩实现为DeflateWithPresetDictCompressionMode,内部使用java.util.zip.Deflater/Inflater(即 DEFLATE)。
Easysearch ZSTD 当前走 JDK 21 FFM 绑定本地 zstd 库(java.lang.foreign);index.compression.zstd.jni=true为当前这套实现的启用方式。
查询模型:JMeter 随机match查询,随机命中service_name、method、error_code、url四个字段,每次返回 10 条文档。
压测起始负载(_cat/nodes快照):
| 负载项 | Easysearch run | Elasticsearch run |
| :---------- | :------------- | :---------------- |
| load_1m | 29.74 | 25.27 |
| load_5m | 27.10 | 28.15 |
| load_15m | 26.09 | 36.96 |
| ram.percent | 99 | 99 |
说明:压测并非在空闲机上进行,而是在已有明显背景负载的生产式环境下完成。
---
核心结果
1. 查询吞吐量(QPS):在高背景负载下,Easysearch 仍领先 372%
稳态阶段(3 轮平均),Easysearch 的查询吞吐是 Elasticsearch 的 4.7 倍:
| 指标 | Elasticsearch (DEFLATE) | Easysearch (ZSTD) | 差异 |
| :-------------------------- | ----------------------: | ----------------: | :----------- |
| 稳态 QPS | 532.8 | 2,518.0 | +372.6% |
| 平均响应时间 | 779.0 ms | 164.3 ms | -78.9% |
| 稳态 CPU 占用(系统总占用) | 92.43% | 89.59% | 仅作背景参考 |
注:压测期间服务器存在明显背景负载(其他进程占用较高),该 CPU 指标是系统总占用,不等价于“仅搜索进程”的纯业务 CPU 对比。
在系统总 CPU 均接近 90% 的背景下,Easysearch 仍达到接近 5 倍吞吐。
查询吞吐量 QPS 对比(稳态均值)
### 2. 响应时间:从近 1 秒降到 164 毫秒
平均响应时间对比(ms,越低越好)
用户体感上,这意味着:同样一个搜索请求,Elasticsearch 还在等解压,Easysearch 已经把结果送到了客户端。
### 3. 各轮次详细数据
各轮次 QPS 趋势
各轮次平均响应时间趋势(ms)
### 4. CPU 使用效率:每 1% CPU 产出的 QPS 差距惊人
单看 CPU 占用率,两者似乎差不多(89.59% vs 92.43%)。但如果换一个视角——**每消耗 1% CPU 能产出多少 QPS**,差距就一目了然了:
| 指标 | Elasticsearch (DEFLATE) | Easysearch (ZSTD) | 倍数 |
| :--------------- | ----------------------: | ----------------: | :-------- |
| 稳态 QPS | 532.8 | 2,518.0 | — |
| 稳态 CPU | 92.43% | 89.59% | — |
| **QPS / 1% CPU** | **5.76** | **28.10** | **4.88×** |
CPU 使用效率:每 1% CPU 产出的 QPS
这意味着什么?
- ES 使用 DEFLATE(best_compression)时,解压路径更可能成为 CPU 热点;结合 ES 的高 CPU(92.43%)与较低 QPS,说明单位 CPU 产出偏低;
- Easysearch 使用 ZSTD(JDK21 FFM/native)时,解压开销更小;在相近 CPU 水位(89.59%)下获得更高 QPS,单位 CPU 产出明显更高。
换句话说,当前这组实测更支持“ZSTD 在该查询模型下单位 CPU 产出更高”。
5. 存储空间:ZSTD 并未膨胀
| 索引 | 压缩算法 | 磁盘占用 |
| :----------------------- | :------------------------------- | -------: |
| nginx_best_10m (ES) | best_compression (DEFLATE) | 1.8 GB |
| nginx_zstd3 (Easysearch) | ZSTD (level=3, JDK21 FFM/native) | 1.9 GB |
两者存储空间接近。若按_cat/indices的 1 位小数展示是1.8GBvs1.9GB;若按_stats/store字节值计算,差异约2.5%。因此可以认为 ZSTD 在 level=3 下与 DEFLATEbest_compression压缩率接近。
磁盘占用对比(GB)
---
## 为什么 ZSTD 能做到"又小又快"?
传统认知中,压缩率和解压速度是一对矛盾。但 ZSTD 算法天然具备**非对称压缩**的特性:
压缩算法特性对比
在搜索引擎场景中,查询会触发存储字段(`_source`)读取与解压路径,命中文件系统页缓存时,可能不发生实际磁盘 I/O,但仍需进行 \_source 解压。
当查询涉及较多 `_source` 读取时:
- DEFLATE 的解压开销成为 CPU 瓶颈,拖慢了整体吞吐;
- ZSTD(JDK21 FFM/native) 的解压速度在该场景下明显更优,单次请求的解压 CPU 成本更低,从而释放出更多 CPU 资源用于并发查询处理。
这就是为什么 Easysearch 在 CPU 占用更低(89.59% vs 92.43%)的情况下,反而能处理近 5 倍的查询量。
---
一张图总结
Easysearch ZSTD vs Elasticsearch DEFLATE — 全维度对比
---
## 结论
Easysearch 的 ZSTD 压缩方案证明了一个事实:**即使在高背景负载下,高压缩率和高查询性能依然可以兼得**。
在 1000 万条 nginx 日志、且系统存在明显背景负载的实测中:
- 查询吞吐提升 372%,从 533 QPS 跃升至 2518 QPS
- 平均响应时间下降 79%,从 779ms 降至 164ms
- CPU 使用效率提升 388%,每 1% CPU 产出 28.10 QPS vs 5.76 QPS
- CPU 占用绝对值下降 2.84 个百分点(相对下降约 3.07%)
- 磁盘占用与 DEFLATE best_compression 接近(按字节口径约 +2.5%)
对于日志分析、可观测性、安全审计等需要兼顾存储成本和查询性能的场景,Easysearch ZSTD 是一个不需要妥协的选择。
---
ZSTD 使用方法
1) 新建索引时启用 ZSTD
bash<br /> curl -k -u 'admin:<password>' -X PUT 'https://127.0.0.1:9200/<index-name>' \<br /> -H 'Content-Type: application/json' -d '{<br /> "settings": {<br /> "index.codec": "ZSTD",<br /> "index.compression.zstd.jni": true<br /> }<br /> }'<br />
可选参数:
index.compression.zstd.level(默认3)
说明:
index.compression.zstd.dict固定为true,无需单独配置index.compression.zstd.dict不作为独立开关来调整
2) 老索引切换到 ZSTD(推荐 reindex)
index.codec是静态设置(打开状态不可动态改;可在关闭索引后调整)。
index.compression.zstd.jni是final设置(关闭索引后也不可修改)。
如果老索引要启用index.compression.zstd.jni=true,建议新建目标索引后reindex迁移:
如果对已有索引执行PUT /<index-name>/_settings直接修改,会报错:final <index-name> setting [index.compression.zstd.jni], not updateable。
```bash先创建目标索引(启用 ZSTD)
curl -k -u 'admin:
' -X PUT 'https://127.0.0.1:9200/ ' \
-H 'Content-Type: application/json' -d '{
"settings": {
"index.codec": "ZSTD",
"index.compression.zstd.jni": true
}
}'
再迁移数据
curl -k -u 'admin:
' -X POST 'https://127.0.0.1:9200/_reindex' \
-H 'Content-Type: application/json' -d '{
"source": { "index": "" },
"dest": { "index": "" }
}'
```
3) 校验是否生效
bash<br /> curl -k -u 'admin:<password>' \<br /> 'https://127.0.0.1:9200/<index-name>/_settings?include_defaults=true&pretty'<br />
重点确认:
index.codec = ZSTDindex.compression.zstd.jni = true
---
关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
官网文档:<https://docs.infinilabs.com/easysearch>
作者:张磊,极限科技(INFINI Labs)搜索引擎研发负责人,对 Elasticsearch 和 Lucene 源码比较熟悉,目前主要负责公司的 Easysearch 产品的研发以及客户服务工作。
---
相关文章:
- [Easysearch 2.0.0 性能测试](https://infinilabs.cn/blog/202 ... ments/)
- [Easysearch 压缩模式深度比较:ZSTD + source_reuse 的优势分析](https://infinilabs.cn/blog/202 ... modes/)
- [Easysearch 压缩功能的显著提升:从 8.7GB 到 1.4GB](https://infinilabs.cn/blog/202 ... 1.4GB/)
APM(二):监控 Python 服务
INFINI Labs 小助手 发表了文章 • 0 个评论 • 15608 次浏览 • 2025-12-31 18:04
[上一篇](https://infinilabs.cn/blog/202 ... earch/)我们已经安装好了 Skywalking 和 Easysearch,这次我们来写个简单的 Python 服务,并把它的服务调用信息发送给 Skywalking,通过 Skywalking 的 Web UI 进行展示。
启动后端服务
先启动好后端服务,包括 Skywalking 和 Easysearch。启动完成后能通过 Web UI 访问 Skywalking。

构建 Python 服务
我们编写一个简单的 Flask 服务程序,只要访问 localhost:8081/a 就会返回 "Hello, I'm Service A!" 信息。
plain<br /> from flask import Flask<br /> <br /> app = Flask(__name__)<br /> <br /> @app.route('/a', methods=['GET'])<br /> <br /> def service_b():<br /> return "Hello, I'm Service A!"<br /> <br /> if __name__ == '__main__':<br /> app.run(host='0.0.0.0', port=8081)<br />
运行前,要安装好依赖。
plain<br /> pip3 install flask<br /> pip3 install apache-skywalking<br />
依赖关系展示如下:

设置环境变量
为了让服务能成功把相关信息发送到 Skywalking 后端,启动前我们还要设置两个环境变量告诉服务程序该往哪里发送信息。
plain<br /> export SW_AGENT_COLLECTOR_BACKEND_SERVICES=localhost:11800<br /> export SW_AGENT_NAME=AService-python<br />
启动 Python 程序
一切准备妥当后,运行我们的服务程序。
plain<br /> sw-python run python3 AService.py<br />
程序启动后会监听 8081 端口。

我们通过浏览器访问下。

在 Skywalking 的 Web UI 上查看服务的信息是否采集到。




可以看到服务 A 的调用信息都已经被记录到 Skywalking 中了。
作者:杨帆,极限科技(INFINI Labs)高级解决方案架构师、《老杨玩搜索》栏目 B 站 UP 主,拥有十余年金融行业服务工作经验,熟悉 Linux、数据库、网络等领域。目前主要从事 Easysearch、Elasticsearch 等搜索引擎的技术支持工作,服务国内私有化部署的客户。
Easy-Es 2.1.0-easysearch 版本发布
INFINI Labs 小助手 发表了文章 • 0 个评论 • 9249 次浏览 • 2025-12-15 17:42

01 | 版本更新概述
经过极限科技与 Dromara 开源社区下 Easy-Es 项目的紧密合作与共同努力,我们很荣幸地联合推出 Easy-Es 2.1.0-easysearch 版本!
作为双方携手打造的第一个合作成果,本版本已正式发布:
- 源码仓库:<https://gitee.com/dromara/easy ... gt%3B
- Maven 依赖:<https://mvnrepository.com/arti ... gt%3B
本次更新的核心内容是将 Easy-Es 框架底层增加兼容极限科技自主研发的 Easysearch 搜索引擎,这标志着国产搜索引擎与国内优秀开源项目深度融合的重要里程碑,是极限科技与 Dromara 社区携手共建国产技术生态的创新实践。
02 | 迁移至 Easysearch 的背景与优势
随着国内对自主可控技术需求的日益增长,特别是在基础设施软件领域,企业对于信创合规的要求不断提升。极限科技自主研发的 Easysearch 搜索引擎具备以下显著优势:
- 国产化自主可控:完全自主研发,符合信创要求,无许可证风险,为企业提供安全可靠的技术保障
- 轻量级架构:相比传统搜索引擎,资源占用更少,启动更快速,显著降低企业运维成本
- 卓越性能表现:查询性能优异,能够满足大部分业务场景需求,用户体验流畅
- 良好兼容性:与 Elasticsearch 的 API 接口基本兼容,迁移成本较低,保护用户现有投资
基于以上优势,双方决定共同将 Easy-Es 框架底层迁移至 Easysearch,这不仅为用户提供更多选择,更是双方携手推动国产搜索引擎生态建设的重要举措。
03 | Easy-Es 框架优势
Easy-Es 框架在搜索开发领域具备以下核心优势:
- 极简代码开发:相比原生 API 可减少 50%-80% 的代码量,大幅提升开发效率。
java<br /> // 使用 Easy-Es 仅需一行代码完成查询<br /> List<Document> documents = documentMapper.selectList(<br /> EsWrappers.lambdaQuery(Document.class).eq(Document::getTitle, "测试")<br /> );<br />
- 自动索引管理:
框架提供全自动智能索引托管功能,开发者无需关心索引的创建、更新及数据迁移等复杂操作,索引全生命周期由框架自动管理,过程零停机。
- SQL 语法兼容:
支持使用 MySQL 语法完成搜索查询操作,无需学习复杂的 DSL 语句。支持 and、or、like、in 等常用 SQL 语法。
- Lambda 表达式支持:
采用 Lambda 风格编程,提供类型安全的字段访问,避免手动输入字段名可能产生的错误,提升代码可读性和开发效率。
- 无缝 Spring Boot 集成:
与 Spring Boot 生态深度集成,提供开箱即用的自动配置,无需复杂的手动配置,支持 Spring Boot Actuator 监控,完美融入企业级应用架构。
- 丰富的查询功能:
支持复杂的嵌套查询、聚合查询、范围查询、高亮显示等高级搜索功能,同时保持 API 的简洁易用,满足各种业务场景需求。
- 分布式架构支持:
完美适配 Easysearch 的分布式特性,支持集群模式部署,具备高可用性和横向扩展能力,满足企业级大规模数据处理需求。
- 成熟稳定的国产 ORM 框架:
作为 Dromara 开源社区下的顶级开源项目,Easy-Es 已在国内众多企业和项目中得到广泛应用和验证,拥有活跃的中文社区和完善的文档支持,为企业级应用提供了可靠的技术保障。
04 | 快速上手示例
1. 添加依赖
根据您使用的构建工具,选择对应的配置方式:
Maven 项目
pom.xml 配置:
```xml
11
11
2.7.0
UTF-8
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.dromara.easy-es
easy-es-boot-starter
2.1.0-easysearch
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
${spring-boot.version}
repackage
```
**Maven 启动命令**:
```bash
# 运行应用
mvn spring-boot:run
# 编译打包
mvn clean package
```
#### Gradle 项目
**build.gradle 配置**:
```gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}
group = 'org.easysearch'
version = '1.0-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'org.dromara.easy-es:easy-es-boot-starter:2.1.0-easysearch'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
```
**Gradle 启动命令**:
```bash
# 运行应用
./gradlew bootRun
# 编译打包
./gradlew clean build
```
### 2. 配置文件设置
**application.yml**(根据实际 Easysearch 部署情况修改):
```yaml
easy-es:
enable: true
# Easysearch 服务地址
address: localhost:9200
# 协议:http 或 https
schema: https
# Easysearch 用户名
username: admin
# Easysearch 密码
password: your_password_here
# 连接保持时间(毫秒)
keep-alive-millis: 18000
global-config:
# 开启彩蛋模式(启动时显示 ASCII 艺术图案)
i-kun-mode: true
# 索引处理模式:smoothly 表示平滑模式(零停机更新索引)
process-index-mode: smoothly
# 异步处理索引时是否阻塞
async-process-index-blocking: true
# 是否打印 DSL 语句(开发调试时可设为 true)
print-dsl: false
db-config:
# 下划线转驼峰
map-underscore-to-camel-case: true
# 索引前缀
index-prefix: dev_
# 主键类型:customize 表示自定义
id-type: customize
# 字段更新策略:not_empty 表示非空时才更新
field-strategy: not_empty
# 刷新策略:immediate 表示立即刷新
refresh-policy: immediate
# 开启追踪总命中数
enable-track-total-hits: true
```
### 3. 实体类定义
```java
package org.dromara.easyes.sample.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.easyes.annotation.HighLight;
import org.dromara.easyes.annotation.IndexField;
import org.dromara.easyes.annotation.IndexId;
import org.dromara.easyes.annotation.IndexName;
import org.dromara.easyes.annotation.Settings;
import org.dromara.easyes.annotation.rely.Analyzer;
import org.dromara.easyes.annotation.rely.FieldStrategy;
import org.dromara.easyes.annotation.rely.FieldType;
import org.dromara.easyes.annotation.rely.IdType;
import java.time.LocalDateTime;
/**
* es 数据模型
*/
@Data
@Accessors(chain = true)
@Settings(shardsNum = 3, replicasNum = 2)
@IndexName(value = "easyes_document", keepGlobalPrefix = true)
public class Document {
/**
* es 中的唯一 id
*/
@IndexId(type = IdType.CUSTOMIZE)
private String id;
/**
* 文档标题,默认为 keyword 类型,可进行精确查询
*/
private String title;
/**
* 文档内容,指定为 TEXT 类型,使用 IK 分词器
* 支持高亮显示,高亮结果映射到 highlightContent 字段
*/
@HighLight(mappingField = "highlightContent")
@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART)
private String content;
/**
* 创建者,字段策略为非空时才更新
*/
@IndexField(strategy = FieldStrategy.NOT_EMPTY)
private String creator;
/**
* 创建时间
*/
@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime gmtCreate;
/**
* 高亮返回值被映射的字段
*/
private String highlightContent;
/**
* 文档点赞数
*/
private Integer starNum;
/**
* 地理位置经纬度坐标,例如: "40.13933715136454,116.63441990026217"
*/
@IndexField(fieldType = FieldType.GEO_POINT)
private String location;
}
```
### 4. Mapper 接口
```java
package org.dromara.easyes.sample.mapper;
import org.dromara.easyes.core.kernel.BaseEsMapper;
import org.dromara.easyes.sample.entity.Document;
/**
* Mapper 接口,继承 BaseEsMapper 即可获得所有 CRUD 方法
*/
public interface DocumentMapper extends BaseEsMapper{
}
```
### 5. 启动类配置
```java
package org.dromara.easyes.sample;
import org.dromara.easyes.spring.annotation.EsMapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*/
@SpringBootApplication
@EsMapperScan("org.dromara.easyes.sample.mapper")
public class EasyEsApplication {
public static void main(String[] args) {
SpringApplication.run(EasyEsApplication.class, args);
}
}
```
### 6. 业务使用示例
```java
package org.dromara.easyes.sample.controller;
import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper;
import org.dromara.easyes.sample.entity.Document;
import org.dromara.easyes.sample.mapper.DocumentMapper;
import org.easysearch.action.search.SearchResponse;
import org.easysearch.search.aggregations.Aggregations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
@RestController
public class SampleController {
@Resource
private DocumentMapper documentMapper;
/**
* 初始化插入数据
*/
@GetMapping("/insert")
public Integer insert() {
int count = 0;
// 插入 5 条测试数据
for (int i = 1; i <= 5; i++) {
Document document = new Document();
document.setId(String.valueOf(i));
document.setTitle("测试" + i);
document.setContent("测试内容" + i);
document.setCreator("创建者" + i);
document.setGmtCreate(LocalDateTime.now());
document.setStarNum(i * 10);
count += documentMapper.insert(document);
}
return count;
}
/**
* 根据标题精确查询
*/
@GetMapping("/listDocumentByTitle")
public ListlistDocumentByTitle(@RequestParam String title) {
LambdaEsQueryWrapperwrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, title);
return documentMapper.selectList(wrapper);
}
/**
* 高亮搜索
*/
@GetMapping("/highlightSearch")
public ListhighlightSearch(@RequestParam String content) {
LambdaEsQueryWrapperwrapper = new LambdaEsQueryWrapper<>();
wrapper.match(Document::getContent, content);
return documentMapper.selectList(wrapper);
}
/**
* 查询所有数据
*/
@GetMapping("/selectAll")
public ListselectAll() {
LambdaEsQueryWrapperwrapper = new LambdaEsQueryWrapper<>();
return documentMapper.selectList(wrapper);
}
/**
* 聚合查询 - 按创建时间和点赞数分组统计
*/
@GetMapping("/aggByDateAndStar")
public Aggregations aggByDateAndStar() {
LambdaEsQueryWrapperwrapper = new LambdaEsQueryWrapper<>();
wrapper.groupBy(Document::getGmtCreate)
.max(Document::getStarNum)
.min(Document::getStarNum);
SearchResponse response = documentMapper.search(wrapper);
return response.getAggregations();
}
/**
* 使用 SQL 语句查询文档
*/
@GetMapping("/queryBySQL")
public String queryBySQL(@RequestParam(required = false) String title) {
String sql;
if (title != null && !title.isEmpty()) {
sql = String.format("SELECT * FROM dev_easyes_document WHERE title = '%s'", title);
} else {
sql = "SELECT * FROM dev_easyes_document LIMIT 10";
}
return documentMapper.executeSQL(sql);
}
}
```
### 7. 快速测试
启动应用后,可以通过以下接口测试:
```bash
# 1. 插入测试数据
curl http://localhost:8080/insert
# 2. 查询所有数据
curl http://localhost:8080/selectAll
# 3. 根据标题精确查询
curl "http://localhost:8080/listDocumentByTitle?title=测试1"
# 4. 高亮搜索
curl "http://localhost:8080/highlightSearch?content=测试"
# 5. SQL 查询
curl "http://localhost:8080/queryBySQL?title=测试1"
# 6. 聚合查询
curl http://localhost:8080/aggByDateAndStar
```
## 05 | 相关链接
- 极简代码开发:相比原生 API 可减少 50%-80% 的代码量,大幅提升开发效率。
- Easy-Es 官方网站:<https://easy-es.cn>
- Gitee 仓库:<https://gitee.com/dromara/easy-es>
- GitHub 仓库:<https://github.com/dromara/easy-es>
- Easysearch 官方网站:<https://infinilabs.cn/products/easysearch>
06 | 特别致谢
在此,极限科技要特别感谢 Easy-Es 项目的核心开发者“老汉”和各位贡献者和维护者们。正是因为有了你们的辛勤付出、专业精神以及对开源事业的热忱奉献,Easy-Es 项目才能在国内外获得如此广泛的认可和应用。
也感谢你们对国产技术生态建设的信任与支持。此次 Easy-Es 与 Easysearch 的深度整合,正是双方通力合作、互利共赢的最佳体现。
我们相信,在 Easy-Es 项目团队的持续推动下,国产开源软件必将迎来更加辉煌的明天。极限科技将继续致力于提供优质的国产技术解决方案,与 Easy-Es 项目团队携手共进,为中国开源生态的发展贡献更多力量!
---
关于 Easy-Es
Easy-Es(简称 EE)是一款基于 Elasticsearch(简称 ES)官方提供的 ElasticsearchClient 打造的 ORM 开发框架,在 ElasticsearchClient 的基础上,只做增强不做改变,为简化开发、提高效率而生,您如果有用过 Mybatis-Plus(简称 MP),那么您基本可以零学习成本直接上手 EE,EE 是 MP 的 ES 平替版,在有些方面甚至比 MP 更简单,同时也融入了更多 ES 独有的功能,助力您快速实现各种场景的开发。
官网:<https://www.easy-es.cn>
Easy-Es for Easysearch 是一款简化 Easysearch 国产化搜索引擎操作的开源框架,全自动智能索引托管。同时也是国内首家专门针对 Easysearch 客户端简化的工具。它简化 CRUD 及其它高阶操作,可以更好的帮助开发者减轻开发负担。底层采用 Easysearch Java Client,保证其原生性能及拓展性。
项目地址:<https://gitee.com/dromara/easy ... gt%3B
关于极限科技
极限科技(全称:极限数据(北京)科技有限公司)是一家专注于实时搜索与数据分析的软件公司。
旗下品牌:极限实验室(INFINI Labs)致力于打造极致易用的数据探索与分析体验,为用户提供安全、稳定、高性能的国产搜索解决方案。
官网:<https://infinilabs.cn>
作者:张磊,极限科技(INFINI Labs)搜索引擎研发负责人,对 Elasticsearch 和 Lucene 源码比较熟悉,目前主要负责公司的 Easysearch 产品的研发以及客户服务工作。
APM(一): Skywalking 与 Easyearch 集成
INFINI Labs 小助手 发表了文章 • 0 个评论 • 8083 次浏览 • 2025-12-12 14:22
概述
SkyWalking 是一个开源的可观测性平台,用于收集、分析、聚合和可视化服务和云原生基础设施的数据。SkyWalking 提供了一种简单的方法,即使在云之间也能保持对分布式系统的清晰视图。它是一个现代的 APM,专门为云原生、基于容器的分布式系统设计。
SkyWalking 涵盖了云原生世界中所有的可观测性需求,包括:
- Tracing: SkyWalking 原生数据格式,以及 v1 和 v2 格式的 Zipkin 跟踪都得到支持。
- Metrics: SkyWalking 支持成熟的指标格式,包括原生计量格式、OTEL 指标格式和 Telegraf 格式。SkyWalking 与服务网格平台(通常为 Istio 和 Envoy)集成,将可观测性构建到数据平面或控制平面。此外,SkyWalking 原生代理可以在指标模式下运行,从而显著提升性能。
- Logging: 包括从磁盘收集或通过网络收集的日志。原生代理可以自动将追踪上下文与日志绑定,或使用 SkyWalking 通过文本内容绑定追踪和日志。
- Profiling: Profiling 是一种强大的工具,帮助开发者从代码行角度理解应用程序的性能。SkyWalking 提供了内置于原生语言代理和独立的 eBPF 代理的剖析功能。
- Event: 事件是一种特殊类型的数据,用于记录系统中的重要时刻,例如版本升级、配置变更等。将事件与指标关联有助于解释指标中的峰值或谷值,将事件与追踪和日志关联有助于排查根本原因。
更详细的信息请大家移步 [Skywalking](https://skywalking.apache.org/) 官方网站。
测试环境
本篇使用的 Skywalking 版本是 10.2.0 ,需要 Java 11/17/21。
[Easyearch](https://easysearch.cn) 使用的版本是 1.14.1,需要开启 Elastic 兼容模式,具体操作参考[文档](https://infinilabs.cn/blog/202 ... earch/) 。
生成 Java 密钥库文件
使用如下命令将 Easysearch 的 CA 证书(ca.crt)导入到一个新的 Java 密钥库文件(es_keystore.jks)中,以便 SkyWalking 能够信任由该 CA 颁发的所有证书。生产环境中使用请替换命令中的密码。
plain<br /> keytool -import -v -trustcacerts -file ca.crt -keystore es_keystore.jks -keypass changeit -storepass changeit<br />
修改配置文件
SkyWalking 后端服务配置文件为 config/application.yml,这也是与 Easyearch 集成时需要修改的文件。Skywalking 与 Easyearch 集成有两种通信方式:http 或 https。http 方式非常简单,留给大家自行探索。本篇采用 https 方式,这也是 Easysearch 初始化后默认对外服务的协议。
拷贝上面生成的密钥库文件到 Skywalking 的 home 目录下,修改 application.yml 的 storage 部分
```plain
storage:
selector: ${SW_STORAGE:elasticsearch}
banyandb:Since 10.2.0, the banyandb configuration is separated to an independent configuration file:
bydb.yaml.elasticsearch:
namespace: ${SW_NAMESPACE:""}
clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:es1.infini.cloud:9200}
protocol: ${SW_STORAGE_ES_HTTP_PROTOCOL:"https"}
connectTimeout: ${SW_STORAGE_ES_CONNECT_TIMEOUT:3000}
socketTimeout: ${SW_STORAGE_ES_SOCKET_TIMEOUT:30000}
responseTimeout: ${SW_STORAGE_ES_RESPONSE_TIMEOUT:15000}
numHttpClientThread: ${SW_STORAGE_ES_NUM_HTTP_CLIENT_THREAD:0}
user: ${SW_ES_USER:"admin"}
password: ${SW_ES_PASSWORD:"infiniyyds@2025"}
trustStorePath: ${SW_STORAGE_ES_SSL_JKS_PATH:"../es_keystore.jks"}
trustStorePass: ${SW_STORAGE_ES_SSL_JKS_PASS:"changeit"}
```
注意 clusterNodes 配置的是域名,需要在 Skywalking 的主机上用 /etc/hosts 解析成具体的地址,如果有多个 Easysearch 节点,可以用逗号分隔。
启动
确保 Easysearch 启动完毕后,再启动 Skywalking。正常启动完成后,可访问 Skywalking 服务页面,默认端口 8080。

正常连接后,Skywalking 会在 Easysearch 中创建很多 sw 开头的索引。

OK,服务集成就到此完毕,后续我们将探索更多的 APM 内容。
关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
官网文档:<https://docs.infinilabs.com/easysearch>
作者:杨帆,极限科技(INFINI Labs)高级解决方案架构师、《老杨玩搜索》栏目 B 站 UP 主,拥有十余年金融行业服务工作经验,熟悉 Linux、数据库、网络等领域。目前主要从事 Easysearch、Elasticsearch 等搜索引擎的技术支持工作,服务国内私有化部署的客户。
Easysearch 2.0.0 性能测试
INFINI Labs 小助手 发表了文章 • 0 个评论 • 9256 次浏览 • 2025-12-04 00:17
概述
Easysearch 2.0.0 正式版带来了显著的性能提升和优化改进。通过与上一个稳定版本 1.15.6 的全面对比测试,我们使用 esrally 基准测试工具在 append-no-conflicts 场景下进行了深入的性能评估。测试结果表明,2.0.0 版本在索引性能、查询延迟、内存管理等核心指标上都实现了突破性改进。
核心性能提升
1. 索引性能更加稳定
写入效率提升 12.81%
Easysearch 2.0.0 索引性能表现更加稳定:
- 累计索引 CPU 时间(所有主分片):从 225.1 分钟缩短至 196.3 分钟,减少 28.8 分钟(-12.81%)
- 索引吞吐量:
- 平均吞吐量从 180,868 docs/s 提升至 190,712 docs/s(+5.44%)
- 最大吞吐量从 198,184 docs/s 提升至 220,460 docs/s(+11.24%)
- 最小吞吐量从 164,263 docs/s 提升至 178,961 docs/s(+8.95%)
累计索引 CPU 时间的减少,表明 2.0.0 版本在索引操作上更加高效,CPU 利用率更优。这意味着在相同硬件条件下,Easysearch 2.0.0 能够更快地完成数据摄入任务,对于需要处理大规模数据写入的场景具有重要意义。
Indexing Throughput (docs/s) - Higher is Better
v1.15.6
v2.0.0
### 2. Refresh 和 Flush 耗时缩短
**Refresh 和 Flush 性能大幅改善**
在 Elasticsearch/Easysearch 中,Refresh 和 Flush 操作对写入性能有直接影响。2.0.0 版本在这两个关键操作上实现了重大优化:
#### Refresh 性能提升 54.46%
- 平均吞吐量从 180,868 docs/s 提升至 190,712 docs/s(+5.44%)
- 累计刷新时间:从 9.14 分钟降至 4.16 分钟
- 中位刷新时间:减少 61.86%(从 0.133 分钟降至 0.051 分钟)
- 最大刷新时间:减少 65.62%(从 1.12 分钟降至 0.39 分钟)
Flush 性能提升 40%
- 累计刷盘时间:从 12.57 分钟降至 7.54 分钟
- 中位刷盘时间:减少 57.57%
- 最大刷盘时间:减少 31.93%
Cumulative Refresh Time (min) - Lower is Better
v1.15.6
v2.0.0
Cumulative Flush Time (min) - Lower is Better
v1.15.6
v2.0.0
这些优化使得 Easysearch 2.0.0 能够更高效地将数据持久化到磁盘,同时减少对写入操作的阻塞。
### 3. 垃圾回收(GC)性能优化
**GC 效率显著提升**
- Young GC 次数:从 525 次降至 426 次,减少 18.86%
- Young GC 时间:从 16.547 秒降至 15.985 秒,减少 3.40%
- Old GC:两个版本均无 Old GC 发生,内存管理健康
更少的 GC 次数意味着:
- 应用程序 STW(Stop-The-World)暂停更少
- 更稳定的查询响应时间
- 更好的系统吞吐量
查询性能提升
1. 基础查询延迟降低
多类型查询性能全面提升
| 查询类型 | 延迟指标 | 改进幅度 |
| ---------------- | ------------ | -------------------------------- |
| Default 查询 | 50 分位延迟 | -11.40% (19.97ms → 17.69ms) |
| | 99 分位延迟 | -15.23% (25.66ms → 21.75ms) |
| Term 查询 | 50 分位延迟 | -19.88% (4049ms → 3244ms) |
| | 90 分位延迟 | -18.73% (4137ms → 3362ms) |
| Range 查询 | 50 分位延迟 | -31.71% (42.19ms → 28.81ms) |
| | 100 分位延迟 | -64.68% (111.42ms → 39.35ms) |
Query Latency Improvements (ms) - Lower is Better
Default Query (50th percentile)
v1.15.6
v2.0.0
Term Query (50th percentile)
v1.15.6
v2.0.0
Range Query (50th percentile)
v1.15.6
v2.0.0
### 2. 排序查询性能飞跃
**时间戳排序查询优化高达 97%**
Easysearch 2.0.0 在排序查询场景下实现了令人瞩目的性能突破:
#### 降序排序(desc_sort_timestamp)
- 50 分位延迟:从 516.07ms 降至 98.89ms(-80.84%)
- 90 分位延迟:从 544.84ms 降至 123.59ms(-77.32%)
- 99 分位延迟:从 603.14ms 降至 139.93ms(-76.80%)
升序排序 + After 分页(asc_sort_with_after_timestamp)
- 50 分位延迟:从 1272.58ms 降至 33.56ms(-97.36%)
- 90 分位延迟:从 1386.92ms 降至 37.25ms(-97.31%)
- 99 分位延迟:从 1474.98ms 降至 38.11ms(-97.42%)
Sort Query Latency (ms) - Lower is Better
Desc Sort
v1.15.6
v2.0.0
Asc Sort + After
v1.15.6
v2.0.0
#### Force Merge 后的排序查询
在强制合并为单段后,排序查询性能更加出色:
**降序排序(force-merge-1-seg)**
- 50 分位延迟:从 131,617ms 降至 115.01ms(-99.91%)
- 这一改进相当于从 2 分钟以上降至 0.1 秒!
升序 + After 分页(force-merge-1-seg)
- 50 分位延迟:从 1387.01ms 降至 132.42ms(-90.45%)
- 90 分位延迟:从 1509.03ms 降至 159.05ms(-89.46%)
3. 聚合查询性能提升
hourly_agg 聚合查询优化
- 50 分位延迟:从 4192.57ms 降至 3866.07ms(-7.79%)
- 90 分位延迟:从 4303.51ms 降至 4053.80ms(-5.80%)
- 99 分位延迟:从 4475.32ms 降至 4269.91ms(-4.59%)
4. Scroll 查询性能改进
大数据量遍历场景优化
- 50 分位延迟:从 6511.65ms 降至 4623.87ms(-28.99%)
- 90 分位延迟:从 6881.70ms 降至 5972.79ms(-13.21%)
- 平均吞吐量:从 24.192 pages/s 提升至 24.485 pages/s(+1.21%)
Scroll Query Latency (ms) - Lower is Better
50th Percentile
v1.15.6
v2.0.0
90th Percentile
v1.15.6
v2.0.0
### 5. 高百分位延迟大幅改善
**极端场景下的稳定性提升**
在衡量系统稳定性的高百分位延迟指标上,2.0.0 版本表现卓越:
| 场景 | 99.9 分位延迟改进 | 99.99 分位延迟改进 | 100 分位延迟改进 |
| ---------------- | ----------------- | ------------------ | ------------------ |
| **index-append** | **-43.40%** | **-65.35%** | **-70.91%** |
| | (3364ms → 1904ms) | (9618ms → 3333ms) | (13427ms → 3906ms) |
这意味着即使在最坏的情况下,2.0.0 版本也能提供更加稳定和可预测的性能表现。
## 范围查询性能提升
**200s-in-range 和 400s-in-range 查询优化**
- 200s-in-range:
- 50 分位延迟降低 15.60%
- 吞吐量提升 1.20%
- 50 分位延迟降低 15.60%
- 400s-in-range:
- 50 分位延迟降低 8.44%
- 吞吐量提升 0.23%
存储优化
磁盘空间使用更高效
- 50 分位延迟降低 8.44%
- 存储大小:从 19.51 GB 降至 19.14 GB(-1.93%)
- 段数量:从 43 个增至 50 个(+16.28%)
虽然段数量略有增加,但总存储空间仍然减少,说明数据压缩和存储效率得到了提升。
Merge 策略调整
合并操作的权衡
需要注意的是,2.0.0 版本在 Merge 方面有以下变化:
- Merge 次数从 184 次增至 192 次(+4.35%)
- Merge 限流时间从 9.53 分钟增至 11.17 分钟(+17.20%)
这是为了平衡写入性能和查询性能所做的策略调整。用户可以根据实际场景需求,通过以下参数进行优化:
json<br /> {<br /> "index.merge.scheduler.max_thread_count": "1",<br /> "index.merge.policy.max_merged_segment": "5gb"<br /> }<br />
技术架构改进
1. 段数据结构优化
通过将段元数据从堆内存迁移到堆外内存,Easysearch 2.0.0 实现了:
- 更低的 JVM 堆压力
- 更少的 GC 频率
- 更稳定的内存使用模式
- 更好的大数据集支持能力
2. 查询缓存优化
排序查询性能的巨大提升表明 2.0.0 版本可能在以下方面进行了优化:
- 改进的 Doc Values 访问机制
- 优化的排序算法
- 更高效的分页实现
- 智能的查询结果缓存
3. I/O 优化
Refresh 和 Flush 时间的大幅减少说明:
- 改进了磁盘 I/O 调度策略
- 优化了文件系统操作
- 可能引入了更高效的批量写入机制
适用场景
Easysearch 2.0.0 的性能提升使其在以下场景中表现更加出色:
1. 大规模日志与事件流处理
- 更高的写入吞吐量(+11.24% 峰值)
- 更低的索引延迟
- 适合 APM、日志分析、安全监控等场景
2. 时序数据存储与分析
- 时间戳排序查询性能提升高达 97%
- 适合 IoT、监控指标、金融交易数据等场景
3. 全文搜索应用
- 多类型查询延迟降低 10-30%
- 高并发场景下更稳定的响应时间
- 适合电商搜索、内容管理系统等场景
4. 实时分析与 Dashboard
- 聚合查询性能提升 5-8%
- 更低的极端延迟,用户体验更好
- 适合实时报表、业务 BI 等场景
5. 大数据量遍历与导出
- Scroll 查询延迟降低 29%
- 适合数据迁移、全量导出等场景
升级建议
兼容性
Easysearch 2.0.0 与 1.15.6 在 API 层面保持兼容,但建议:
- 测试环境验证:先在测试环境进行充分验证
- 配置审查:检查 Merge 相关配置是否需要调整
- 监控指标:升级后密切关注 GC、内存、延迟等指标
- 滚动升级:生产环境建议采用滚动升级方式
性能测试环境
本次测试使用 esrally 基准测试工具,测试配置如下:
- 测试环境验证:先在测试环境进行充分验证
- 测试场景:append-no-conflicts
- 测试时间:
- Baseline (1.15.6): 2025-11-14
- Contender (2.0.0): 2025-11-21
- Baseline (1.15.6): 2025-11-14
- 部署方式:External(独立部署)
- CPU 绑定:使用
taskset绑定 Easysearch 进程 0 到 15 cpu - JVM 配置:
-Xms16g -Xmx16g
总结
Easysearch 2.0.0 版本在性能方面取得了全面提升:
- 索引性能提升 12.81%
- 查询延迟降低 10-97%(不同场景)
- 内存使用优化 100%(堆内段数据)
- GC 频率降低 18.86%
- Refresh 性能提升 54.46%
- Flush 性能提升 40%
- 高百分位延迟改善 43-70%
这些改进使得 Easysearch 2.0.0 成为一个更加高效、稳定和可靠的搜索与分析引擎,特别适合处理大规模数据和实时查询场景。无论是日志分析、时序数据处理,还是全文搜索应用,2.0.0 版本都能提供更优秀的性能表现。
我们强烈建议用户升级到 Easysearch 2.0.0,以获得这些显著的性能提升和更好的使用体验。
---
关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
- 官网: https://easysearch.cn
- 文档: https://docs.infinilabs.com/easysearch/main
作者:张磊,极限科技(INFINI Labs)搜索引擎研发负责人,对 Elasticsearch 和 Lucene 源码比较熟悉,目前主要负责公司的 Easysearch 产品的研发以及客户服务工作。
原文:https://infinilabs.cn/blog/202 ... ents/
搜索百科(5):Easysearch — 自主可控的国产分布式搜索引擎
liaosy 发表了文章 • 0 个评论 • 7875 次浏览 • 2025-10-20 15:54
大家好,我是 INFINI Labs 的石阳。
欢迎关注 《搜索百科》 专栏!每天 5 分钟,带你速览一款搜索相关的技术或产品,同时还会带你探索它们背后的技术原理、发展故事及上手体验等。
在上一篇我们介绍了 [OpenSearch](https://infinilabs.cn/blog/202 ... search) —— 那个因协议争议而诞生的开源搜索分支。今天,我们把目光转向国内,聊聊极限科技研发的一款轻量级搜索引擎:Easysearch。
引言
在搜索技术的世界里,从 Lucene 的出现到 Solr、Elasticsearch 的崛起,搜索引擎技术已经发展了二十余年。然而,随着开源协议的变更与国际形势的变化,国产自主搜索引擎的需求愈发迫切。在这样的背景下,Easysearch 作为一款自主可控、轻量高效、兼容 Elasticsearch 的分布式搜索引擎应运而生,为国内企业带来了全新的选择。

Easysearch 概述
Easysearch 是一款分布式搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析、AI 集成等。Easysearch 衍生自开源协议 Apache 2.0 的 Elasticsearch 7.10 版本,并不断往前迭代更新,紧跟 Lucene 最新版本的更新。Easysearch 可以替代 Elasticsearch,同时添加和完善多项企业级功能。
- 首次发布:2023 年 4 月
- 最新版本:1.15.4(截止 2025 年 10 月)
- 主导企业:极限科技 (INFINI Labs)
- 官方网址:[https://easysearch.cn](http://easysearch.cn)
诞生背景:为什么要有 Easysearch?
Easysearch 由极限科技(INFINI Labs)团队推出。项目的起点源于团队长期在搜索引擎和大数据领域的深厚实践积累,团队深刻认识到国内企业在使用 Elasticsearch 时普遍面临以下痛点:
- 开源协议变化带来的商业风险 —— Elastic 于 2021 年将许可更改为 SSPL,导致社区分裂,增加了企业在合规和商用上的不确定性;
- 高并发与高可靠性场景下对稳定可控方案的需求 —— 企业级应用亟需一个性能可靠、可深度优化的搜索基础设施;
- 技术栈自主可控的迫切需求 —— 随着国产化进程加快,国内生态中缺乏轻量化、易部署、且完全可控的搜索引擎产品;
- 本地化服务与快速响应能力的缺口 —— 国内企业更需要本地团队提供高效的技术支持与服务,对本土化、个性化功能需求能得到及时响应与反馈。
基于这些考虑,Easysearch 在设计之初就明确了目标:构建一款兼容 Elasticsearch API、简洁易用、性能出众且完全自主可控的国产搜索引擎。
核心特性
- 开源协议变化带来的商业风险 —— Elastic 于 2021 年将许可更改为 SSPL,导致社区分裂,增加了企业在合规和商用上的不确定性;
- 轻量级:安装包大小不到 60 MB,安装部署简洁,资源占用低,开箱即用;
- 跨平台:支持主流操作系统和 CPU 架构,支持国产信创运行环境;
- 高性能:针对不同场景进行的极致优化,可用更少硬件成本获得更高服务性能,降本增效。
- 稳定可靠:修复大量内核问题,解决内存泄露,集群卡顿、查询缓慢等问题,久经严苛业务环境考验。
- 安全增强:默认就提供完整的企业级安全功能,支持 LDAP/AD 集成,支持索引、文档、字段粒度细权管控。
- 兼容性强:兼容 Elasticsearch 7.x 的 REST API 和数据格式,迁移成本低;
- 可视化运维:无需 Kibana 即可通过内置 Web UI 插件界面管理索引、节点与监控指标等。
对比优势
| 对比维度 | Easysearch | Elasticsearch | OpenSearch |
| ---------------- | ----------------- | ----------------- | -------------------------------- |
| 用户协议 | 社区免费+商业授权 | SSPL/AGPL v3 | Apache 2.0 |
| API 兼容性 | 高度兼容 ES | 原生 | 高度兼容 ES |
| 最小安装体积 | 57MB | 482MB | 682MB |
| 部署复杂度 | 简单 | 中等 | 相对复杂 |
| 信创环境支持 | 全面兼容 | 无 | 无 |
| 可视化管理 | 开箱即用管理后台 | 需独立部署 Kibana | 需独立部署 OpenSearch Dashboards |
| 本地化与中文支持 | 强 | 弱 | 弱 |
| AI 插件支持 | 较弱 | 强 | 较强 |
| 社区与生态 | 快速成长中 | 成熟广泛 | 活跃增长 |
快速开始:5 分钟体验 Easysearch
1. 使用 Docker 启动
```bash直接运行镜像使用随机密码(数据及配置未持久化)
docker run --name easysearch \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.4
```
2. 验证集群状态
bash<br /> curl -ku "username:password" -X GET "<a href="https://localhost:9200/"" rel="nofollow" target="_blank">https://localhost:9200/"</a><br />
返回结果示例:
json<br /> {<br /> "name": "easysearch-node",<br /> "cluster_name": "easysearch-6yhwn91v80gf",<br /> "cluster_uuid": "Gfu_fuF1QViJfeUWVbiFCA",<br /> "version": {<br /> "distribution": "easysearch",<br /> "number": "1.15.4",<br /> "distributor": "INFINI Labs",<br /> "build_hash": "9110128946b0af3de639966cfa74b5498346949d",<br /> "build_date": "2025-10-14T03:30:41.948590Z",<br /> "build_snapshot": false,<br /> "lucene_version": "8.11.4",<br /> "minimum_wire_lucene_version": "7.7.0",<br /> "minimum_lucene_index_compatibility_version": "7.7.0"<br /> },<br /> "tagline": "You Know, For Easy Search!"<br /> }<br />
3. 索引与搜索示例
```bash
写入文档
curl -ku "username:password" -X POST "<a href="https://localhost:9200/my_index/_doc"" rel="nofollow" target="_blank">https://localhost:9200/my_index/_doc" -H 'Content-Type: application/json' -d'
{
"title": "Easysearch 入门",
"content": "这是一个轻量级搜索引擎的示例文档。",
"tags": ["搜索", "国产", "轻量级"]
}'
搜索文档
curl -ku "username:password" -X GET "<a href="https://localhost:9200/my_index/_search"" rel="nofollow" target="_blank">https://localhost:9200/my_index/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"content": "搜索引擎"
}
}
}'
```
4. 使用 Easysearch UI
Easysearch 提供了轻量级界面化管理功能,不再依赖第三方组件即可对集群进行管理,真正做到开箱即用。如果你安装了 Easysearch UI 插件或者下载捆绑包,可通过 _https://localhost:9200/_ui/_ 访问,进行节点、索引、分片、查询调试和监控查看等管理。
图 1:系统登录

图 2:集群概览

图 3:节点列表

图 4:节点概览

图 5:索引列表

图 6:索引概览

图 7:分片管理

图 8:开发工具

以上仅列出了一些基本功能,其他如安全管理、主从复制、备份管理、生命周期管理等更多高级功能由于篇幅限制不一一展示,有兴趣的朋友可自行部署探索。
结语
Easysearch 的诞生,不仅填补了国产搜索引擎在分布式与轻量化领域的空白,也让更多企业在面对开源协议变动与外部技术依赖时,拥有了更加安全、灵活、可控的选择。
它既是国产替代方案的有力代表,更是新一代搜索技术生态的积极探索者,为企业级实时搜索与分析带来新的可能。
🚀 下期预告
下一篇我们将介绍 一款 AI 驱动的现代搜索引擎 - Meilisearch,基于 Rust 构建的开源搜索引擎,性能高、部署简单。号称比 Elasticsearch 快 10 倍,真的这么牛吗?
💬 三连互动
- 你是否在使用或考虑国产搜索替代方案?
- 在实际项目中,你最看重搜索引擎的哪些特性?(性能、兼容性、运维、成本)
- 对 Easysearch 有什么功能上的期待?
对搜索技术感兴趣的朋友,也欢迎加我微信(ID:lsy965145175)备注“搜索百科”,拉你进 搜索技术交流群,一起探讨与学习!
✨ 推荐阅读
- 你是否在使用或考虑国产搜索替代方案?
- [搜索百科(4):OpenSearch — 开源搜索的新选择](https://infinilabs.cn/blog/202 ... search)
- [搜索百科(3):Elasticsearch — 搜索界的"流量明星"](https://infinilabs.cn/blog/202 ... earch/)
- [搜索百科(2):Apache Solr — 企业级搜索的开源先锋](https://infinilabs.cn/blog/2025/search-wiki-2-solr/)
- [搜索百科(1):Lucene — 打开现代搜索世界的第一扇门](https://infinilabs.cn/blog/202 ... ucene/)
🔗 参考资源
- [Easysearch 官方文档](https://docs.infinilabs.com/easysearch)
- [Easysearch 安装指南](https://docs.infinilabs.com/ea ... stall/)
- [Elasticsearch VS Easysearch 性能测试](https://infinilabs.cn/blog/202 ... sting/)
- [国产搜索引擎崛起:Elasticsearch 国产化加速](https://infinilabs.cn/blog/202 ... ative/)
原文:https://infinilabs.cn/blog/202 ... arch/
Easysearch 冷热架构实战
INFINI Labs 小助手 发表了文章 • 0 个评论 • 11472 次浏览 • 2025-10-01 20:11
在之前的文章中,我们介绍了如何使用[索引生命周期策略](https://infinilabs.cn/blog/202 ... guide/)来管理索引。如果要求索引根据其生命周期阶段自动在不同的节点之间迁移,还需要用到冷热架构。我们来看看具体如何实现。
冷热架构
冷热架构其实就是在 Easyearch 集群中定义不同属性的节点,这些节点共同组成冷热架构。比如给所有热节点一个 hot 属性,给所有冷节点一个 cold 属性。在 Easyearch 中分配节点属性是通过配置文件(easysearch.yml)来实现的,比如我要定义一个热节点和一个冷节点,我可以在对应节点的配置文件中添加如下行:
```plain
热节点添加下面的行
node.attr.temp: hot
冷节点添加下面的行
node.attr.temp: cold
<br /> <br /> 有了这些属性,我们就可以指定索引分片在分配时,是落在 hot 节点还是 cold 节点。<br /> <br /> **查看节点属性**<br /> <br /> 测试环境是个 2 节点的 Easysearch 集群。<br /> <br /> <br /> <br /> 比如我创建新索引 test-index,希望它被分配到 hot 节点上。<br /> <br /> plain
PUT test-index
{
"settings": {
"number_of_replicas": 0,
"index.routing.allocation.require.temp": "hot"
}
}
<br /> <br /> <br /> <br /> 可以看到 test-index 索引的分片分配到 hot 节点 node-1 上。我们修改索引分配节点的属性,让其移动到 cold 节点 node-2 上。<br /> <br /> plain
PUT test-index/_settings
{
"settings": {
"index.routing.allocation.require.temp": "cold"
}
}
```

生命周期与冷热架构
在上面的例子中,我们通过索引分配节点属性对索引“坐落”的节点进行了控制。在索引生命周期策略中也支持对该属性进行修改,实现索引根据生命周期阶段自动在不同的节点之间移动的目的。
比如我们定义一个简单的索引策略:
- 索引创建后进入 hot 阶段,此阶段的索引被分配到 hot 节点
- 创建索引 3 分钟后,索引进入 cold 阶段,此阶段索引分片移动到 cold 节点
创建策略
plain<br /> PUT _ilm/policy/ilm_test<br /> {<br /> "policy": {<br /> "phases": {<br /> "hot": {<br /> "min_age": "0m",<br /> },<br /> "cold": {<br /> "min_age": "3m",<br /> "actions": {<br /> "allocate" : {<br /> "require" : {<br /> "temp": "cold"<br /> }<br /> }<br /> }<br /> }<br /> }<br /> }<br /> }<br />
生命周期策略后台是定期触发的任务,为了更快的观测到效果,可以修改任务触发周期为每分钟 1 次。
plain<br /> PUT _cluster/settings<br /> {<br /> "transient": {<br /> "index_lifecycle_management.job_interval":"1"<br /> }<br /> }<br />
创建索引模板
创建完索引生命周期策略,还需要索引模板把索引和生命周期策略关联起来。我们创建一个模板把所有 ilm_test 开头的索引与 ilm_test 生命周期策略关联,为了便于观察,指定索引没有副本分片。
plain<br /> PUT _template/ilm_test<br /> {<br /> "order" : 100000,<br /> "index_patterns" : [<br /> "ilm_test*"<br /> ],<br /> "settings" : {<br /> "index" : {<br /> "lifecycle" : {<br /> "name" : "ilm_test"<br /> },<br /> "number_of_replicas" : "0",<br /> "routing.allocation.require.temp": "hot"<br /> }<br /> }<br /> }<br />
创建索引
创建一个 ilm_test 开头的索引,应用上一步创建的索引模板。
plain<br /> POST ilm_test_1/_doc<br /> {<br /> "test":"test"<br /> }<br />
查看索引分片分配情况。

目前索引存储在 node-1 节点,按计划 3 分钟后将会移动到 node-2 上。


至此我们已通过索引生命周期策略实现了索引分片的移动,其实支持的操作还有很多,比如: rollover、close、snapshot 等,详情请参阅官方[文档](https://docs.infinilabs.com/ea ... D%259C)。
有任何问题,欢迎加我微信沟通。

关于 Easysearch

INFINI Easysearch 是一个分布式的搜索型数据库,实现非结构化数据检索、全文检索、向量检索、地理位置信息查询、组合索引查询、多语种支持、聚合分析等。Easysearch 可以完美替代 Elasticsearch,同时添加和完善多项企业级功能。Easysearch 助您拥有简洁、高效、易用的搜索体验。
官网文档:<https://docs.infinilabs.com/easysearch>
Easysearch 字段'隐身'之谜:source_reuse 与 ignore_above 的陷阱解析
INFINI Labs 小助手 发表了文章 • 0 个评论 • 11866 次浏览 • 2025-09-30 16:08
## 背景问题
前阵子,社区有小伙伴在使用 [Easysearch](https://infinilabs.cn/products/easysearch/) 的数据压缩功能时发现,在开启 source_reuse 和 ZSTD 后,一个字段的内容看不到了。
索引的设置如下:
```
{
......
"settings": {
"index": {
"codec": "ZSTD",
"source_reuse": "true"
}
},
"mappings": {
"dynamic_templates": [
{
"message_field": {
"path_match": "message",
"mapping": {
"norms": false,
"type": "text"
},
"match_mapping_type": "string"
}
},
{
"string_fields": {
"mapping": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"match_mapping_type": "string",
"match": "*"
}
}
]
......
}
```
然后产生的一个多字段内容能**被搜索到,但是不可见**。
类似于下面的这个情况:

## 原因分析
我们先来看看整个字段展示经历的环节:

1. 字段写入索引的时候,不仅写了 text 字段也写了 keyword 字段。
2. keyword 字段产生倒排索引的时候,会忽略掉长度超过 ignore_above 的内容。
3. 因为开启了 source_reuse,**\_source 字段中与 doc_values 或倒排索引重复的部分会被去除**。
4. 产生的数据文件进行了 ZSTD 压缩,进一步提高了数据的压缩效率。
5. 索引进行倒排或者 docvalue 的查询,检索到这个文档进行展示。
6. 展示的时候通过文档 id **获取 `_source`或者`docvalues_fields`的内容**来展示文本,但是文本内容是空的。
其中步骤 4 中的 ZSTD 压缩,是**作用于数据文件的,并不对数据内容进行修改**。因此,我们来专注于其他环节。
## 问题复现
首先,这个字段索引的配置也是一个 es 常见的设置,并不会带来内容显示缺失的问题。
```
"mapping": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
```
那么,source_reuse 就成了我们可以重点排查的环节。
### source 发生了什么
source_reuse 的作用描述如下:
```
source_reuse: 启用 source_reuse 配置项能够去除 _source 字段中与 doc_values 或倒排索引重复的部分,从而有效减小索引总体大小,这个功能对日志类索引效果尤其明显。
source_reuse 支持对以下数据类型进行压缩:keyword,integer,long,short,boolean,float,half_float,double,geo_point,ip, 如果是 text 类型,需要默认启用 keyword 类型的 multi-field 映射。 以上类型必须启用 doc_values 映射(默认启用)才能压缩。
```
这是一个对 `_source` 字段进行产品化的功能实现。为了减少索引的存储体量,简单粗暴的操作是直接将`_source`字段进行关闭,利用其他数据格式去存储,在查询的时候对应的利用 docvalue 或者 indexed 去展示文本内容。
那么 `_source`关闭后,会不会也有这样的问题呢?
测试的步骤如下:
```
# 1. 创建不带source的双字段索引
PUT test_source
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"msg": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
}
}
}
}
# 2. 写入测试数据
POST test_source/_doc/1
{"msg":"""[08-27 14:28:45] [DBG] [config.go:273] config contain variables, try to parse with environments
[08-27 14:28:45] [DBG] [config.go:214] load config files: []
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: pipeline_logging_merge
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: ingest_pipeline_logging
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: async_messages_merge
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: metrics_merge
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: request_logging_merge
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: ingest_merged_requests
[08-27 14:28:45] [INF] [pipeline.go:419] creating pipeline: async_ingest_bulk_requests
[08-27 14:28:45] [INF] [module.go:159] started module: pipeline
[08-27 14:28:45] [DBG] [module.go:163] all system module are started
[08-27 14:28:45] [DBG] [floating_ip.go:348] setup floating_ip, root privilege are required
[08-27 14:28:45] [DBG] [queue_config.go:121] init new queue config:e60457c6eae50a4eabbb62fc1001dccc,bulk_requests
[08-27 14:28:45] [DBG] [queue_config.go:121] init new queue config:e60457c6eae50a4eabbb62fc1001dccc,bulk_requests
[08-27 14:28:45] [DBG] [queue_config.go:121] init new queue config:e60457c6eae50a4eabbb62fc1001dccc,bulk_requests
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: indexing_merge
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: metrics_merge
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: when
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: ingest_merged_requests
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: indexing_merge
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: request_logging_merge
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: indexing_merge
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: async_messages_merge
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: bulk_indexing
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: ingest_pipeline_logging
[08-27 14:28:45] [DBG] [queue_config.go:121] init new queue config:1216c96eb876eee5b177d45436d0a362,gateway-pipeline-logs
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: bulk_indexing
[08-27 14:28:45] [DBG] [processor.go:139] generated new processors: indexing_merge
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: pipeline_logging_merge
[08-27 14:28:45] [DBG] [pipeline.go:466] processing pipeline_v2: async_ingest_bulk_requests
[08-27 14:28:45] [DBG] [badger.go:110] init badger database [queue_consumer_commit_offset]
[08-27 14:28:45] [INF] [floating_ip.go:290] floating_ip entering standby mode
[08-27 14:28:45] [DBG] [badger.go:110] init badger database [dis_locker]
[08-27 14:28:45] [DBG] [time.go:208] refresh low precision time in background
[08-27 14:28:45] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found
[08-27 14:28:45] [DBG] [bulk_indexing.go:355] metadata for [backup] is nil
[08-27 14:28:50] [INF] [module.go:178] started plugin: floating_ip
[08-27 14:28:50] [INF] [module.go:178] started plugin: force_merge
[08-27 14:28:50] [DBG] [network.go:78] network io stats will be included for map[]
[08-27 14:28:50] [INF] [module.go:178] started plugin: metrics
[08-27 14:28:50] [INF] [module.go:178] started plugin: statsd
[08-27 14:28:50] [DBG] [entry.go:100] reuse port 0.0.0.0:7005
[08-27 14:28:50] [DBG] [metrics.go:205] collecting network metrics
[08-27 14:28:50] [DBG] [metrics.go:174] collecting instance metrics
[08-27 14:28:50] [DBG] [elasticsearch.go:128] init elasticsearch proxy instance: prod
[08-27 14:28:50] [DBG] [filter.go:103] generated new filters: when, elasticsearch
[08-27 14:28:50] [DBG] [entry.go:142] apply filter flow: [*] [/_bulk] [ filters ]
[08-27 14:28:50] [DBG] [entry.go:142] apply filter flow: [*] [/{any_index}/_bulk] [ filters ]
[08-27 14:28:50] [DBG] [elasticsearch.go:128] init elasticsearch proxy instance: prod
[08-27 14:28:50] [DBG] [filter.go:103] generated new filters: request_path_limiter, elasticsearch
[08-27 14:28:50] [INF] [module.go:178] started plugin: gateway
[08-27 14:28:50] [DBG] [module.go:182] all user plugin are started
[08-27 14:28:50] [INF] [module.go:184] all modules are started
[08-27 14:28:50] [INF] [app.go:556] gateway is up and running now.
[08-27 14:28:50] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found
[08-27 14:28:50] [DBG] [bulk_indexing.go:355] metadata for [backup] is nil
[08-27 14:28:55] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found
[08-27 14:28:55] [DBG] [bulk_indexing.go:355] metadata for [backup] is nil
[08-27 14:29:00] [DBG] [metrics.go:205] collecting network metrics
[08-27 14:29:00] [DBG] [metrics.go:174] collecting instance metrics
[08-27 14:29:00] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found
[08-27 14:29:00] [DBG] [bulk_indexing.go:355] metadata for [backup] is nil
[08-27 14:29:05] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found
[08-27 14:29:05] [DBG] [bulk_indexing.go:355] metadata for [backup] is nil
[08-27 14:29:10] [DBG] [metrics.go:205] collecting network metrics
[08-27 14:29:10] [DBG] [metrics.go:174] collecting instance metrics
[08-27 14:29:10] [DBG] [domain_actions.go:278] elasticsearch metadata [backup] was not found"""}
# 3. 查询数据
GET test_source/_search
```
此时,可以看到,存入的文档检索出来是空的

`_source` 字段是用于索引时传递的原始 JSON 文档主体。它本身未被索引成倒排(因此不作用于 `query` 阶段),只是在执行查询时用于 `fetch` 文档内容。
对于 text 类型,关闭`_source`,则字段内容自然不可被查看。
而对于 keyword 字段,查看`_source`也是不行的。可是 keyword 不仅存储`source`,还存储了 doc_values。因此,对于 keyword 字段类型,可以考虑关闭`_source`,使用 `docvalue_fields` 来查看字段内容。
测试如下:
```
# 1. 创建测试条件的索引
PUT test_source2
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"msg": {
"type": "keyword"
}
}
}
}
# 2. 写入数据
POST test_source2/_doc
{"msg":"1111111"}
# 3. 使用 docvalue_fields 查询数据
POST test_source2/_search
{"docvalue_fields": ["msg"]}
# 返回结果
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test_source2",
"_type": "_doc",
"_id": "yBvTj5kBvrlGDwP29avf",
"_score": 1,
"fields": {
"msg": [
"1111111"
]
}
}
]
}
}
```
在`如果是 text 类型,需要默认启用 keyword 类型的 multi-field 映射。 以上类型必须启用 doc_values 映射(默认启用)才能压缩。`这句介绍里,也可以看到 `source_reuse` 的正常使用需要 `doc_values`。_那是不是一样使用 `doc_values` 进行内容展示呢?既然用于 `docvalue_fields` 内容展示,为什么还是内容看不了(不可见)呢?_
### keyword 的 ignore_above
仔细看问题场景里 keyword 的配置,它使用了 ignore_above。那么,会不会是这里的问题?
我们将 ignore_above 配置带入上面的测试,这里为了简化测试,ignore_above 配置为 3。**为区分问题现象,这里两条长度不同的文本进去,一条为 `11`,一条为`1111111`,可以作为参数作用效果的对比**。
```
# 1. 创建测试条件的索引,ignore_above 设置为3
PUT test_source3
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"msg": {
"type": "keyword",
"ignore_above": 3
}
}
}
}
# 2. 写入数据,
POST test_source3/_doc
{"msg":"1111111"}
POST test_source3/_doc
{"msg":"11"}
# 3. 使用 docvalue_fields 查询数据
POST test_source3/_search
{"docvalue_fields": ["msg"]}
# 返回内容
{
"took": 363,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test_source3",
"_type": "_doc",
"_id": "yhvjj5kBvrlGDwP22KsG",
"_score": 1
},
{
"_index": "test_source3",
"_type": "_doc",
"_id": "yxvzj5kBvrlGDwP2Nav6",
"_score": 1,
"fields": {
"msg": [
"11"
]
}
}
]
}
}
```
OK! 问题终于复现了。我们再来看看作为关键因素的 ignore_above 参数是用来干嘛的。
```
ignore_above:任何长度超过此整数值的字符串都不应被索引。默认值为 2147483647。默认动态映射会创建一个 ignore_above 设置为 256 的 keyword 子字段。
```
也就是说,ignore_above 在(倒排)索引时会截取内容,防止产生的索引内容过长。
但是从测试的两个文本来看,**面对在参数范围内的文档,docvalues 会正常创建,而超出参数范围的文本而忽略创建(**至于这个问题背后的源码细节我们可以另外开坑再鸽,此处省略)。
那么,在 source_reuse 下,keyword 的 ignore_above 是不是起到了相同的作用呢?
我们可以在问题场景上去除 ignore_above,参数试试,来看下面的测试:
```
# 1. 创建测试条件的索引,使用 source_reuse,设置 ignore_above 为3
PUT test_source4
{
"settings": {
"index": {
"source_reuse": "true"
}
},
"mappings": {
"properties": {
"msg": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 3,
"type": "keyword"
}
}
}
}
}
}
# 2. 写入数据
POST test_source4/_doc
{"msg":"1111111"}
POST test_source4/_doc
{"msg":"11"}
# 3. 使用 docvalue_fields 查询数据
POST test_source4/_search
# 返回内容
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test_source4",
"_type": "_doc",
"_id": "",
"_score": 1,
"_source": {}
},
{
"_index": "test_source4",
"_type": "_doc",
"_id": "zRv2j5kBvrlGDwP2_qsO",
"_score": 1,
"_source": {
"msg": "11"
}
}
]
}
}
```
可以看到,数据“不可见”的问题被完整的复现了。
## 小结
从上面一系列针对数据“不可见”问题的测试,我们可以总结以下几点:
1. 在 source_reuse 的压缩使用中,keyword 字段的 ignore_ablve 参数尽量使用默认值,不要进行过短的设置(这个 tip 已补充在 Easysearch 文档中)。
2. 在 source_reuse 是对数据压缩常见方法-关闭 source 字段的产品化处理,在日志压缩场景中有效且便捷,可以考虑多加利用。
3. keyword 的 ignore_above 参数,不仅超出长度范围不进行倒排索引,也不会写入 docvalues。
特别感谢:**社区@牛牪犇群**
更多 Easysearch 资料请查看 [官网文档](https://docs.infinilabs.com/easysearch)。
> 作者:金多安,极限科技(INFINI Labs)搜索运维专家,Elastic 认证专家,搜索客社区日报责任编辑。一直从事与搜索运维相关的工作,日常会去挖掘 ES / Lucene 方向的搜索技术原理,保持搜索相关技术发展的关注。
> 原文:https://infinilabs.cn/blog/202 ... ield/
Easysearch 国产替代 Elasticsearch:8 大核心问题解读
liaosy 发表了文章 • 0 个评论 • 14048 次浏览 • 2025-09-18 09:43
近年来,随着数据安全与自主可控需求的不断提升,越来越多的企业开始关注国产化的搜索与日志分析解决方案。作为极限科技推出的国产 Elasticsearch 替代产品,Easysearch 凭借其对搜索场景的深入优化、轻量级架构设计以及对 ES 生态的高度兼容,成为众多企业替代 Elasticsearch 的新选择。

我们在近期与用户的交流中,整理出了大家最关心的八大问题,并将它们浓缩为一篇技术解读,希望帮助你快速了解 Easysearch 的优势与定位。
用户最关心的八大问题
- Easysearch 对数据量的支撑能力如何,能应对 PB 级数据存储吗?
答:完全可以。Easysearch 支持水平扩展,通过增加节点即可线性提升存储与计算能力。在实际应用中,已成功支撑 PB 级日志与检索数据。同时,其存储压缩率相比 Elasticsearch 7.10.2 平均高出 2.5~3 倍,显著节省硬件成本。
- 在高并发写入场景下,Easysearch 和 ES 的性能差异有多大?
答:在相同硬件配置下,使用 Nginx 日志进行 bulk 写入压测,Easysearch 在多种分片配置下的写入性能相比 Elasticsearch 7.10.2 提升 40%-70%,更适合高并发写入场景。
- 是否支持中文分词?需要额外插件吗?
答:中文分词一直是 Elasticsearch 用户的「必装插件」。而在 Easysearch 中,中文分词是开箱即用的,同时支持 ik、pinyin 等主流分词器,还能自定义词典,方便电商、内容平台等场景。
- 从 ES 迁移到 Easysearch 是否复杂?会影响业务吗?
答:迁移往往是国产替代的最大顾虑。为此,Easysearch 提供了 极限网关 工具,支持全量同步和实时增量同步。迁移过程中业务可继续读写,只需短暂切换连接地址,几乎无感知。
- 监控与运维工具是否完善?是否支持 Kibana?
答:Easysearch 提供完整的监控与运维体系。从 Easysearch 1.15.x 版本起自带 Web UI 管理控制台(类似简化版 Kibana),支持索引管理、查询调试、权限控制等功能。同时还提供 INFINI Console 实现多集群管理与深度监控等。也可以通过配置让 Kibana 连接 Easysearch(部分高级功能可能受限)。
- 小型团队技术能力有限,用 Easysearch 运维难度高吗?
答:Easysearch 的一大设计理念就是降低运维门槛。Easysearch 提供一键部署脚本,减少手动配置参数,支持自动分片均衡与故障节点恢复,无需专职运维人员也能稳定运行,非常适合技术资源有限的团队。
- Easysearch 是否支持数据备份与恢复?操作复杂吗?
答:支持快照(Snapshot),可备份到本地磁盘或对象存储(S3、OSS 等)。恢复时仅需执行快照恢复命令,满足企业级数据安全需求。
- 对比 ES,Easysearch 在使用体验上最大的不同是什么?
答:Easysearch 保持与 Elasticsearch 类似的接口与查询 DSL,用户几乎无学习成本即可上手。同时,它针对国产化环境和搜索场景做了优化,运维更轻量,成本更可控。
结语:Easysearch,国产化搜索的新选择
作为一款国产自主可控的搜索与日志分析引擎,Easysearch 不仅继承了 Elasticsearch 的核心能力,更在性能、易用性、资源效率和中文支持等方面进行了深度优化。对于希望实现国产化替代、降低运维成本、提升系统性能的企业来说,Easysearch 是一个值得认真考虑的新选择。

如果你正在评估 Elasticsearch 的替代方案,不妨从 Easysearch 开始,体验更轻量、更高效的搜索新架构。
如需了解更多技术细节与使用案例,欢迎访问官方文档与社区资源:
- [Easysearch 官网文档](https://docs.infinilabs.com/easysearch)
- [Elasticsearch VS Easysearch 性能测试](https://infinilabs.cn/blog/202 ... sting/)
- [使用 Easysearch,日志存储少一半](https://infinilabs.cn/blog/202 ... ssion/)
- [Kibana OSS 7.10.2 连接 Easysearch](https://infinilabs.cn/blog/202 ... earch/)
- [自建 ES 集群通过极限网关无缝迁移到云上](https://infini-share.yuque.com ... 3DSPaV)
- [INFINI Console 一站式的数据搜索分析与管理平台](https://docs.infinilabs.com/console/main/zh/)

kibana和es的跨集群搜索是否可以连接easysearch?
medcl 回复了问题 • 2 人关注 • 1 个回复 • 5064 次浏览 • 2025-09-11 19:03
IK 字段级别词典的升级之路
INFINI Labs 小助手 发表了文章 • 0 个评论 • 3854 次浏览 • 2025-07-29 13:01
背景知识:词库的作用
IK 分词器是一款基于词典匹配的中文分词器,其准确性和召回率与 IK 使用的词库也有不小的关系。
这里我们先了解一下词典匹配法的作用流程:
- 预先准备一个大规模的词典,用算法在文本中寻找词典里的最长匹配项。这种方法实现简单且速度快。
- 但面临歧义切分和未登录词挑战:同一序列可能有不同切分方式(例如“北京大学生”可以切成“北京大学/生”或“北京/大学生”),需要规则或算法消除歧义;
- 而词典中没有的新词(如网络流行语、人名等)无法正确切分。
可以看到词库是词元产生的比对基础,一个完善的中文词库能大大提高分词器的准确性和召回率。
IK 使用的词库是中文中常见词汇的合集,完善且丰富,ik_smart 和 ik_max_word 也能满足大部分中文分词的场景需求。但是针对一些专业的场景,比如医药这样的行业词库、电商搜索词、新闻热点词等,IK 是很难覆盖到的。这时候就需要使用者自己去维护自定义的词库了。
IK 的自定义词库加载方式
IK 本身也支持自定义词库的加载和更新的,但是只支持一个集群使用一个词库。
这里主要的制约因素是,词库对象与 ik 的中文分词器执行对象是一一对应的关系。

这导致了 IK 的词库面对不同中文分词场景时较低的灵活性,使用者并不能做到字段级别的词库加载。并且基于文件或者 http 协议的词库加载方式也需要不小的维护成本。
字段级别词库的加载
鉴于上述的背景问题,INFINI lab 加强了 IK 的词库加载逻辑,做到了字段级别的词库加载。同时将自定义词库的加载方式由外部文件/远程访问改成了内部索引查询。
主要逻辑如图:

这里 IK 多中文词库的加载优化主要基于 IK 可以加载多词类对象(即下面这段代码)的灵活性,将原来遍历一个 CJK 词类对象修改成遍历多个 CJK 词类对象,各个自定义词库可以附着在 CJK 词库对象上实现不同词库的分词。
<br /> do{<br /> //遍历子分词器<br /> for(ISegmenter segmenter : segmenters){<br /> segmenter.analyze(context);<br /> }<br /> //字符缓冲区接近读完,需要读入新的字符<br /> if(context.needRefillBuffer()){<br /> break;<br /> }<br /> }<br />
对默认词库的新增支持
对于默认词库的修改,新版 IK 也可以通过写入词库索引方式支持,只要将 dict_key 设置为 default 即可。
<br /> POST .analysis_ik/_doc<br /> {<br /> "dict_key": "default",<br /> "dict_type": "main_dicts",<br /> "dict_content":"杨树林"<br /> }<br />
效率测试
测试方案 1:单条测试
测试方法:写入一条数据到默认 ik_max_word 和自定义词库,查看是否有明显的效率差距
- 创建测试索引,自定义一个包括默认词库的 IK 分词器
<br /> PUT my-index-000001<br /> {<br /> "settings": {<br /> "number_of_shards": 3,<br /> "analysis": {<br /> "analyzer": {<br /> "my_custom_analyzer": {<br /> "type": "custom",<br /> "tokenizer": "my_tokenizer"<br /> }<br /> },<br /> "tokenizer": {<br /> "my_tokenizer": {<br /> <br /> "type": "ik_max_word",<br /> "custom_dict_enable": true,<br /> "load_default_dicts":true,<br /> "lowcase_enable": true,<br /> "dict_key": "test_dic"<br /> }<br /> }<br /> }<br /> },<br /> "mappings": {<br /> "properties": {<br /> "test_ik": {<br /> "type": "text",<br /> "analyzer": "my_custom_analyzer"<br /> }<br /> }<br /> }<br /> }<br />
- 将该词库重复默认词库的内容
```
POST .analysis_ik/_doc
{
"dict_key": "test_dic",
"dict_type": "main_dicts",
"dict_content":"""xxxx #词库内容
"""
}
debug 日志
[2025-07-09T16:37:43,112][INFO ][o.w.a.d.Dictionary ] [ik-1] Loaded 275909 words from main_dicts dictionary for dict_key: test_dic
```
- 测试默认词库和自定义词库的分词效率
<br /> GET my-index-000001/_analyze<br /> {<br /> "analyzer": "my_custom_analyzer",<br /> "text":"自强不息,杨树林"<br /> }<br /> <br /> GET my-index-000001/_analyze<br /> {<br /> "analyzer": "ik_max_word",<br /> "text":"自强不息,杨树林"<br /> }<br /> <br />


打开 debug 日志,可以看到自定义分词器在不同的词库找到了 2 次“自强不息”
<br /> ...<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CN_QuantifierSegmenter] [ik-1] 当前扫描词元[息]不需要启动量词扫描<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [自强不息] from dict [default]<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [不息] from dict [default]<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [自强不息] from dict [test_dic]<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [不息] from dict [test_dic]<br /> [2025-07-09T16:52:22,937][INFO ][o.w.a.c.CN_QuantifierSegmenter] [ik-1] 当前扫描词元[,]不需要启动量词扫描<br /> ...<br />
而默认词库只有一次
<br /> ...<br /> [2025-07-09T16:54:22,618][INFO ][o.w.a.c.CN_QuantifierSegmenter] [ik-1] 当前扫描词元[息]不需要启动量词扫描<br /> [2025-07-09T16:54:22,618][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [自强不息] from dict [default]<br /> [2025-07-09T16:54:22,618][INFO ][o.w.a.c.CJKSegmenter ] [ik-1] >>> WORD FOUND [不息] from dict [default]<br /> [2025-07-09T16:54:22,618][INFO ][o.w.a.c.CN_QuantifierSegmenter] [ik-1] 当前扫描词元[,]不需要启动量词扫描<br /> ...<br />
测试方案 2:持续写入测试
测试方法:在 ik_max_word 和自定义词库的索引里,分别持续 bulk 写入,查看总体写入延迟。
测试索引:
```ik_max_word索引
PUT ik_max_test
{
"mappings": {
"properties": {
"chapter": {
"type": "keyword"
},
"content": {
"type": "text",
"analyzer": "ik_max_word"
},
"paragraph_id": {
"type": "keyword"
},
"random_field": {
"type": "text"
},
"timestamp": {
"type": "keyword"
},
"word_count": {
"type": "integer"
}
}
},
"settings": {
"index": {
"number_of_shards": "1",
"number_of_replicas": "0"
}
}
}
自定义词库索引
PUT ik_custom_test
{
"mappings": {
"properties": {
"chapter": {
"type": "keyword"
},
"content": {
"type": "text",
"analyzer": "my_custom_analyzer"
},
"paragraph_id": {
"type": "keyword"
},
"random_field": {
"type": "text"
},
"timestamp": {
"type": "keyword"
},
"word_count": {
"type": "integer"
}
}
},
"settings": {
"index": {
"number_of_shards": "1",
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"load_default_dicts": "true",
"type": "ik_max_word",
"dict_key": "test_dic",
"lowcase_enable": "true",
"custom_dict_enable": "true"
}
}
},
"number_of_replicas": "0"
}
}
}
<br /> <br /> 这里利用脚本循环写入了一段《四世同堂》的文本,比较相同次数下,两次写入的总体延迟。<br /> <br /> 测试脚本内容如下:<br /> <br />python!/usr/bin/env python3
-- coding: utf-8 --
"""
四世同堂中文内容随机循环写入 Elasticsearch 脚本
目标:生成指定 bulk 次数的索引内容
"""
import random
import time
import json
from datetime import datetime
import requests
import logging
import os
import argparse
import urllib3
配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(name)
class ESDataGenerator:
def init(self, es_host='localhost', es_port=9200, index_name='sisitontang_content',
target_bulk_count=10000, batch_size=1000, use_https=False, username=None, password=None, verify_ssl=True):
"""
初始化 ES 连接和配置
"""
protocol = 'https' if use_https else 'http'
self.es_url = f'{protocol}://{es_host}:{es_port}'
self.index_name = index_name
self.target_bulk_count = target_bulk_count # 目标 bulk 次数
self.batch_size = batch_size
self.check_interval = 1000 # 每 1000 次 bulk 检查一次进度
设置认证信息
self.auth = None<br /> if username and password:<br /> self.auth = (username, password)<br /> logger.info(f"使用用户名认证: {username}")<br />设置请求会话
self.session = requests.Session()<br /> if self.auth:<br /> self.session.auth = self.auth<br />处理HTTPS和SSL证书验证
if use_https:<br /> self.session.verify = False # 始终禁用SSL验证以避免证书问题<br /> logger.info("警告:已禁用SSL证书验证(适合开发测试环境)")<br /> urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)<br />设置SSL适配器以处理连接问题
from requests.adapters import HTTPAdapter<br /> from urllib3.util.retry import Retry<br />配置重试策略
retry_strategy = Retry(<br /> total=3,<br /> backoff_factor=1,<br /> status_forcelist=[429, 500, 502, 503, 504],<br /> )<br />
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("<a href="https://"" rel="nofollow" target="_blank">https://", adapter)
设置更宽松的SSL上下文
self.session.verify = False<br />
logger.info(f"ES连接地址: {self.es_url}")
创建索引映射
self.create_index()<br />
def create_index(self):
"""创建索引和映射"""
mapping = {
"mappings": {
"properties": {
"chapter": {"type": "keyword"},
"content": {"type": "text", "analyzer": "ik_max_word"},
"timestamp": {"type": "date"},
"word_count": {"type": "integer"},
"paragraph_id": {"type": "keyword"},
"random_field": {"type": "text"}
}
}
}
try:检查索引是否存在
response = self.session.head(f"{self.es_url}/{self.index_name}")<br /> if response.status_code == 200:<br /> logger.info(f"索引 {self.index_name} 已存在")<br /> else:<br /> # 创建索引<br /> response = self.session.put(<br /> f"{self.es_url}/{self.index_name}",<br /> headers={'Content-Type': 'application/json'},<br /> json=mapping<br /> )<br /> if response.status_code in [200, 201]:<br /> logger.info(f"创建索引 {self.index_name} 成功")<br /> else:<br /> logger.error(f"创建索引失败: {response.status_code} - {response.text}")<br /> except Exception as e:<br /> logger.error(f"创建索引失败: {e}")<br />
def load_text_content(self, file_path='sisitontang.txt'):
"""
从文件加载《四世同堂》的完整文本内容
如果文件不存在,则返回扩展的示例内容
"""
if os.path.exists(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
logger.info(f"从文件 {file_path} 加载了 {len(content)} 个字符的文本内容")
return content
except Exception as e:
logger.error(f"读取文件失败: {e}")
如果文件不存在,返回扩展的示例内容
logger.info("使用内置的扩展示例内容")<br /> return self.get_extended_sample_content()<br />
def get_extended_sample_content(self):
"""
获取扩展的《四世同堂》示例内容
"""
content = """
小羊圈胡同是北平城里的一个小胡同。它不宽,可是很长,从东到西有一里多路。在这条胡同里,从东边数起,有个小茶馆,几个小门脸,和一群小房屋。小茶馆的斜对面是个较大的四合院,院子里有几棵大槐树。这个院子就是祁家的住所,四世同堂的大家庭就在这里度过了最困难的岁月。
祁老人是个善良的老头儿,虽然年纪大了,可是还很有精神。他的一生见证了太多的变迁,从清朝的衰落到民国的建立,再到现在的战乱,他都以一种达观的态度面对着。他的儿子祁天佑是个教书先生,为人正直,在胡同里很有威望。祁家的儿媳妇韵梅是个贤惠的女人,把家里打理得井井有条,即使在最困难的时候,也要维持着家庭的尊严。
钱默吟先生是个有学问的人,他的诗写得很好,可是性格有些古怪。他住在胡同深处的一个小院子里,平时很少出门,只是偶尔到祁家坐坐,和祁天佑聊聊古今。他对时局有着自己独特的见解,但更多的时候,他选择在自己的小天地里寻找精神的慰藉。战争的残酷现实让这个文人感到深深的无力,但他依然坚持着自己的文人气节。
小顺子是个活泼的孩子,他每天都在胡同里跑来跑去,和其他的孩子们一起玩耍。他的笑声总是能感染到周围的人,让这个古老的胡同充满了生机。即使在战争的阴霾下,孩子们依然保持着他们的天真和快乐,这或许就是生活的希望所在。小顺子不懂得大人们的烦恼,他只是简单地享受着童年的快乐。
李四大爷是个老实人,他在胡同里开了个小杂货铺。虽然生意不大,但是童叟无欺,街坊邻居们都愿意到他这里买东西。他的妻子是个能干的女人,把小铺子管理得很好。在那个物资匮乏的年代,能够维持一个小铺子的经营已经很不容易了。李四大爷经常帮助邻居们,即使自己的生活也不宽裕。
胡同里的生活是平静的,每天清晨,人们就开始忙碌起来。有的人挑着水桶去井边打水,有的人牵着羊去街上卖奶,有的人挑着菜担子去菜市场。这种平静的生活在战争来临之前是那么的珍贵,人们都珍惜着这样的日子。邻里之间相互照顾,孩子们在院子里玩耍,老人们在门口晒太阳聊天。
冠晓荷是个复杂的人物,他有文化,也有野心。在日本人占领北平的时候,他选择了与敌人合作,这让胡同里的人们都看不起他。但是他的妻子还是个好人,只是被丈夫连累了。冠晓荷的选择代表了那个时代一部分知识分子的软弱和妥协,他们在民族大义和个人利益之间选择了后者。
春天来了,胡同里的槐树发芽了,小鸟们在枝头歌唱。孩子们在院子里玩耍,老人们在门口晒太阳。这样的日子让人感到温暖和希望。即使在最黑暗的时期,生活依然要继续,人们依然要保持对美好未来的希望。春天的到来总是能够给人们带来新的希望和力量。
战争的阴云笼罩着整个城市,胡同里的人们也感受到了压力。有的人选择了抗争,有的人选择了妥协,有的人选择了逃避。每个人都在用自己的方式应对这个艰难的时代。祁瑞宣面临着痛苦的选择,他既不愿意与日本人合作,也不敢公开反抗,这种内心的煎熬让他备受折磨。
老舍先生用他细腻的笔触描绘了胡同里的众生相,每个人物都有自己的特点和命运。他们的喜怒哀乐构成了这部伟大作品的丰富内涵。从祁老爷子的达观,到祁瑞宣的痛苦,从韵梅的坚强,到冠晓荷的堕落,每个人物都是那个时代的缩影。
在那个动荡的年代,普通人的生活是不容易的。他们要面对战争的威胁,要面对生活的困难,要面对道德的选择。但是他们依然坚强地活着,为了家人,为了希望。即使在最困难的时候,人们依然保持着对美好生活的向往。
胡同里的邻里关系是复杂的,有友好的,也有矛盾的。但是在大的困难面前,大家还是会相互帮助。这种邻里之间的温情是中华民族传统文化的重要组成部分。在那个特殊的年代,这种人与人之间的温情显得更加珍贵。
祁瑞宣是个有理想的青年,他受过良好的教育,有自己的抱负。但是在日本人占领期间,他的理想和现实之间产生了尖锐的矛盾。他不愿意做汉奸,但是也不能完全抵抗。这种内心的矛盾和痛苦是那个时代很多知识分子的真实写照。
小妞子是个可爱的孩子,她的天真无邪给这个沉重的故事增添了一丝亮色。她不懂得大人们的复杂心理,只是简单地生活着,快乐着。孩子们的天真和快乐在那个黑暗的年代显得格外珍贵,它们代表着生活的希望和未来。
程长顺是个朴实的人,他没有什么文化,但是有自己的原则和底线。他不愿意向日本人低头,宁愿过艰苦的生活也要保持自己的尊严。他的坚持代表了中国人民不屈不挠的精神,即使在最困难的时候也不愿意妥协。
胡同里的生活节奏是缓慢的,人们有时间去观察周围的变化,去思考生活的意义。这种慢节奏的生活在今天看来是珍贵的,它让人们有机会去体验生活的细节。在那个年代,即使生活艰难,人们依然能够从平凡的日常中找到乐趣。
老二是个有个性的人,他不愿意受约束,喜欢自由自在的生活。但是在战争年代,这种个性给他带来了麻烦,也给家人带来了担忧。他的反叛精神在某种程度上代表着年轻一代对传统束缚的反抗,但在那个特殊的时代,这种反抗往往会带来意想不到的后果。
胡同里的四合院是北京传统建筑的代表,它们见证了一代又一代人的生活。每个院子里都有自己的故事,每个房间里都有自己的记忆。这些古老的建筑承载着深厚的历史文化底蕴,即使在战争的破坏下,依然坚强地屹立着。
在《四世同堂》这部作品中,老舍先生不仅描绘了个人的命运,也反映了整个民族的命运。小胡同里的故事其实就是大中国的缩影。每个人物的遭遇都代表着那个时代某一类人的命运,他们的选择和结局反映了整个民族在那个特殊历史时期的精神状态。
战争结束了,但是人们心中的创伤需要时间来愈合。胡同里的人们重新开始了正常的生活,但是那段艰难的经历永远不会被忘记。历史的教训提醒着人们珍惜和平,珍惜现在的美好生活。四世同堂的故事将永远流传下去,成为后人了解那个时代的重要窗口。
"""
return content.strip()
def split_text_randomly(self, text, min_length=100, max_length=200):
"""
将文本按100-200字的随机长度进行分割
"""清理文本,移除多余的空白字符
text = ''.join(text.split())<br />
segments = []
start = 0
while start < len(text):随机选择段落长度
segment_length = random.randint(min_length, max_length)<br /> end = min(start + segment_length, len(text))<br />
segment = text[start:end]
if segment.strip(): # 确保段落不为空
segments.append(segment.strip())
start = end
return segments
def generate_random_content(self, base_content):
"""
基于基础内容生成随机变化的内容
"""随机选择一个基础段落
base_paragraph = random.choice(base_content)<br />随机添加一些变化
variations = [<br /> "在那个年代,",<br /> "据说,",<br /> "人们常常说,",<br /> "老一辈人总是提到,",<br /> "历史记录显示,",<br /> "根据回忆,",<br /> "有人说,",<br /> "大家都知道,",<br /> "传说中,",<br /> "众所周知,"<br /> ]<br />
endings = [
"这就是当时的情况。",
"这样的事情在那个年代很常见。",
"这个故事至今还在流传。",
"这是一个值得回忆的故事。",
"这样的经历让人难以忘怀。",
"这就是老北京的生活。",
"这种精神值得我们学习。",
"这个时代已经过去了。",
"这样的生活现在已经很难看到了。",
"这是历史的见证。"
]
随机组合内容
if random.random() < 0.3:<br /> content = random.choice(variations) + base_paragraph<br /> else:<br /> content = base_paragraph<br />
if random.random() < 0.3:
content += random.choice(endings)
return content
def generate_document(self, text_segments, doc_id):
"""基于文本段落生成一个文档"""随机选择一个文本段落
content = random.choice(text_segments)<br />生成随机的额外字段以增加文档大小
random_field = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=random.randint(100, 500)))<br />
doc = {
"chapter": f"第{random.randint(1, 100)}章",
"content": content,
"timestamp": datetime.now(),
"word_count": len(content),
"paragraphid": f"para{doc_id}",
"random_field": random_field
}
return doc
def get_index_size_gb(self):
"""获取索引大小(GB)"""
try:
response = self.session.get(f"{self.es_url}/_cat/indices/{self.index_name}?bytes=b&h=store.size&format=json")
if response.status_code == 200:
data = response.json()
if data and len(data) > 0:
size_bytes = int(data[0]['store.size'])
size_gb = size_bytes / (1024 1024 1024)
return size_gb
return 0
except Exception as e:
logger.error(f"获取索引大小失败: {e}")
return 0
def bulk_insert(self, documents):
"""批量插入文档使用HTTP bulk API"""构建bulk请求体
bulk_data = []<br /> for doc in documents:<br /> # 添加action行<br /> action = {"index": {"_index": self.index_name}}<br /> bulk_data.append(json.dumps(action))<br /> # 添加文档行<br /> bulk_data.append(json.dumps(doc, ensure_ascii=False, default=str))<br />每行以换行符结束,最后也要有换行符
bulk_body = '\n'.join(bulk_data) + '\n'<br />
try:
response = self.session.post(
f"{self.es_url}/_bulk",
headers={'Content-Type': 'application/x-ndjson'},
data=bulk_body.encode('utf-8'),
timeout=30 # 添加超时设置
)
if response.status_code == 200:
result = response.json()检查是否有错误
if result.get('errors'):<br /> error_count = 0<br /> error_details = []<br /> for item in result['items']:<br /> if 'error' in item.get('index', {}):<br /> error_count += 1<br /> error_info = item['index']['error']<br /> error_details.append(f"类型: {error_info.get('type')}, 原因: {error_info.get('reason')}")<br />
if error_count > 0:
logger.warning(f"批量插入有 {error_count} 个错误")打印前5个错误的详细信息
for i, error in enumerate(error_details[:5]):<br /> logger.error(f"错误 {i+1}: {error}")<br /> if len(error_details) > 5:<br /> logger.error(f"... 还有 {len(error_details)-5} 个类似错误")<br /> return True<br /> else:<br /> logger.error(f"批量插入失败: HTTP {response.status_code} - {response.text}")<br /> return False<br /> except requests.exceptions.SSLError as e:<br /> logger.error(f"SSL连接错误: {e}")<br /> logger.error("建议检查ES集群的SSL配置或使用 --no-verify-ssl 参数")<br /> return False<br /> except requests.exceptions.ConnectionError as e:<br /> logger.error(f"连接错误: {e}")<br /> logger.error("请检查ES集群地址和端口是否正确")<br /> return False<br /> except requests.exceptions.Timeout as e:<br /> logger.error(f"请求超时: {e}")<br /> logger.error("ES集群响应超时,可能负载过高")<br /> return False<br /> except Exception as e:<br /> logger.error(f"批量插入失败: {e}")<br /> logger.error(f"错误类型: {type(e).__name__}")<br /> return False<br />
def run(self):
"""运行数据生成器"""
start_time = time.time()
start_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
logger.info(f"开始生成数据,开始时间: {start_datetime},目标bulk次数: {self.target_bulk_count}")
加载文本内容
text_content = self.load_text_content()<br />将文本分割成100-200字的段落
text_segments = self.split_text_randomly(text_content, min_length=100, max_length=200)<br /> logger.info(f"分割出 {len(text_segments)} 个文本段落")<br />
doc_count = 0
bulk_count = 0
bulk_times = [] # 记录每次bulk的耗时
while bulk_count < self.target_bulk_count:生成批量文档
documents = []<br /> for i in range(self.batch_size):<br /> doc = self.generate_document(text_segments, doc_count + i)<br /> documents.append(doc)<br />记录单次bulk开始时间
bulk_start = time.time()<br />批量插入
if self.bulk_insert(documents):<br /> bulk_end = time.time()<br /> bulk_duration = bulk_end - bulk_start<br /> bulk_times.append(bulk_duration)<br />
doc_count += self.batch_size
bulk_count += 1
定期检查和报告进度
if bulk_count % self.check_interval == 0:<br /> current_size = self.get_index_size_gb()<br /> avg_bulk_time = sum(bulk_times[-self.check_interval:]) / len(bulk_times[-self.check_interval:])<br /> logger.info(f"已完成 {bulk_count} 次bulk操作,插入 {doc_count} 条文档,当前索引大小: {current_size:.2f}GB,最近{self.check_interval}次bulk平均耗时: {avg_bulk_time:.3f}秒")<br />避免过于频繁的插入
#time.sleep(0.01) # 减少延迟,提高测试速度<br />
end_time = time.time()
end_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
total_duration = end_time - start_time
计算统计信息
final_size = self.get_index_size_gb()<br /> avg_bulk_time = sum(bulk_times) / len(bulk_times) if bulk_times else 0<br /> total_docs_per_sec = doc_count / total_duration if total_duration > 0 else 0<br /> bulk_per_sec = bulk_count / total_duration if total_duration > 0 else 0<br />
logger.info(f"数据生成完成!")
logger.info(f"开始时间: {start_datetime}")
logger.info(f"结束时间: {end_datetime}")
logger.info(f"总耗时: {total_duration:.2f}秒 ({total_duration/60:.2f}分钟)")
logger.info(f"总计完成: {bulk_count} 次bulk操作")
logger.info(f"总计插入: {doc_count} 条文档")
logger.info(f"最终索引大小: {final_size:.2f}GB")
logger.info(f"平均每次bulk耗时: {avg_bulk_time:.3f}秒")
logger.info(f"平均bulk速率: {bulk_per_sec:.2f}次/秒")
logger.info(f"平均文档写入速率: {total_docs_per_sec:.0f}条/秒")
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='四世同堂中文内容写入 Elasticsearch 脚本')
parser.add_argument('--host', default='localhost', help='ES 主机地址 (默认: localhost)')
parser.add_argument('--port', type=int, default=9200, help='ES 端口 (默认: 9200)')
parser.add_argument('--index', required=True, help='索引名称 (必填)')
parser.add_argument('--bulk-count', type=int, default=1000, help='目标 bulk 次数 (默认: 10000)')
parser.add_argument('--batch-size', type=int, default=1000, help='每次 bulk 的文档数量 (默认: 1000)')
parser.add_argument('--https', action='store_true', help='使用 HTTPS 协议')
parser.add_argument('--username', help='ES 用户名')
parser.add_argument('--password', help='ES 密码')
parser.add_argument('--no-verify-ssl', action='store_true', help='禁用 SSL 证书验证(默认已禁用)')
args = parser.parse_args()
protocol = "HTTPS" if args.https else "HTTP"
auth_info = f"认证: {args.username}" if args.username else "无认证"
ssl_info = "禁用SSL验证" if args.https else ""
logger.info(f"开始运行脚本,参数: {protocol}://{args.host}:{args.port}, 索引={args.index}, bulk次数={args.bulk_count}, {auth_info} {ssl_info}")
try:
generator = ESDataGenerator(
args.host,
args.port,
args.index,
args.bulk_count,
args.batch_size,
args.https,
args.username,
args.password,
not args.no_verify_ssl # 传入verify_ssl参数,但实际上总是False
)
generator.run()
except KeyboardInterrupt:
logger.info("用户中断了程序")
except Exception as e:
logger.error(f"程序运行出错: {e}")
logger.error(f"错误类型: {type(e).name}")
if name == "main":
main()
<br /> <br /> 根据脚本中的测试文本添加的词库如下:<br /> <br />
POST .analysis_ik/_doc
{
"dict_type": "main_dicts",
"dict_key": "test_dic",
"dict_content": """祁老人
祁天佑
韵梅
祁瑞宣
老二
钱默吟
小顺子
李四大爷
冠晓荷
小妞子
程长顺
老舍
李四大爷
小羊圈胡同
北平城
胡同
小茶馆
小门脸
小房屋
四合院
院子
祁家
小院子
杂货铺
小铺子
井边
街上
菜市场
门口
枝头
城市
房间
北京
清朝
民国
战乱
战争
日本人
抗战
大槐树
槐树
小鸟
羊
门脸
房屋
水桶
菜担子
铺子
老头儿
儿子
教书先生
儿媳妇
女人
大家庭
孩子
孩子们
街坊邻居
妻子
老人
文人
知识分子
青年
汉奸
岁月
一生
变迁
衰落
建立
态度
威望
尊严
学问
诗
性格
时局
见解
小天地
精神
慰藉
现实
无力
气节
笑声
生机
阴霾
天真
快乐
希望
烦恼
童年
生意
生活
物资
年代
经营
日子
邻里
文化
野心
敌人
选择
软弱
妥协
民族大义
个人利益
温暖
时期
未来
力量
压力
抗争
逃避
方式
时代
煎熬
折磨
笔触
众生相
人物
特点
命运
喜怒哀乐
内涵
达观
痛苦
坚强
堕落
缩影
威胁
困难
道德
家人
向往
关系
矛盾
温情
传统文化
组成部分
理想
教育
抱负
占领
写照
亮色
心理
原则
底线
节奏
意义
细节
乐趣
个性
约束
麻烦
担忧
反叛精神
束缚
反抗
后果
建筑
代表
故事
记忆
历史文化底蕴
破坏
作品
创伤
经历
教训
和平
窗口
清晨
春天
内心
玩耍
聊天
晒太阳
歌唱
合作
打水
卖奶
帮助
"""
}
<br /> <br /> 进行 2 次集中写入的记录如下:<br /> <br />ik_max_test
2025-07-13 20:15:33,294 - INFO - 开始时间: 2025-07-13 19:45:07
2025-07-13 20:15:33,294 - INFO - 结束时间: 2025-07-13 20:15:33
2025-07-13 20:15:33,294 - INFO - 总耗时: 1825.31秒 (30.42分钟)
2025-07-13 20:15:33,294 - INFO - 总计完成: 1000 次bulk操作
2025-07-13 20:15:33,294 - INFO - 总计插入: 1000000 条文档
2025-07-13 20:15:33,294 - INFO - 最终索引大小: 0.92GB
2025-07-13 20:15:33,294 - INFO - 平均每次bulk耗时: 1.790秒
2025-07-13 20:15:33,294 - INFO - 平均bulk速率: 0.55次/秒
2025-07-13 20:15:33,294 - INFO - 平均文档写入速率: 548条/秒
ik_custom_test
2025-07-13 21:17:47,309 - INFO - 开始时间: 2025-07-13 20:44:03
2025-07-13 21:17:47,309 - INFO - 结束时间: 2025-07-13 21:17:47
2025-07-13 21:17:47,309 - INFO - 总耗时: 2023.53秒 (33.73分钟)
2025-07-13 21:17:47,309 - INFO - 总计完成: 1000 次bulk操作
2025-07-13 21:17:47,309 - INFO - 总计插入: 1000000 条文档
2025-07-13 21:17:47,309 - INFO - 最终索引大小: 0.92GB
2025-07-13 21:17:47,309 - INFO - 平均每次bulk耗时: 1.986秒
2025-07-13 21:17:47,309 - INFO - 平均bulk速率: 0.49次/秒
2025-07-13 21:17:47,309 - INFO - 平均文档写入速率: 494条/秒
```
可以看到,有一定损耗,自定义词库词典的效率是之前的 90%。
相关阅读
- [IK 字段级别词典升级:IK reload API
](https://infinilabs.cn/blog/202 ... rys-2/) - [Easysearch 新功能: IK 字段级别词典
](https://infinilabs.cn/blog/202 ... narys/)
关于 IK Analysis

IK Analysis 插件集成了 Lucene IK 分析器,并支持自定义词典。它支持 Easysearch\Elasticsearch\OpenSearch 的主要版本。由 INFINI Labs 维护并提供支持。
该插件包含分析器:ik_smart 和 ik_max_word,以及分词器:ik_smart 和 ik_max_word
开源地址:<https://github.com/infinilabs/analysis-ik>
作者:金多安,极限科技(INFINI Labs)搜索运维专家,Elastic 认证专家,搜索客社区日报责任编辑。一直从事与搜索运维相关的工作,日常会去挖掘 ES / Lucene 方向的搜索技术原理,保持搜索相关技术发展的关注。
原文:https://infinilabs.cn/blog/202 ... ys-3/
- [IK 字段级别词典升级:IK reload API
IK 字段级别词典升级:IK reload API
INFINI Labs 小助手 发表了文章 • 0 个评论 • 3390 次浏览 • 2025-07-29 10:43
之前介绍 [IK 字段级别字典](https://infinilabs.cn/blog/202 ... narys/) 使用的时候,对于字典的更新只是支持词典库的新增,并不支持对存量词典库的修改或者删除。经过这段时间的开发,已经可以兼容词典库的更新,主要通过 IK reload API 来实现。
IK reload API
IK reload API 通过对词典库的全量重新加载来实现词典库的更新或者删除。用户可以通过下面的命令实现:
```
测试索引准备
PUT my-index-000001
{
"settings": {
"number_of_shards": 3,
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"type": "ik_smart",
"custom_dict_enable": true,
"load_default_dicts":false, # 这里不包含默认词库
"lowcase_enable": true,
"dict_key": "test_dic"
}
}
}
},
"mappings": {
"properties": {
"test_ik": {
"type": "text",
"analyzer": "my_custom_analyzer"
}
}
}
}
原来词库分词效果,只预置了分词“自强不息”
GET my-index-000001/_analyze
{
"analyzer": "my_custom_analyzer",
"text":"自强不息,杨树林"
}
{
"tokens": [
{
"token": "自强不息",
"start_offset": 0,
"end_offset": 4,
"type": "CN_WORD",
"position": 0
},
{
"token": "杨",
"start_offset": 5,
"end_offset": 6,
"type": "CN_CHAR",
"position": 1
},
{
"token": "树",
"start_offset": 6,
"end_offset": 7,
"type": "CN_CHAR",
"position": 2
},
{
"token": "林",
"start_offset": 7,
"end_offset": 8,
"type": "CN_CHAR",
"position": 3
}
]
}
更新词库
POST .analysis_ik/_doc
{
"dict_key": "test_dic",
"dict_type": "main_dicts",
"dict_content":"杨树林"
}
删除词库,词库文档的id为coayoJcBFHNnLYAKfTML
DELETE .analysis_ik/_doc/coayoJcBFHNnLYAKfTML?refresh=true
重载词库
POST _ik/_reload
{}
更新后的词库效果
GET my-index-000001/_analyze
{
"analyzer": "my_custom_analyzer",
"text":"自强不息,杨树林"
}
{
"tokens": [
{
"token": "自",
"start_offset": 0,
"end_offset": 1,
"type": "CN_CHAR",
"position": 0
},
{
"token": "强",
"start_offset": 1,
"end_offset": 2,
"type": "CN_CHAR",
"position": 1
},
{
"token": "不",
"start_offset": 2,
"end_offset": 3,
"type": "CN_CHAR",
"position": 2
},
{
"token": "息",
"start_offset": 3,
"end_offset": 4,
"type": "CN_CHAR",
"position": 3
},
{
"token": "杨树林",
"start_offset": 5,
"end_offset": 8,
"type": "CN_WORD",
"position": 4
}
]
}
<br /> <br /> 这里是实现索引里全部的词库更新。<br /> <br /> 也可以实现单独的词典库更新<br /> <br />
POST _ik/_reload
{"dict_key":"test_dic”}
debug 日志
[2025-07-09T15:30:29,439][INFO ][o.e.a.i.ReloadIK ] [ik-1] 收到重载IK词典的请求,将在所有节点上执行。dict_key: test_dic, dict_index: .analysis_ik
[2025-07-09T15:30:29,439][INFO ][o.e.a.i.a.TransportReloadIKDictionaryAction] [ik-1] 在节点 [R6ESV5h1Q8OZMNoosSDEmg] 上执行词典重载操作,dict_key: test_dic, dict_index: .analysis_ik
<br /> <br /> 这里传入的 dict_key 对应的词库 id。<br /> <br /> 对于自定义的词库存储索引,也可以指定词库索引的名称,如果不指定则默认使用 .analysis_ik<br /> <br />
POST _ik/_reload
{"dict_index":"ik_index"}
debug 日志
[2025-07-09T15:32:59,196][INFO ][o.e.a.i.a.TransportReloadIKDictionaryAction] [ik-1] 在节点 [R6ESV5h1Q8OZMNoosSDEmg] 上执行词典重载操作,dict_key: null, dict_index: test_ik
[2025-07-09T15:32:59,196][INFO ][o.w.a.d.ReloadDict ] [ik-1] Reloading all dictionaries
```
注:
- 更新或者删除词库重载后只是对后续写入的文档生效,对已索引的文档无效;
- 因为用户无法直接更改 IK 内置的词库(即默认配置路径下的词库文件),因此 reload API 不会影响内置词库的信息。
相关阅读
- [IK 字段级别词典的升级之路
](https://infinilabs.cn/blog/202 ... rys-3/) - [Easysearch 新功能: IK 字段级别词典
](https://infinilabs.cn/blog/202 ... narys/)
关于 IK Analysis

IK Analysis 插件集成了 Lucene IK 分析器,并支持自定义词典。它支持 Easysearch\Elasticsearch\OpenSearch 的主要版本。由 INFINI Labs 维护并提供支持。
该插件包含分析器:ik_smart 和 ik_max_word,以及分词器:ik_smart 和 ik_max_word
开源地址:<https://github.com/infinilabs/analysis-ik>
作者:金多安,极限科技(INFINI Labs)搜索运维专家,Elastic 认证专家,搜索客社区日报责任编辑。一直从事与搜索运维相关的工作,日常会去挖掘 ES / Lucene 方向的搜索技术原理,保持搜索相关技术发展的关注。
原文:https://infinilabs.cn/blog/202 ... ys-2/
- [IK 字段级别词典的升级之路