trustgraph/docs/tech-specs/entity-centric-graph.zh-cn.md

270 lines
12 KiB
Markdown
Raw Normal View History

---
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 聚类) |
输出合同(必须严格遵守以下格式):