称呼计算引擎
族谱软件最容易被忽视、却最难做对的一件事:给定任意两个节点,自动算出他们之间该怎么称呼——堂哥、表姐、姑父、二嫂、外孙、连襟……
这是云族记技术密度最高的模块。本文讲清楚它的思路,以及一个反直觉的设计决策:为什么没有用"最短路径"算法。
称呼不是"距离",是"关系结构"
初学者(包括我最初的版本)很容易想到:把家族建成一张图,两个人之间的关系就是图上的路径,用 BFS 求最短路径就行。
这个思路是错的,原因有三个:
- 父系血亲是一棵严格的树,路径本来就唯一。 每个人有确定的父和母,两个人的血亲关系只有一条确定的上行路径,根本不需要"搜索",需要的是定位他们的最近共同祖先(LCA)。
- 最短路径不等于正确称呼。 亲属称呼由一串多维特征共同决定:共同祖先是谁、各自离共同祖先几代、走父系还是母系、双方性别、谁年长。同样相隔 3 跳,可能是"表哥"也可能是"姑父",跳数完全不携带这些信息。
- 配偶是双向边,图里有环。 BFS 在含环的图里会绕圈,并且可能选中一条"跳数最短但语义错误"的路径(比如通过配偶绕回血亲)。
所以最终的方案不是图搜索,而是**"定位共同祖先 → 提取关系特征 → 查规则表"**。
四步流水线
对"观察者(当前登录的人)"和"目标节点",称呼计算分四步:
观察者 + 目标节点
│
▼
① 构造观察者节点
登录者在当前这棵树上以什么身份存在?
是本家血亲,还是嫁/娶进来的配偶?挂到正确的位置。
│
▼
② 提取关系特征 extractFeature
沿父/母边向上找最近共同祖先(LCA)
得到:代差、父系/母系、双方性别、长幼、排行 …
(姻亲关系再套一层"桥接特征")
│
▼
③ 规则表匹配 lookupKinship
拿特征去查一张 200+ 条的亲属规则表
命中一个关系代号(如 elderMaleCousinPaternal)
│
▼
④ 渲染成中文称谓
关系代号 → 中文词(堂哥/表姐/姑父…)
再按排行加前缀(大哥、二嫂)+ 性别/长幼修正
实在算不出 → 安全兜底为对方姓名,而不是瞎叫第②步:关系特征长什么样
找到最近共同祖先后,两个人的关系可以被一组特征确定性地描述,例如:
- 共同祖先往下,观察者这一支几代、目标那一支几代(代差)
- 这一支走的是父系还是母系(决定"堂"还是"表")
- 双方性别(决定"哥/嫂""姐/姐夫")
- 谁年长、排行第几(决定"大哥/二哥")
"堂"和"表"的区别,本质就是父系/母系这一个特征位;"哥/姐/弟/妹"的区别是性别 + 长幼。特征对了,称呼就是唯一的。
第③步:规则表,而不是无穷的 if-else
中文亲属称谓极其庞杂(父系/母系、血亲/姻亲、长辈/平辈/晚辈、排行、性别组合下来上千种)。没有写成一大坨嵌套 if,而是沉淀成一张声明式规则表:
- 每条规则是"一组特征模板 → 一个关系代号";
- 匹配方式是子集匹配:规则模板里的特征都被满足就算命中;
- 规则在表里的先后顺序就是优先级,特殊情况排在通用情况前面;
- 姻亲通过"桥接特征"嵌套表达,比如"我血亲的配偶""我配偶的血亲"。
这样新增一种称呼,往往只是往表里加一行规则,不用改动计算引擎本身。
第④步:双向各算一遍
关系是双向的:我叫他"表哥",他就该叫我"表弟/表妹"。引擎对两个方向各自完整重算一次,而不是简单取反——因为反过来时代差、性别、长幼全都变了。最后把关系代号翻译成中文词,并按排行加上"大/二/三"前缀。
姻亲关系的五级回落
血亲靠共同祖先就能定位,但一旦关系里夹了"婚姻"这座桥,就要分情况。引擎按下面的顺序逐级回落判定,命中即停:
- 血亲(有共同祖先)
- 真实直系(跨分身、跨家族的直接血缘)
- 我血亲的配偶(如我的"嫂子""姑父")
- 我配偶的血亲(如我的"岳父""内兄")
- 我配偶的血亲的配偶
这保证了无论两个人之间隔了几段婚姻,都能落到一个确定的关系类型上。
几个容易做错、被专门处理的细节
- 堂/表是中性词,兄弟/姐妹是性别配对词。 "堂亲、表亲"不要求同性;但"哥哥/妹妹"必须双方性别已知且符合同性条件,否则不显示该称呼。
- 同母异父的半同胞走独立的母系规则,不能和普通兄弟混为一谈。
- 母系/外亲的长幼按出生日期判定,父系按族谱排行判定——因为不同家族之间的"排行"不可横向比较。
- 配偶分身不能回落去用他在自己本家的血缘,否则会把"配偶家的某人"误判成我的直系亲属。
- 性别未知就不硬猜:该节点称呼留空、回落到姓名,绝不根据位置推断性别后乱叫。
为什么这套设计经得起复杂数据
它在项目里经过了真实的复杂家族样本和回归测试验证:多段婚姻、跨代配偶、同族婚姻、过继/外亲、一个人在多个家族的分身……这些在传统"树"模型里会直接报错或画错的情况,在"特征 + 规则表"模型下都能落到一个确定、可解释的称呼上。
核心算法是纯函数、无副作用的:同样的节点和关系,任何人来算结果都一样,这让它可以被大量样例做自动化回归,而不依赖手动点界面验证。
