当前位置: 首页 > 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.jsqmd.com/news/1002152/

相关文章:

  • 2026年研磨液实力厂家:广东金刚石粗磨精磨研磨液与镜面抛光液生产商深度解析 - 品牌发掘
  • 从冷却塔到核电站:双曲面与旋转曲面在工程中的神奇应用与数学原理
  • MyBatis-Plus的Wrappers.lambdaQuery(),你真的用对了吗?盘点那些容易被忽略的‘坑’和高级用法
  • 2026年成都四害消杀市场格局分析:从灭鼠到白蚁防治的行业实测与趋势解读 - 优质品牌商家
  • 从Flask到Scrapy:盘点那些用Python Hook提升开发效率的真实场景与避坑指南
  • stm32使用Jlink进行GDB脚本调试
  • 洞察2026年6月模具温控系统市场:五家评价高的制造厂深度解析 - 品牌鉴赏官2026
  • 3大技术突破:MMD Tools如何打通Blender与MikuMikuDance的次元壁
  • 永城奔驰宝马奥迪保养多少钱?真实花费指南 - 品牌排行榜
  • 下雨天再也不用狂奔回家收衣服:30元DIY一个智能晾晒助手
  • Unity URP 法线贴图如何生成 用什么工具创建
  • 流体智能体强化学习:动态群体协作的新范式
  • 3分钟上手:英雄联盟玩家的智能游戏助手完全指南
  • MC9S08GT系列8位MCU:低功耗架构与丰富外设的嵌入式经典设计解析
  • AI 驱动的会议效率提升:从语音转写到行动项提取的工程实践
  • Zotero GPT终极指南:如何用AI智能插件5分钟打造高效文献助手
  • 2026年上海松江区权威金条回收+银条回收机构推荐:称重准 报价实 - 沪上贵金属口碑推荐官
  • 5分钟解决日文游戏乱码:Locale-Emulator终极配置指南
  • 儿童增高床垫品牌哪家好?自己用过才敢说 - 深圳市民HLL
  • Kemono下载器:Windows平台终极批量下载解决方案
  • 56800TDC开发套件实战指南:从硬件安装到CodeWarrior环境搭建
  • 上海嘉定区金条回收别乱找!2026公认靠谱的机构都在这 - 沪上贵金属口碑推荐官
  • 【毕业设计】基于 SpringBoot 的个性化旅游行程规划系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • 68HC908LJ12深度解析:8位MCU的Flash管理与低功耗设计实战
  • 嵌入式安全实践:基于IEC 60730标准的MCU硬件特性与软件自检设计
  • 汽车电子MCU选型与开发实战:MPC5646C架构解析与应用指南
  • 南京日语培训班哪家强 2026年实力机构选择参考 - 品牌排行榜
  • 别再死记硬背了!用Wireshark抓包实战,帮你彻底搞懂TCP确认与重传(附谢希仁习题解析)
  • 别再死记硬背公式了!图解OpenCV C++灰度变换:线性、对数、伽马变换的本质与视觉原理
  • 如何训练使用——焊接焊缝缺陷检测数据集,5类,1400张。