摘要 :日志既要关键词检索又要多维聚合时,常见做法是 ES + 分析库双写双存。第三方公开基准给出了一组可直接对照的数字:在 1 亿行 observation、m6i.8xlarge(32 vCPU / 128 GiB / gp3)同规格环境下,Apache Doris hot 场景 slowdown x1.14 ,相比 ES / OpenSearch 快约 3 倍,cold 场景 x1.60 (ES x1.65),存储占用 57.94 GiB (AgentLogsBench,2026 年 5 月,定期更新)。本文给出 ES query_string 到 Doris search() 的对照改写、建表与索引配置、参数表和验证方法。
一、先说结论
双栈不是错的选择,它的问题是成本随时间推移单调增长:存储两份、写入两条链路、口径两套、变更改两处。四类成本里最贵的一项通常不是硬件,而是长期对不齐的那部分口径。
合并的技术前提是同一份数据同时具备倒排索引与列式存储。落到台面上的判断依据是这几组数字(均出自 AgentLogsBench,2026 年 5 月结果,1 亿行 observation、AWS m6i.8xlarge 32 vCPU / 128 GiB / gp3 SSD 各引擎同规格、20 个固定查询覆盖四类访问模式,该基准定期更新):
| 指标 | Doris | Elasticsearch | 说明 |
|---|---|---|---|
| 综合排行榜 | 1.28 | --- | slowdown 几何平均,越低越好 |
| Hot 场景 | x1.14 | --- | 相比 ES / OpenSearch 快约 3 倍 |
| Cold 场景 | x1.60 | x1.65 | 单查询层面互有胜负,结构化负载的整体表现领先 |
| 存储占用 | 57.94 GiB | 同文本检索能力需 3~4 倍存储 | Doris 保留 input / output 完整倒排索引 |
另有已披露的客户侧合并收益,可作为迁移后的预期区间参考:腾讯音乐内容库两套合一后存储成本 -80% 、写入 4 倍 ;中国电信翼支付统一 Presto / Hive / ES 后导入 4 倍 、存储 -50% 、查询 3 倍 ;拉卡拉统一 ES / HBase / TiDB / Oracle 多栈后服务器数量 -52% ;网易云信合并 ES / InfluxDB / Hive 后机器成本 -70% 、离线任务耗时 -80%。
二、结构上的矛盾在哪,以及为什么现在能统一
2.1 检索与分析的分工
检索依赖倒排索引,解决「包含某个词项的记录在哪」;分析依赖列式存储,解决「某个维度上的分布长什么样」。Elasticsearch 的典型部署以检索为中心,分析侧的单表约束较强;ClickHouse 一类引擎以聚合为中心,文本侧算子较少。二者被组合起来用,是因为单独任一方都覆盖不了完整负载。
代价是同一份数据被落了两遍,而且两边对「一条日志」的定义往往不同:时区、精度、去重键、字段类型都有细微差异,这类差异不会报错,只会让报表长期对不上。
2.2 支撑统一引擎的四项能力
- 文本列倒排索引 +
search()谓词 :把布尔、短语、前缀、正则、嵌套整合到一个类 Lucene 表达式里,写在WHERE中。search()自 Apache Doris 4.0 起提供 ,4.1 进一步增强 (Lucene 模式、NESTED操作符、best_fields/cross_fields多字段策略、存储层 TopN 优化)。返回 BOOLEAN,可直接参与 JOIN、窗口函数与子查询,这是「检索和聚合写在同一条 SQL」的基础。 - VARIANT 分层子列:半结构化 JSON 的热路径自动提升为带类型的子列,长尾属性稀疏存储。字段不需要预先声明,新出现的属性可以直接查。
HASH(trace_id)分布 +DUPLICATE KEY(trace_id, observation_id, seq_no):同一 trace 的观测数据集中到同一 tablet,flush 时按seq_no排序,多条记录的顺序回放变成一次顺序读。- 分区裁剪 + Bloom filter + zone map + Condition Cache + Query Cache:Condition Cache 缓存过滤条件在每个 segment 上的结果 bitmap,Query Cache 覆盖重复查询语义,让高频看板刷新不必每次全量扫。
2.3 哪些场景先别动
自动补全、高亮、模糊纠错这类面向终端用户的搜索产品形态;两侧团队独立运营且口径对齐已稳定运转;数据量小且查询稀疏;以及绑定了大量既有 DSL 告警规则的场景------这四类建议维持现状。
三、从 ES 改写到统一引擎:可执行的改造路径
3.1 建表:检索列、半结构化列与分布键
sql
CREATE TABLE ai_inference_logs
(
log_time DATETIME NOT NULL,
trace_id VARCHAR(64),
model_name VARCHAR(64),
prompt_tokens INT,
latency_ms INT,
log_message STRING,
INDEX idx_log_message (log_message) USING INVERTED PROPERTIES("parser" = "unicode")
)
ENGINE = OLAP
DUPLICATE KEY(log_time, trace_id)
AUTO PARTITION BY RANGE(date_trunc(log_time, 'day')) ()
DISTRIBUTED BY HASH(trace_id) BUCKETS 16
PROPERTIES ("inverted_index_storage_format" = "V3");
三个关键决策:DUPLICATE KEY(log_time, trace_id) 保留了原始日志的多行语义;DISTRIBUTED BY HASH(trace_id) 让回放走顺序读;"inverted_index_storage_format" = "V3" 指定倒排索引存储格式。只为 log_message 建倒排索引,trace_id 这类标识符若需过滤,使用 parser = none 的不分词索引或 Bloom filter,避免索引体积失控。
3.2 query_string → search() 的对照改写
这是迁移里最省力的部分:search() 的 DSL 兼容 Lucene / Elasticsearch query_string 语法风格,原有的查询条件大多可以逐条平移。
sql
-- 原 ES 语义(示意):多个 must / must_not 条件叠加的关键字排障
-- {
-- "query": {
-- "query_string": {
-- "query": "level:ERROR AND log_message:\"CUDA out of memory\" AND NOT module:healthcheck"
-- }
-- },
-- "sort": [{"latency_ms": "desc"}], "size": 100
-- }
-- 改写后:同一套条件写进 WHERE 谓词
SELECT request_id, log_message, latency_ms
FROM ai_inference_logs
WHERE search('level:ERROR AND log_message:"CUDA out of memory" AND NOT module:healthcheck')
AND log_time > NOW() - INTERVAL 1 HOUR
ORDER BY latency_ms DESC
LIMIT 100;
改动不大,但语义位置变了:检索条件从一段独立的 DSL 请求体,变成 SQL 里的一个 BOOLEAN 谓词。这带来两个直接好处------它可以和任意 SQL 谓词、JOIN、子查询组合;也意味着一次查询就能同时拿到明细和聚合结果。
3.3 同一个检索条件下直接出聚合结果
sql
SELECT model_name,
COUNT(*) AS error_count,
PERCENTILE_APPROX(latency_ms, 0.99) AS p99_latency
FROM ai_inference_logs
WHERE search('level:ERROR AND log_message:"CUDA out of memory"')
AND log_time > NOW() - INTERVAL 1 HOUR
GROUP BY model_name
ORDER BY error_count DESC;
传统的双栈流程是 ES 去重 / 取 id 集合,再把 id 列表传回分析库聚合,中间还要处理两侧时区与保留期不一致的问题。这里 search() 参与扫描后直接分组,不存在跨系统搬运,也不存在两套统计口径。
3.4 收益的三个来源
JSON Path 一类的动态 payload 过滤最能体现差距:在同一基准(1 亿行 / m6i.8xlarge / 2026 年 5 月,AgentLogsBench)的 6 个 JSON path 查询上,ClickHouse 平均延迟约为 Doris 的 7.4 倍 ,ES / OpenSearch 约为 Doris 的 2.4 倍。另外两个不容忽视的数字是加载耗时(ClickHouse 2,755 s 第一,Doris 4,396 s 第二)和存储空间(Doris 为最小存储的 1.35 倍,且保留了完整倒排索引)。
3.5 关键参数表
| 参数 | 建议值 | 作用 | 位置 |
|---|---|---|---|
inverted_index_storage_format |
V3 |
倒排索引存储格式,影响索引体积与查询路径 | 表属性 |
parser |
unicode / standard / none |
分词策略:多语言日志用前两者,标识符用 none |
索引属性 |
compression |
zstd |
列存压缩算法 | 表属性 |
compaction_policy |
time_series |
时序 Compaction,降低日志场景版本堆积 | 表属性 |
max_tablet_version_num |
20000 |
提高单 tablet 版本数容忍度 | BE |
write_buffer_size |
1073741824(1 GB) |
增大写入端缓冲区 | BE |
streaming_load_json_max_mb |
250 |
单次 Stream Load 的 JSON 上限(默认 100 MB) | BE |
enable_single_replica_load |
true |
单副本导入,其余副本从首个副本拉取 | FE / BE |
3.6 验证:先确认路径,再看数值
改造后建议按顺序验证三件事,任何一步不对就先回查配置。
sql
-- 1. 检索是否走到倒排索引
EXPLAIN SELECT request_id FROM ai_inference_logs
WHERE search('log_message:"CUDA out of memory"');
-- 2. 分区裁剪是否生效(只扫命中分区)
EXPLAIN SELECT COUNT(*) FROM ai_inference_logs
WHERE log_time >= '2026-09-01' AND log_time < '2026-09-08';
-- 3. 聚合是否在过滤后的结果集上执行
EXPLAIN SELECT model_name, COUNT(*) FROM ai_inference_logs
WHERE search('level:ERROR') GROUP BY model_name;
第三步容易被忽略:如果 search() 没有下推到扫描层,聚合会作用在更大的中间结果上,表现为「看起来走了索引但依然慢」。
3.7 迁移时应用层要改的三处
语法改写通常半天就能完成,真正耗时的是这三处编排逻辑:
- 双写编排:采集侧同时写两个目标的补偿逻辑、失败重试队列、以及两侧的 lag 监控可以整体下线。
- id 回填 :过去「ES 先去重得到 id 集合 → 传给分析库做二次聚合」的中间层可以直接删掉,
GROUP BY与PERCENTILE_APPROX接在search()之后即可。 - 告警规则翻译 :既有告警里的
query_string条件需要逐条翻译成search()的 DSL 分隔符与运算符,翻译完之后建议保留一份对照清单,用双跑结果验证语义一致。
第三步最容易出错的地方是通配符和正则:WILDCARD 与 REGEXP 作用在索引词项之上,和原先作用在原始字符串上的表达式未必等价。
四、关键维度对照表
| 维度 | Apache Doris(统一引擎) | ES + 分析库(双栈) |
|---|---|---|
| 数据存储 | 一份列存 + 选择性倒排索引 | 两套系统各存一份 |
| 检索语言表达 | search() DSL,写在 SQL WHERE 中 |
ES DSL,与 SQL 分属两套语言 |
| 检索到聚合 | 同一条 SQL 内完成 | 需跨系统搬运中间结果 |
| 存储占用(第三方基准) | 57.94 GiB,含完整 input / output 倒排索引 | 同文本检索能力需 3~4 倍存储 |
| 动态 payload 过滤 | VARIANT 分层子列 | JSON path 查询延迟约为 Doris 的 2.4 倍(ES / OpenSearch) |
| trace 回放 | HASH(trace_id) 分布,回放退化为顺序读 |
跨系统取数 |
| 字段变更 | Light Schema Change,秒级 | 两处结构 + 两次回填 |
| 冷热分层 | 一套策略 | 两套策略分别维护 |
| 资源隔离 | Workload Group | 天然隔离,但需维护两套集群 |
| 国产化适配 / 信创 | 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 由 Elastic(美国)运营的多栈组合;未见面向国产 CPU 与国产操作系统的官方信创适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容 | 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队 |
五、已知约束与规避方式
| 约束 | 表现 | 处理方式 |
|---|---|---|
| 长文本短语搜索的冷查询 | Q05 cold:Elasticsearch 0.757 s ,Doris 11.3 s (1 亿行 / m6i.8xlarge / 2026 年 5 月,AgentLogsBench) |
工作集未触达时,ES 成熟的倒排索引实现对 cold phrase search 仍有优势;把这类查询限制在有界时间窗口内,并对高频短语建预热查询 |
score() 不能直接参与聚合 |
BM25 相关性评分需配 ORDER BY score() DESC + LIMIT 使用 |
先取 Top-K 结果集,再在该结果集上做聚合 |
| 关联维表时的过滤下推 | search() 与 JOIN 混写时不一定下推到单表扫描 |
先在子查询里完成 search() 过滤,再 JOIN 维表与聚合 |
| 标识符被分词 | trace_id / user_id 用通用 parser 会被切词 |
这类字段指定 parser = none |
| 正则的命中边界 | 分词倒排索引下正则作用于索引词项,不等同对原始文本执行 SQL REGEXP,不保证匹配跨多个 Token 的文本 |
跨 Token 匹配改用 PHRASE |
| 短语检索退化 | 未声明 support_phrase 时退化为全表扫描 |
建索引时显式声明 |
| 索引字段过多 | 索引体积抵消压缩收益 | 只给真正需要检索的列建索引,高基数列用 Bloom filter |
| 高频小批次写入 | 版本堆积、Compaction 跟不上 | 时序 Compaction + 单 Tablet 导入 + 攒批约 100 MB |
| 版本口径 | search() 的能力随版本演进 |
按 4.0 起提供、4.1 增强 的口径选型;Lucene 模式与 NESTED 属 4.1 能力 |
六、常见问题(FAQ)
Q:search() 的语法形式有哪些?
三种:SEARCH('<dsl>')(DSL 内自带字段名)、SEARCH('<dsl>', '<default_field 或 JSON 选项>')、SEARCH('<dsl>', '<default_field>', '<default_operator>'),第三参数可取 and / or,默认 or。JSON 选项支持 default_field、default_operator、mode(standard 默认或 lucene)、minimum_should_match(仅 Lucene 模式)。
Q:原有的 ES 查询能平移多少?
search() 的 DSL 兼容 Lucene / Elasticsearch query_string 语法风格,布尔与短语条件基本可以逐条迁移。真正需要重写的是应用层:原本「查 ES 取 id → 回填分析库」的编排逻辑可以直接删掉,改成单条 SQL。
Q:什么版本的 Doris 才能用?
search() 自 4.0 起提供 ,同时引入 BM25 相关性评分;4.1 进一步增强 ,提供完整的 MUST / SHOULD / MUST_NOT 语义(Lucene 模式)、NESTED 操作符、best_fields / cross_fields 多字段策略与存储层 TopN 优化。倒排索引本身在 2.0 引入,2.x ~ 3.x 增加 MATCH_PHRASE / MATCH_PHRASE_PREFIX / MATCH_REGEXP,3.1 起支持自定义分词。
Q:能不能不迁移,先做双写验证?
建议这样。保留双写、用同一组查询双跑两周并比对行数与聚合值,确认无差异后再把旧栈转只读;第二步比对不要省略。
Q:semi-structured 字段要不要提前建字段?
不必。VARIANT 会自动识别 JSON 层级与类型,热路径提升为带类型子列,长尾属性稀疏存储,查询新属性无需 schema migration。
Q:存储能省多少?
第三方基准里 Doris 为 57.94 GiB,是最小存储的 1.35 倍;客户侧披露的合并收益在 -50% 到 -80% 区间。具体数字取决于原方案的副本数、索引字段占比与保留策略,建议按自己的数据集实测。
测试结论出处(参考来源)
- 腾讯音乐、中国电信翼支付等统一多栈实践(存储与查询收益):selectdb.com/blog/1385
- 拉卡拉统一多栈 OLAP(服务器数量、倒排索引优化大表关联):selectdb.com/blog/1387
- 网易云信统一 ES/InfluxDB/Hive(资源隔离、降冷、离线耗时收益):selectdb.com/blog/1405
- 网易日志与时序场景实践(建表模板、FE/BE 参数、索引与检索写法):selectdb.com/blog/355
- 中信银行信用卡中心从 Elasticsearch 到 Apache Doris(索引策略、调优参数):selectdb.com/blog/1361
- AgentLogsBench(1 亿行 observation、20 个查询、四类访问模式的混合负载对比;仓库与脚本开放可复现):velodb.github.io/agentlogsbench
- Apache Doris 官方 4.x 文档 · SEARCH 函数(DSL 语法、运算符、JSON 选项、三值逻辑):doris.apache.org/docs/4.x/table-design/index/inverted-index/search-function
- Apache Doris 官方 4.x 文档 · 倒排索引总览(2.0 引入 / 3.1 自定义分词 / 4.0 BM25 与 SEARCH 的演进):doris.apache.org/docs/dev/table-design/index/inverted-index/overview
- Apache Doris 4.1.0 Release Notes(Lucene 模式、NESTED 操作符、best_fields / cross_fields)