跳到主要内容

第 4 章:DataPoint 统一模型与三库分工

本章讲底座:为什么图里所有东西(实体、文本块、文档)都是同一个 DataPoint,以及关系库 / 图库 / 向量库三者各管什么、怎么协作落库。

4.1 一个模型统治所有节点:DataPoint

Cognee 里凡是要进图的东西都继承 DataPoint(cognee/infrastructure/engine/models/DataPoint.py):EntityEntityTypeDocumentChunkTextSummaryEvent…… 全是它的子类。这带来一致性:id 生成、嵌入字段声明、provenance 字段、版本,所有节点共享一套规则。

DataPoint 的关键字段(DataPoint.py:48-72):

# 真实源码节选
class DataPoint(BaseModel):
id: UUID = Field(default_factory=uuid4) # 默认随机;声明 identity_fields 后变确定性
version: int = 1
topological_rank: int | None = 0 # 流水线第几步产出(可视化用)
metadata: MetaData = {"index_fields": []} # 哪些字段要做向量嵌入
type: str = Field(default_factory=lambda: DataPoint.__name__)
# provenance:这条数据从哪来(第 1 章盖的戳)
source_pipeline: str | None = None
source_task: str | None = None
source_node_set: str | None = None
source_user: str | None = None
source_content_hash: str | None = None
feedback_weight: float = 0.5 # 反馈权重(第 3 章打分用)
importance_weight: float | None = 0.5 # 重要性权重(第 3 章打分用)

两个 metadata 子字段是全局关节:

字段作用在哪用到
index_fields列出要做向量嵌入的字段第 3 章召回的向量集合名 {类名}_{字段}
identity_fields列出算确定性 id的字段第 2 章的去重/合并

4.2 确定性 id 回顾(为什么是去重底座)

第 2 章讲过 id_for;这里强调它的默认 vs 显式对比,因为这是新手最容易踩的点:

情况id 怎么来后果
没声明 identity_fieldsdefault_factory=uuid4,随机每次都是新节点,不会合并,无法按内容回查
声明了 identity_fields(如 Entity)uuid5(NAMESPACE_OID, "Entity:name")同名 → 同 id → 自动合并/幂等

DataPoint.py:48-53 的注释直说了这个「随机 uuid 陷阱」:随机 id 的节点「没有稳定身份,跨运行不去重、也无法用重算 id 的方式查回」。所以自定义节点模型想被合并,必须主动声明 identity_fields

4.3 三类数据库分工

Cognee 把存储拆成三块,各管一摊(CLAUDE.md:130-141「Interface-Based Database Adapters」):

存什么默认实现接口文件
关系库Dataset / Data / User / 权限等元数据SQLitecognee/infrastructure/databases/relational/
图库节点 + 边(知识图谱本体)Ladybug.../graph/graph_db_interface.py
向量库index_fields 字段的嵌入LanceDB.../vector/vector_db_interface.py

默认图库是 ladybug(cognee/infrastructure/databases/graph/config.py:45,graph_database_provider: str = Field("ladybug", ...)),可换 Kuzu / Neo4j / Neptune / Postgres(.../graph/ 下各 driver 目录)。向量默认 LanceDB,可换 pgvector(add.py docstring「VECTOR_DB_PROVIDER」)。

怎么读这张分工: 同一个 Entity 节点,它的「身份和关系」在图库,它的「name 嵌入向量」在向量库,而「它属于哪个 dataset、哪个用户能看」在关系库。三库通过节点 id 串起来。

4.4 落库那一步:add_data_points

cognify 流水线的第 ④ 步 add_data_points(cognee/tasks/storage/add_data_points.py:30)负责把内存里的 DataPoint 写进图库和向量库。它做的事:

  • 从每个 DataPointget_graph_from_model 抽出节点和边;
  • deduplicate_nodes_and_edges 按 id 去重(add_data_points.py:9-12 的 import);
  • upsert_nodes / upsert_edges 写图库;
  • index_data_points / index_graph_edgesindex_fields 字段嵌入并写向量库(add_data_points.py:14-16)。

因为节点 id 是确定性的(4.2),upsert 天然幂等:重复跑同一份数据不会产生重复节点。

4.5 统一引擎

召回侧不直接碰三个库,而是通过 get_unified_engine() 拿一个「统一存储引擎」,它把 .graph / .vector 包在一起(GraphCompletionRetriever.get_retrieved_objectsself._unified_engine = await get_unified_engine(),graph_completion_retriever.py:115;接口在 cognee/infrastructure/databases/unified/)。这层抽象让检索代码不关心底层用的是 Ladybug 还是 Neo4j。

4.6 节点 id 的另一个工具:generate_node_id

除了类带命名空间的 id_for,还有一个更轻的 generate_node_id(cognee/infrastructure/engine/utils/generate_node_id.py:4),用同样的归一化规则但不带类名前缀:

# 真实源码
def generate_node_id(node_id: str) -> UUID:
return uuid5(NAMESPACE_OID, node_id.lower().replace(" ", "_").replace("'", ""))

区别:id_forf"{cls.__name__}:{values}" 保证不同节点类型不撞 id;generate_node_id 是裸字符串。新代码里实体类节点应优先走 id_for(类名命名空间更安全)。

4.7 边界

  • 改了 metadata 别丢 identity_fields:子类覆盖 metadata 时若漏掉父类的 identity_fields,_get_identity_fields 会沿 MRO 检查并打 warning,但不会自动补——节点会退回随机 id(DataPoint.py:90-110)。
  • 时间戳是毫秒整数(created_atint(... timestamp() * 1000),DataPoint.py:54-59),不是 datetime 对象。

4.8 代码地图

主题文件路径符号名
统一节点基类cognee/infrastructure/engine/models/DataPoint.pyDataPoint, id_for, _get_identity_fields
轻量节点 idcognee/infrastructure/engine/utils/generate_node_id.pygenerate_node_id
落库任务cognee/tasks/storage/add_data_points.pyadd_data_points
图库接口 + 默认cognee/infrastructure/databases/graph/graph_db_interface, config(graph_database_provider)
向量库接口cognee/infrastructure/databases/vector/vector_db_interface
统一引擎cognee/infrastructure/databases/unified/get_unified_engine