图数据模型:节点、边、属性
上一章我们把图数据库环境搭起来了,也跑了几个简单的查询。但你有没有想过一个问题——图到底是怎么组织数据的?
嗯,我自己刚入行那会儿,总觉得图就是个“大网”,节点就是人,边就是关系。但其实如果不搞清楚节点、边、属性这三者的边界,后面做变量衍生很容易翻车。今天我们就来拆开揉碎了讲清楚。
节点:图的“故事主角”
节点是图里最基本的实体。在反欺诈场景里,它可以是用户、设备、手机号、IP地址、银行卡……总之,任何你想分析的对象,只要能独立存在,就应该建成一个节点。
节点之间是平等的,没有谁“挂”在谁下面。不像关系型数据库里一张订单表必须依赖用户表的主键。图里每个节点都有自己的唯一标识(通常是 id 或 uuid),你可以通过标签(Label)来分类。
我的经验:早期我做设备指纹关联时,把IMEI和MAC地址都放在一个“设备”标签下,结果发现同一台设备在不同时间可能换MAC。后来我改成按“硬件指纹”唯一标识,IMEI和MAC分别作为属性存。这个坑让我多花了三天重跑数据。
节点标签的命名,我建议用驼峰式、名词单数::User、:Device、:Phone、:IpAddress。别偷懒用拼音或缩写,团队其他人会恨你的。
边:关系的“动词”
边就是两个节点之间的连接。如果说节点是名词,那边就是动词——它描述了节点之间“做了什么”或“是什么关系”。
在反欺诈中常见的边类型:
:LOGIN— 用户登录设备:TRANSFER— 用户向银行卡转账:SHARE_SAME_IP— 两个用户共用过同一个IP(过去式):CONTACT— 用户之间有过通话或短信
边必须有方向。图数据库里的边默认是有向的,从起始节点指向目标节点。如果关系是双向的(比如“共用一个设备”),你可以建两条边,或者用无向语义处理(查询时忽略方向)。我个人倾向建单向边,查询时通过方向参数控制,更灵活。
注意:别把边当属性存!我曾经见过有人把“登录次数”做成边上的属性,然后在边标签里写 :LOGIN_TIMES。逻辑上不对,因为一条边只代表一次事件,次数是聚合后才有的东西。老老实实把每次登录事件存成一条边,查询时再 count。
属性:附着在节点和边上的“细节”
节点和边都可以挂属性。属性就是键值对,比如用户的注册时间、设备的操作系统版本、转账的金额和时间。
属性不能独立存在,必须依附于节点或边。它的作用就是补充描述——让节点更具体,让边更丰富。
属性设计有几个原则:
- 原子性— 一个属性只存一个值。别把“北京-朝阳-望京”塞到一个 address 字段里,拆成 province/city/district 三个属性。
- 选择合适类型— 数值、字符串、布尔、时间戳、列表(有些图库支持列表类型)。时间戳统一用
datetime或毫秒级整数,避免时区问题。 - 避免存储大文本— 比如用户简介、备注等长篇文字,建议放外部存储,图里只存一个索引或指针。否则查询性能会急剧下降。
| 元素 | 示例 | 关键点 |
|---|---|---|
| 节点 :User | id=1, name='张三', reg_time=2023-01-15 | 每个用户一个节点,标签统一为 :User |
| 节点 :Device | id=D001, os='iOS 16', screen_size=6.1 | 硬件指纹做唯一标识,属性存设备特征 |
| 边 :LOGIN | from (:User) → to (:Device), time=2024-03-10 08:30:00 | 每次登录创建一条边,时间戳作为边属性 |
| 边 :TRANSFER | from (:User) → to (:BankCard), amount=5000, time=... | 金额、时间、渠道都可以放边属性 |
属性在变量衍生中的作用:节点属性可以直接拿来当特征,比如“用户注册天数”。边上的属性往往需要聚合,比如“过去7天该用户登录不同设备的次数”。这部分我们后面几章会专门讲,这里先记住:属性是手工造特征的原材料。
一个小技巧:建图之前,先画一张数据模型的草图——哪些实体是节点,哪些关系是边,节点和边上需要哪些属性。我当时拿着白板画了半小时,后面写导入脚本只花了半天。相反,我有个同事直接上手写 Cypher,结果改了四版数据模型,心态都崩了。
图数据模型的直观理解
下面这张图展示了反欺诈中典型的“用户-设备-IP”模型。节点用不同颜色区分标签,边用带方向箭头的关系连接,属性用小标签挂载在旁边。
画完这个图,你应该能立刻理解:节点是“谁”,边是“干了什么”,属性是“具体细节”。这三者缺一不可。后续所有变量衍生,本质上都是在节点和边的属性和关系上做统计、聚合、路径计算。
避坑指南
我曾经犯过一个错:把日期直接以字符串形式存成属性,比如 '2024-03-10'。后来做时间差计算时,发现图数据库不支持字符串减法,还得先转换。我花了一下午改全量数据。正确做法:存成 datetime 类型或毫秒级整数 1709993400000。
个人习惯:在一个新图谱项目开始时,我会先用一个简单的 CREATE 语句创建 3-5 个节点和几条边,然后用 MATCH 把图全量返回看看。确保节点标签、边类型和属性名称都没问题后,再正式写大批量导入脚本。这个“快速验证”的习惯帮我避免了至少 80% 的返工。