数据截至 (上游 commit 69155611c59f)
01 · schema 怎么变成一张图
本章讲离线建图:连库那一下,QueryWeaver 到底往 FalkorDB 里塞了什么。理解这张图的形状,后面 02 章的「图上找表」才看得懂。
1.1 为什么要建图,而不是直接存 schema 文本
它要解决的小问题: LLM 生成 SQL 需要 schema,但整库 schema 太大,得按问题动态挑一小片。要「按语义挑」就需要向量检索,要「顺着外键补齐 join 路径」就需要图。FalkorDB 恰好同时是向量库和图库,一份存储两用。
所以建图阶段做三件事:
- 从真实库把结构(表、列、类型、主外键)抽出来;
- 给每张表、每列、整个库各生成一句自然语言描述,并算成向量(供后面语义检索);
- 把这些写成图的节点和边,并建向量索引。
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_keysJSON 字符串(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_keys与extract_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)是落盘的地方,顺序是:
- 先建向量索引再插数据:对
Table.embedding和Column.embedding各建一个向量索引,维度取自 embedding 模型、相似度用欧氏距离(graph_loader.py:40-54):
CREATE VECTOR INDEX FOR (t:Table) ON (t.embedding)
OPTIONS {dimension:$size, similarityFunction:'euclidean'}
- 建
Database节点(带库描述、URL)。 - 逐表建
Table节点(表描述的向量),列描述分批 embedding(默认 100 一批,graph_loader.py:104-113)后逐列建Column节点并连BELONGS_TO。 - 最后按
relationships建REFERENCES边;建不起来的关系跳过不报错(graph_loader.py:193-195),保证个别脏外键不阻断整库加载。
embedding 维度靠一次「embed 一个
Hello World数它的长度」拿到(api/config.py:58-68get_vector_size),这样换任何供应商的 embedding 模型都不用手配维度。