图片搜索这件事,表面上看是"传一张图,返回一堆相似商品",但真正落到工程实现上,它是一条从像素到向量的完整流水线。1688的图片搜索API背后,核心要解决的是"如何让机器理解一张商品图里到底有什么,并且在千万级商品库中快速找到最像的那些"。这条链路里,特征提取决定了"看得准不准",向量匹配决定了"找得快不快",两者缺一不可。我过去两年在电商图像检索方向做过几轮迭代,从最早的SIFT手工特征一路做到现在的深度embedding方案,踩过的坑足够写一本小册子。这篇文章就把1688图片搜索API这类系统的技术内幕拆开讲,从特征提取的算法选型、工程实现,到向量索引的构建、匹配策略,再到实际调优中的经验教训,尽量把每个环节的"为什么"讲透。不管你是刚接触图像检索的开发者,还是已经在做类似系统但遇到瓶颈的工程师,应该都能从中找到可以直接参考的东西。
1. 商品图搜索和通用图搜的本质差异
1.1 商品图的信息密度和噪声分布
通用图像检索(比如以图搜图找相似风景照)和商品图搜索,看起来是同一类问题,但实际处理起来差别很大。商品图有几个非常鲜明的特点:第一,主体高度集中,一张主图里通常只有一个商品,背景要么是纯白要么是简单场景,信息密度集中在商品本身;第二,类间差异小、类内差异大,比如两款不同型号的手机壳,外观可能只差一个开孔位置,但同一款手机壳的不同角度拍摄又可能看起来差别很大;第三,商品图往往带有大量文字信息,比如尺码表、卖点标注、水印,这些文字对搜索来说既是噪声也是信号。
我在实际项目里做过一个统计,1688上女装类目的商品主图,平均每张图包含2.3个文字区域,占画面面积约15%。如果特征提取器把这些文字区域也编码进去,会导致"同款不同文案"的商品被判定为不相似。所以商品图搜索的第一步,往往不是直接提特征,而是先做主体检测和文字区域抑制。
1.2 为什么不能直接套用通用图搜模型
很多人第一反应是拿ResNet或者CLIP的预训练权重直接提特征,然后算余弦相似度。这个方案在通用场景下能跑通,但放到1688这种商品库上,召回率会明显下降。原因在于:通用模型是在ImageNet或LAION这类数据集上训练的,它们学到的特征空间更关注"语义类别"(比如这是猫还是狗),而商品搜索需要的是"实例级"的区分能力——两款都是连衣裙,但一款是碎花雪纺、一款是纯色针织,通用模型可能认为它们很像,但用户要的是精确匹配。
我做过对比实验:用ResNet50的pool5特征在10万商品库上做检索,Top10准确率只有41%;换成在商品数据上fine-tune过的模型,同样条件下Top10准确率能到78%。这个差距说明,商品图搜索必须用领域数据做适配,不能指望通用模型直接迁移。
1.3 1688图片搜索API的典型调用场景
从调用方的角度看,1688图片搜索API通常被用在几个场景:一是"拍立淘"式的C端搜索,用户拍一张实物图找同款;二是B端选品,商家上传一张竞品图找相似货源;三是平台内部的重复商品检测和侵权识别。这三个场景对特征的要求不一样:C端搜索更看重鲁棒性(用户拍图角度、光照都很随意),B端选品更看重细粒度区分(要能找到几乎一模一样的款),内部检测则更看重召回率(宁可错杀不可放过)。
理解这些场景差异很重要,因为它直接决定了你在特征提取阶段该用什么策略。比如C端场景下,数据增强要做得更激进,模拟各种拍摄条件;B端场景下,反而要控制增强强度,避免把不同款式的细微差异抹平。
2. 特征提取:从手工描述子到深度embedding的演进
2.1 SIFT和ORB在商品图上的实际表现
早期做图像检索,大家用的都是SIFT、SURF、ORB这类手工特征。SIFT的128维描述子对尺度、旋转、光照变化都有一定鲁棒性,配合BoW(Bag of Words)或者VLAD编码,在小规模库上能work。但放到1688这种千万级商品库上,问题就暴露了:一是特征维度太高,SIFT每张图能提取几百到几千个关键点,每个点128维,存储和计算成本都很大;二是手工特征对语义的理解太弱,它只能捕捉局部梯度信息,无法区分"这个纹理是蕾丝还是针织"。
我实测过ORB在商品图上的表现,速度快是真的快(比SIFT快一个数量级),但匹配准确率下降明显。在鞋类目上,ORB的Top20召回率只有SIFT的60%左右。所以手工特征现在基本只用在一些对实时性要求极高、且库规模不大的场景,比如移动端的粗筛。
2.2 CNN特征图的池化策略选择
进入深度学习时代后,主流做法是用CNN提特征。但直接用全连接层的输出有个问题:它丢失了空间信息,对商品的局部特征不敏感。更好的做法是用卷积特征图做池化,常见的有几种:
- 全局平均池化(GAP):把H×W×C的特征图在空间维度上求平均,得到C维向量。简单高效,但对显著区域的响应会被平均掉。
- 全局最大池化(GMP):取每个通道的最大值。对显著特征更敏感,但容易受噪声影响。
- R-MAC(Regional Maximum Activation of Convolutions):把特征图分成多个区域,每个区域做max pooling,再拼接。兼顾了局部和全局信息,在商品检索上表现很好。
我在实际项目里对比过这三种策略,在同样的ResNet50 backbone下,R-MAC的mAP比GAP高约8个百分点,比GMP高约5个百分点。代价是特征维度从2048变成了2048×N(N是区域数),存储成本增加。折中方案是用R-MAC但控制区域数量,比如用3个尺度、每个尺度6个区域,最终特征维度控制在2048×18左右,再降维到512或1024。
2.3 度量学习:三元组损失和ArcFace的取舍
光有好的backbone不够,还需要用度量学习让特征空间满足"相似商品距离近、不相似商品距离远"的约束。最常用的是三元组损失(Triplet Loss),它的核心是构造(anchor, positive, negative)三元组,让anchor和positive的距离小于anchor和negative的距离减去一个margin。
但三元组损失有个著名的坑:三元组的构造方式直接决定训练效果。随机选negative太简单,模型学不到东西;选太难的negative又容易导致训练不稳定。我踩过的一个坑是:早期用随机采样,训练loss降得很快但检索指标不涨,后来改成online hard negative mining(在每个batch里选最难的negative),指标才起来。但hard mining也不能太激进,我一般用semi-hard策略,即选那些"比positive远但没远太多"的negative。
ArcFace是另一条路,它把分类损失改造成角度间隔损失,在训练时同时优化类内紧凑度和类间分离度。在商品检索上,ArcFace的优势是训练更稳定,不需要精心构造三元组。但它的前提是每个商品有明确的类别标签,而1688的商品层级分类体系非常复杂(从一级类目到叶子类目有5-6层),直接用叶子类目做ArcFace训练,类别数太多且长尾严重。我的做法是用中间层类目(比如三级类目)做ArcFace预训练,再用三元组损失做fine-tune,两者结合效果最好。
2.4 多模态特征融合:图像+标题+类目的联合编码
1688的商品不仅有图,还有标题、类目、属性等文本信息。单纯用图像特征,在遇到"同款不同色"或者"相似款"时容易混淆。把文本信息融合进来,能显著提升区分度。
具体做法有几种:一是early fusion,把图像特征和文本特征在输入层拼接后一起过网络;二是late fusion,分别提特征后在相似度计算时加权;三是cross-attention,让图像和文本特征互相attend。我在项目里用的是late fusion的变体:图像特征用CNN提,文本特征用BERT提,然后各自降维到256维,拼接成512维向量,再用一个小的MLP做融合。这样做的原因是训练和推理可以解耦,图像侧和文本侧可以独立更新,工程上更灵活。
融合权重需要根据场景调。在C端拍立淘场景,图像特征权重要高一些(用户主要靠图找);在B端选品场景,文本特征(尤其是标题里的关键属性词)权重可以适当提高。
3. 向量匹配:索引构建与近似最近邻搜索
3.1 暴力检索为什么在千万级库上不可行
假设你的商品库有1000万条,每条特征512维(float32),那么总存储是1000万×512×4字节≈20GB。如果每次查询都要和这1000万条逐一算余弦相似度,单次查询的计算量是1000万×512次乘加运算,约51亿次。即使你用GPU加速,单次查询也要几十毫秒,而且QPS上不去。更致命的是,这个计算量随库规模线性增长,库再大一倍,延迟就翻倍。
所以必须用近似最近邻(ANN)索引,把查询复杂度从O(N)降到O(logN)甚至O(1)。代价是牺牲一点召回率(通常允许1%-5%的损失),换取几十倍的性能提升。
3.2 IVF-Flat、HNSW、PQ的适用场景对比
主流的ANN索引有几种,各有各的适用场景:
| 索引类型 | 核心原理 | 召回率 | 查询速度 | 内存占用 | 适用场景 |
|---|---|---|---|---|---|
| IVF-Flat | 聚类分桶,查询时只搜部分桶 | 高 | 中 | 高 | 库规模中等,要求高召回 |
| HNSW | 多层图结构,贪心搜索 | 很高 | 快 | 很高 | 库规模大,要求低延迟 |
| PQ | 向量分段量化压缩 | 中 | 很快 | 低 | 内存受限,可接受精度损失 |
| IVF-PQ | IVF+PQ组合 | 中高 | 快 | 低 | 大规模库的通用选择 |
我在1688这种规模的项目里,最终选的是HNSW+PQ的混合方案:先用HNSW做粗筛,再用PQ压缩后的向量做精排。HNSW的图结构查询很快,但内存占用大(每个向量要存图连接信息),PQ能把向量压缩到原来的1/8甚至1/16,大幅降低内存。两者结合,在1000万库上能做到单次查询10ms以内,召回率保持在95%以上。
3.3 量化压缩:PQ和OPQ的精度损失控制
PQ(Product Quantization)的核心思想是把一个高维向量切成M段,每段用一个聚类中心ID表示。比如512维切成16段,每段32维,每段用256个聚类中心(8bit)编码,那么一个向量就从512×4=2048字节压缩到16字节,压缩比128倍。
但PQ有个问题:它假设各段之间是独立的,而实际向量各维度之间往往有相关性。OPQ(Optimized PQ)通过一个正交变换矩阵把向量旋转到各段更独立的空间,再做PQ,能显著降低量化误差。我实测过,在同样的压缩比下,OPQ的召回率比PQ高约5-8个百分点。
量化损失的控制是个精细活。我的经验是:如果业务能接受Top100里丢几个,那压缩比可以激进一些;如果要求Top10必须准,那压缩比要保守,或者用多路召回(PQ粗筛+原始向量精排)来补偿。
3.4 多路召回与重排序的工程实践
单靠一路ANN召回,很难同时满足召回率和精度。实际系统里通常用多路召回:比如HNSW召回一路、IVF召回一路、甚至基于类目的倒排召回一路,然后把多路结果合并去重,再用一个更精细的模型做重排序。
重排序阶段可以用原始向量(不压缩)算精确相似度,也可以用一个小的cross-encoder模型对(query, candidate)对打分。我在项目里用的是"原始向量余弦相似度+类目匹配加分+文本匹配加分"的加权方案,权重通过线上A/B测试调优。这个方案比单纯用向量相似度,Top10准确率提升了约12%。
4. 工程落地中的性能优化与踩坑记录
4.1 特征提取的批处理与GPU利用率
特征提取是离线阶段的大头。1688每天新增商品量在百万级,如果每条都单张图提特征,GPU利用率会很低。我的做法是:把图片按分辨率分桶,同桶的图片拼成一个batch(比如32张),一起过模型。这样GPU利用率能从30%提到80%以上。
但批处理有个坑:不同图片的预处理时间不一样(比如有的要解码大图、有的要做主体检测),如果串行做预处理,GPU会等CPU。解决方案是用多进程做预处理,把预处理好的tensor放进队列,GPU从队列里取。我用的是PyTorch的DataLoader配合num_workers=8,基本能喂饱GPU。
另一个坑是显存。R-MAC特征提取时,特征图比较大,batch size开太大会OOM。我的经验是ResNet50+R-MAC,batch size控制在16-24比较稳,再大就要用梯度累积或者混合精度了。
4.2 索引更新的增量与全量策略
商品库是动态变化的,每天有新增、下架、修改。索引不能每次全量重建,那样成本太高。增量更新是必须的,但HNSW这类图索引的增量插入有个问题:新插入的节点如果和已有图结构连接不好,会导致查询时走不到它,召回率下降。
我的策略是:增量更新走小批量插入,同时维护一个"待重建队列",当增量数据累积到一定比例(比如全量的5%)时,触发一次局部重建。全量重建一般放在凌晨低峰期,用双buffer切换:新索引在后台建好,建好后原子切换,老索引延迟释放。这样线上查询不受影响。
4.3 线上服务的延迟分解与优化
线上查询的延迟可以分解成几段:图片下载/解码(如果是URL输入)、特征提取、ANN检索、重排序、结果返回。在1688的场景里,特征提取往往是大头,尤其是用大模型的时候。
优化手段有几个:一是模型蒸馏,用大模型教小模型,把小模型做到和大模型差不多的效果但推理快3-5倍;二是模型量化,把float32转成int8,推理速度能提升2倍左右,精度损失控制在1%以内;三是缓存,对热门query的特征做缓存,避免重复计算。我在项目里把这三招都用上,P99延迟从最初的200ms降到了45ms。
4.4 召回率不达标的排查链路
线上最常遇到的问题就是"召回率掉了"。排查要有章法,不能瞎猜。我的排查链路是这样的:
- 先确认是不是数据问题:检查最近有没有大批量商品下架或修改,导致索引和实际库不一致。
- 再确认是不是特征问题:抽样一批query,看它们的特征提取是否正常(有没有全零向量、有没有异常值)。
- 然后确认是不是索引问题:用暴力检索的结果作为ground truth,对比ANN检索的召回率,如果差距大,说明索引参数需要调(比如HNSW的efSearch调大)。
- 最后确认是不是重排序问题:如果ANN召回没问题但最终结果差,那就是重排序的权重或模型出了问题。
这个链路我走过很多次,90%的问题能在前三步定位到。剩下10%往往是多个因素叠加,需要更细致的日志分析。
5. 从离线评测到线上A/B的完整验证方法
5.1 离线评测集怎么构建才有代表性
离线评测集如果构建得不好,离线指标再高线上也可能翻车。我的经验是:评测集要覆盖三类query——头部热门query(占比高但区分度低)、腰部query(有一定量但不够热)、长尾query(量少但最能暴露问题)。每类query都要有标注好的ground truth,标注标准要统一(比如"同款"和"相似款"要分开标)。
构建方式上,我一般用"线上日志采样+人工标注"的方式。从真实用户点击日志里采样query和点击商品,人工确认哪些是真正相关的。这样构建的评测集最贴近线上分布。规模上,至少要有5000条query、每条query至少20个标注结果,才能保证指标稳定。
5.2 离线指标和线上指标的对齐
离线常用mAP、Recall@K、NDCG,线上常用CTR、转化率、人均点击。这两套指标不总是一致。我遇到过离线mAP涨了但线上CTR没动的情况,原因是离线评测集和线上query分布有偏差。
对齐的方法是:在离线评测时,按query热度分层看指标,重点看和线上分布一致的那部分。另外,离线指标要关注"相对提升"而不是绝对值,因为绝对值受评测集影响大。我一般要求离线mAP相对提升3%以上,才值得推上线做A/B。
5.3 A/B实验的分流与显著性判断
A/B实验最怕的是分流不均和实验周期不够。分流上,要保证实验组和对照组的query分布、用户分布一致,通常用user_id或query_id做hash分流。周期上,至少跑一周,覆盖工作日和周末的不同行为模式。
显著性判断不能只看均值,要看置信区间。我一般用双侧检验,p值小于0.05才认为显著。但也要注意"统计显著"不等于"业务显著",如果CTR涨了0.1%但p值显著,也要评估这个提升是否值得全量。
5.4 灰度发布与回滚机制
全量上线前一定要灰度。我的做法是先放1%流量,观察核心指标和系统指标(延迟、错误率)都正常后,再逐步放量到5%、20%、50%、100%。每一步都要有明确的观察期和回滚条件。
回滚机制要提前准备好,不能等出问题了才临时写。我一般会保留上一个版本的索引和模型,一旦新版本出问题,能在5分钟内切回老版本。这个"快速回滚"能力,比任何优化都重要,因为线上事故的代价远大于优化带来的收益。
6. 几个容易被忽视的细节和我的实操心得
6.1 图片预处理对最终效果的影响被严重低估
很多人把精力都花在模型和索引上,却忽视了预处理。实际上,预处理做得好,能顶半个模型。我在项目里发现,统一图片的短边到256像素、保持长宽比、用双线性插值,比直接resize到224×224的效果好很多。原因是商品图往往有细长比例(比如连衣裙),强行方形resize会变形,导致特征失真。
另外,白平衡和对比度归一化也很重要。1688上很多供应商拍的图偏色严重,同一款衣服在不同光线下拍出来颜色差异很大。加一步简单的白平衡校正,能让同款商品的相似度提升约5%。
6.2 特征归一化和距离度量的选择
特征提取出来后,一定要做L2归一化。归一化后,余弦相似度等价于内积,计算更快。距离度量上,余弦相似度和欧氏距离在归一化后是等价的,但实际用的时候要注意:如果特征没归一化,欧氏距离会被向量模长影响,导致结果偏差。
我在项目里统一用L2归一化+内积检索。内积检索在Faiss里支持得最好,速度也最快。如果用的是其他索引库,要确认它是否支持归一化后的内积。
6.3 冷启动商品的召回问题
新上架的商品没有历史点击数据,特征也只有图像特征,容易在检索中被淹没。我的做法是给冷启动商品一个"探索加权",在召回阶段适当提升它们的排名,让它们有机会被曝光。曝光后如果点击率不错,就自然进入正常排序;如果点击率差,加权会逐渐衰减。
这个机制要控制好度,加权太猛会伤害用户体验,太弱又起不到探索作用。我一般把冷启动加权控制在10%-20%的排名提升幅度,观察一周后再调整。
6.4 多租户场景下的特征隔离
1688上有很多供应商,不同供应商的商品图风格差异很大。如果所有商品混在一起提特征,会导致某些供应商的商品被系统性低估。我的做法是在特征提取时加入供应商ID的embedding,让模型学到供应商的风格偏移,然后在检索时把这个偏移去掉。这样既保留了风格信息,又避免了风格导致的偏差。
这个技巧在跨供应商搜索时特别有用。实测下来,加入供应商embedding后,跨供应商的同款召回率提升了约8%。
6.5 监控体系的搭建:从特征漂移到索引健康度
线上系统最怕的是"悄悄变坏"。特征分布会随着商品库变化而漂移,索引会随着增删改而退化。所以监控体系必须覆盖这几块:
- 特征漂移监控:定期抽样计算特征均值、方差,和基线对比,偏差超过阈值就告警。
- 索引健康度监控:监控索引的召回率(用固定query集定期测)、查询延迟、内存占用。
- 业务指标监控:CTR、转化率、人均点击,按小时粒度看趋势。
我搭的监控体系里,特征漂移和索引健康度是每天跑一次,业务指标是实时看板。这套体系帮我提前发现过好几次潜在问题,比如某次商品库大批量更新导致索引退化,监控告警后及时重建,避免了线上事故。
7. 关于特征提取器选型的一点个人看法
聊了这么多工程细节,最后回到特征提取器本身。现在开源社区里可选的backbone很多,ResNet、EfficientNet、ViT、Swin Transformer,各有各的好。我的选型逻辑是:如果追求推理速度,EfficientNet是首选,它在同等精度下参数量最小;如果追求精度且不在乎延迟,Swin Transformer的窗口注意力机制对商品图的局部细节捕捉很好;如果要平衡,ResNet50加R-MAC池化是经过大量验证的稳妥方案。
但选型不是一成不变的。我现在的做法是先用小模型快速迭代,验证整个链路跑通后,再逐步换大模型看收益。因为链路本身的问题(比如索引参数不对、预处理有bug)往往比模型选型影响更大,先解决链路问题,再优化模型,投入产出比最高。
另外,特征提取器的更新要谨慎。换backbone意味着所有商品的特征都要重提,索引要重建,成本很高。我一般会保留新旧两套特征并行一段时间,用A/B测试确认新特征确实更好后,再全量切换。这个"双特征"过渡期通常要两到四周,急不得。
至于fpga边缘检测特征提取这类硬件加速方案,我在一些对实时性要求极高的场景(比如移动端实时搜索)做过探索。FPGA的优势是功耗低、延迟稳定,适合把特征提取的前几层(比如边缘检测、卷积)卸载到硬件上。但开发成本高、灵活性差,模型一改就要重新综合,所以更适合模型稳定后的量产阶段,不适合快速迭代期。这个方向值得关注,但不要为了用而用。