一、业务背景与需求

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化的优选方案。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐