--- layout: default title: "结构化数据技术规范 (第二部分)" parent: "Chinese (Beta)" --- # 结构化数据技术规范 (第二部分) > **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. ## 概述 本规范解决了在 TrustGraph 结构化数据集成初始实施过程中发现的问题和差距,如 `structured-data.md` 中所述。 ## 问题陈述 ### 1. 命名不一致:"对象" 与 "行" 当前实现方案在整个代码中使用 "对象" 术语 (例如,`ExtractedObject`,对象提取,对象嵌入)。 这种命名方式过于通用,容易造成混淆: "对象" 在软件中是一个 overloaded 的术语 (Python 对象,JSON 对象等)。 正在处理的数据本质上是表格数据,即具有定义模式的表格中的行。 "行" 更准确地描述了数据模型,并且与数据库术语一致。 这种不一致性出现在模块名称、类名称、消息类型和文档中。 ### 2. 行存储查询限制 当前的行存储实现方案存在重大的查询限制: **自然语言匹配问题**: 查询难以处理真实世界数据的变化。 例如: 很难在包含 `"CHESTNUT ST"` 的街道数据库中找到关于 `"Chestnut Street"` 的信息。 缩写、大小写差异和格式变化会破坏精确匹配查询。 用户期望语义理解,但存储系统只提供字面匹配。 **模式演化问题**: 更改模式会导致问题: 现有数据可能不符合更新后的模式。 表结构的变化可能会破坏查询和数据完整性。 没有明确的模式更新迁移路径。 ### 3. 需要行嵌入 与问题 2 相关,系统需要行数据的向量嵌入,以便: 在结构化数据上进行语义搜索 (找到 "Chestnut Street" 时,数据包含 "CHESTNUT ST")。 模糊查询的相似性匹配。 结合结构化过滤器和语义相似性的混合搜索。 更好的自然语言查询支持。 嵌入服务已指定,但尚未实施。 ### 4. 行数据摄取不完整 结构化数据摄取管道尚未完全投入使用: 存在诊断提示,用于对输入格式进行分类 (CSV,JSON 等)。 使用这些提示的摄取服务尚未集成到系统中。 没有将预结构化数据加载到行存储的端到端路径。 ## 目标 **模式灵活性**: 允许模式演化,而不会破坏现有数据或需要迁移。 **命名一致性**: 在整个代码库中采用 "行" 术语。 **语义可查询性**: 通过行嵌入支持模糊/语义匹配。 **完整的摄取管道**: 提供将结构化数据加载到存储的端到端路径。 ## 技术设计 ### 统一的行存储模式 之前的实现方案为每个模式创建了一个单独的 Cassandra 表。 这在模式演化时会导致问题,因为表结构的变化需要迁移。 新的设计使用一个统一的表来存储所有行数据: ```sql CREATE TABLE rows ( collection text, schema_name text, index_name text, index_value frozen>, data map, source text, PRIMARY KEY ((collection, schema_name, index_name), index_value) ) ``` #### 列定义 | 列 | 类型 | 描述 | |--------|------|-------------| | `collection` | `text` | 数据收集/导入标识符(来自元数据)| | `schema_name` | `text` | 此行所遵循的模式名称 | | `index_name` | `text` | 索引字段名称,以逗号分隔的字符串表示复合索引 | | `index_value` | `frozen>` | 索引值列表 | | `data` | `map` | 行数据,以键值对形式存储 | | `source` | `text` | 可选的 URI,链接到知识图谱中的来源信息。空字符串或 NULL 表示没有来源。| #### 索引处理 每行数据会被存储多次,对于模式中定义的每个索引字段,都会存储一次。主键字段被视为索引,没有特殊的标记,以提供未来的灵活性。 **单字段索引示例:** 模式定义 `email` 为索引 `index_name = "email"` `index_value = ['foo@bar.com']` **复合索引示例:** 模式定义在 `region` 和 `status` 上创建复合索引 `index_name = "region,status"` (字段名称按排序方式连接) `index_value = ['US', 'active']` (值与字段名称的顺序相同) **主键示例:** 模式定义 `customer_id` 为主键 `index_name = "customer_id"` `index_value = ['CUST001']` #### 查询模式 无论使用哪个索引,所有查询都遵循相同的模式: ```sql SELECT * FROM rows WHERE collection = 'import_2024' AND schema_name = 'customers' AND index_name = 'email' AND index_value = ['foo@bar.com'] ``` #### 设计权衡 **优点:** 模式更改不需要更改表结构 行数据对 Cassandra 隐藏 - 字段的添加/删除是透明的 所有访问方法都具有一致的查询模式 不需要 Cassandra 的二级索引(这在大型环境中可能会很慢) 整个过程中都使用 Cassandra 的原生类型(`map`,`frozen`) **权衡:** 写入放大:每个行插入 = N 次插入(每个索引字段一次) 重复行数据带来的存储开销 类型信息存储在模式配置中,应用程序层进行转换 #### 一致性模型 该设计接受某些简化: 1. **没有行更新**:系统是只追加的。这消除了关于更新同一行的多个副本的一致性问题。 2. **模式更改容错性**:当模式发生更改(例如,添加/删除索引)时,现有行保留其原始索引。旧行将无法通过新索引发现。如果需要,用户可以删除并重新创建模式以确保一致性。 ### 分区跟踪和删除 #### 问题 借助分区键 `(collection, schema_name, index_name)`,高效删除需要知道所有要删除的分区键。仅通过 `collection` 或 `collection + schema_name` 删除需要知道所有具有数据的 `index_name` 值。 #### 分区跟踪表 一个辅助查找表跟踪哪些分区存在: ```sql CREATE TABLE row_partitions ( collection text, schema_name text, index_name text, PRIMARY KEY ((collection), schema_name, index_name) ) ``` 这使得能够高效地发现用于删除操作的分区。 #### 行写入器行为 行写入器维护一个已注册的 `(collection, schema_name)` 对的内存缓存。 在处理一行时: 1. 检查 `(collection, schema_name)` 是否在缓存中 2. 如果未缓存(对于此对的第一个行): 查找模式配置以获取所有索引名称 将条目插入到 `row_partitions` 中,对于每个 `(collection, schema_name, index_name)` 将该对添加到缓存中 3. 继续写入行数据 行写入器还监视模式配置更改事件。 当模式发生更改时,相关的缓存条目将被清除,以便下一行触发使用更新的索引名称重新注册。 这种方法确保: 查找表写入仅针对每个 `(collection, schema_name)` 对发生一次,而不是针对每行。 查找表反映了数据写入时活动的索引。 导入过程中发生的模式更改可以正确处理。 #### 删除操作 **删除集合:** ```sql -- 1. Discover all partitions SELECT schema_name, index_name FROM row_partitions WHERE collection = 'X'; -- 2. Delete each partition from rows table DELETE FROM rows WHERE collection = 'X' AND schema_name = '...' AND index_name = '...'; -- (repeat for each discovered partition) -- 3. Clean up the lookup table DELETE FROM row_partitions WHERE collection = 'X'; ``` **删除集合 + 模式:** ```sql -- 1. Discover partitions for this schema SELECT index_name FROM row_partitions WHERE collection = 'X' AND schema_name = 'Y'; -- 2. Delete each partition from rows table DELETE FROM rows WHERE collection = 'X' AND schema_name = 'Y' AND index_name = '...'; -- (repeat for each discovered partition) -- 3. Clean up the lookup table entries DELETE FROM row_partitions WHERE collection = 'X' AND schema_name = 'Y'; ``` ### 行嵌入 行嵌入允许对索引值进行语义/模糊匹配,从而解决自然语言不匹配的问题(例如,在搜索“Chestnut Street”时找到“CHESTNUT ST”)。 #### 设计概述 每个索引值都会被嵌入,并存储在向量存储中(Qdrant)。在查询时,查询内容也会被嵌入,找到相似的向量,然后使用相关的元数据来查找 Cassandra 中的实际行。 #### Qdrant 集合结构 每个 `(user, collection, schema_name, dimension)` 元组对应一个 Qdrant 集合: **集合命名:** `rows_{user}_{collection}_{schema_name}_{dimension}` 名称会被清理(非字母数字字符替换为 `_`,转换为小写,数字前缀添加 `r_` 前缀) **理由:** 允许通过删除匹配的 Qdrant 集合来干净地删除一个 `(user, collection, schema_name)` 实例;维度后缀允许不同的嵌入模型共存。 #### 哪些内容会被嵌入 索引值的文本表示: | 索引类型 | 示例 `index_value` | 要嵌入的文本 | |------------|----------------------|---------------| | 单字段 | `['foo@bar.com']` | `"foo@bar.com"` | | 组合 | `['US', 'active']` | `"US active"` (空格连接) | #### 点结构 每个 Qdrant 点包含: ```json { "id": "", "vector": [0.1, 0.2, ...], "payload": { "index_name": "street_name", "index_value": ["CHESTNUT ST"], "text": "CHESTNUT ST" } } ``` | Payload Field | Description | |---------------|-------------| | `index_name` | 此嵌入所代表的索引字段。| | `index_value` | 原始值列表(用于 Cassandra 查找)。| | `text` | 嵌入的文本(用于调试/显示)。| 注意:`user`、`collection` 和 `schema_name` 可以从 Qdrant 集合名称中推断出来。| #### 查询流程 1. 用户查询用户 U、集合 X、模式 Y 中的“Chestnut Street”。| 2. 嵌入查询文本。| 3. 确定与前缀 `rows_U_X_Y_` 匹配的 Qdrant 集合名称。| 4. 在匹配的 Qdrant 集合中搜索最接近的向量。| 5. 获取包含 `index_name` 和 `index_value` 的有效负载的匹配点。| 6. 查询 Cassandra:| ```sql SELECT * FROM rows WHERE collection = 'X' AND schema_name = 'Y' AND index_name = '' AND index_value = ``` 7. 返回匹配的行 #### 可选:按索引名称过滤 查询可以选择性地按 `index_name` 进行过滤,以便在 Qdrant 中仅搜索特定字段: **"查找所有匹配 'Chestnut' 的字段"** → 搜索集合中的所有向量 **"查找 'Chestnut' 匹配的 street_name"** → 过滤 `payload.index_name = 'street_name'` #### 架构 行嵌入遵循 GraphRAG 使用的 **两阶段模式**(图嵌入、文档嵌入): **第一阶段:嵌入计算** (`trustgraph-flow/trustgraph/embeddings/row_embeddings/`) - 消耗 `ExtractedObject`,通过嵌入服务计算嵌入,输出 `RowEmbeddings` **第二阶段:嵌入存储** (`trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/`) - 消耗 `RowEmbeddings`,将向量写入 Qdrant Cassandra 行写入器是一个独立的并行消费者: **Cassandra 行写入器** (`trustgraph-flow/trustgraph/storage/rows/cassandra`) - 消耗 `ExtractedObject`,将行写入 Cassandra 所有三个服务都从同一个流中读取,从而使它们彼此解耦。 这允许: 独立地扩展 Cassandra 写入与嵌入生成与向量存储 如果不需要,可以禁用嵌入服务 一个服务中的故障不会影响其他服务 与 GraphRAG 管道保持一致的架构 #### 写入路径 **第一阶段(行嵌入处理器):** 接收到 `ExtractedObject` 时: 1. 查找模式以找到索引字段 2. 对于每个索引字段: 构建索引值的文本表示 通过嵌入服务计算嵌入 3. 输出一个包含所有计算向量的 `RowEmbeddings` 消息 **第二阶段(行嵌入-写入-Qdrant):** 接收到 `RowEmbeddings` 时: 1. 对于消息中的每个嵌入: 从 `(user, collection, schema_name, dimension)` 确定 Qdrant 集合 如果需要,创建集合(首次写入时延迟创建) 使用向量和有效载荷进行更新 #### 消息类型 ```python @dataclass class RowIndexEmbedding: index_name: str # The indexed field name(s) index_value: list[str] # The field value(s) text: str # Text that was embedded vectors: list[list[float]] # Computed embedding vectors @dataclass class RowEmbeddings: metadata: Metadata schema_name: str embeddings: list[RowIndexEmbedding] ``` #### 删除集成 Qdrant 集合通过在集合名称模式上进行前缀匹配来发现: **删除 `(user, collection)`:** 1. 列出所有与前缀 `rows_{user}_{collection}_` 匹配的 Qdrant 集合 2. 删除每个匹配的集合 3. 删除 Cassandra 行分区(如上文所述) 4. 清理 `row_partitions` 条目 **删除 `(user, collection, schema_name)`:** 1. 列出所有与前缀 `rows_{user}_{collection}_{schema_name}_` 匹配的 Qdrant 集合 2. 删除每个匹配的集合(处理多个维度) 3. 删除 Cassandra 行分区 4. 清理 `row_partitions` #### 模块位置 | 阶段 | 模块 | 入口点 | |-------|--------|-------------| | 阶段 1 | `trustgraph-flow/trustgraph/embeddings/row_embeddings/` | `row-embeddings` | | 阶段 2 | `trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/` | `row-embeddings-write-qdrant` | ### 行嵌入查询 API 行嵌入查询是与 GraphQL 行查询服务**不同的 API**: | API | 目的 | 后端 | |-----|---------|---------| | 行查询 (GraphQL) | 对索引字段进行精确匹配 | Cassandra | | 行嵌入查询 | 模糊/语义匹配 | Qdrant | 这种分离保持了关注点的清晰: GraphQL 服务专注于精确、结构化的查询 嵌入 API 处理语义相似性 用户工作流程:通过嵌入进行模糊搜索以查找候选对象,然后进行精确查询以获取完整的行数据 #### 请求/响应模式 ```python @dataclass class RowEmbeddingsRequest: vectors: list[list[float]] # Query vectors (pre-computed embeddings) user: str = "" collection: str = "" schema_name: str = "" index_name: str = "" # Optional: filter to specific index limit: int = 10 # Max results per vector @dataclass class RowIndexMatch: index_name: str = "" # The matched index field(s) index_value: list[str] = [] # The matched value(s) text: str = "" # Original text that was embedded score: float = 0.0 # Similarity score @dataclass class RowEmbeddingsResponse: error: Error | None = None matches: list[RowIndexMatch] = [] ``` #### 查询处理器 模块:`trustgraph-flow/trustgraph/query/row_embeddings/qdrant` 入口点:`row-embeddings-query-qdrant` 处理器: 1. 接收带有查询向量的 `RowEmbeddingsRequest` 2. 通过前缀匹配找到合适的 Qdrant 集合 3. 搜索最近的向量,并可选择使用 `index_name` 过滤器 4. 返回包含匹配索引信息的 `RowEmbeddingsResponse` #### API 网关集成 网关通过标准的请求/响应模式公开行嵌入查询: | 组件 | 位置 | |-----------|----------| | 调度器 | `trustgraph-flow/trustgraph/gateway/dispatch/row_embeddings_query.py` | | 注册 | 将 `"row-embeddings"` 添加到 `request_response_dispatchers` 中,位于 `manager.py` | 流程接口名称:`row-embeddings` 流程蓝图中的接口定义: ```json { "interfaces": { "row-embeddings": { "request": "non-persistent://tg/request/row-embeddings:{id}", "response": "non-persistent://tg/response/row-embeddings:{id}" } } } ``` #### Python SDK 支持 SDK 提供了用于行嵌入查询的方法: ```python # Flow-scoped query (preferred) api = Api(url) flow = api.flow().id("default") # Query with text (SDK computes embeddings) matches = flow.row_embeddings_query( text="Chestnut Street", collection="my_collection", schema_name="addresses", index_name="street_name", # Optional filter limit=10 ) # Query with pre-computed vectors matches = flow.row_embeddings_query( vectors=[[0.1, 0.2, ...]], collection="my_collection", schema_name="addresses" ) # Each match contains: for match in matches: print(match.index_name) # e.g., "street_name" print(match.index_value) # e.g., ["CHESTNUT ST"] print(match.text) # e.g., "CHESTNUT ST" print(match.score) # e.g., 0.95 ``` #### 命令行工具 命令:`tg-invoke-row-embeddings` ```bash # Query by text (computes embedding automatically) tg-invoke-row-embeddings \ --text "Chestnut Street" \ --collection my_collection \ --schema addresses \ --index street_name \ --limit 10 # Query by vector file tg-invoke-row-embeddings \ --vectors vectors.json \ --collection my_collection \ --schema addresses # Output formats tg-invoke-row-embeddings --text "..." --format json tg-invoke-row-embeddings --text "..." --format table ``` #### 典型用法示例 行嵌入查询通常用作模糊到精确查找流程的一部分: ```python # Step 1: Fuzzy search via embeddings matches = flow.row_embeddings_query( text="chestnut street", collection="geo", schema_name="streets" ) # Step 2: Exact lookup via GraphQL for full row data for match in matches: query = f''' query {{ streets(where: {{ {match.index_name}: {{ eq: "{match.index_value[0]}" }} }}) {{ street_name city zip_code }} }} ''' rows = flow.rows_query(query, collection="geo") ``` 这种两步模式可以实现: 当用户搜索 "Chestnut Street" 时,找到 "CHESTNUT ST" 检索包含所有字段的完整行数据 结合语义相似性和结构化数据访问 ### 行数据导入 暂定于后续阶段。将与其他导入更改一起设计。 ## 实施影响 ### 当前状态分析 现有的实现包含两个主要组件: | 组件 | 位置 | 行数 | 描述 | |-----------|----------|-------|-------------| | 查询服务 | `trustgraph-flow/trustgraph/query/objects/cassandra/service.py` | ~740 | 整体式:GraphQL 模式生成、过滤器解析、Cassandra 查询、请求处理 | | 写入器 | `trustgraph-flow/trustgraph/storage/objects/cassandra/write.py` | ~540 | 每个模式的表创建、二级索引、插入/删除 | **当前的查询模式:** ```sql SELECT * FROM {keyspace}.o_{schema_name} WHERE collection = 'X' AND email = 'foo@bar.com' ALLOW FILTERING ``` **新的查询模式:** ```sql SELECT * FROM {keyspace}.rows WHERE collection = 'X' AND schema_name = 'customers' AND index_name = 'email' AND index_value = ['foo@bar.com'] ``` ### 关键变更 1. **查询语义简化:** 新的模式仅支持对 `index_value` 的精确匹配。当前的 GraphQL 过滤器(`gt`、`lt`、`contains` 等)要么: 变为对返回数据的后过滤(如果仍然需要) 被移除,转而使用嵌入 API 进行模糊匹配 2. **GraphQL 代码紧密耦合:** 当前的 `service.py` 包含了 Strawberry 类型生成、过滤器解析以及 Cassandra 相关的查询。添加另一个行存储后端会重复大约 400 行的 GraphQL 代码。 ### 提出的重构 重构分为两部分: #### 1. 提取 GraphQL 代码 将可重用的 GraphQL 组件提取到共享模块中: ``` trustgraph-flow/trustgraph/query/graphql/ ├── __init__.py ├── types.py # Filter types (IntFilter, StringFilter, FloatFilter) ├── schema.py # Dynamic schema generation from RowSchema └── filters.py # Filter parsing utilities ``` 这实现了: 在不同的行存储后端中实现重用 更清晰的分层 更容易独立地测试 GraphQL 逻辑 #### 2. 实现新的表模式 重构特定于 Cassandra 的代码,以使用统一的表: **写入器 (Writer)** (`trustgraph-flow/trustgraph/storage/rows/cassandra/`): 使用单个 `rows` 表,而不是每个模式的表 写入 N 个副本到每行(每个索引一个) 注册到 `row_partitions` 表 更简单的表创建(一次性设置) **查询服务 (Query Service)** (`trustgraph-flow/trustgraph/query/rows/cassandra/`): 查询统一的 `rows` 表 使用提取的 GraphQL 模块进行模式生成 简化的过滤处理(仅在数据库级别进行精确匹配) ### 模块重命名 作为“object”→“row”命名清理的一部分: | 当前 (Current) | 新 (New) | |---------|-----| | `storage/objects/cassandra/` | `storage/rows/cassandra/` | | `query/objects/cassandra/` | `query/rows/cassandra/` | | `embeddings/object_embeddings/` | `embeddings/row_embeddings/` | ### 新模块 | 模块 (Module) | 目的 (Purpose) | |--------|---------| | `trustgraph-flow/trustgraph/query/graphql/` | 共享 GraphQL 工具 | `trustgraph-flow/trustgraph/query/row_embeddings/qdrant/` | 行嵌入查询 API | `trustgraph-flow/trustgraph/embeddings/row_embeddings/` | 行嵌入计算(第一阶段) | `trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/` | 行嵌入存储(第二阶段) ## 引用 [结构化数据技术规范](structured-data.md)