trustgraph/docs/tech-specs/zh-cn/entity-centric-graph.zh-cn.md
cybermaggedon e7efb673ef
Structure the tech specs directory (#836)
Tech spec some subdirectories for different languages
2026-04-21 16:06:41 +01:00

269 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: default
title: "基于实体的知识图谱在 Cassandra 上的存储"
parent: "Chinese (Beta)"
---
# 基于实体的知识图谱在 Cassandra 上的存储
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
## 概述
本文档描述了一种在 Apache Cassandra 上存储 RDF 风格知识图谱的存储模型。该模型采用一种**以实体为中心**的方法,其中每个实体都知道它参与的所有四元组以及它所扮演的角色。这用两个表替换了传统的多表 SPO 组合方法。
## 背景和动机
### 传统方法
在 Cassandra 上,标准的 RDF 四元组存储需要多个反规范化的表来覆盖查询模式,通常需要 6 个或更多的表,这些表代表主语、谓词、宾语和数据集 (SPOD) 的不同组合。每个四元组都会写入到每个表中,从而导致显著的写入放大、运维开销和模式复杂性。
此外,标签解析(用于实体的人类可读名称)需要单独的往返查询,这在 AI 和 GraphRAG 用例中尤其昂贵,因为标签对于 LLM 上下文至关重要。
### 以实体为中心的洞察
每个四元组 `(D, S, P, O)` 涉及最多 4 个实体。通过为每个实体参与四元组的记录创建一个行,我们保证**任何至少包含一个已知元素的查询都会命中分区键**。这使用单个数据表覆盖所有 16 种查询模式。
主要优点:
**2 个表**,而不是 7 个以上
**每个四元组 4 次写入**,而不是 6 次以上
**免费的标签解析**——实体的标签与其关系共存,从而自然地预热应用程序缓存
**所有 16 种查询模式**都由单分区读取提供
**更简单的操作**——只有一个数据表需要调整、压缩和修复
## 模式
### 表 1quads_by_entity
主要数据表。每个实体都有一个分区,其中包含它参与的所有四元组。该名称反映了查询模式(按实体查找)。
```sql
CREATE TABLE quads_by_entity (
collection text, -- Collection/tenant scope (always specified)
entity text, -- The entity this row is about
role text, -- 'S', 'P', 'O', 'G' — how this entity participates
p text, -- Predicate of the quad
otype text, -- 'U' (URI), 'L' (literal), 'T' (triple/reification)
s text, -- Subject of the quad
o text, -- Object of the quad
d text, -- Dataset/graph of the quad
dtype text, -- XSD datatype (when otype = 'L'), e.g. 'xsd:string'
lang text, -- Language tag (when otype = 'L'), e.g. 'en', 'fr'
PRIMARY KEY ((collection, entity), role, p, otype, s, o, d, dtype, lang)
);
```
**分区键 (Partition key)**: `(collection, entity)` — 作用域限定在集合中,每个实体对应一个分区。
**聚类列顺序的理由 (Clustering column order rationale)**:
1. **role** — 大部分查询都以“这个实体是哪个主语/客体”开始。
2. **p** — 下一个最常见的过滤条件,例如“给我所有 `knows` 关系”。
3. **otype** — 允许根据 URI 值与字面值关系进行过滤。
4. **s, o, d** — 剩余的列用于保证唯一性。
5. **dtype, lang** — 区分具有相同值但不同类型元数据的字面值(例如,`"thing"``"thing"@en``"thing"^^xsd:string`)。
### 表 2: quads_by_collection
支持集合级别的查询和删除。提供属于某个集合的所有四元组的清单。命名方式反映了查询模式(按集合查找)。
```sql
CREATE TABLE quads_by_collection (
collection text,
d text, -- Dataset/graph of the quad
s text, -- Subject of the quad
p text, -- Predicate of the quad
o text, -- Object of the quad
otype text, -- 'U' (URI), 'L' (literal), 'T' (triple/reification)
dtype text, -- XSD datatype (when otype = 'L')
lang text, -- Language tag (when otype = 'L')
PRIMARY KEY (collection, d, s, p, o, otype, dtype, lang)
);
```
首先按照数据集进行聚类,从而可以在集合或数据集级别进行删除。 `otype``dtype``lang` 列包含在聚类键中,用于区分具有相同值但不同元数据类型的字面量——在 RDF 中,`"thing"``"thing"@en``"thing"^^xsd:string` 是语义上不同的值。
## 写入路径
对于每个在集合 `C` 中的四元组 `(D, S, P, O)`,需要向 `quads_by_entity` 写入 **4 行**,并向 `quads_by_collection` 写入 **1 行**
### 示例
假设在集合 `tenant1` 中的四元组是:
```
Dataset: https://example.org/graph1
Subject: https://example.org/Alice
Predicate: https://example.org/knows
Object: https://example.org/Bob
```
将 4 行写入到 `quads_by_entity`
| collection | entity | role | p | otype | s | o | d |
|---|---|---|---|---|---|---|---|
| tenant1 | https://example.org/graph1 | G | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
| tenant1 | https://example.org/Alice | S | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
| tenant1 | https://example.org/knows | P | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
| tenant1 | https://example.org/Bob | O | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
将 1 行写入到 `quads_by_collection`
| collection | d | s | p | o | otype | dtype | lang |
|---|---|---|---|---|---|---|---|
| tenant1 | https://example.org/graph1 | https://example.org/Alice | https://example.org/knows | https://example.org/Bob | U | | |
### 示例
对于一个标签三元组:
```
Dataset: https://example.org/graph1
Subject: https://example.org/Alice
Predicate: http://www.w3.org/2000/01/rdf-schema#label
Object: "Alice Smith" (lang: en)
```
`otype``'L'``dtype``'xsd:string'``lang``'en'`。 原始值 `"Alice Smith"` 存储在 `o` 中。 `quads_by_entity` 中只需要 3 行,因为没有为原始值作为实体的行,因为原始值不是可以独立查询的实体。
## 查询模式
### 所有 16 种 DSPO 模式
在下表中,“完美前缀”表示查询使用聚类列的连续前缀。“分区扫描 + 过滤”表示 Cassandra 读取一个分区的一部分并在内存中进行过滤,这仍然有效,但不是纯粹的前缀匹配。
| # | 已知 | 查找实体 | 聚类前缀 | 效率 |
|---|---|---|---|---|
| 1 | D,S,P,O | entity=S, role='S', p=P | 完全匹配 | 完美前缀 |
| 2 | D,S,P,? | entity=S, role='S', p=P | 在 D 上过滤 | 分区扫描 + 过滤 |
| 3 | D,S,?,O | entity=S, role='S' | 在 D, O 上过滤 | 分区扫描 + 过滤 |
| 4 | D,?,P,O | entity=O, role='O', p=P | 在 D 上过滤 | 分区扫描 + 过滤 |
| 5 | ?,S,P,O | entity=S, role='S', p=P | 在 O 上过滤 | 分区扫描 + 过滤 |
| 6 | D,S,?,? | entity=S, role='S' | 在 D 上过滤 | 分区扫描 + 过滤 |
| 7 | D,?,P,? | entity=P, role='P' | 在 D 上过滤 | 分区扫描 + 过滤 |
| 8 | D,?,?,O | entity=O, role='O' | 在 D 上过滤 | 分区扫描 + 过滤 |
| 9 | ?,S,P,? | entity=S, role='S', p=P | — | **完美前缀** |
| 10 | ?,S,?,O | entity=S, role='S' | 在 O 上过滤 | 分区扫描 + 过滤 |
| 11 | ?,?,P,O | entity=O, role='O', p=P | — | **完美前缀** |
| 12 | D,?,?,? | entity=D, role='G' | — | **完美前缀** |
| 13 | ?,S,?,? | entity=S, role='S' | — | **完美前缀** |
| 14 | ?,?,P,? | entity=P, role='P' | — | **完美前缀** |
| 15 | ?,?,?,O | entity=O, role='O' | — | **完美前缀** |
| 16 | ?,?,?,? | — | 全扫描 | 仅探索 |
**关键结果:** 15 种非平凡模式中有 7 种是完美的聚类前缀匹配。 剩下的 8 种是单分区读取,并在分区内进行过滤。 包含至少一个已知元素的每个查询都会命中分区键。
模式 16 (?,?,?,?) 在实践中不会出现,因为集合始终是指定的,这将其简化为模式 12。
### 常见查询示例
**关于一个实体的所有信息:**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice';
```
**实体的所有出方向关系:**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
AND role = 'S';
```
**特定实体谓词:**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
AND role = 'S' AND p = 'https://example.org/knows';
```
**实体标签(特定语言):**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
AND role = 'S' AND p = 'http://www.w3.org/2000/01/rdf-schema#label'
AND otype = 'L';
```
然后,如果需要,可以在应用程序端通过 `lang = 'en'` 进行过滤。
**仅限 URI 值的关系(实体到实体的链接):**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
AND role = 'S' AND p = 'https://example.org/knows' AND otype = 'U';
```
**反向查找 — 指向此实体的对象:**
```sql
SELECT * FROM quads_by_entity
WHERE collection = 'tenant1' AND entity = 'https://example.org/Bob'
AND role = 'O';
```
## 标签解析和缓存预热
实体中心模型最显著的优势之一是,**标签解析成为一个免费的副作用**。
在传统的基于多表的模型中,获取标签需要单独的轮询查询:检索三元组,识别结果中的实体 URI然后为每个 URI 获取 `rdfs:label`。 这种 N+1 模式非常昂贵。
在实体中心模型中,查询一个实体会返回其**所有**四元组,包括其标签、类型和其他属性。 当应用程序缓存查询结果时,标签会在任何客户端请求之前被预热。
两种使用模式证实了这在实践中效果良好:
**面向用户的查询**:结果集通常很小,标签至关重要。 实体读取会预热缓存。
**AI/批量查询**:结果集很大,但有严格的限制。 标签要么是不必要的,要么只需要用于已缓存的实体子集。
解决大型结果集(例如 30,000 个实体)的标签的理论问题,可以通过实际观察来缓解,即没有人类或 AI 消费者能够有效地处理如此多的标签。 应用程序级别的查询限制可确保缓存压力保持在可管理范围内。
## 宽分区和重构
重构RDF-star 风格的关于语句的语句)会创建中心实体,例如,一个源文档支持数千个提取的事实。 这可能会导致宽分区。
缓解因素:
**应用程序级别的查询限制**:所有 GraphRAG 和面向用户的查询都强制执行严格的限制,因此宽分区永远不会在热读取路径上被完全扫描。
**Cassandra 可以高效地执行部分读取**:即使在大型分区上,具有早期停止的聚类列扫描也是快速的。
**集合删除**(唯一可能遍历整个分区的操作)是一个可接受的后台过程。
## 集合删除
由 API 调用触发,在后台运行(最终一致)。
1. 读取 `quads_by_collection` 以获取目标集合的所有四元组
2. 从四元组中提取唯一的实体s、p、o、d 值)
3. 对于每个唯一的实体,从 `quads_by_entity` 中删除该分区
4.`quads_by_collection` 中删除行
`quads_by_collection` 表提供了用于定位所有实体分区而无需进行全表扫描所需的索引。 由于 `(collection, entity)` 是分区键,因此分区级别的删除是高效的。
## 从多表模型迁移的路径
在迁移过程中,实体中心模型可以与现有的多表模型共存:
1.`quads_by_entity``quads_by_collection` 表与现有表一起部署
2. 同时将新的四元组写入旧表和新表
3. 将现有数据回填到新表中
4. 每次迁移一种查询模式
5. 在迁移所有读取路径后,停用旧表
## 总结
| 方面 | 传统 (6 个表) | 实体中心 (2 个表) |
|---|---|---|
| 表 | 7+ | 2 |
| 每个四元组的写入次数 | 6+ | 5 (4 个数据 + 1 个清单) |
| 标签解析 | 单独的轮询 | 通过缓存预热免费 |
| 查询模式 | 6 个表中的 16 种 | 1 个表中的 16 种 |
| 模式复杂性 | 高 | 低 |
| 运维开销 | 6 个表需要调整/修复 | 1 个数据表 |
| 重构支持 | 额外的复杂性 | 完美契合 |
| 对象类型过滤 | 不可用 | 原生 (通过 otype 聚类) |
输出合同(必须严格遵守以下格式):