GraphIndex 技术文档¶
GraphIndex 是一种面向知识图谱的图索引实现,使用了面向对象的封装思想,将数据库中的文档、页面和元素进行索引化,以便于快速查询、检索和操作。此类主要用于对图数据进行增、删、查、更新等操作,并提供了详细的补偿(回滚)机制,确保数据一致性。
封装思路¶
核心结构¶
- 基础框架:
GraphIndex继承了BaseDPEIndex基类,通过该继承实现了索引管理的统一接口。 - 异步与同步支持:实现了同步和异步版本的增、删、改操作,以适应不同的业务场景。
- 工具的灵活使用:通过
get_tools方法对不同的元素构建增、删、改的相关工具,为操作提供支持。 - 补偿机制:每个增、删、改操作均生成回滚信息(rollback info),在异常情况下可以回滚,保证操作的原子性。
核心功能¶
- 图数据的CRUD操作:对文档、页面和元素提供增删查改操作。
- 补偿与回滚:所有操作均带有回滚信息,若操作失败可根据记录进行补偿,确保数据一致性。
- 索引的清理与重建:
rebuild_index和clean_index提供了对整个图索引的重建与清理操作。 - 异步与同步查询:通过
query和aquery实现了针对消息的查询操作,支持从图数据中获取与消息内容最相关的元素、页面或文档。
关键字段和功能模块说明¶
在 GraphIndex 中,一些字段和方法承担了重要的功能,下面进行详细说明。
graph_db¶
graph_db 是一个基于 Owlready2 的数据库接口。该字段用于实现数据的增删改查操作,具体应用如下:
- 通过 graph_db.query_individuals 实现对知识图谱中的实体查询。
- 通过 graph_db.clear_db 清理数据库,为索引重建提供支持。
- 通过自定义 SPARQL 查询,结合工具实现对实体的精准控制。
index_type¶
该字段用于指定当前的索引类型为“graph”,这是为不同索引类型定义标准化属性的约定,便于后续扩展。
get_tools¶
该方法动态生成并返回当前元素的操作工具,主要分为三类工具:
- 类别编辑工具:例如 AddOwlClsTool、UpdateOwlClsTool 等,用于创建和更新类别。
- 属性编辑工具:例如 AddOwlPropertyTool、UpdateOwlPropertyTool 等,用于对属性进行编辑。
- 实体编辑工具:例如 AddOwlEntityTool、UpdateOwlEntityTool 等,用于对实体的操作。
这些工具确保了对图数据进行详细的 CRUD 操作。
construct_additional_info 和 aconstruct_additional_info¶
这两个方法构造当前上下文信息,用于知识图谱操作的上下文展示和辅助查询。通过 ele_expander 扩展元素上下文,以 json.dumps 生成全局字典等信息,用于增强图索引的数据提取能力。
异步与同步支持¶
add_element和aadd_element方法是对元素的异步与同步添加操作。delete_element和adelete_element方法实现了异步与同步的删除功能。rebuild_index和arebuild_index提供了索引重建的同步和异步实现。compensate_*与acompensate_*系列方法确保了在异常情况下进行补偿,支持异步补偿操作。
查询方法 query 和 aquery¶
这两个方法使用 msg 查询图索引,可以在不同级别上查询元素、页面和文档。使用图数据库的 query_individuals 方法,结合查询到的相关实体和它们的分数,选取最高分相关的 ID 返回。
selectinload 使用说明¶
在 GraphIndex 的 rebuild_index 方法中,我们使用了 SQLAlchemy 的 selectinload 技术来优化数据库查询的性能。
使用方式¶
在 rebuild_index 方法中,通过以下代码段实现了 selectinload 的使用:
from sqlalchemy.orm import selectinload
documents = (
session.query(SqlDocument)
.options(selectinload(SqlDocument.pages).selectinload(SqlDocPage.elements))
.order_by(SqlDocument.create_timestamp.asc())
.all()
)
这里 selectinload(SqlDocument.pages) 预加载了每个 SqlDocument 的关联 pages,而 .selectinload(SqlDocPage.elements) 进一步加载了 pages 表的 elements。通过这种方式,可以避免遍历每个文档时逐一查询页面和元素,减少了查询次数,提高了查询效率。
使用场景¶
selectinload 在一对多和多对多关系中表现最佳,适用于以下情况:
1. 批量加载:如上例中的 SqlDocument 对象拥有多个 pages,pages 又包含多个 elements。使用 selectinload 可以在一次额外的查询中批量加载所有相关数据。
2. 优化 N+1 查询:selectinload 是一种非延迟加载策略,可以防止每次访问关联数据时触发额外查询,适合需要预加载大量数据的场景。
优势与其他策略对比¶
| 加载策略 | 适用场景 | 特点 |
|---|---|---|
| lazy | 一般情况下 | 延迟加载,可能会导致 N+1 查询 |
| joinedload | 一对一或少量一对多 | 使用 JOIN 查询,但会导致数据冗余 |
| selectinload | 一对多或多对多关联的大数据量 | 通过额外查询批量加载,避免数据冗余 |
示例对比¶
假设我们有一个 User 类,其关联了多个 Order,每个 Order 又包含多个 OrderItem。以下是使用 selectinload 的代码:
from sqlalchemy.orm import selectinload
users = (
session.query(User)
.options(selectinload(User.orders).selectinload(Order.items))
.order_by(User.id.asc())
.all()
)
该代码会生成一次 User 查询,和额外的 Order 和 OrderItem 查询,避免了 N+1 查询问题。
结构化数据与二进制数据的抽取强化¶
在 GraphIndex 的封装过程中,我们遇到了一系列的麻烦,比如:
- 大模型批量调用工具 导致乱序操作,无法保证数据的一致性。比如调用逻辑中,添加DataProperty与创建Class操作一起调用,这在Async模式下会引发问题,因为工具调用的顺序在Async无法强制保证。另外,即使是在Sync模式下,也可能因为调用顺序的问题导致数据不一致。
对于这个问题,我们使用SeqChain配合LLMWithTools的设计,将抽取过程分为多个阶段,类别抽取-属性强化-实体抽取-关系抽取-全局强化。通过这样的设计,我们可以通过保证思维链顺序,进而保证工具调用顺序,再进一步保证数据的一致性。 2. 创建与更新工具混用 大模型往往无法非常好的分辨创建与更新时机,或者说因为上下文问题,导致它无法精准识别是应该创建还是应该更新。多数情况下,它会在需要更新的时候调用到创建。
对于这个问题,我们使用了Create工具与Update工具的结合,在调用创建时,我们动态判断是否已经存在,如果存在则调用更新工具,否则调用创建工具。这样的设计,可以保证数据的一致性。
经过不断优化,最终仍然遗留了两个问题如下:
-
结构化数据,比如Table,识别不够精准,结构化数据在工程时代反而是最容易转换为图数据,因为二者兼具结构化信息,只需要写好转换函数即可,但通过大模型抽取的时候,往往会因为大模型的复杂性,导致结构化数据映射到图谱的识别不够精准。比如缺失字段,或者关系未正确建立,或者建立错位。
这个现象出现的原因有二: 1. 大模型原本便不是100%精确的转换。 2. 结构化数据往往携带的大模型可用的上下文数据非常少,它的上下文是体现在“结构”中的,而不是文本中的,而大模型对“结构”中的上下文提取能力不足,导致了这个问题。
如何解决这个问题?解决方案如下: 1. Unstructured-IO转换完成的Element -> DocElement转换的过程中,进行适当强化,针对结构化数据,添加一个StructuredMixIn,使结构化的DocElement可以借助StructuredMixIn的能力,配合上大模型实现高效的图转换。同时StructuredMixIn还可以提供更好的Embedding建议,便EmbeddingIndex也得到更好的效果。 2. 二进制数据。比如图片,音频,视频等,这些数据无法通过大模型直接转换,因为大模型无法识别二进制数据。这个问题的解决方案是: 1. 添加一个BinaryMixIn,使得二进制数据可以通过BinaryMixIn的能力,配合上大模型实现高效的图转换。同时BinaryMixIn还可以提供更好的Embedding建议,便EmbeddingIndex也得到更好的效果。