OpenSearch 与 ClickHouse/Doris 接入边界:何时从搜索转向分析
“找到标题里包含 blue 的商品”和“按租户统计所有当前商品价格”都能读取同一批业务数据,但需要不同的工作:前者要分词、匹配和排序,后者要确定统计范围并汇总数值。进一步加入历史版本后,还得先决定每次更新算一条新事实,还是只保留每个商品的最终状态。
表结构、索引和客户端都围绕这些查询含义选择。即使驱动能够发送相似的 SQL,旧版本覆盖、去重和删除仍由具体存储模型决定。
查询形状决定数据怎样组织
三种常见访问方式
| 查询任务 | 主要工作 | 通常需要关注 |
|---|---|---|
| 关键词搜索、筛选、相关度列表 | 分词、倒排求交、评分、Top K | analyzer、字段类型、排序质量、分页 |
| 宽时间范围、多维分组、趋势和报表 | 裁剪数据块、列式读取、聚合、关联 | 排序键、分区、扫描量、内存与并发 |
| 订单修改、库存扣减、精确事务查询 | 按业务键读写、约束、事务提交 | 事务隔离、并发冲突、索引和锁 |
OpenSearch 可以做聚合,ClickHouse 与 Doris 也提供文本检索或相关索引能力。实际取舍在于主查询工作量、更新模式和操作成本,不宜用“某产品只能搜索”“某产品只能统计”作绝对划分。
需要保留一份主业务存储时,通常由业务事务完成事实记录,再将所需数据投影到搜索或分析系统。投影可以针对访问方式构造成商品宽文档、事件明细或当前状态表;各副本都需要可解释的同步和删除策略。
OpenSearch:先找候选,再取值或聚合
倒排结构把词项关联到文档,适合从大量内容中快速找到匹配候选。text 用 analyzer 分析,keyword 用于精确过滤;doc values 提供按文档读取字段值的结构,服务排序和聚合。
一份商品文档可以展开品牌、分类和规格,减少查询时跨对象关联。代价是相关字段变更时,需要更新所有受影响的商品投影。字段和 nested 的具体机制见Mapping 与索引建模。
OpenSearch 的 sum、terms 和 date_histogram 能直接支持很多筛选统计与小型看板。高基数、多层聚合、跨大范围扫描和复杂关联增长时,要重新评估堆、分片与协调开销,而不是默认把所有分析都塞进同一个在线搜索集群。聚合入口见 Aggregations。
ClickHouse:排序、数据块和列式计算
MergeTree 家族把数据写入 part。每个 part 内按 ORDER BY 排序,稀疏主索引记录部分位置的键值,再以 granule 为读取和裁剪单位。查询若能约束排序键前缀,就有机会跳过不相关的范围;读取剩余范围时,通常只读取需要的列并批量执行计算。结构见 MergeTree。
part
├─ 排序键:(tenant, id)
├─ 稀疏主索引:定位可能包含目标 tenant 的 granule
└─ 列数据:只读取本次需要的 tenant、amount 等列PARTITION BY 用于数据管理与分区裁剪,ORDER BY 决定 part 内的排序和主要访问路径;两者需要分别选择。把每个用户或每个商品都做一个分区,会制造过多碎片,不能简单理解为“分得越细查询越快”。
MergeTree 的 PRIMARY KEY 主要服务跳过数据范围,不自动提供业务唯一性。要表达唯一当前行,必须选择相应引擎和查询方式。
Doris:Key Model 与分布式执行
Doris 按表模型决定保留明细、聚合相同键,还是维护唯一键的状态。Duplicate、Aggregate、Unique 分别适用于不同数据含义;模型选错,查询再正确也会统计错误对象。取舍见 Table Model Best Practices。
分区帮助管理与裁剪数据,分桶决定数据分布到哪些 tablet,排序键与前缀索引帮助缩小读取范围;倒排索引、物化视图等进一步服务特定查询。排序与前缀索引见 Prefix Index and Sort Key。
FE 负责元数据、解析和查询计划等工作,BE 执行存储和查询计算。生产环境要根据部署模式配置多个节点、数据持久化、副本和资源隔离。后面的 all-in-one 镜像将 FE、BE 放在同一容器,只用于集成实验。
更新、删除与统计对象
历史变化与当前商品是两张不同的表
共同的实验输入为:
| 租户 / 商品 | 版本 | 标题 | 价格 |
|---|---|---|---|
| a / p1 | 1 | Blue Shirt | 100 |
| a / p1 | 2 | Blue Shirt | 120 |
| a / p2 | 1 | Red Mug | 200 |
| b / p3 | 1 | Blue Book | 300 |
这是四条版本记录、三件商品。对历史记录直接 sum 得到 720;每件商品取最新版本后再 sum,得到 620。这里统计的是实验商品价格合计,不是销售收入;实际财务统计还需明确订单明细、币种、退款和发生时间。
事件明细表适合分析“发生过哪些变更”。当前状态表适合回答“现在有哪些有效商品”。如果把后者的查询直接用于前者,会重复计算旧价格。
ReplacingMergeTree 的去重键是完整 ORDER BY
当前商品表可以使用:
ENGINE = ReplacingMergeTree(version)
ORDER BY (tenant, id)后台合并按相同排序键保留较新版本,但合并时机不确定。需要在查询时得到归并后的结果,可以使用 FINAL;对事件明细也可以显式按业务键聚合最新行。Replacing 行为见 ReplacingMergeTree。
如果写成 ORDER BY (tenant, id, version),不同版本已经属于不同的排序键。FINAL 也不会把 p1 的版本 1 和 2 当成一组去重。实验中的错误键表在 FINAL 后仍保留四行。
选择当前字段时,应让多个字段来自同一个版本:
SELECT sum(latest.1)
FROM
(
SELECT argMax(tuple(price, deleted), version) AS latest
FROM events
GROUP BY tenant, id
)
WHERE latest.2 = 0;把价格和删除标记放进同一个 tuple,可以一起取到最大版本对应的状态。若同键同版本允许不同载荷,argMax 的并列最大值选择就不足以定义稳定业务结果;应保证版本唯一,或加入明确的排序字段。NULL 处理也需按函数和 tuple 语义核对,见 argMax。
不要先把所有 deleted=true 的历史行过滤掉,再找最大版本。这样会先删掉最新的删除记录,留下旧的有效状态,造成数据复活。应先选最终版本,再决定是否参与统计。
Doris 的唯一键还需要业务更新顺序
Unique Key 模型以相同键维护更新后的行。Merge-on-Write 在写入阶段处理覆盖,减少查询时合并旧版本的工作,适合常见的实时更新分析。完整模式见 Unique Key Model。
要让迟到旧事件不覆盖新状态,可将业务版本映射为 Sequence:
CREATE TABLE products_current (
tenant VARCHAR(20) NOT NULL,
id VARCHAR(20) NOT NULL,
version BIGINT NOT NULL,
title VARCHAR(80) NOT NULL,
price BIGINT NOT NULL,
deleted BOOLEAN NOT NULL
)
UNIQUE KEY(tenant, id)
DISTRIBUTED BY HASH(tenant, id) BUCKETS 1
PROPERTIES(
"replication_num"="1",
"enable_unique_key_merge_on_write"="true",
"function_column.sequence_col"="version"
);这里的单桶、单副本专供实验。Sequence 比较同键的版本,较大值决定替换方向;只依赖导入的到达与提交次序,较晚到达的旧业务状态仍可能覆盖新状态。规则见 Concurrent Update Control。
实验明确将 version 定义为 NOT NULL,并验证 NULL 输入被真实引擎拒绝。允许空版本、同版本不同载荷,或由两个数据流分别维护不可比较的版本,都需要额外契约;不要把它们当作“默认自动选择最新”的安全输入。
多个来源独立更新不同列时,单一全局 Sequence 可能无法表达各列的次序。Doris 还提供专门的多流 Sequence Mapping,但具有存储模式、列映射等限制,不能直接套用这张全行 MoW 表的配置。Multi-Stream Updates给出了相应条件。
删除和导入确认需要单独约定
实验使用业务字段 deleted 保留最新软删除状态,然后在查询中排除。它与 Doris 内部删除标记、ClickHouse 引擎特定的删除参数不是同一个机制。物理清理前,仍需防止上游旧事件再次写回。
导入操作是否“返回成功”,也要按模式解释。ClickHouse 26.3 对异步插入默认行为有调整;若异步等待刷入关闭,收到响应时数据可能尚在缓冲,并且后续错误不再直接返回给原请求。版本说明见 ClickHouse 26.3。实验 SQL 显式使用 async_insert=0,以同步插入为对照条件。
Doris 的批量导入应检查状态、过滤行、错误行和导入标识。SQL INSERT、Stream Load 与持续消费任务有不同的确认和恢复接口,参数见 Load Best Practices。接入程序不能只检查“HTTP 成功”就推进所有上游位点。
用同一组数据运行三个引擎
环境、身份与启动顺序
下载并解压搜索与分析引擎实验工程,进入 search-analytics-lab。需要 Linux Bash、Docker Engine/Compose、宿主 Java 17 或 25。构建使用 Maven 容器,运行驱动使用宿主 Java,再调用已授权的宿主 Docker CLI;不挂载 Docker socket。
镜像和资源如下:
| 服务 | 镜像 | 运行条件 |
|---|---|---|
| OpenSearch | public.ecr.aws/opensearchproject/opensearch:3.8.0 | 2 GiB,512 MiB 堆;镜像 UID 1000;回环 29222 |
| ClickHouse | clickhouse/clickhouse-server:26.3.25.2 | 1 GiB,UID/GID 101;容器内 default 账号 |
| Doris | apache/doris:all-in-one-4.1.3 | 4 GiB;镜像默认 root;容器内 SQL root 无密码 |
后两个默认数据库身份仅用于隔离的实验容器。ClickHouse 的 default 用户默认限制外部网络访问,实验通过容器内客户端连接;Doris 对宿主的端口绑定在回环。不要把这些身份配置复制到共享服务器。
ClickHouse 的 CPU 架构要求和容器身份配置见 Docker 安装。Doris all-in-one 的定位、资源和 FE/BE 健康检查见 All-in-One Image。OpenSearch 的内核参数与安全选项见 Docker 安装说明。
三个服务依次启动和停止,避免叠加资源占用。构建与检查宿主环境:
set -euo pipefail
command -v docker java
java -version
docker version
mkdir -p .m2
docker run --rm --memory 2g --entrypoint mvn \
--user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD":/work -v "$PWD/.m2":/m2 -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Duser.home=/tmp -Dmaven.repo.local=/m2 clean packagepackage 只完成构建,真实验证在后面的 EngineLab 命令中执行。驱动每次建立随机名称的专用索引或数据库,检查真实响应与 SQL 结果,最后删除自己创建的数据。失败时保留原始异常,清理错误作为附加异常返回。
OpenSearch:覆盖旧版本,再搜索与聚合
docker compose -p analytics22 --profile search up -d \
--wait --wait-timeout 180 opensearch
java -cp 'target/classes:target/dependency/*' \
dev.example.analytics.EngineLab opensearch
docker compose -p analytics22 stop opensearch驱动按业务 ID 写入四条版本变化,使用 external 版本保证更新次序。refresh 后,当前索引中有三份文档,价格 sum 为 620;租户 a 加 blue 的全文条件只返回 p1,价格为 120。
这说明 OpenSearch 可以直接对当前商品投影做聚合。若需要分析 p1 从 100 涨到 120 的历史,它的当前文档已经不包含完整变化过程,需要另行保存事件或历史版本。
ClickHouse:对照历史、最新行与错误排序键
docker compose -p analytics22 --profile column up -d \
--wait --wait-timeout 120 clickhouse
java -cp 'target/classes:target/dependency/*' \
dev.example.analytics.EngineLab clickhouse
docker compose -p analytics22 stop clickhouse实际 SQL 检查以下结果:
| 查询或变化 | 结果 |
|---|---|
| MergeTree 历史表行数 / sum | 4 / 720 |
| argMax 取每商品最新版本后 sum | 620 |
| 正确 ReplacingMergeTree 表 FINAL 后 sum | 620 |
| p1 的旧版本随后写入,FINAL 读取价格 | 120 |
| ORDER BY 错误包含 version,FINAL 后行数 | 4 |
| p2 软删除后再重放旧版本,有效商品 sum | 420 |
驱动还建立 6,400 行的独立扫描表,按 tenant、id 排序,设置每 granule 64 行以便观察。查询 tenant=3 得到 640 行,真实 EXPLAIN 展示 PrimaryKey 范围裁剪。在该单 part 数据组织下,观察到 Granules: 11/100;程序检查选中粒度少于全部粒度,不把固定 11 当成所有环境的性能标准。
读取粒度多于精确命中行所在的理想范围很正常,稀疏索引定位的是可能包含目标的块。它既不是逐行哈希索引,也不会承诺一次点查只读一行。计划字段见 EXPLAIN。
Doris:验证 Sequence、到达顺序与非法版本
docker compose -p analytics22 --profile mpp up -d \
--wait --wait-timeout 180 doris
docker compose -p analytics22 exec -T doris \
mysql --protocol=TCP -h127.0.0.1 -P9030 -uroot -e 'SHOW BACKENDS'
java -cp 'target/classes:target/dependency/*' \
dev.example.analytics.EngineLab doris
docker compose -p analytics22 stop dorisSHOW BACKENDS 应看到 BE 存活及 Doris 4.1.3 产品版本。SELECT VERSION() 可能返回 MySQL 协议兼容版本,例如 5.7.99,不能拿它判断当前安装的是哪个 Doris 发布包。协议差异见 MySQL Compatibility Notes。
驱动执行的真实结果为:
- 带 Sequence 的当前表有三件有效商品,sum 为 620。
- p1 的旧业务版本后来写入,价格仍为 120。
- 不配置 Sequence 的对照表按先新后旧两次独立 INSERT,最终价格变回 100。
- p2 软删除后重放旧版本,有效 sum 仍为 420。
- version=NULL 的 INSERT 非零退出,错误涉及 NULL;随后查询确认 bad 行没有写入。
容器显示 running 而 BE 还没有就绪时,不要执行导入。应等待镜像健康检查完成,并检查 SHOW BACKENDS;启动日志中的具体失败和 OOM 状态比仅看容器名更有用。
需要 Java 25 对照时,以 Java 25 的 java 命令运行相同三个入口。驱动编译目标仍为 17。每个入口失败会非零退出;只运行 package 没有执行三种服务上的检查。
接入应用后怎样维持一致理解
客户端负责协议,服务层负责查询含义
Java 应用可以通过 OpenSearch 官方客户端访问 JSON DSL,通过 ClickHouse 支持的客户端或 JDBC 访问分析查询,通过 MySQL 兼容驱动访问 Doris。选用兼容驱动后,仍需要校验类型转换、时区、批量插入、超时和连接池行为;不能据此假定所有 MySQL 事务、DDL 或 ORM 自动建表功能都兼容。
查询服务可以分开提供全文列表、当前汇总和历史分析接口。每个接口明确统计对象、过滤范围、时间单位和近似计算规则,避免共用一个名为 search 的接口却在后端切换不同含义的结果。
金额、时间和标识字段尤其值得统一:整数最小金额单位或 Decimal 的精度、UTC 与业务时区、字符串 ID 的前导零、空值与缺失字段,都应在投影契约中固定。
多份投影需要共同的处理位置
同一页面从 OpenSearch 取列表、从 ClickHouse 或 Doris 取统计时,两边同步进度可能不同。页面中的商品数量与汇总暂时不一致,可能来自消费滞后,也可能来自“当前状态”和“历史事件”统计口径混用。
要对齐同一批变化,需要保留业务键、版本、删除状态及各有序来源的已确认位置。最大事件时间无法排除中间漏事件;单个全局 lag 数字也可能掩盖某个分区长期卡住。出现差异时,先统一范围和统计对象,再按键检查版本及内容。
历史归档或重建必须能够解释旧 schema。给新字段填默认值会改变统计含义时,需要明确迁移规则;相关的回填与双写问题见重建索引、Alias 与 CDC。
以代表性负载判断是否分离引擎
适合评估独立分析系统的迹象包括:聚合和导出长期挤占在线搜索,查询需要扫描大量字段或宽时间区间,复杂分组与关联成为主要工作,或历史事件与当前文档的存储需求已经明显不同。
评估时使用真实字段基数、数据体积、分区分布、并发查询、更新频率和恢复要求,记录扫描行/字节、CPU、内存、尾延迟及运维成本。几条实验数据只能验证数据语义和计划结构,无法形成产品性能排名。
OpenSearch 已能以可接受成本完成常用筛选统计时,可以继续保留这一套方案。增加引擎的收益需要覆盖新增的同步、权限、备份、监控和故障恢复工作。
最后停止并移除本实验的容器:
docker compose -p analytics22 --profile search --profile column --profile mpp downOpenSearch 和 ClickHouse 的命名卷仍保留;Doris all-in-one 未配置持久卷,移除该实验容器会删除其容器内数据。EngineLab 已正常结束时,自己创建的临时数据库已清理。若中途退出,先依据错误中的精确资源名核对,不要批量删除其他数据库或 Docker 卷。
权威资料与规范地址
OpenSearch 检索聚合与部署
- OpenSearch 聚合:https://docs.opensearch.org/latest/aggregations/
- OpenSearch Docker:https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
ClickHouse 存储、去重与查询计划
- MergeTree 结构:https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/mergetree
- ReplacingMergeTree:https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/replacingmergetree
- argMax:https://clickhouse.com/docs/reference/functions/aggregate-functions/argMax
- ClickHouse 26.3 行为变化:https://clickhouse.com/blog/clickhouse-release-26-03
- ClickHouse Docker:https://clickhouse.com/docs/get-started/setup/self-managed/docker
- ClickHouse EXPLAIN:https://clickhouse.com/docs/reference/statements/explain
Doris 表模型、更新与导入
- Doris 表模型:https://doris.apache.org/docs/4.x/table-design/data-model/tips/
- Doris 排序与前缀索引:https://doris.apache.org/docs/4.x/table-design/index/prefix-index/
- Doris Unique Key:https://doris.apache.org/docs/4.x/table-design/data-model/unique/
- Doris Sequence:https://doris.apache.org/docs/4.x/data-operate/update/unique-update-concurrent-control/
- Doris 多流更新:https://doris.apache.org/docs/4.x/data-operate/update/multi-stream-update-for-unique-model/
- Doris 导入:https://doris.apache.org/docs/4.x/data-operate/import/load-best-practices/
- Doris all-in-one:https://doris.apache.org/community/developer-guide/all-in-one-image/
- Doris MySQL 兼容说明:https://doris.apache.org/docs/4.x/query-data/mysql-compatibility/
