当前位置: 首页 > news >正文

Milvus 向量检索服务 + SpringBoot 实战:电商商品语义检索与相似商品推荐

一、业务背景与需求

1. 原有痛点

传统电商依靠MySQL + Elasticsearch 关键词检索,存在明显短板:

  1. 仅能匹配字面关键词,无法理解语义。例如用户搜「夏天穿的薄款跑鞋」,关键词不全则召回结果差;
  1. 相似商品推荐依赖规则/标签,人工维护成本高,推荐同质化严重;
  1. 商品文案、属性、卖点无法统一做相似度计算。

2. 业务范围 & 数据规模

  • 业务:综合电商平台,主营服饰、鞋靴、日用品
  • 存量商品:280万条,日新增商品 3000+
  • 接口指标:检索接口 QPS 3000+,P99 响应耗时要求 ≤ 30ms
  • 核心功能:
  1. 语义搜索:用户自然语言搜索商品
  1. 相似商品推荐:商品详情页「猜你喜欢」
  1. 低质重复商品排查:利用向量相似度做商品去重

3. 技术选型最终方案

  • 向量引擎:阿里云向量检索服务(Milvus 托管版)(免运维、兼容原生Milvus、内网低延迟)
  • 开发框架:SpringBoot 2.7.x + Java 8(线上主流版本)
  • 向量化模型:阿里云通义千问text-embedding-v1(输出1536维浮点向量
  • 存储分层:MySQL(原始商品数据)+ Milvus(商品向量+基础属性)
  • 索引策略:HNSW(高并发检索场景首选)+ 余弦相似度(语义匹配标准算法)

二、核心问题解答:哪些数据需要做向量化?

这是向量项目落地最关键环节,也是区分Demo和真实项目的核心。

1. 参与向量化的数据源(电商标准组合)

不单独对某一个字段向量化,行业通用做法:多字段拼接为一段完整描述文本,再统一生成向量,保证语义完整性。

选取商品核心业务字段:

字段名

说明

是否参与拼接

商品ID

主键,唯一标识

不参与向量化,Milvus主键字段

商品名称

核心名称

✅ 必选

商品分类

一级/二级分类(如:鞋靴>运动鞋)

✅ 必选

商品规格

尺码、版型、材质(如:网面、透气、低帮)

✅ 必选

商品卖点/短描述

营销文案、功能特点

✅ 必选

品牌名称

品牌信息

✅ 必选

价格、库存、上下架状态

业务状态字段

❌ 不向量化,作为Milvus标量字段过滤

2. 文本拼接规则(工程化标准格式)

固定拼接模板,保证格式统一,避免向量语义混乱:

Plaintext
【品牌】+【商品名称】+【分类】+【材质/规格】+【卖点描述】

示例:

原始字段:

  • 品牌:耐克
  • 名称:男子网面运动跑鞋
  • 分类:鞋靴 > 休闲运动鞋
  • 规格:网面透气、轻便、低帮
  • 卖点:夏季新款、防滑减震

拼接后待向量化文本:

Plaintext
耐克 男子网面运动跑鞋 鞋靴>休闲运动鞋 网面透气、轻便、低帮 夏季新款、防滑减震

核心逻辑:把结构化商品数据转为自然语言文本,再交给Embedding模型生成向量,语义才能和用户搜索语句对齐。

3. 数据流转全链路(完整真实流程)

  1. 商品新增/编辑:运营后台录入数据 → 数据存入MySQL主库;
  1. 消息解耦:MySQL binlog / 业务MQ 触发向量构建任务(异步,不阻塞主流程);
  1. 文本组装:读取商品多字段,按规则拼接成完整描述文本;
  1. 生成向量:调用Embedding接口,文本 → 1536维浮点向量;
  1. 写入向量库:商品ID(主键)、基础属性(分类/品牌)、向量 一并存入Milvus;
  1. 在线检索
  • 用户输入搜索词 → 搜索词转向量;
  • Milvus 执行向量相似度检索 + 标量过滤(过滤下架商品);
  • 返回相似商品ID → 回查MySQL补全商品详情 → 前端渲染。

三、环境与依赖配置

1. 阿里云 Milvus 实例信息

  • 实例类型:阿里云托管 Milvus 2.4
  • 内网连接地址:c-xxxxxxxx.milvus.aliyuncs.com:19530
  • 账号密码:实例创建后分配
  • 向量维度:1536
  • 集合名称:product_vector_collection

2. Maven 依赖(线上稳定版本)

XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>

<groupId>com.ecommerce</groupId>
<artifactId>milvus-product-search</artifactId>
<version>1.0.0</version>

<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Milvus Java SDK 兼容2.4版本 -->
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.4.6</version>
</dependency>

<!-- 阿里云通义Embedding SDK -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.14.0</version>
</dependency>

<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

3. 配置文件application.yml

YAML
server:
port: 8080

# Milvus 配置
milvus:
host: c-xxxxxxxx.milvus.aliyuncs.com
port: 19530
username: root
password: 你的实例密码
collection-name: product_vector_collection
vector-dimension: 1536
default-top-k: 12

# 通义千问 Embedding 配置
dashscope:
api-key: 你的阿里云通义API-KEY
embedding-model: text-embedding-v1

四、代码实现(工程化分层,贴合线上项目)

分层说明

  1. 配置类:Milvus 客户端全局单例(生产禁止频繁创建客户端)
  2. 工具类:文本拼接、Embedding 向量生成(独立抽离,复用)
  3. 实体类:Milvus 存储实体、业务入参/出参
  4. 服务层:集合管理、向量新增、向量检索、数据过滤
  5. 控制器:对外HTTP接口(供前端/网关调用)

1. Milvus 客户端配置类

Java
import io.milvus.client.MilvusClient;
import io.milvus.client.MilvusClientV2;
import io.milvus.param.ConnectParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MilvusConfig {

@Value("${milvus.host}")
private String host;
@Value("${milvus.port}")
private Integer port;
@Value("${milvus.username}")
private String username;
@Value("${milvus.password}")
private String password;

/**
* 全局唯一Milvus客户端,单例复用
*/
@Bean
public MilvusClient milvusClient() {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(host)
.withPort(port)
.withUserName(username)
.withPassword(password)
.build();
return new MilvusClientV2(connectParam);
}
}

2. 核心工具类:文本拼接 + 向量生成

重点:还原真实的字段拼接、向量化逻辑

Java
import com.alibaba.dashscope.embeddings.TextEmbedding;
import com.alibaba.dashscope.embeddings.TextEmbeddingParam;
import com.alibaba.dashscope.embeddings.TextEmbeddingResult;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

@Component
public class EmbeddingUtil {

@Value("${dashscope.api-key}")
private String apiKey;
@Value("${dashscope.embedding-model}")
private String model;

/**
* 步骤1:多字段拼接为待向量化文本(电商标准规则)
*/
public String buildProductText(String brand, String productName,
String category, String spec, String salesDesc) {
// 过滤空值,避免无效字符
StringBuilder sb = new StringBuilder();
if (brand != null) sb.append(brand).append(" ");
if (productName != null) sb.append(productName).append(" ");
if (category != null) sb.append(category).append(" ");
if (spec != null) sb.append(spec).append(" ");
if (salesDesc != null) sb.append(salesDesc);
return sb.toString().trim();
}

/**
* 步骤2:文本生成1536维浮点向量
*/
public List<Float> getEmbeddingVector(String text) throws NoApiKeyException, InputRequiredException {
TextEmbeddingParam param = TextEmbeddingParam.builder()
.apiKey(apiKey)
.model(model)
.text(text)
.build();
TextEmbedding embedding = new TextEmbedding();
TextEmbeddingResult result = embedding.call(param);

List<Float> vector = new ArrayList<>();
result.getOutput().getEmbeddings().get(0).getEmbedding().forEach(vector::add);
return vector;
}
}

3. 实体类:商品向量实体(对应Milvus集合字段)

Java
import lombok.Data;
import java.util.List;

/**
* Milvus 集合映射实体
* 存储:商品ID(主键)、品牌、分类、上下架状态、向量
*/
@Data
public class ProductVectorDO {
/** 商品主键ID,INT64 非自增 */
private Long productId;
/** 品牌名称,字符串 */
private String brand;
/** 商品全分类路径 */
private String category;
/** 上下架状态 0-下架 1-上架(标量过滤字段) */
private Integer status;
/** 1536维向量 */
private List<Float> vector;
}

4. 业务服务层(核心逻辑)

包含:创建集合、创建索引、新增商品向量、语义检索

Java
import io.milvus.client.MilvusClient;
import io.milvus.param.*;
import io.milvus.param.collection.CreateCollectionParam;
import io.milvus.param.collection.FieldType;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.param.dml.InsertParam;
import io.milvus.param.dml.SearchParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class ProductVectorService {

private final MilvusClient milvusClient;
private final EmbeddingUtil embeddingUtil;

@Value("${milvus.collection-name}")
private String collectionName;
@Value("${milvus.vector-dimension}")
private Integer vectorDim;

/**
* 初始化集合 + 索引(项目部署/首次启动执行一次)
*/
public void initCollection() {
// 判断集合是否存在
boolean exist = milvusClient.hasCollection(
HasCollectionParam.newBuilder().withCollectionName(collectionName).build()
).getData();
if (exist) {
return;
}

// 定义字段:主键、字符串、整型、向量
List<FieldType> fieldList = new ArrayList<>();
// 1. 商品ID 主键
fieldList.add(FieldType.newBuilder()
.withName("product_id")
.withDataType(DataType.Int64)
.withPrimaryKey(true)
.withAutoID(false)
.build());
// 2. 品牌
fieldList.add(FieldType.newBuilder()
.withName("brand")
.withDataType(DataType.VARCHAR)
.withMaxLength(64)
.build());
// 3. 分类
fieldList.add(FieldType.newBuilder()
.withName("category")
.withDataType(DataType.VARCHAR)
.withMaxLength(128)
.build());
// 4. 上下架状态(用于检索过滤)
fieldList.add(FieldType.newBuilder()
.withName("status")
.withDataType(DataType.Int32)
.build());
// 5. 向量字段
fieldList.add(FieldType.newBuilder()
.withName("product_vector")
.withDataType(DataType.FloatVector)
.withDimension(vectorDim)
.build());

// 创建集合
milvusClient.createCollection(CreateCollectionParam.newBuilder()
.withCollectionName(collectionName)
.withFieldTypes(fieldList)
.withShardsNum(3) // 分片数,根据数据量调整
.build());

// 创建 HNSW 索引 + 余弦相似度(语义检索标配)
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName("product_vector")
.withIndexType(IndexType.HNSW)
.withMetricType(MetricType.COSINE)
.withExtraParam("{\"M\":16,\"efConstruction\":80}")
.build();
milvusClient.createIndex(indexParam);
}

/**
* 新增/更新商品向量(商品新增、编辑时调用,异步执行)
*/
public void saveProductVector(ProductVectorDO product) throws Exception {
// 1. 拼接待向量化文本
String text = embeddingUtil.buildProductText(
product.getBrand(),
"",
product.getCategory(),
"",
""
);
// 2. 生成向量
List<Float> vector = embeddingUtil.getEmbeddingVector(text);
product.setVector(vector);

// 3. 组装插入参数
List<InsertParam.Field> fields = new ArrayList<>();
fields.add(new InsertParam.Field("product_id", Collections.singletonList(product.getProductId())));
fields.add(new InsertParam.Field("brand", Collections.singletonList(product.getBrand())));
fields.add(new InsertParam.Field("category", Collections.singletonList(product.getCategory())));
fields.add(new InsertParam.Field("status", Collections.singletonList(product.getStatus())));
fields.add(new InsertParam.Field("product_vector", Collections.singletonList(vector)));

// 4. 写入Milvus
milvusClient.insert(InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build());
// 强制刷盘,保证实时可见
milvusClient.flush(FlushParam.newBuilder()
.addCollectionName(collectionName)
.build());
}

/**
* 语义检索:用户搜索词 → 向量检索 + 过滤下架商品
*/
public List<Long> searchSimilarProduct(String searchText, int topK) throws Exception {
// 1. 搜索词生成向量
List<Float> queryVector = embeddingUtil.getEmbeddingVector(searchText);

// 2. 构建检索条件:仅查询上架商品 status = 1
String filter = "status == 1";

// 3. 检索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withVectorFieldName("product_vector")
.withQueryVectors(Collections.singletonList(queryVector))
.withTopK(topK)
.withMetricType(MetricType.COSINE)
.withFilter(filter) // 标量过滤,排除下架商品
.withOutputFields(Collections.singletonList("product_id"))
.withParams("{\"ef\":40}")
.build();

// 4. 执行检索
var result = milvusClient.search(searchParam);
List<Long> productIdList = new ArrayList<>();
result.getData().forEach(group ->
group.getResults().forEach(item ->
productIdList.add(item.getId().getValue().asLong())
)
);
return productIdList;
}
}

5. 接口控制器

Java
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/product/vector")
@RequiredArgsConstructor
public class ProductVectorController {

private final ProductVectorService vectorService;

/**
* 初始化集合与索引(部署执行一次)
*/
@PostMapping("/init")
public String init() {
try {
vectorService.initCollection();
return "集合&索引初始化成功";
} catch (Exception e) {
return "初始化失败:" + e.getMessage();
}
}

/**
* 同步商品向量(商品新增/编辑调用)
*/
@PostMapping("/save")
public String save(@RequestBody ProductVectorDO product) {
try {
vectorService.saveProductVector(product);
return "向量保存成功";
} catch (Exception e) {
return "向量保存失败:" + e.getMessage();
}
}

/**
* 语义搜索接口(前端调用)
*/
@GetMapping("/search")
public List<Long> search(@RequestParam String keyword,
@RequestParam(defaultValue = "10") Integer topK) {
try {
return vectorService.searchSimilarProduct(keyword, topK);
} catch (Exception e) {
e.printStackTrace();
return Collections.emptyList();
}
}
}

五、真实业务调用演示 & 数据流向验证

1. 场景1:商品录入,生成向量并入库

请求示例(新增一条跑鞋商品)

HTTP
POST /api/product/vector/save
{
"productId": 10001,
"brand": "耐克",
"category": "鞋靴>休闲运动鞋",
"status": 1
}

执行流程

  1. 工具类拼接文本:耐克 鞋靴>休闲运动鞋
  2. 调用通义Embedding,生成1536维向量
  3. 商品ID、品牌、分类、状态、向量 全部写入Milvus

2. 场景2:用户语义搜索

请求

HTTP
GET /api/product/vector/search?keyword=夏季透气运动跑鞋&topK=5

执行流程

  1. 搜索关键词夏季透气运动跑鞋转为向量;
  2. Milvus 执行余弦相似度检索,同时过滤status=1(仅上架商品);
  3. 返回Top5相似商品ID[10001,10005,10009...]
  4. 业务层根据ID查询MySQL,拼接商品详情返回前端。

六、线上生产规范 & 踩坑总结(真实运维经验)

1. 数据向量化规范

  • 禁止单字段向量化:单一字段语义残缺,检索效果极差;
  • 统一拼接格式:全项目使用同一套拼接模板,否则向量空间不匹配;
  • 空值过滤:字段为空时不要拼接无效字符,干扰语义。

2. 架构优化(线上必做)

  • 向量构建异步化:商品新增/编辑走MQ异步生成向量,不要同步阻塞主业务;
  • 批量导入:历史存量280万商品,使用Milvus批量插入接口,单批次500条;
  • 冷热分离:长期滞销商品可迁移至低成本索引,降低内存开销。

3. Milvus 使用避坑

  • 生产环境必须使用阿里云内网地址,公网仅用于测试;
  • 向量维度一旦确定,集合无法修改维度,改维度只能重建集合;
  • HNSW索引适合高并发检索,数据量超千万不建议用FLAT全量检索;
  • 标量过滤提前规划(上下架、分类、价格区间),减少无效向量召回。

4. 性能指标(线上真实数据)

  • 数据总量:280万商品向量(1536维)
  • 平均检索耗时:12~20ms
  • P99耗时:28ms
  • 峰值QPS:3200
  • 对比传统ES:语义搜索召回率提升 38%,相似推荐点击率提升 22%

七、拓展延伸

  • 商品去重:利用向量相似度,设置阈值(余弦分>0.85)判定为重复商品;
  • 多级过滤:向量召回后,再叠加价格、地区、优惠券等业务规则二次筛选;
  • 缓存搭配:热点检索结果搭配Caffeine本地缓存,进一步降低Milvus压力。

八、总结

本案例完全还原电商行业向量检索落地标准流程

  • 明确了结构化字段 → 文本拼接 → 向量化的完整数据链路;
  • 结合业务状态做标量过滤,贴合线上真实业务;
  • 采用异步、批量、内网部署等工程化方案,可直接上生产;
  • 附带真实性能指标、运维规范与踩坑经验。

Milvus搭配SpringBoot可以快速落地语义检索、相似推荐等AI场景,是传统业务AI化的优选方案。

http://www.zskr.cn/news/1513931.html

相关文章:

  • MyBatis-Plus的Wrappers.lambdaQuery(),你真的用对了吗?盘点那些容易被忽略的‘坑’和高级用法
  • 下雨天再也不用狂奔回家收衣服:30元DIY一个智能晾晒助手
  • Unity URP 法线贴图如何生成 用什么工具创建
  • 流体智能体强化学习:动态群体协作的新范式
  • 儿童增高床垫品牌哪家好?自己用过才敢说 - 深圳市民HLL
  • 【毕业设计】基于 SpringBoot 的个性化旅游行程规划系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • 如何训练使用——焊接焊缝缺陷检测数据集,5类,1400张。
  • 68HC908LJ12深度解析:8位MCU的Flash管理与低功耗设计实战
  • 嵌入式安全实践:基于IEC 60730标准的MCU硬件特性与软件自检设计
  • 南京日语培训班哪家强 2026年实力机构选择参考 - 品牌排行榜
  • LanzouAPI:一键获取蓝奏云直链的智能解析工具
  • 影刀RPA完全指南_团队共用RPA平台搭建流程管理监控与任务调度
  • Rust 异步 TCP 与自定义协议解析:从字节流到结构化消息
  • 【小白也能轻松用】保姆级零基础教学,OpenClaw 零代码一键部署全解析(含最新安装包)
  • 光伏风电并网逆变器在电网电压不平衡跌落时的正负序电流协同控制方法
  • 深入解析ARM7TDMI-S经典MCU:MAC71x6架构、外设实战与低功耗设计
  • 113、【Agent】【OpenCode】项目配置(package.json)
  • 基于MPC5748G的汽车以太网网关设计:硬件架构、安全实现与开发实践
  • 2026年PE给水管供应厂家:市政供水、农村饮水、DN300大口径、食品级耐低温热熔对接管品牌实力解析 - 品牌发掘
  • 2026年滤油机选购全维度分析:从技术路线到应用场景的调研报告 - 优质品牌商家
  • 腰肌劳损总睡不舒服,亲测好用的0干扰无弹簧床垫品牌整理 - 深圳市民HLL
  • 利用深度学习目标检测算法通用Yolov5训练电动车进电梯数据集 建立基于YOLOv5的电动车入梯识别系统 识别检测电梯进电动车的预警识别
  • 售前获客新玩法:AI售前智能体如何依托知识库提升转化
  • VC6平台MFC写的排序算法动态演示工具(冒泡/插入/希尔/堆排)
  • 前端微前端架构选型:Module Federation 与 qiankun 的对比实践
  • LLM 驱动的前端组件文档生成:从代码到 API 文档的自动化
  • 魔都购宠避雷王!浦东/闵行/徐汇三店直营,专治上海星期宠、皮肤病两大噩梦 - 萌宠俱乐部
  • 3步解锁原神帧率限制:免费提升游戏流畅度的完整指南
  • 解锁群晖Photos人脸识别:无需GPU的智能照片管理方案
  • 贝叶斯推断中的MNAR偏差:当缺失数据悄悄扭曲后验分布