先说清问题:平台不拥有酒店,房源来自多家供应商。同一家酒店,供应商 A 叫「北京王府井希尔顿欢朋酒店」,供应商 B 叫「Hampton by Hilton Beijing Wangfujing」,供应商 C 叫「希尔顿欢朋(王府井店)」,地址格式各不相同,坐标差几十米,电话可能一个带区号一个不带。如果不做归一,用户搜「王府井希尔顿欢朋」会看到三个几乎一样的结果,价格还各不相同,这既是体验灾难,也让「同一家酒店的最低价」这个核心功能压根做不出来。
所以必须做实体归一:判断多条记录是否指向现实世界里同一家酒店,把它们映射到一个平台侧的房源 ID 上。
第一个要解决的是规模。最朴素的做法是两两比较,但比较量随记录数的平方增长——十万条记录就是五十亿次比较,每次还要算字符串相似度,压根跑不完。所以必须先做分块:只在「有可能是同一家」的候选集内比较。我们用地理网格加名称指纹做分块——两家酒店如果相距几公里,压根不用比名称。分块是这类问题的第一性手段,它把平方级降到可接受量级,这一步没做对后面全是空谈。
第二个是怎么判断相似。单靠名称不行(同一品牌不同门店名字极像,「希尔顿欢朋北京王府井」和「希尔顿欢朋北京朝阳」只差几个字,但是两家店);单靠坐标也不行(供应商给的坐标精度差异大,同一栋楼里可能有两家不同的酒店)。所以要多特征加权:名称相似度、地址相似度、坐标距离、电话与邮箱等强标识。强标识(电话、邮箱)一旦匹配,权重应该压倒性地大,因为它们几乎不会碰巧相同。
第三个,也是这类问题最关键的认知:不存在一个阈值能同时避免漏合并和误合并。阈值调高,漏合并多(同一家酒店在平台上有两条,价格比不了);阈值调低,误合并多(两家不同的酒店被合成一家,用户订了 A 店结果到了 B 店,这是恶性事故)。这两类错误的代价严重不对称。
所以设计是双阈值三档:高于上阈值自动建立映射,低于下阈值自动排除,中间那一段进人工审核队列。这个设计承认了「机器判不准」这件事,把不确定性交给人,而不是强行让机器给一个二元答案。面试里这一点是最能体现判断力的地方——很多人会去纠结「用什么算法能判得更准」,而实际的工程解法是「把判不准的部分识别出来交给人,并且让人的结论回流去改进机器」。
最后是可恢复性。误合并一定会发生,所以映射关系必须版本化并且可以定点拆分——不能是一个不可逆的合并操作。
为什么用双阈值三档而不是单阈值二分,这是这个模块最该讲的判断。单阈值意味着强迫机器对每一对记录给出「是」或「不是」,但现实里有一大批记录就是判不准的——两家同品牌的酒店在同一条街上,名称差三个字,坐标差两百米,机器给出的分数一定落在中间,无论阈值定在哪都会错一批。三档的价值在于把「判不准」这件事显式表达出来,交给人,并且让人的结论回流改进机器。取舍是引入了人工成本,而且中间档的量必须控制住——如果一半的对都进人工,这个方案就不可用。所以要持续监控中间档的比例,它是这个系统健康度的核心指标。
阈值往哪边偏,取决于两类错误的代价对比。我们把上阈值定得保守(宁可漏合并,宁可多送人工),因为漏合并只是体验差(用户看到两条相似结果、比不了最低价),误合并是「订了 A 店到了 B 店」的恶性事故——用户在异国深夜到了一家不是自己订的酒店,这个后果不是退款能解决的。代价不对称时,阈值必须往代价小的那一侧偏,这不是保守,是算过账的选择。
为什么先投入做分块而不是先调算法。这是一个优先级判断:再好的相似度算法,在跑不完的比较量下都没有意义。分块把问题从「不可解」变成「可解」,收益是量级的;而算法调优是在可解范围内提高准确率,收益是百分点级的。做这类问题时先看规模再看准确率,顺序反了会浪费很多时间。
踩过的坑:误合并把两家同品牌酒店合成一家,用户订错了店。同一条街上的两家同品牌门店,名称只差门店后缀,坐标差两百米,早期阈值偏低直接自动合并了,结果两家店的房型和价格混在一起,用户订了便宜的那家却被安排到另一家。修法有三条:上阈值调保守、门店后缀词做成强区分特征(「王府井店」和「朝阳店」这类后缀一旦不同,直接大幅降分)、映射支持定点拆分让运营能立刻纠正。教训是:做实体归一时必须先想清「合错了怎么恢复」,可恢复性要和准确率一起设计,只追求准确率而没有回退路径,出一次事就没法收拾。
踩过的坑二:人工审核的结论没有回流,审核队列越来越长。审核员每天判几百对,判完就存个结果,系统一点没变好,第二天来的还是同样类型的争议。修法是把审核结论落成正负样本,定期重新调权重与阈值,并把高频争议模式反哺归一化规则(比如发现大量争议来自「大厦/广场/中心」这类通用词干扰,就把它们加入停用词)。教训是:任何「机器判不准转人工」的设计,都必须带一条从人回到机器的反馈路径,否则人工是纯消耗,系统的准确率永远停在上线那天。
踩过的坑三:归一化规则改了,历史判定结果无法复现。调整了名称归一化的停用词表,之前的打分全都对不上了,运营质疑某个映射为什么当初被自动通过时,我们复算出来的分数和当时不一样,解释不清。修法是归一化规则版本化,打分记录里存规则版本。教训是:凡是参与自动决策的规则都要版本化并记录在决策结果上——这和金额计算要快照规则版本是同一条原则,只要事后可能被质疑,就要能复现。
没做的部分:没有引入图片相似度做辅助判定。理论上同一家酒店的照片会高度重合,是很强的信号,但涉及图像特征提取与向量检索,成本和链路都重,当时优先做了文本与地理特征。也没做跨语言的名称匹配(中文名与英文名的对应),这块靠供应商提供的多语言字段和人工审核兜着。
比较量的下降:这是分块效果的直接度量。报「不分块的理论比较次数」与「分块后的实际比较次数」的对比,以及跑完一轮全量归一的耗时。用量级对比而不是百分比——从平方级降到可接受量级,说清数量级比说「下降 99%」更有信息量。
准确率必须用人工标注的评测集,不能用线上数据自评。做法是抽样人工标注一批「确定是同一家」和「确定不是同一家」的记录对作为评测集,然后报在这个集上的表现。关键是要分开报两类错误:漏合并率和误合并率。只报一个「准确率」是在掩盖问题,因为两类错误的代价完全不同,合成一个数字就看不出保守的阈值策略是否生效。
中间档(进人工)的比例:这是这个系统健康度的核心指标,要持续监控。报的是它稳定在哪个区间,以及审核队列是否收敛(进的比处理的多就会越积越长)。如果中间档占比过高,方案就不可用,这个数字必须诚实报。
人工审核的产出:报审核量、平均处理时长、以及回流样本带来的阈值调整次数。最后这个数字很值得报——它证明反馈闭环真的在运转,而不是写了个队列没人用。
误合并的处理:报发现的误合并笔数(绝对数)以及是否都能通过定点拆分恢复。诚实的表述是「出现过若干笔误合并,均可通过拆分恢复」,而不是声称没有误合并。
不要报「实体归一准确率 99.9%」。这个数字高度依赖评测集的构造方式(评测集里简单样本多还是难样本多,结果差很远),而且它掩盖了两类错误的不对称性。正确表述是「分块把比较量降到什么量级、在人工标注的评测集上漏合并与误合并各是什么水平、中间档送人工的比例稳定在多少、误合并可定点拆分恢复」——四个可核查的事实,比一个准确率有说服力得多。
实体归一之后,一个平台房源 ID 下挂着三四份来自不同供应商的属性:名称、地址、星级、设施列表、房型描述、政策(能否带宠物、几点入住)、照片。它们互相冲突——A 说有免费停车,B 说没有;A 说 14 点入住,B 说 15 点;照片有一半是重复的(同一张房间照片被两个供应商都提供了,只是分辨率不同)。
要对外展示,必须合并成一份。第一版的做法是整条记录择优:给每个供应商打一个总的可信度分,取分最高的那家的数据作为主数据,其他的丢掉。这个做法简单,但错得很实际——供应商的可信度不是一个数,它是分字段的。有的供应商是酒店集团直连,名称和政策非常准,但照片是几年前的;有的供应商是聚合商,照片新且多,但设施列表明显是机器抓的,错误率高。用整条择优,就等于为了拿到准确的名称而接受了过期的照片。
所以第一个设计是按字段独立择源,并维护一个字段级的可信度矩阵(供应商 × 字段 → 可信度)。名称取 A 的、照片取 B 的、政策取 A 的、设施取多家取交集或并集(取决于字段语义)。这个矩阵的初值靠人工抽检定,之后靠纠错反馈调整。
第二个设计来自一个很疼的坑。运营人工修正过的字段,第二天被供应商同步冲掉了。运营发现某家酒店的地址写错了,手动改对,结果当晚的同步任务又把供应商的错误地址写了回去,第二天用户还是导航到错的地方。根因是人工修正和供应商数据存在同一个字段上,同步就是覆盖。
修法是把数据分层:供应商层(每家一份原始数据)、修正层(人工改的)、对外层(合并结果)。同步只更新供应商层,永远不碰修正层;合并时修正层优先级最高。这样人工修正是稳定的,而且供应商的原始数据也没丢(后面纠错、追责、重新合并都要用)。这个分层是这个模块最值得讲的设计——它把「谁能写哪一层」这件事定死了,而不是靠流程约定。
第三个是图片去重。跨供应商的照片大量重复,但字节不同(分辨率、压缩、水印不同),没法用文件哈希去重。用感知哈希(对图像内容做哈希,视觉相似的图哈希也相近)能跨分辨率识别出同一张图,然后在重复组里按清晰度和有无水印择优。
最后是可溯源。每个对外字段要记住它来自哪家供应商、什么时候更新的。因为客诉一定会来(「你们写的有停车场,我到了没有」),能定位到具体数据源才能去找供应商纠错,也才能调整可信度矩阵。没有溯源,纠错就只能靠猜。
为什么按字段择源而不是整条择优,代价是什么。整条择优简单:给每家一个总分,取最高的那家。但它的问题很实际——没有一家供应商在所有字段上都最好。按字段择源能在每个字段上取到最好的数据,代价是合并出来的这份数据「现实中不存在于任何一家供应商」,所以一旦出错,排查要跨多个来源,这也正是为什么必须做字段级溯源。另一个代价是可信度矩阵的维护成本(供应商数乘字段数),缓解手段是只对重要字段精细维护,其余字段用供应商的默认可信度。
设施列表取交集而不是并集,这是个有明确方向的选择。并集能让页面看起来内容更丰富(对转化有好处),但会写上实际不存在的设施——用户因为「有免费停车」订了,到店发现没有,这不是信息误差,是承诺违约,在有消费者保护法规的市场是有实质风险的。交集会少写一些卖点,转化可能略低。取舍是明确的:宁可少写,不可乱承诺。这个判断和「超时按不可售处理」是同一个取向——面对不确定,往「不承诺」的方向偏。
踩过的坑:运营人工修正的字段被同步冲掉了。运营发现某家酒店地址错误,手动改对,当晚同步任务又把供应商的错误地址写了回去,第二天用户还是导航到错的地方,运营改了三次以为自己没保存成功。根因是人工修正和供应商数据存在同一个字段上,同步就是覆盖。修法是分层:同步只写供应商层,修正层独立且优先级最高。教训是:当一份数据有多个写入方时,必须给它们各自的存储层并定义合并优先级,而不是让它们抢同一个字段——「谁后写谁赢」在有自动化写入方参与时,人永远赢不了。
踩过的坑二:图片用文件哈希去重,一张没去掉。跨供应商的同一张房间照片,分辨率、压缩、水印各不相同,字节层面完全不同,文件哈希算出来全不一样。详情页上同一个房间出现四张几乎一样的照片,用户以为图片加载重复了。修法是换感知哈希。教训是:去重的哈希粒度要和「重复」的业务定义对齐——业务说的重复是「视觉上是同一张」,那就得用内容哈希而不是字节哈希。这个判断在文本去重(洗稿检测)上也一样。
踩过的坑三:房型图混进公共图池,用户看到别的房型的照片。合并时把所有图片放进一个池子按质量排序,结果标准间的详情页展示了套房的照片,用户到店后投诉「和图片不符」。修法是房型图必须带房型关联,只在对应房型下展示;只有确实是公共区域的图才进公共池。教训是:合并数据时不能丢掉字段的归属维度,图片不只是「一张图」,它是「某个房型的一张图」,丢了这个维度合并就出错。
没做的部分:没做多语言内容的自动生成与校验(把中文描述译成多语言并保证专有名词一致)。当时靠供应商提供的多语言字段加人工,覆盖不全的语言就回退到英文。也没做设施描述的语义归一(「免费 Wi-Fi」「无线网络免费」「WIFI 免费」映射到同一个设施项),这块只做了关键词规则,没有做语义模型。
修正层被覆盖的问题:这是确定性验证。构造场景——人工修正某字段,然后跑一次全量同步,检查该字段仍是修正后的值。改造前每次同步都会被冲掉,改造后不会。这类问题用「能不能复现」描述比用数字描述更有说服力。
图片去重效果:报合并前后的图片数量对比,以及抽样人工检查「剩下的图里还有没有视觉重复」。要说明抽样量。另外可以报感知哈希的阈值是怎么定的——调太松会把不同房间的相似照片误去掉,这个边界值得讲。
字段准确率:必须抽样人工核验(打电话给酒店或对照官网),报核验样本量和发现的错误数。用绝对数不用比率——「抽检 200 家发现 N 处字段错误」比「准确率 98%」信息量大得多,因为前者能追问「是哪些字段错、错在哪家供应商」。
可信度矩阵的调整:报因纠错反馈而下调过可信度的「供应商 × 字段」组合数。这个数字证明反馈闭环在运转,而不是矩阵上线后就没动过。
修正层的规模趋势:这是个健康度指标。修正层应该是个薄层,如果它持续增厚,说明源头治理没做。报它的规模和趋势,比报一个静态的准确率更能说明治理有没有效果。
不要报「房源信息准确率 99%」。「准确」在这里压根没有统一定义(设施列表少写一项算不算错?照片是去年的算不算错?),而且这个数字无法核查——要核查就得给每家酒店打电话。正确表述是「抽检多少家、发现哪些类型的错误、修正层规模与趋势、可信度矩阵调整了多少次」。也不要报「零投诉」,信息类投诉不可能没有。
供应商每天推数据,有的推全量文件,有的推增量事件。第一版的处理很朴素:收到就写库。这个做法在供应商数据正常时没问题,但供应商也会出错,而且出错的时候是成批出错。
真实发生过一次:某供应商的导出程序编码配置错了,一次推送里几千家酒店的名称全变成了乱码。我们照单全收,几分钟后线上搜索结果里大批酒店名称是乱码,用户投诉涌进来。回滚的时候才发现更麻烦——我们没有批次概念,改动是逐条落库的,只能从备份里捞,恢复花了几个小时。
这件事教出三个设计。
第一,变更要分级,不能所有字段一个待遇。价格和库存是高频变更(一天几十次),必须直通,加人工环节业务就跑不动了;静态描述类(设施、周边介绍)低频,正常入库;名称、地址、坐标这类关键字段,改错的后果严重且改动本应极少,所以转人工确认。分级的依据是「变更频率」乘「改错的后果」——高频且后果小的直通,低频且后果大的转人工。
第二,也是最重要的:要在批次层面做异常检测,而不是只在单条上做校验。单条校验只能发现「这条数据格式不对」,发现不了「这一批数据整体不对」——乱码名称每一条单独看格式都合法。但如果统计整批:一次推送里 30% 的酒店都改名了,这在正常业务里几乎不可能发生,它是供应商出错的强信号。所以要做批次级阈值:关键字段变更占比、下架占比、坐标位移量,超过阈值整批拦截并告警,而不是逐条放行。
这一条的思路值得单独说:异常检测的粒度要和异常发生的粒度一致。供应商是成批出错的,那就得在批次粒度上检测。在单条粒度上无论怎么加校验都拦不住成批的错误。
第三,变更必须可整批回滚。没有批次概念的话,出事只能从备份恢复。所以每次同步生成一个批次号,所有变更挂在批次下,保留变更前的值,回滚就是按批次反向应用。
另外还有幂等重放:同步任务会失败重试、也会需要手动重跑,幂等键用批次号加记录版本,重放不会产生重复变更也不会把新数据覆盖回旧的。
批次级异常检测的阈值怎么定,这是个纯权衡。阈值紧:正常的业务波动(供应商做了一次批量的房型整理、某个城市集中调价)会被误拦,运营要频繁处理误报,最后会要求把阈值放宽甚至关掉。阈值松:拦不住真正的异常。我们的做法是先用历史数据回放定初值——把过去几个月的批次跑一遍,看阈值定在哪能拦住已知的那几次事故又不误拦正常批次;然后按供应商分别设阈值,因为不同供应商的推送习惯差异很大(有的每天小量增量,有的每周一次大批量整理)。用一套全局阈值套所有供应商,一定会同时出现误拦和漏拦。
为什么关键字段转人工,而不是靠灰度兜。灰度能减少影响面(只有小比例用户看到错数据),但它拦不住错误本身,而且关键字段的错误在灰度期间未必能被观测到——地址错了,用户要到出行那天才发现,灰度窗口早过了。所以对「错误的发现滞后于灰度窗口」的字段,灰度是无效的防护,必须前置人工确认。取舍是人工环节引入延迟,缓解手段是这类字段的正常变更量本来就很小(酒店不会天天改名),人工成本可控。
踩过的坑:供应商编码配置出错,一次推送几千家酒店名称变乱码,照单全收上了线。几分钟内线上搜索结果里大批酒店名是乱码,投诉涌进来。更麻烦的是回滚——当时没有批次概念,改动是逐条落库的,只能从备份捞,恢复花了几个小时。修法三条:批次级异常检测(这类问题在批次画像上极其明显)、批次号加旧值保留支持整批回滚、关键字段转人工确认。教训是最重要的那条:异常检测的粒度必须和异常发生的粒度一致。我们之前所有的校验都在单条粒度,而供应商是成批出错的,单条校验对成批错误结构上就无能为力——不是校验写得不够严,是层次不对。
踩过的坑二:重放旧批次把新数据覆盖回了旧值。同步任务失败后手动重跑了一个旧批次,那期间已经有更新的数据进来了,重放把它们盖回去了,表现是「明明改过的数据又变回去了」。修法是幂等键加版本比对,只在版本更新时才写。教训是:幂等不等于可安全重放——幂等保证「重复执行结果一致」,但如果中间有别的写入,重复执行就会覆盖它;要安全重放还需要版本或时间戳的单调性检查。这个区别很容易被混过去。
踩过的坑三:批次被拦下来之后没人处理,数据几天没更新。拦截做好了,告警也发了,但没有指定责任人和处理时限,告警在群里刷过去就没了,直到运营反馈「某家供应商的价格好像很久没动了」。修法是给拦截队列加责任人、处理时限和超时升级。教训是:拦截机制必须配处理闭环,否则只是把「数据错」换成了「数据不更新」,后者时间长了同样是故障,而且更隐蔽。
没做的部分:没做供应商数据质量的自动评分与对外通报(把检测结果定期反馈给供应商并纳入合作评估)。这更多是商务流程而不是技术,当时只做到内部记录。也没做跨供应商的交叉校验(同一家酒店在 A 和 B 处的信息突然分叉,说明其中一家出错了)——这个思路很好但需要成熟的实体归一作为前提,当时归一的覆盖率还不够。
拦截效果:报实际拦下的异常批次数量与它们的类型(编码错误、批量误下架、坐标整体偏移)。这个数字比任何比率都有说服力——「上线以来拦下 N 个异常批次,其中一次是供应商编码错误导致几千家名称异常」是具体、可核查、能追问的。
误拦率:这个必须主动报,否则会被追问。报被拦截后人工判定为「实际正常」的批次占比。诚实地承认有误拦,并说明阈值是按供应商分别调的,比声称没有误拦可信。误拦率过高会导致运营要求关掉拦截,所以它是这个机制能不能活下来的关键指标。
阈值是怎么定的:这一点要主动讲——用历史批次数据回放,把过去几个月的批次跑一遍,看阈值定在哪能拦住已知事故又不误拦正常批次。「用历史数据回放定阈值」这个方法本身就是个加分项,比说「凭经验定的」强得多。
回滚能力:确定性验证——人为同步一个错误批次,然后执行整批回滚,检查所有字段恢复到变更前的值。报的是「可整批回滚,无需逐条修复」这个能力,以及回滚一个批次的耗时。
重放安全性:确定性验证——同步批次 A,再有新数据写入,然后重放批次 A,检查新数据没有被覆盖回旧值。这是踩过坑之后必须固化的用例。
人工确认队列:报关键字段变更的日均量、处理时长、以及超时升级触发的次数。关键字段的变更量本来就应该很小(酒店不会天天改名),如果这个量很大,说明分级分错了或者上游有问题。
不要报「数据同步准确率 100%」或「零污染」。供应商数据本身就有错,我们能做的是拦住成批的异常,单条的错误一定会漏过去。正确表述是「拦下了哪几类异常批次、误拦率多少、阈值如何用历史数据回放确定、可整批回滚」——把能力边界说清楚,比声称零污染可信得多。
没有匹配的内容,换个关键词试试。
项目拆解 · 房源映射与内容治理(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据