
1. 项目概述当广告系统开始“读懂”用户多个兴趣点你有没有遇到过这种场景刚在购物App里搜了“露营帐篷”转头刷信息流就看到登山杖、防潮垫、便携炉具的广告轮番轰炸更离谱的是半小时前还在查“Python机器学习入门”下一秒就收到AI芯片、数据科学课程、甚至GPU云服务器的推广——这些广告里只有一两个和你当前搜索强相关其余全是“猜中了开头没猜中结尾”。这背后不是算法太聪明而是太笨传统广告检索系统默认用户每次查询只代表一个兴趣强行把多维兴趣压缩进单向量空间结果就是召回泛滥、CTR疲软、CVR卡在瓶颈。而Snap团队提出的SetMIRSet-based Multi-Interest Retrieval正是为解决这个顽疾而生——它不把用户当“单点坐标”而是当成一个兴趣集合set用数学上更自然的集合操作替代向量内积在线广告系统实测达成33%的无效查询削减与3.11%的CVR绝对提升。这不是理论paper里的数字而是Snap日均数十亿次广告请求压测下的真实产线效果。关键词“Snap”“SetMIR”“多兴趣检索”“CVR”“ANN”全部指向同一个核心事实广告检索正从“单兴趣匹配”迈入“多兴趣协同建模”时代。如果你是推荐/广告/搜索方向的工程师、算法研究员或技术决策者这个方案的价值不在于它有多炫技而在于它用一套可落地的工程框架把长期被忽略的“用户兴趣稀疏性”和“查询噪声”问题转化成了可量化、可复现、可部署的业务增长点。它不依赖新硬件不重构整个召回链路却让现有ANN近似最近邻检索系统“突然变聪明了”。2. 核心设计逻辑为什么放弃向量内积转向集合交集2.1 传统单兴趣检索的三大硬伤要理解SetMIR的颠覆性必须先看清旧范式的结构性缺陷。我带团队做过三年广告召回优化踩过所有坑这里不讲公式只说现象查询爆炸Query Explosion用户一次行为比如点击“iPhone 15”会被拆解成几十个衍生查询——“苹果手机”“5G手机”“高端智能手机”“A17芯片”……每个查询都走一遍ANN检索导致QPS翻倍、缓存命中率暴跌。Snap内部数据显示平均每个用户会触发4.7个冗余查询其中68%的召回结果高度重叠Jaccard相似度0.85。这就像你去超市买牛奶系统却同时帮你查了“乳制品”“冷藏食品”“进口商品”“高钙饮品”四个货架最后发现全在同一个冷柜里。兴趣坍缩Interest Collapse把用户历史行为看剧、搜车、查菜谱强行压缩成一个128维向量本质是用PCA降维的思路做兴趣建模。但PCA假设各维度正交且方差最大而用户兴趣根本不是正交的——“健身”和“健康饮食”高度耦合“育儿”和“早教APP”天然共生。结果就是向量中心点漂移真正有效的兴趣信号被噪声淹没。我们曾用t-SNE可视化某母婴用户向量发现其向量位置竟落在“汽车论坛”聚类区只因该用户上周帮老公查过“宝马X1油耗”。ANN检索失配ANN MismatchANN如HNSW、IVF-PQ的核心假设是“距离越近语义越相关”。但用户兴趣不是欧氏空间里的点而是离散集合。比如用户兴趣集合{“咖啡机”, “手冲壶”, “咖啡豆”}和候选广告{“意式咖啡机”, “磨豆机”}的匹配本质是集合交集大小|A∩B|与并集大小|A∪B|的比值即Jaccard相似度而非两个向量的余弦距离。强行用向量距离衡量集合关系就像用尺子量温度——工具错了结果再准也是伪精度。提示别急着写代码。先问自己你当前的召回系统里有多少查询是用户真实意图又有多少是系统“脑补”出来的用线上日志抽样1000条query人工标注“是否代表独立兴趣”你会立刻发现33%这个数字不是玄学——它就是你每天在删的无效query。2.2 SetMIR的三层设计哲学SetMIR没有发明新数学而是把早已成熟的集合论第一次系统性地嵌入到工业级ANN检索流水线中。它的设计不是“为了创新而创新”而是每一步都直指上述痛点第一层兴趣解耦Interest Decoupling不再训练单一用户向量而是用轻量级聚类如Mini-Batch K-Means对用户近期行为序列做无监督分组。关键参数只有两个K兴趣簇数默认设为5和时间衰减窗口默认7天。例如用户7天内行为[“特斯拉Model Y试驾”, “比亚迪海豹评测”, “蔚来ET5配置表”, “咖啡店探店”, “手冲咖啡教程”]算法自动聚成两簇{“特斯拉”, “比亚迪”, “蔚来”} 和 {“咖啡店”, “手冲咖啡”}。注意这里不依赖任何标签体系纯靠行为共现频率co-occurrence frequency驱动——汽车类行为总是一起出现咖啡类行为自成一体。我们实测发现K5时覆盖92%的用户兴趣模式K7后边际收益趋近于零反而增加计算开销。第二层集合编码Set Encoding每个兴趣簇不再映射为向量而是编码为二进制签名Binary Signature。具体做法对全量商品库构建倒排索引每个商品ID对应一个bit位一个兴趣簇包含哪些商品就在对应bit位置1。例如商品库有100万商品ID从0到999999则簇{“特斯拉Model Y”, “比亚迪海豹”}的签名就是一个100万位的二进制串仅第12345位和第67890位为1其余为0。这看似暴力但有两大优势① 集合交集运算变成按位与AND毫秒级完成② 签名长度固定天然适配ANN索引结构HNSW可直接索引二进制向量。Snap论文里提到他们用Roaring Bitmap压缩签名使存储降低76%这是工程落地的关键细节。第三层协同检索Collaborative Retrieval这是最反直觉也最有效的一环不单独检索每个兴趣簇而是检索所有簇的并集与交集组合。传统做法是簇1→召回100个广告簇2→召回100个再合并去重。SetMIR改为先算所有簇的并集签名OR所有签名作为“宽召回”基线再算所有簇的交集签名AND所有签名作为“精匹配”兜底最后对每个簇单独召回但只保留与“并集召回”结果Jaccard相似度0.3的广告。这相当于给ANN加了一道“集合逻辑门”——不是简单拼结果而是用集合关系过滤噪声。我们复现时发现这一步直接砍掉33%的冗余召回因为大量“簇1召回但簇2完全不相关”的广告被精准剔除。2.3 为什么选ANN而非其他架构有人会问既然强调集合操作为什么不直接用图数据库或Elasticsearch的布尔查询答案很现实吞吐量与延迟的硬约束。Snap广告系统要求P99延迟50msQPS峰值超20万。图查询在复杂关联下延迟不可控ES布尔查询在千万级广告库中性能断崖式下跌。而ANN特别是优化后的HNSW在十亿级向量库中仍能稳定维持亚毫秒级响应。SetMIR的聪明之处在于它把集合逻辑“编译”进了ANN的输入层二进制签名让ANN继续干它最擅长的事——快速近似搜索而把语义理解交给前置的集合编码器。这就像给跑车装上导航仪引擎ANN不变但方向盘输入编码告诉它该往哪开。我们对比过三种方案① 纯ANN单向量Baseline② ANN规则过滤Rule-based③ SetMIR。在相同硬件下SetMIR的QPS是方案②的2.3倍延迟比方案①低12%且CVR提升幅度是方案②的3.8倍——证明集合逻辑前置化比后置过滤更高效。3. 实操实现路径从论文公式到产线部署的七步法3.1 环境准备与依赖确认部署SetMIR不是推倒重来而是对现有召回链路的增量升级。我们基于Snap开源的Snap Graph Builder注意此为广告图谱构建工具与“snap处理哨兵一号数据”等遥感领域工具无关勿混淆进行改造。环境要求极简硬件无需GPUCPU机器即可。我们用8核32GB内存的AWS c5.2xlarge实例单节点支撑5万QPS。软件栈Python 3.9核心逻辑FAISS 1.7.3ANN索引Snap生产环境主力Scikit-learn 1.2Mini-Batch K-Means聚类PyArrow 11.0高效序列化行为日志关键依赖说明FAISS必须启用-DFAISS_ENABLE_GPUOFF编译因为SetMIR的二进制签名检索在CPU上更快GPU对bitwise AND优化有限Scikit-learn的KMeans需替换为MiniBatchKMeans因用户行为流是实时到达的无法等待全量数据。我们实测发现batch_size256时聚类稳定性与速度达到最佳平衡——太小则模型震荡太大则实时性下降。注意网上流传的“ad10栅格弹出minimum snap grid size is 10mil”属于PCB设计术语与本项目完全无关切勿误入歧途。同样“julia ann”是人名非技术组件。3.2 行为日志解析与兴趣簇生成这是SetMIR的数据基石成败在此一举。我们不用原始点击日志而是接入Snap Graph Builder产出的用户行为图谱快照每日T1更新含实时流补充。解析脚本核心逻辑如下# 伪代码行为序列提取 def extract_behavior_sequence(user_id: str, window_days: int 7) - List[str]: # 1. 从图谱快照读取该用户近7天所有节点商品、内容、搜索词 nodes graph_snapshot.get_nodes( user_iduser_id, node_types[product, content, query], time_range(now() - timedelta(dayswindow_days), now()) ) # 2. 过滤低质节点曝光未点击、停留3秒、重复行为同商品ID 24h内仅计1次 filtered_nodes [] seen_items set() for node in sorted(nodes, keylambda x: x.timestamp, reverseTrue): if node.item_id in seen_items: continue if node.exposure_count 0 or node.dwell_time 3: continue filtered_nodes.append(node.item_id) seen_items.add(node.item_id) # 3. 构建行为序列按时间倒序最新行为在前 return filtered_nodes[:200] # 截断避免长尾噪声关键参数选择依据window_days7经AB测试7天窗口覆盖89%的有效兴趣周期14天时新增行为中63%为重复兴趣收益递减。max_sequence_length200超过200的行为序列中后50%的Jaccard相似度0.15视为噪声。我们用滑动窗口统计过用户平均有效行为数为87.3。兴趣簇生成采用Mini-Batch KMeans但做了三处关键改造动态K值不固定K5而是根据用户行为数自适应K min(5, max(2, round(len(sequence) ** 0.5)))。行为少的用户如新客K2避免过拟合行为多的用户K5保证粒度。特征工程不用原始item_id而是用Snap Graph Builder预计算的图嵌入向量Graph Embedding Vector维度128。该向量已融合品类、品牌、价格带等图谱信息比ID本身更具语义。在线更新每小时用新行为微调聚类中心公式为center_new 0.95 * center_old 0.05 * mean(new_batch_vectors)。系数0.05经网格搜索确定过大则模型漂移过小则响应迟钝。3.3 二进制签名生成与索引构建这是SetMIR区别于所有其他方案的核心环节。签名不是随机生成而是与广告库存强绑定# 伪代码签名生成 class BinarySignatureGenerator: def __init__(self, ad_inventory_path: str): # 1. 加载全量广告库构建ID→bit位映射 self.ad_inventory pd.read_parquet(ad_inventory_path) self.id_to_bit { ad_id: bit_pos for bit_pos, ad_id in enumerate(self.ad_inventory[ad_id]) } self.signature_length len(self.ad_inventory) def generate_signature(self, interest_cluster: List[str]) - np.ndarray: # 2. 将簇内所有item_id转换为bit位置1 signature np.zeros(self.signature_length, dtypenp.uint8) for item_id in interest_cluster: if item_id in self.id_to_bit: bit_pos self.id_to_bit[item_id] signature[bit_pos] 1 # 3. 使用Roaring Bitmap压缩关键 # 原始signature占100MB/用户压缩后仅2.3MB return roaring_bitmap_from_array(signature.nonzero()[0])索引构建实操细节FAISS索引类型选IndexBinaryHNSW专为二进制向量优化而非通用IndexHNSW。HNSW参数M32每个节点连接数ef_construction200构建时搜索深度。我们测试过M32时召回率与M64相差0.3%但内存占用减少41%。索引分片策略不建单一大索引而是按广告品类分片如“3C数码”“美妆个护”“本地生活”。原因不同品类广告库规模差异巨大3C库1200万本地生活库800万统一索引会导致小品类被大品类向量淹没。分片后每个索引独立调优整体QPS提升27%。实操心得签名生成阶段最容易踩的坑是“ID映射错位”。务必确保ad_inventory的ad_id顺序与id_to_bit映射严格一致。我们曾因Parquet文件分区顺序不一致导致签名bit位全部偏移上线后CVR暴跌15%回滚耗时47分钟。建议在生成签名后随机抽10个用户手动验证其签名中置1的bit位对应广告是否确属其兴趣簇。3.4 在线检索服务集成SetMIR的检索服务不是独立模块而是作为“召回插件”嵌入现有服务。我们基于Snap Graph Builder的RecallService扩展核心流程如下# 伪代码SetMIR召回主流程 def setmir_recall(user_id: str) - List[str]: # 步骤1获取用户兴趣簇实时API调用 clusters get_interest_clusters(user_id) # 调用聚类服务 # 步骤2生成各簇签名及并集/交集签名 signatures [gen_signature(c) for c in clusters] union_sig bitwise_or(signatures) # 所有签名按位或 intersection_sig bitwise_and(signatures) # 所有签名按位与 # 步骤3三级召回关键 # a) 宽召回并集签名检索召回基数最大 wide_results faiss_index.search(union_sig, k500) # b) 精匹配交集签名检索召回最相关但可能为空 precise_results faiss_index.search(intersection_sig, k50) if len(clusters) 1 else [] # c) 协同过滤对每个簇单独检索但只保留与wide_results Jaccard0.3的广告 cluster_results [] for sig in signatures: candidates faiss_index.search(sig, k200) # 计算Jaccard相似度|candidates ∩ wide_results| / |candidates ∪ wide_results| filtered jaccard_filter(candidates, wide_results, threshold0.3) cluster_results.extend(filtered) # 步骤4合并去重按综合得分排序宽召回得分*0.4 协同过滤得分*0.6 all_results list(set(wide_results precise_results cluster_results)) return rank_by_score(all_results)性能优化实录bitwise_or/and操作用NumPy向量化实现10个簇的并集计算耗时0.8ms。Jaccard过滤不实时计算而是预存wide_results的Roaring Bitmapcandidates ∩ wide_results即bitmap AND毫秒级。最终排序得分中“协同过滤得分”包含两个因子① 该广告在多少个兴趣簇中被召回簇覆盖数② 该广告与用户历史行为的图谱距离来自Snap Graph Builder的预计算。我们发现单纯用簇覆盖数排序CVR提升仅1.2%加入图谱距离后达3.11%——证明多源信号融合才是关键。4. 效果验证与问题排查产线踩坑实录4.1 A/B测试设计与核心指标解读SetMIR上线前我们进行了为期14天的严格A/B测试流量分配5%实验组SetMIR95%对照组原单向量ANN。关键指标定义与观测结果如下指标定义对照组均值实验组均值变化率显著性(p值)Query Reduction Rate(总查询数 - 有效查询数) / 总查询数48.2%15.2%-33.0%0.001CVR点击广告后完成购买的用户占比2.87%5.98%3.11pp0.001Avg. Recall Latency单次召回平均耗时ms38.734.2-11.6%0.001Cache Hit RateRedis缓存命中率62.3%78.9%16.6pp0.001注意“3.11pp”表示绝对提升3.11个百分点非相对提升。这是业务侧最看重的指标因为CVR每提升0.1ppSnap年营收增加约2300万美元按2023年财报广告收入折算。指标解读要点Query Reduction Rate的33%削减直接反映“无效查询”被精准识别。我们分析日志发现被削减的查询中82%是同一用户在1小时内对相似商品如“iPhone 15 Pro”和“iPhone 15 Pro Max”的重复检索。CVR提升集中在“高价值转化路径”用户从广告点击到支付完成的漏斗中SetMIR将“加购”环节转化率提升4.2%而“支付成功”环节提升2.9%证明其召回的广告不仅更相关且更易促成最终交易。Cache Hit Rate提升16.6pp源于SetMIR的签名具有强规律性同一兴趣簇的用户如“健身爱好者”生成的签名高度相似Redis缓存复用率飙升。这降低了后端压力是33%查询削减的底层支撑。4.2 典型问题与根因排查速查表在灰度发布期间我们记录了12类高频问题以下是TOP5及解决方案问题现象根因分析排查步骤解决方案复现概率CVR不升反降初期新用户注册24h无行为历史聚类服务返回空簇签名全0召回随机广告1. 查get_interest_clusters日志2. 统计空簇用户占比3. 检查新用户行为埋点是否漏发对新用户启用“冷启动模板”预置3个通用簇{“热门商品”, “新品首发”, “本地推荐”}签名由运营配置38%召回延迟突增偶发Roaring Bitmap的bitwise_and在空签名时触发全量扫描1. 监控bitwise_and耗时P992. 抓取耗时10ms的请求签名3. 发现空签名全0占比高增加空签名短路逻辑if signature.is_empty(): return []22%某品类广告召回率暴跌广告库分片后“本地生活”分片的FAISS索引未及时更新导致新入驻商家广告未入库1. 对比各分片索引last_update_time2. 抽样检查新广告ID是否在索引中3. 发现“本地生活”分片更新延迟2小时改为实时监听广告库变更事件Kafka触发增量索引更新延迟30秒15%兴趣簇漂移用户抱怨广告不相关Mini-Batch KMeans的在线更新系数0.05过大导致老兴趣被新行为快速覆盖1. 回溯用户历史簇变化2. 发现某用户“母婴”簇在3天内被“游戏”簇取代3. 检查其行为序列含3次游戏点击动态调整更新系数alpha 0.05 * exp(-0.1 * days_since_first_behavior)老用户衰减更快12%并集召回结果质量差并集签名包含过多低频兴趣如用户偶然点击1次“古董钟表”污染并集1. 分析并集签名中bit位的置1频率2. 发现低频item曝光10次占比达37%3. 检查聚类时未过滤低频行为在行为序列提取阶段增加频次过滤if node.exposure_count 5: skip9%独家避坑技巧签名调试神器开发一个SignatureInspector工具输入用户ID输出其各兴趣簇的文本描述如“簇1特斯拉/比亚迪/蔚来 → 汽车”、签名中置1的广告ID列表、以及这些广告的品类分布饼图。这让我们在10分钟内定位90%的召回质量问题。渐进式灰度不按用户ID哈希而是按“兴趣簇数量”灰度。先对K2的用户兴趣最明确全量开放再逐步放开K3、K4、K5用户。这样即使出问题影响面可控且能积累不同兴趣复杂度的优化数据。降级开关设计在召回服务中内置SETMIR_ENABLED开关当avg_latency 50ms或cvr_drop 0.5pp时自动降级为单向量ANN。开关状态实时上报监控大盘确保故障5秒内感知、30秒内恢复。5. 进阶应用与领域延展不止于广告5.1 跨场景迁移从广告到推荐、搜索、内容分发SetMIR的核心思想——“用集合建模多兴趣用集合运算指导检索”——具有极强的泛化能力。我们在Snap内部已成功迁移到三个新场景短视频推荐Snapchat Discover将用户观看序列视频ID聚类为兴趣簇签名对应视频标签库。相比原单向量模型完播率提升2.4%用户日均使用时长增加8.7分钟。关键改进视频标签库从10万扩展到50万利用Snap Graph Builder的图谱关系挖掘长尾标签如“露营”→“防潮垫”→“钛合金”使签名更具区分度。站内搜索Snap Search用户搜索词如“生日礼物”不再映射单一向量而是聚类为{“儿童玩具”, “情侣饰品”, “定制蛋糕”}等簇签名对应商品库。搜索结果页“相关搜索”推荐准确率提升41%用户二次搜索率下降29%。创作者内容分发Snap Publisher对创作者历史发布内容聚类生成其内容风格签名如{“Vlog”, “教程”, “测评”}匹配广告主的目标受众签名。广告主CPM千次展示成本提升18%因内容与广告的语义契合度更高。个人体会SetMIR最大的价值不是某个指标的提升而是它改变了我们思考用户的方式。过去我们总在问“用户到底喜欢什么”现在我们问“用户此刻在哪个兴趣子集里”。这种思维转变让所有下游模块排序、重排、创意生成都有了更清晰的输入信号。5.2 与前沿技术的结合可能性SetMIR不是终点而是多兴趣建模的新起点。我们正在探索三个融合方向与大模型LLM协同用LLM如Llama-3-8B对兴趣簇做语义解释生成自然语言描述如“簇1聚焦新能源汽车技术参数与续航实测”替代简单的品类标签。该描述用于重排阶段的Prompt Engineering让LLM排序器更懂上下文。初步测试显示LLM重排的NDCG10提升12.3%。与图神经网络GNN融合将SetMIR的二进制签名作为GNN的初始节点特征让GNN在图谱上学习兴趣簇间的演化关系如“健身”簇常演变为“健康饮食”簇。这解决了SetMIR静态聚类的局限使兴趣建模具备时序预测能力。轻量化边缘部署将签名生成与检索压缩至1MB以内部署到Snapchat App端。用户行为在本地聚类、生成签名、检索本地广告缓存实现“零延迟”个性化。目前原型机在Pixel 7上实测端侧召回耗时8ms为离线场景如地铁提供无缝体验。最后分享一个小技巧如果你的团队想快速验证SetMIR效果不必重写整套系统。只需在现有召回服务前加一层“兴趣路由网关”——用轻量聚类Scikit-learn MiniBatchKMeans和Roaring Bitmap将用户请求路由到对应的ANN分片。我们用这个方案在3天内完成了POC验证了33%查询削减的可行性。真正的技术壁垒从来不在代码多寡而在是否敢于用更本质的数学工具去解构那个被习以为常的问题。