
从个人项目到真实业务一次皮革图片检索的演进复盘很久没有写文章了。以前写个人项目我经常自己设想业务再给它补上约束。过去几个月我有了几段真实的项目实践。今天想挑其中一个回头看看自己当时做过的选择。做个人项目时我喜欢从架构角度思考复杂度为了获得一些效果要增加多少实现和维护成本值不值得我一直把这件事当作开发中的重要准则。真正做起业务后我发现还有一件事需要考虑当初用来判断“值不值得”的条件也会随着项目推进而变化。这篇文章聊一个皮革图片检索项目从核心算法的演进说起再谈它带来的工程取舍以及一段后来重新审视的主图处理流程。从“把图片变成向量”开始系统的目标很直接用户拍一张皮革图片系统返回相似的产品。我之前做过 RAG 项目所以遇到这个问题最先想到的也是向量检索。把用户图片和产品图片编码成向量在同一个空间中比较相似度再返回对应产品。思路明确实际效果却没有那么容易得到。最初试过一些通用图片向量模型。它们能表达较粗的语义差异但在我的测试里很多不同皮革的向量相似度都很高难以区分具体产品。准确的数值已经记不清了留给我的印象是判断“这是皮革”与区分“是哪一种皮革”对模型的要求差了不少。于是我转向微调希望模型能够拉开不同皮革之间的距离同时把同一种皮革在不同拍摄条件下的图片联系起来。经过几轮实验模型本身逐渐有了可用的效果。但接下来发现模型怎么训练只是一部分图库里放什么、怎样表达一个产品也会明显影响结果。模型之外先改变一个产品的表达方式业务方最初给我的数据是每个产品一张扫描主图加四张拍摄的模拟图。按我的回忆早期测试规模是两百多个产品。最初想做的是拿模拟图当用户拍摄的图片去找到这个产品的扫描主图。模型已经优化过几轮效果仍不理想。扫描和拍照毕竟有明显差别。扫描图平整成像条件相对固定拍摄图里会有光照、角度、距离和设备带来的变化。让手机拍出的样子去匹配扫描得到的样子本身就有难度。既然已经有模拟图为什么不让它们也成为检索参考当时的测试方式是用一张主图加三张模拟图构成参考留出另一张模拟图作为查询。参考图片分别编码再对向量求均值、归一化形成一条产品向量。这次效果明显改善路线也由此确定下来用模拟图补充真实用户拍摄时的外观变化让产品向量同时包含扫描和拍摄参考。这里求均值是为了形成一个相对稳定的产品参考。产品仍然对应一条向量查询、存储和更新都很直接。后来核对历史算法仓库的另一批实验也支持当时采用多参考均值它优于仅使用扫描主图也优于那一轮逐图取最大相似度的方案。至少在当时的数据和评估方式下这个选择有依据。参考图越来越多原来的表达方式还合适吗和业务方沟通后采集继续增加。老板希望覆盖更多设备、光照和拍摄角度认为模拟图多一些检索效果应该更好。我提过图片未必越多越好。但我们的确无法要求真实用户都按同一种方式拍照增加条件覆盖也有它的道理所以我没有尽力阻止这件事。后来每个产品有了十多张模拟图。问题出现在录入三百多个产品之后。仍然是一张主图加上全部模拟图最后聚合成一个向量但进一步测试和使用时效果没有达到预期。我又开始做实验。比如小程序拍的图片和手机原相机拍的图片看起来就有差别于是尝试按来源分别构建产品参考。这个方向有改善但还不够。之后又比较了主图权重、逐图匹配等方式。与业务方继续沟通时皮革纹理的走向也是一个线索。同样的纹理换个方向图片外观就会变化这也成为后续优化需要考虑的因素。这些实验逐渐把问题指向了参考图片的使用方式。一个产品在十多种拍摄条件下有十多个不同的外观。全部求均值之后只剩下一个固定中心。某一张参考图本来很接近当前用户图片它的特点也会与其他参考一起被平均。后续方案不再将一个产品的所有参考图片压缩成固定向量而是保留多张参考图片的特征在查询时动态选择更相关的参考综合计算产品相似度。同时针对皮革纹理方向变化进行了适配。经过实验这一方案在数据规模扩大后取得了更稳定的效果。更多采集图片因此成为可供选择的参考而不必全部平均进同一个固定向量。主图也在这一轮演进中退出了新的评分。回头看聚合成一个向量有错吗站在现在的实现往回看很容易说既然每张图都有自己的信息早就应该逐图保存、逐图匹配。但是我不认为这能准确评价当时的选择。初期参考图较少测试效果已经不错一条产品向量也让实现和维护简单。如果那个时候直接采用后来的多向量方案就要提前承担更多存储、查询聚合和更新处理而当时还没有证据表明这些成本值得付出其实当时也没有思考到这方面来。后来产品数量、采集条件、参考图片和实际查询都在变化原来的实验结论需要重新检查也应该重新检查。对我来说这段经历正好补上了我最近在反思的一个东西一个方案的收益和成本要放在当时的条件下评价保留逐图信息当然更灵活但这份灵活性是否值得要有具体问题来回答。新算法的效果也带来了新的工程成本算法调整之后那么应用层也要适当的修改。原来每个产品只有一条向量可以直接让 Qdrant 返回最相似的产品。现在一个产品对应多条参考向量需要根据查询结果进一步计算产品层面的相似度。一次普通的向量 Top-K 查询返回的是 K 条向量它们可能集中在少数产品上。提前截断之后不一定还能完整计算产品的最终分数。现在回头看一个可选方案是从 Qdrant 召回候选产品再补齐参考向量做精排。但候选取多少也需要验证。总共只有几百个产品先取前100还是前200能节省多少计算又会漏掉多少结果项目实际采用的方式是从 Qdrant 加载有效图库在 Python 内存中用矩阵运算计算全部产品的分数。下面按量级描述当时的隔离性能实验帮助我重新评价这个选择。500 左右的产品2 万余条向量单份向量矩阵占用为百 MiB 量级。新方案纯检索平均耗时低于 10 ms。图片预处理和模型编码平均耗时在 100 ms 左右。这组数据来自当时规模下的单并发实验。在测量的图片计算环节中完整评分的计算成本较小主要耗时在图片编码手机上传、OSS 下载和整条 HTTP 请求未计入其中。今天看这个选择的合理性在于当时的计算成本可以接受也省去了候选召回与精排之间的效果调试。它仍然有实际成本。向量需要常驻内存更新时会构建新查询快照新旧快照短暂共存当前代码即使只更新一个产品也会重新拼接全库查询矩阵。随着产品和参考图片增加向量的内存占用也会增长进程还要容纳模型、产品数组和临时数据。短期内数据总量大概在两千个产品以内后续需要更多关注并发查询、更新时的内存峰值以及数据增长后的计算成本再据此调整方案。更新方式从一条记录变成了一批记录。为了避免查询使用写到一半的新数据服务给每批向量分配代次先写齐新代再更新有效版本记录切换查询快照最后清理旧代。这减少了直接覆盖旧数据时的恢复难度也增加了版本和清理管理。后续需要更多关注更新过程中的状态一致性、异常恢复和旧数据回收让检索效果的改善有相应的工程保障。更好的检索效果确确实实换来了更多工程工作。还有一段主图去尺的经历前面一直提到扫描主图。它还有一条独立于检索效果的业务变化。最初主图没有标尺。后来客户反馈只看图片无法判断纹理的实际大小。比如鳄鱼纹客户需要知道每一块大概有多大缩放图片本身提供不了这个信息。于是业务方要求给主图加入标尺。但那个时候主图还参与产品向量生成。为了避免尺子进入检索特征又需要一份去掉标尺的图。最终形成了带标尺的展示主图以及去标尺的向量主图。我为这套处理投入了不少精力。本地工具要定位原点、摆正并裁切服务端又要定位原点生成去尺图。除了让处理本身跑通还需要在两端维护相近的算法和约定。后来新增主图尺寸时我重新回顾整条流程才再次意识到主图已经在之前的算法演进中退出当前评分了。那前面做去尺是不是白做了看时间线当时主图确实参与检索。展示需要保留标尺模型输入又希望排除标尺拆成两份输出有当时的用途。去尺带来的准确率收益没有独立实验量化但当时要满足的输入要求是明确的。到了后来带尺展示仍然有价值去尺图对当前评分的价值已经变化。此前的投入也不该成为继续维护它的理由。我当时评估了保留和移除直接撤掉仍要修改输入校验、图片关系和旧聚合库的依赖沿用输出的增量工作反而有限。因此暂时保留但减少重复处理。新的分工是本地工具把已经确定的原点写进图片 EXIF。服务端读取这个结果按坐标裁剪保留最终预览确认。这样不用再让两端分别猜一次原点。这件事也提醒我技术行为变化之后需要把变化同步回业务讨论。主图退出评分时我没有及时重新说明它的职责直到后续需求触及这段代码才再次整理清楚。再回到复杂度与收益以前做个人项目我会给出一组业务约束再讨论哪种方案更合适。真实项目里这组约束也在不断变化客户提出新的展示需要业务方扩大采集新的数据又暴露原先没有看见的问题。所以除了判断一个方案要增加多少复杂度、能获得多少收益我现在还会追问当时支持这个选择的条件现在还成立吗数据变多、采集方式改变后原来的实验需要重新验证一个功能的用途变化后保留、简化和删除也值得重新比较。这些检查如果能在变化发生时就做就不必等到下一次需求碰到旧代码才重新想起它为什么存在。这几个月让我对“基于业务约束做技术决策”有了更具体的理解做决定时要看业务业务往前走之后也要回头看已经做出的决定。