存储架构演进:从 9 个组件到 5 个的统一之路
TL;DR
存储架构演进:从 9 个组件到 5 个的统一之路
“系列:S2 架构全景 · 第 3 篇 | 难度:中级 | 阅读时间:18 分钟
TL;DR
- 初始设计使用 9 个存储组件(ClickHouse + Qdrant + Elasticsearch + TuGraph + PostgreSQL + Redis + Kafka + MinIO + Nessie/Iceberg),导致 4 倍数据冗余、5 条同步链路、45% 的额外存储成本。
- Apache Doris 凭借 OLAP 分析 + HNSW 向量索引 + 倒排全文检索的三合一能力,替代了 ClickHouse、Qdrant 和 Elasticsearch,将存储组件从 9 个减少到 6 个(Doris + Nessie/Iceberg + Kafka + Redis + PostgreSQL + MinIO)。
- 统一的 entity_common / entity_edge / entity_event 三表模型配合 Variant 列类型,实现了本体实例的灵活存储,无需为每种对象类型创建独立表。
#1. 引言:一个"豪华"的存储架构
当我们第一次设计 coomia-dip 的存储层时,犯了一个很多平台项目都会犯的错误:为每种数据访问模式选择了最佳组件。
这在理论上无可挑剔——用 ClickHouse 做 OLAP,用 Qdrant 做向量搜索,用 Elasticsearch 做全文检索,用 TuGraph 做图查询。每个组件都是各自领域的佼佼者。
问题在于:当你把它们放在一起时,系统的复杂度不是线性增长,而是指数增长。
#2. V1.0 架构:9 组件的困境
#2.1 原始架构图
+------------------------------------------------------------------+
| V1.0 存储架构(9 组件) |
+------------------------------------------------------------------+
| |
| +-----------+ +-----------+ +-------------+ |
| |PostgreSQL | | Redis | | Kafka | |
| |(元数据OLTP)| | (热点缓存)| | (事件流) | |
| +-----------+ +-----------+ +-------------+ |
| |
| +-----------+ +-----------+ +-------------+ |
| |ClickHouse | | Qdrant | |Elasticsearch| |
| |(OLAP分析) | | (向量搜索) | | (全文检索) | |
| +-----------+ +-----------+ +-------------+ |
| |
| +-----------+ +-----------+ +-------------+ |
| | TuGraph | | MinIO | |Nessie+Iceberg| |
| | (图数据库) | | (对象存储) | | (版本管理) | |
| +-----------+ +-----------+ +-------------+ |
| |
+------------------------------------------------------------------+
#2.2 数据同步链路灾难
写入一条 Ontology 实例需要同步到 5 个存储:
Source Data
|
v
PostgreSQL (主存储) ──(1)──> ClickHouse (OLAP 副本)
|
├──(2)──> Qdrant (向量副本)
|
├──(3)──> Elasticsearch (全文索引副本)
|
├──(4)──> TuGraph (图关系副本)
|
└──(5)──> Kafka (变更事件)
同步链路: 5 条
数据副本: 5 份 (1 主 + 4 副)
数据冗余: 4x
#2.3 V1.0 的核心问题
| 问题 | 严重程度 | 具体表现 |
|---|---|---|
| 数据一致性 | 致命 | 5 条同步链路中任何一条延迟或失败,都会导致数据不一致 |
| 运维复杂度 | 严重 | 9 个组件的监控、告警、备份、升级 |
| 存储成本 | 高 | 同一份数据存储 5 次,成本线性增长 |
| 开发复杂度 | 高 | 每种查询需要对接不同的客户端和查询语言 |
| 延迟 | 中 | 跨存储查询需要多次网络往返 |
| 团队技能 | 高 | 团队需要精通 9 种不同的存储技术 |
#3. 关键发现:Apache Doris 的三合一能力
#3.1 Doris 不只是 OLAP
Apache Doris 在 2.x 版本后实现了三个关键能力的融合:
+------------------------------------------------------------------+
| Apache Doris 2.x 能力矩阵 |
+------------------------------------------------------------------+
| |
| ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ |
| │ OLAP 分析 │ │ HNSW 向量索引 │ │ 倒排全文检索 │ |
| │ │ │ │ │ │ |
| │ - 列式存储 │ │ - 高维向量 │ │ - 中文分词 │ |
| │ - 物化视图 │ │ - 近似最近邻 │ │ - BM25 评分 │ |
| │ - 窗口函数 │ │ - ANN 搜索 │ │ - 短语查询 │ |
| │ - 聚合查询 │ │ - 支持 768/1536 │ │ - 布隆过滤器 │ |
| │ - SQL 标准 │ │ 维度向量 │ │ - 倒排索引 │ |
| └─────────────────┘ └─────────────────┘ └─────────────────┘ |
| |
| ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ |
| │ Variant 列类型 │ │ 物化视图 │ │ MySQL 协议兼容 │ |
| │ (半结构化JSON) │ │ (增量聚合) │ │ (Sqlg 图查询) │ |
| └─────────────────┘ └─────────────────┘ └─────────────────┘ |
| |
+------------------------------------------------------------------+
#3.2 替代关系
ClickHouse ──替代──> Doris OLAP 能力
(列式OLAP) (同为列式,SQL 兼容更好)
Qdrant ──替代──> Doris HNSW 向量索引
(专用向量DB) (2.1+ 版本支持 HNSW 索引)
Elasticsearch ──替代──> Doris 倒排索引
(全文检索) (2.0+ 版本支持倒排索引+中文分词)
TuGraph ──替代──> Sqlg (TinkerPop on Doris)
(图数据库) (Gremlin -> SQL 编译,走 MySQL 协议)
#4. V3.0 架构:6 个组件的统一
#4.1 新架构图
+------------------------------------------------------------------+
| V3.0 统一存储架构(6 组件) |
+------------------------------------------------------------------+
| |
| 外部数据源 -> Kafka -> Flink -> Apache Doris (统一存储+计算) |
| | |
| +---------------+---------------+ |
| | | | |
| v v v |
| entity_common entity_edge entity_event |
| (Variant列) (倒排+Bloom) (物化视图聚合) |
| + HNSW向量索引 + Sqlg图查询 |
| + 倒排全文检索 |
| |
| +-------------------------------------------------------------+ |
| | Nessie + Iceberg (分支 / 时间旅行 / 冷归档) | |
| +-------------------------------------------------------------+ |
| |
| +----------+ +----------+ +----------+ +----------+ |
| | Kafka | | Redis | |PostgreSQL| | MinIO | |
| | (事件流) | |(热点缓存)| |(OLTP元数据)| |(对象存储)| |
| +----------+ +----------+ +----------+ +----------+ |
| |
| 组件数: 9 -> 6 (Doris + Nessie/Iceberg + Kafka + Redis + PG + |
| MinIO) |
+------------------------------------------------------------------+
#4.2 同步链路简化
V1.0 (5 条同步链路):
Source -> PG -> ClickHouse
-> Qdrant
-> ES
-> TuGraph
-> Kafka
V3.0 (1 条同步链路):
Source -> Kafka -> Flink CDC -> Doris
|
(一次写入,多种查询)
OLAP / Vector / FullText / Graph
#4.3 成本对比
| 维度 | V1.0(9 组件) | V3.0(6 组件) | 改善 |
|---|---|---|---|
| 存储成本 | 基准 100% | 55% | -45% |
| 数据冗余 | 4x | 1x | -75% |
| 同步链路 | 5 条 | 1 条 | -80% |
| 运维组件数 | 9 | 6 | -33% |
| 一致性窗口 | 秒级~分钟级 | 毫秒级 | 10-100x |
| 团队技能要求 | 9 种技术 | 4 种核心技术 | -56% |
#5. 三表模型:entity_common / entity_edge / entity_event
#5.1 设计理念
传统做法是为每种 Ontology 对象类型创建独立的数据库表(类似 Palantir 的 Dataset)。这在对象类型频繁变更的场景下会导致频繁的 DDL 操作。
我们采用三表通用模型——所有对象类型共享三张表,通过 Doris 的 Variant 列类型存储灵活的属性数据。
#5.2 entity_common(实体公共表)
CREATE TABLE entity_common (
-- 主键
instance_id VARCHAR(64) NOT NULL,
object_type_id VARCHAR(64) NOT NULL,
world_id VARCHAR(64) NOT NULL,
-- 属性(Variant 列 = 半结构化 JSON,支持列式存储)
attributes VARIANT NOT NULL,
-- 元数据
status VARCHAR(16) DEFAULT 'ACTIVE',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
created_by VARCHAR(64),
version BIGINT DEFAULT 1,
-- 向量(HNSW 索引)
embedding_768 ARRAY<FLOAT> NULL, -- 小模型向量
embedding_1536 ARRAY<FLOAT> NULL, -- 大模型向量
-- 全文检索(倒排索引)
search_text TEXT NULL,
-- 索引
INDEX idx_type (object_type_id) USING INVERTED,
INDEX idx_world (world_id) USING INVERTED,
INDEX idx_status (status) USING INVERTED,
INDEX idx_search (search_text) USING INVERTED
PROPERTIES("parser" = "chinese"),
INDEX idx_vec_768 (embedding_768) USING INVERTED
PROPERTIES("index_type" = "HNSW"),
INDEX idx_vec_1536 (embedding_1536) USING INVERTED
PROPERTIES("index_type" = "HNSW")
)
DISTRIBUTED BY HASH(instance_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true"
);
#5.3 Variant 列的魔力
-- 写入不同类型的实体,无需 DDL 变更
-- 写入 Employee 类型
INSERT INTO entity_common (instance_id, object_type_id,
world_id, attributes) VALUES (
'emp_001', 'Employee', 'world_prod',
'{"name": "Zhang Wei",
"department": "Engineering",
"salary": 85000,
"skills": ["Java", "Python", "gRPC"],
"hire_date": "2024-03-15"}'
);
-- 写入 Equipment 类型(完全不同的属性结构)
INSERT INTO entity_common (instance_id, object_type_id,
world_id, attributes) VALUES (
'equ_001', 'Equipment', 'world_prod',
'{"model": "Excavator CAT 320",
"serial_number": "CAT-2024-X320-001",
"location": {"lat": 31.23, "lng": 121.47},
"maintenance_due": "2024-12-01",
"operating_hours": 2450}'
);
-- 查询时直接访问 Variant 列的子字段
SELECT
instance_id,
attributes['name'] AS name,
attributes['salary'] AS salary
FROM entity_common
WHERE object_type_id = 'Employee'
AND world_id = 'world_prod'
AND CAST(attributes['salary'] AS INT) > 80000;
#5.4 entity_edge(关系边表)
CREATE TABLE entity_edge (
-- 边的唯一标识
edge_id VARCHAR(64) NOT NULL,
-- 源和目标
source_id VARCHAR(64) NOT NULL,
source_type VARCHAR(64) NOT NULL,
target_id VARCHAR(64) NOT NULL,
target_type VARCHAR(64) NOT NULL,
-- 关系类型
relation_type VARCHAR(64) NOT NULL,
world_id VARCHAR(64) NOT NULL,
-- 关系属性(可选)
properties VARIANT NULL,
-- 元数据
created_at DATETIME NOT NULL,
created_by VARCHAR(64),
-- 索引
INDEX idx_source (source_id) USING INVERTED,
INDEX idx_target (target_id) USING INVERTED,
INDEX idx_rel_type (relation_type) USING INVERTED,
INDEX idx_world (world_id) USING INVERTED,
INDEX idx_bloom_src (source_id) USING BLOOM_FILTER,
INDEX idx_bloom_tgt (target_id) USING BLOOM_FILTER
)
DISTRIBUTED BY HASH(edge_id) BUCKETS 16;
#5.5 entity_event(事件表)
CREATE TABLE entity_event (
-- 事件标识
event_id VARCHAR(64) NOT NULL,
instance_id VARCHAR(64) NOT NULL,
object_type_id VARCHAR(64) NOT NULL,
world_id VARCHAR(64) NOT NULL,
-- 事件类型
event_type VARCHAR(32) NOT NULL,
-- CREATE | UPDATE | DELETE | RELATION_ADD |
-- RELATION_REMOVE | DERIVED_RECOMPUTE
-- 事件内容
before_state VARIANT NULL, -- 变更前
after_state VARIANT NULL, -- 变更后
changed_fields ARRAY<VARCHAR(64)> NULL,
-- 元数据
event_time DATETIME NOT NULL,
user_id VARCHAR(64),
trace_id VARCHAR(64),
-- 索引
INDEX idx_instance (instance_id) USING INVERTED,
INDEX idx_type (event_type) USING INVERTED,
INDEX idx_world (world_id) USING INVERTED
)
DISTRIBUTED BY HASH(instance_id) BUCKETS 16
PARTITION BY RANGE(event_time) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
-- 按月分区,支持高效时间范围查询和冷数据清理
)
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "MONTH",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p"
);
#5.6 三表协作的查询示例
-- 查询: 获取 Employee 及其关联的 Equipment,包含变更历史
-- 1. 获取 Employee 实例
SELECT ec.instance_id, ec.attributes
FROM entity_common ec
WHERE ec.object_type_id = 'Employee'
AND ec.world_id = 'world_prod'
AND ec.instance_id = 'emp_001';
-- 2. 获取关联的 Equipment
SELECT
ec.instance_id,
ec.attributes['model'] AS model,
ee.relation_type
FROM entity_edge ee
JOIN entity_common ec
ON ee.target_id = ec.instance_id
WHERE ee.source_id = 'emp_001'
AND ee.relation_type = 'operates'
AND ee.world_id = 'world_prod';
-- 3. 获取变更历史
SELECT event_type, changed_fields,
before_state, after_state, event_time
FROM entity_event
WHERE instance_id = 'emp_001'
AND world_id = 'world_prod'
ORDER BY event_time DESC
LIMIT 20;
#6. Nessie + Iceberg:版本化湖仓
#6.1 为什么需要版本化?
coomia-dip 的核心概念之一是 World(世界分支)。每个 World 是一个完整的数据快照分支,支持:
- 场景推演:创建 World 分支,修改数据,观察影响,而不影响生产
- 时间旅行:查询任意历史时刻的数据状态
- 分支合并:将推演结果合并回生产
#6.2 Nessie 分支管理
main (Production World)
|
+--- dev/feature-001 (Development Branch)
| |
| +--- commit_a1b2c3 (Schema change)
| +--- commit_d4e5f6 (Data migration)
|
+--- scenario/supply-chain-disruption
| |
| +--- commit_g7h8i9 (What-if: supplier fails)
| +--- commit_j0k1l2 (Impact assessment)
|
+--- scenario/price-optimization
|
+--- commit_m3n4o5 (Price adjustment)
#6.3 Iceberg 表格式
Nessie (Branch/Tag Manager)
|
v
Iceberg (Table Format)
|
+--- Metadata Layer
| ├── snapshot_001.json
| ├── snapshot_002.json
| └── manifest_list.avro
|
+--- Data Layer (Parquet Files on MinIO)
├── data_001.parquet
├── data_002.parquet
└── data_003.parquet
#6.4 热/温/冷数据分层
+---------+ 实时写入 +---------+ 定期归档 +---------+
| Doris | ---------> | Iceberg | ---------> | MinIO |
| (热) | | (温) | | (冷) |
+---------+ +---------+ +---------+
< 7 天 7-90 天 > 90 天
毫秒级查询 秒级查询 分钟级查询
SSD 存储 SSD/HDD HDD/S3
#7. Kafka 在存储架构中的角色
+------------------------------------------------------------------+
| Kafka 在存储架构中的位置 |
+------------------------------------------------------------------+
| |
| [外部数据源] |
| | |
| v |
| +----------+ |
| | Kafka | <-- 数据接入的统一入口 |
| | | |
| | Topics: | |
| | - ontology.ingestion.{type} 数据接入 |
| | - ontology.change.{type} 变更事件 |
| | - audit.events 审计事件 |
| | - pipeline.triggers 管道触发 |
| | - derived.invalidation 派生属性失效 |
| | - reasoning.triggers 推理触发 |
| | - cross-Layer.coordination 跨Plane协调 |
| +----------+ |
| | |
| v |
| +-----------+ +-----------+ |
| | Flink CDC | -----> | Doris | |
| +-----------+ +-----------+ |
| |
+------------------------------------------------------------------+
#8. Redis 缓存策略
#8.1 缓存层级
请求到达
|
v
+------------------+
| L1: 进程内缓存 | Caffeine (Java) / lru_cache (Python)
| TTL: 10s | 容量: 10K entries per process
+------------------+
| miss
v
+------------------+
| L2: Redis 缓存 | Redis Cluster
| TTL: 5min | 容量: 按需扩展
+------------------+
| miss
v
+------------------+
| L3: Doris 查询 | entity_common / entity_edge
| 无 TTL | 权威数据源
+------------------+
#8.2 缓存键设计
Schema 缓存:
schema:{world_id}:{object_type_id}:{version}
TTL: 30min (Schema 变更触发主动失效)
实例缓存:
instance:{world_id}:{instance_id}:{version}
TTL: 5min
派生属性缓存:
derived:{world_id}:{instance_id}:{property_id}:{version}
TTL: 按计算策略决定 (CACHED 策略 = 10min)
查询结果缓存:
query:{world_id}:{query_hash}
TTL: 1min (查询结果变化频繁)
#9. PostgreSQL:OLTP 元数据
#9.1 为什么不用 Doris 存元数据?
Doris 是 OLAP 引擎,擅长大批量扫描分析,但在以下场景中 PostgreSQL 更适合:
| 场景 | Doris | PostgreSQL |
|---|---|---|
| 单行精确查询 | 慢(列式扫描开销) | 快(B-Tree 索引) |
| 高频写入小事务 | 不适合(批量写入优化) | 适合(WAL + MVCC) |
| ACID 事务 | 有限支持 | 完整支持 |
| 外键约束 | 不支持 | 支持 |
#9.2 PostgreSQL 中存储的数据
PostgreSQL 元数据库:
├── tenant_config (租户配置)
├── organization (组织结构)
├── user_account (用户账户)
├── role_assignment (角色分配)
├── policy_definition (策略定义)
├── world_metadata (World 元数据)
├── pipeline_definition (Pipeline 定义)
├── scheduler_config (调度配置)
├── connection_config (连接配置)
└── system_config (系统配置)
#10. 存储适配器 SPI 设计
#10.1 为什么需要 SPI?
虽然 Doris 是当前的选择,但我们通过 SPI(Service Provider Interface)保留了替换存储引擎的能力:
// 存储适配器接口
public interface OntologyStorageAdapter {
// 实例 CRUD
OntologyInstance create(
WorldContext world,
String objectTypeId,
Map<String, Object> attributes);
Optional<OntologyInstance> findById(
WorldContext world,
String instanceId);
List<OntologyInstance> query(
WorldContext world,
OntologyQuery query);
void update(
WorldContext world,
String instanceId,
Map<String, Object> changes);
void delete(
WorldContext world,
String instanceId);
// 关系管理
void createRelation(
WorldContext world,
String sourceId,
String targetId,
String relationType);
List<Relation> getRelations(
WorldContext world,
String instanceId,
RelationDirection direction);
// 向量搜索
List<VectorSearchResult> vectorSearch(
WorldContext world,
float[] queryVector,
int topK,
VectorSearchOptions options);
// 全文检索
List<FullTextSearchResult> fullTextSearch(
WorldContext world,
String query,
FullTextSearchOptions options);
}
// Doris 实现
@Service
@Primary
public class DorisOntologyStorageAdapter
implements OntologyStorageAdapter {
// Doris SQL 实现...
}
// 未来可能的替代实现
// public class StarRocksOntologyStorageAdapter
// implements OntologyStorageAdapter { ... }
#10.2 SPI 注册机制
// application.yml
onto:
storage:
adapter: doris # 或 starrocksm 或 custom
doris:
fe-host: doris-fe
fe-port: 9030
database-prefix: onto_
#11. 数据库级别的 World 隔离
#11.1 Project → Doris Database 映射
Tenant: acme_corp
└── Organization: engineering
└── Space: product_dev
└── Project: supply_chain
|
+---> Doris DB: onto_acme_engineering_supply_chain
| ├── entity_common
| ├── entity_edge
| └── entity_event
|
+---> MinIO Bucket: onto-acme-engineering-supply-chain
|
+---> Nessie Namespace: acme.engineering.supply_chain
├── Branch: main (Production World)
├── Branch: dev/feature-001
└── Branch: scenario/disruption-sim
#11.2 World 隔离通过 WHERE 条件实现
-- 所有查询都自动附加 world_id 过滤
-- OntologyRuntimeService 在查询构建时注入
SELECT * FROM entity_common
WHERE world_id = 'world_prod' -- 强制 World 隔离
AND object_type_id = 'Employee'
AND CAST(attributes['department'] AS VARCHAR)
= 'Engineering';
-- 如果遗漏 world_id 过滤,OntologyRuntimeService
-- 会抛出 WORLD_CONTEXT_REQUIRED 异常
-- 这是架构级别的强制,不可绕过
#12. 与其他存储方案的对比
| 方案 | OLAP | 向量 | 全文 | 图 | 版本化 | 复杂度 |
|---|---|---|---|---|---|---|
| 9 组件(V1.0) | ClickHouse | Qdrant | ES | TuGraph | Nessie | 极高 |
| Doris 统一(V3.0) | Doris | Doris HNSW | Doris 倒排 | Sqlg on Doris | Nessie | 低 |
| Snowflake+Pinecone | Snowflake | Pinecone | Snowflake | 无 | Time Travel | 中 |
| Databricks+Chroma | Spark SQL | Chroma | Delta Lake | 无 | Delta Lake | 中 |
| PostgreSQL+pgvector | PG(慢) | pgvector | PG FTS | Apache AGE | 无 | 低 |
#12.1 Doris 方案的局限性
已知局限:
1. 向量搜索精度: Doris HNSW 精度略低于专用向量数据库
影响: 对于 RAG 等对召回率要求极高的场景,可能需要额外优化
缓解: 可以通过调整 HNSW 参数(ef_construction, M)提高精度
2. 全文检索功能: 不如 Elasticsearch 丰富
影响: 复杂的 NLP 查询(同义词、模糊匹配、拼音搜索)
缓解: 80% 的查询场景 Doris 倒排索引足够
3. 图查询性能: Sqlg 编译 Gremlin->SQL 有额外开销
影响: 深度遍历(> 3 跳)性能下降
缓解: 物化视图预计算常用图查询路径
4. 写入延迟: Doris 批量写入优化,单条写入延迟较高
影响: 实时单条更新场景
缓解: 通过 Kafka 缓冲 + 微批量写入
#13. 迁移路径:从 V1.0 到 V3.0
Phase 1: 部署 Doris,双写
- 保持原有 ClickHouse/Qdrant/ES 运行
- 新数据同时写入 Doris
- 验证 Doris 查询结果与原有系统一致
Phase 2: 读取切换
- OLAP 查询从 ClickHouse 切换到 Doris
- 向量搜索从 Qdrant 切换到 Doris HNSW
- 全文检索从 ES 切换到 Doris 倒排索引
Phase 3: 清理旧组件
- 停止向 ClickHouse/Qdrant/ES 写入
- 下线旧组件
- 回收资源
历时: 约 6 周(含充分测试)
风险: 低(双写阶段可随时回滚)
#Key Takeaways
-
"为每种查询选最好的组件"是一个陷阱。 当你有 5 种不同的存储引擎时,数据同步的复杂度会吞噬所有性能优势。Apache Doris 的三合一能力(OLAP + 向量 + 全文)让我们用 1 个组件替代了 4 个,消除了 4 倍数据冗余和 5 条同步链路。
-
Variant 列类型是灵活本体存储的关键。 entity_common 表的
attributes VARIANT列让任何类型的本体实例都能存入同一张表,避免了为每种对象类型做 DDL 变更。这与 Doris 的列式存储完美契合——Variant 内部仍然是列式存储,查询性能不打折。 -
版本化不是可选功能,是核心架构。 Nessie + Iceberg 的分支管理让 World 概念从"想法"变成了"实现"。每个 Nessie 分支就是一个 World,分支创建是 O(1) 操作(只创建元数据指针),数据共享底层 Iceberg 文件,直到发生修改才 Copy-on-Write。
“下一篇预告: [S2-04] 我们的 Ontology 内核:一切操作必须经过本体层——深入理解为什么直接访问数据库是技术红线,以及 OntologyRuntimeService 如何成为平台的"数据宪法"。
Tags: #storage #doris #iceberg #nessie #vector-search #fulltext #olap #data-architecture #coomia-dip #智策平台