项目背景和定位

SPX-Algorithm 本质上是一个图像搜索推荐模块。用户输入文本描述,系统从图片库里找出最相关的结果。听起来简单,但实际做起来发现挑战不少。

传统的图片搜索主要靠标签匹配,但这种方式局限性很明显。图片的标签往往不够丰富,用户的表达又很灵活。所以我的目标很明确:用AI来理解文本和图片的语义,让搜索更智能。

整体架构思路

经过一段时间的摸索,最终采用了分层架构设计:

API层 ← 对外接口,简洁易用

协调层 ← 编排各种算法模块

服务层 ← 图文匹配 + 重排序

数据层 ← 向量存储 + 用户反馈数据存储

为什么选择分层架构?

1. 职责清晰:每一层只关心自己的事情,降低耦合

2. 易于扩展:加新功能时只需要在对应层面改动

3. 便于测试:每层都可以独立测试和调试

4. 未来团队协作:不同人可以专注不同层面的开发

核心模块设计

图文匹配服务

这是算法实现的核心模块,负责理解用户查询并召回相关图片。

选择 CLIP[2] 模型主要是因为它能把文本和图片映射到同一个语义空间。我们用的是 OpenCLIP[3] 的 ViT-B-32,512 维向量输出。这个选择是经过权衡的:

• 效果足够好,能理解复杂的语义关系

• 推理速度快,满足线上服务需求

• 社区成熟,资料多坑少

向量存储用的 Milvus[4],主要看中它对大规模向量检索的优化。

重排序服务

图文匹配召回的结果虽然相关,但排序质量还有提升空间。这时候就需要重排序来精调。

我们采用 LTR[5] 的思路,基于用户的真实反馈来训练排序模型。整个设计围绕"用户反馈闭环"展开:

1. 反馈收集:用户每次搜索后的点击行为都会被记录下来

2. 训练数据构建:从点击行为中提取 pairwise 训练样本,被选中的图片作为正样本,未选中的作为负样本

3. 模型训练:使用神经网络学习用户偏好模式

4. 在线重排序:训练好的模型对搜索结果进行重新排序

这种设计的好处是能够持续学习用户偏好,越用越准。但也有挑战,比如冷启动问题、反馈噪声处理等。

搜索协调器

为了统一管理多个算法服务,我们设计了SearchCoordinator[6]。它的作用是编排整个搜索流程:

1. 调用图文匹配服务进行粗排召回

2. 如果启用了重排序,调用重排序服务进行细排

3. 返回最终的排序结果

这个设计让我们能够灵活控制算法流程,比如可以随时开关重排序功能,方便做A/B测试。

API设计理念

在接口设计上,我们遵循了 “对外简洁,对内丰富” 的原则,主要分为三类接口:

资源管理接口 (/v1/resource/)

• POST /add:添加单个资源

• POST /batch:批量添加资源

• POST /search:基于文本的语义搜索

• POST /match:基于图片的语义搜索

用户反馈接口 (/v1/feedback/)

• POST /submit:提交用户点击反馈

• GET /stats:查看反馈统计信息

• GET /recent:获取最近的反馈数据

• POST /train:触发模型训练

• GET /model/status:查看模型状态

• POST /model/enable|disable:启用/禁用重排序

内部调试接口 (/v1/internal/)

• 向量数据管理:查看、搜索向量数据

• 资源管理:列表查询、删除操作

• 数据库状态:详细的系统状态信息

这样设计的好处是业务方使用简单(只需要关心 resource 接口),反馈收集独立成模块(便于数据科学团队使用),同时我们内部有足够的工具来监控和调试系统。

踩过的坑和解决方案

SVG 处理问题

我们的图片库有很多 SVG 格式的图片,但 CLIP 模型只能处理位图。最初直接用 Pillow[7] 转换,发现效果很差—— SVG的矢量信息丢失了。

后来改用 CairoSVG[8] 来处理,质量明显提升。

这个经历让我意识到:看似简单的问题往往有很多细节。

向量存储优化

最初我们把所有向量数据都放在内存里,随着数据量增长很快就撑不住了。

迁移到 Milvus 后,发现索引类型的选择很关键。我们试了 IVF、HNSW[9] 等几种索引,最终选择 HNSW 是因为它在我们的数据规模下召回率和速度表现最均衡。

重排序特征设计

重排序模块我们改了好几版。最初用 10 维手工特征,包括余弦相似度、欧氏距离、向量模长等。后来发现 CLIP 向量已经归一化了,模长特征基本没用。

最终改成了神经网络方案,用 1500 多维的丰富特征,包括原始向量、统计特征等。虽然维度高了很多,但信息保留更完整。

配置管理混乱

项目初期配置管理比较随意,开发、测试、生产环境的配置经常搞混。后来统一用 dataclass[10] 重新设计了配置系统,按环境分离,清爽了很多。

一些工程实践心得

日志和监控

每个关键模块都加了详细的日志,包括:

• 请求响应时间

• 数据库操作耗时

• 搜索结果统计

• 异常情况记录

这些数据在出问题时特别有用,能快速定位根因。

错误处理

对于算法服务来说,容错很重要。我们的策略是:

• 单个服务出错不影响整体功能

• 重排序失败时返回粗排结果

• 向量服务异常时走降级逻辑

测试策略

除了常规的单元测试,我们还设计了端到端的功能测试。特别是对于机器学习模块,我们会构造一些已知结果的测试用例,确保算法逻辑正确。

未来的一些想法

1. 多模态融合:除了 CLIP,考虑加入其他视觉理解能力

2. 个性化搜索:结合用户历史行为做个性化排序

3. 特征学习:让模型能更好地适应用户偏好

4. 效果评估:建立更完善的线上效果评估体系

总结

做 SPX-Algorithm 这个项目最大的收获是:好的架构设计比炫酷的算法更重要。

技术选型要务实,不要为了用新技术而用新技术。我们选择 CLIP、Milvus 这些相对成熟的方案,主要考虑的是稳定性和可维护性。

工程实践要细致,很多看起来简单的问题往往有很多坑。配置管理、错误处理、日志监控这些"脏活累活"往往决定了系统能不能稳定运行。

最后,用户反馈闭环很重要。我们通过收集用户的真实行为数据来持续优化算法效果,这比闭门造车要有效得多。

希望这些分享对大家有用。如果有问题欢迎交流讨论!

引用链接

[1] SPX-Algorithm: https://github.com/goplus/builder/tree/trailhead_copilot/spx-algorithm

[2] CLIP: https://openai.com/index/clip/

[3] OpenCLIP: https://github.com/mlfoundations/open_clip

[4] Milvus: https://github.com/milvus-io/milvus

[5] LTR: https://en.wikipedia.org/wiki/Learning_to_rank

[6] SearchCoordinator: https://github.com/goplus/builder/blob/trailhead_copilot/spx-algorithm/coordinator/search_coordinator.py

[7] Pillow: https://github.com/python-pillow/Pillow

[8] CairoSVG: https://github.com/Kozea/CairoSVG

[9] HNSW: https://milvus.io/docs/zh/hnsw.md

[10] dataclass: https://github.com/goplus/builder/blob/trailhead_copilot/spx-algorithm/config/base.py