跳到主要内容

01 · schema 怎么变成一张图

本章讲离线建图:连库那一下,QueryWeaver 到底往 FalkorDB 里塞了什么。理解这张图的形状,后面 02 章的「图上找表」才看得懂。

1.1 为什么要建图,而不是直接存 schema 文本

它要解决的小问题: LLM 生成 SQL 需要 schema,但整库 schema 太大,得按问题动态挑一小片。要「按语义挑」就需要向量检索,要「顺着外键补齐 join 路径」就需要图。FalkorDB 恰好同时是向量库和图库,一份存储两用。

所以建图阶段做三件事:

  1. 从真实库把结构(表、列、类型、主外键)抽出来;
  2. 给每张表、每列、整个库各生成一句自然语言描述,并算成向量(供后面语义检索);
  3. 把这些写成图的节点和边,并建向量索引

1.2 图长什么样(数据模型)

三类节点、两类边,外加每张表存一份 JSON 外键。

(:Database {name, description, url, user_rules})

(:Table {name, description, foreign_keys(JSON), embedding})

│ [:BELONGS_TO]

(:Column {name, type, nullable, key_type, description, embedding})

│ [:REFERENCES {rel_name, note}] ← 外键:源列指向被引用列

(:Column ...)
  • Column -[:BELONGS_TO]-> Table:列属于表(api/loaders/graph_loader.py:137-158)。
  • Column -[:REFERENCES]-> Column:外键关系,建在列到列之间(api/loaders/graph_loader.py:173-192)。注意边在上,不是表上——这让 02 章能沿着「列→列」精确走 join 路径。
  • 每个 Table 还冗余存了一份 foreign_keys JSON 字符串(graph_loader.py:73-92),方便直接读、不用再遍历边。

get_schema(api/core/text2sql.py:92)反过来把这张图读成前端要的 {nodes, links}:表为 node,REFERENCES 边聚合成表到表的 link。

1.3 schema 是怎么抽出来的

以 Postgres 为例,Loader 全靠查 information_schema / pg_catalog:

  • 表 + 表注释:extract_tables_info(api/loaders/postgres_loader.py:216)。
  • 列 + 主外键标记 + 列注释:extract_columns_info(postgres_loader.py:274),一个大 join 同时算出每列是 PRIMARY KEY / FOREIGN KEY / NONE
  • 外键:extract_foreign_keysextract_relationships(postgres_loader.py:367 / :409)。
  • schema 选择:支持 URL 里用 ?options=-csearch_path%3Dxxx 指定 schema,parse_schema_from_url 解析(postgres_loader.py:100),默认 public

一个巧点:随机采样列值

光有列名和类型,LLM 很难分清「status 里装的是 active/inactive 还是 0/1」。QueryWeaver 对每列随机取 3 个去重样本值塞进描述:

-- api/loaders/postgres_loader.py:54 _execute_sample_query(真实源码,已简化)
SELECT DISTINCT col FROM table WHERE col IS NOT NULL
ORDER BY RANDOM() LIMIT 3

采样值不进 embedding 文本、而是在 embedding 之后追加到最终描述里(api/loaders/graph_loader.py:130-135):... (Sample values: (active), (inactive))。这样列的语义向量保持干净,但喂给分析 agent 的描述里带了真实取值——分析 agent 的 P13「按值选列」规则(见 03 章)正是靠这个。extract_sample_values_for_column 还做了类型过滤:只保留 str/int/float,避开 dict/bytes(api/loaders/base_loader.py:42-67)。

1.4 描述是 LLM 批量生成的

表和库的描述不是硬编码,而是 LLM 现生成:

  • 每张表一句描述:create_combined_description(api/utils.py:36)用 batch_completion 批量(默认每批 10 张表)让 LLM 生成「以表名开头、一句话说这表存什么」。某张表生成失败就回退成表名本身(api/utils.py:98-103)。
  • 整库一句描述:generate_db_description(api/utils.py:107)把表名列表拼进 prompt 让 LLM 概括。这个库级描述后面被相关性判断、找表、分析全阶段复用。

1.5 写图 + 建向量索引

load_to_graph(api/loaders/graph_loader.py:12)是落盘的地方,顺序是:

  1. 先建向量索引再插数据:对 Table.embeddingColumn.embedding 各建一个向量索引,维度取自 embedding 模型、相似度用欧氏距离(graph_loader.py:40-54):
CREATE VECTOR INDEX FOR (t:Table) ON (t.embedding)
OPTIONS {dimension:$size, similarityFunction:'euclidean'}
  1. Database 节点(带库描述、URL)。
  2. 逐表建 Table 节点(表描述的向量),列描述分批 embedding(默认 100 一批,graph_loader.py:104-113)后逐列建 Column 节点并连 BELONGS_TO
  3. 最后按 relationshipsREFERENCES 边;建不起来的关系跳过不报错(graph_loader.py:193-195),保证个别脏外键不阻断整库加载。

embedding 维度靠一次「embed 一个 Hello World 数它的长度」拿到(api/config.py:58-68 get_vector_size),这样换任何供应商的 embedding 模型都不用手配维度。

1.6 命名空间与刷新

  • 每个图名是 {user_id}_{db_name}(graph_name,api/core/pipeline.py:62),做多租户隔离;demo 库有 GENERAL_PREFIX 前缀、绕过命名空间且只读。
  • schema 变了(DDL)或用户手动触发时,refresh_graph_schema(postgres_loader.py:490)先 graph.delete() 整图,再走一遍 load 重建——全量重灌,不做增量 diff

1.7 代码地图

主题文件符号
Postgres 抽 schema 主流程api/loaders/postgres_loader.pyPostgresLoader.load
抽表/列/外键api/loaders/postgres_loader.pyextract_tables_info · extract_columns_info · extract_relationships
随机采样列值api/loaders/postgres_loader.py_execute_sample_query
采样值类型过滤api/loaders/base_loader.pyextract_sample_values_for_column
写图 + 建向量索引api/loaders/graph_loader.pyload_to_graph
表描述批量生成api/utils.pycreate_combined_description
库描述生成api/utils.pygenerate_db_description
图读回 nodes/linksapi/core/text2sql.pyget_schema
图命名空间api/core/pipeline.pygraph_name
全量刷新api/loaders/postgres_loader.pyrefresh_graph_schema