用 Java + Spring Boot 整合 Milvus 打造智能搜索引擎

阿里云教程3个月前发布
19 1 0

Milvus 是一款开源的向量数据库,专为海量向量数据的高效检索设计,超级适合构建智能搜索引擎(如语义检索、图像类似性搜索等)。本文将手把手教你用 Spring Boot 整合 Milvus,从零搭建一个基于向量检索的智能搜索引擎。

一、前置准备

1. 环境要求

  • JDK 11+(Milvus Java SDK 推荐)
  • Spring Boot 2.7.x / 3.x
  • Milvus 2.3+(推荐用 Docker 快速部署)
  • Maven/Gradle
  • 向量生成工具(示例用 OpenAI Embedding 或本地模型如 Sentence-BERT)

用 Java + Spring Boot 整合 Milvus 打造智能搜索引擎

2. 部署 Milvus 单机版

用 Docker Compose 快速启动 Milvus(推荐):

# docker-compose.yml
version: '3.5'

services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
    command: minio server /minio_data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  milvus-standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.3.2
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
    ports:
      - "19530:19530"  # Milvus 主端口
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

启动命令:

docker-compose up -d

二、Spring Boot 项目搭建

1. 引入依赖

在 pom.xml 中添加 Milvus Java SDK 和 Spring Boot 核心依赖:

<!-- Spring Boot 基础依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Milvus Java SDK -->
<dependency>
    <groupId>io.milvus</groupId>
    <artifactId>milvus-sdk-java</artifactId>
    <version>2.3.2</version>
</dependency>

<!-- 向量生成:示例用 OpenAI Embedding(可选) -->
<dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>service</artifactId>
    <version>0.10.0</version>
</dependency>

<!-- JSON 工具 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>

<!-- 测试依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2. 配置 Milvus 连接

在 application.yml 中添加 Milvus 配置:

spring:
  application:
    name: milvus-search-demo

# Milvus 配置
milvus:
  host: 127.0.0.1  # Milvus 服务地址
  port: 19530      # Milvus 端口
  database-name: default  # 默认数据库
  collection-name: document_vector  # 向量集合名称
  dim: 1536  # 向量维度(OpenAI Embedding 是 1536,Sentence-BERT 可自定义)

3. 封装 Milvus 配置类

创建配置类读取 Milvus 配置:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "milvus")
public class MilvusProperties {
    private String host;
    private Integer port;
    private String databaseName;
    private String collectionName;
    private Integer dim; // 向量维度
}

用 Java + Spring Boot 整合 Milvus 打造智能搜索引擎

4. 初始化 Milvus 客户端

创建 Milvus 客户端配置类,初始化连接并创建向量集合:

import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
import io.milvus.param.collection.*;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.param.index.IndexType;
import io.milvus.param.index.MetricType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MilvusConfig {

    @Autowired
    private MilvusProperties milvusProperties;

    // 初始化 Milvus 客户端
    @Bean
    public MilvusServiceClient milvusServiceClient() {
        ConnectParam connectParam = ConnectParam.newBuilder()
                .withHost(milvusProperties.getHost())
                .withPort(milvusProperties.getPort())
                .build();

        MilvusServiceClient client = new MilvusServiceClient(connectParam);
        // 检查连接
        if (client.getConnectState()) {
            System.out.println("Milvus 连接成功!");
            // 创建向量集合(如果不存在)
            createCollection(client);
            // 创建索引
            createIndex(client);
        } else {
            throw new RuntimeException("Milvus 连接失败!");
        }
        return client;
    }

    // 创建向量集合
    private void createCollection(MilvusServiceClient client) {
        String collectionName = milvusProperties.getCollectionName();
        // 检查集合是否存在
        HasCollectionParam hasParam = HasCollectionParam.newBuilder()
                .withCollectionName(collectionName)
                .build();
        boolean exists = client.hasCollection(hasParam).getData();
        if (exists) {
            System.out.println("集合 " + collectionName + " 已存在");
            return;
        }

        // 定义字段:主键 ID + 文本内容 + 向量字段
        FieldType idField = FieldType.newBuilder()
                .withName("id")
                .withDataType(DataType.Int64)
                .withPrimaryKey(true)
                .withAutoID(false) // 手动指定 ID
                .build();

        FieldType contentField = FieldType.newBuilder()
                .withName("content")
                .withDataType(DataType.VarChar)
                .withMaxLength(4096) // 文本最大长度
                .build();

        FieldType vectorField = FieldType.newBuilder()
                .withName("vector")
                .withDataType(DataType.FloatVector)
                .withDimension(milvusProperties.getDim()) // 向量维度
                .build();

        CreateCollectionParam createParam = CreateCollectionParam.newBuilder()
                .withCollectionName(collectionName)
                .withDescription("文档向量集合")
                .addFieldType(idField)
                .addFieldType(contentField)
                .addFieldType(vectorField)
                .build();

        client.createCollection(createParam);
        System.out.println("集合 " + collectionName + " 创建成功");
    }

    // 创建向量索引(提升检索效率)
    private void createIndex(MilvusServiceClient client) {
        CreateIndexParam createIndexParam = CreateIndexParam.newBuilder()
                .withCollectionName(milvusProperties.getCollectionName())
                .withFieldName("vector")
                .withIndexType(IndexType.IVF_FLAT) // 索引类型(IVF_FLAT 适合中小数据量)
                .withMetricType(MetricType.COSINE) // 类似度计算方式(余弦类似度)
                .withExtraParam("{"nlist": 1024}") // 索引参数
                .build();

        client.createIndex(createIndexParam);
        System.out.println("向量索引创建成功");

        // 加载集合到内存(必须加载才能检索)
        LoadCollectionParam loadParam = LoadCollectionParam.newBuilder()
                .withCollectionName(milvusProperties.getCollectionName())
                .build();
        client.loadCollection(loadParam);
    }
}

三、核心功能实现

1. 向量生成工具(示例用 OpenAI Embedding)

创建向量生成服务,将文本转换为向量(如果没有 OpenAI 密钥,可替换为本地模型如 Sentence-BERT):

import com.theokanning.openai.OpenAiService;
import com.theokanning.openai.embedding.Embedding;
import com.theokanning.openai.embedding.EmbeddingRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class EmbeddingService {

    // 替换为你的 OpenAI API Key
    @Value("${openai.api.key:your-api-key}")
    private String openaiApiKey;

    // 将文本转换为向量
    public float[] getEmbedding(String text) {
        // 初始化 OpenAI 服务
        OpenAiService service = new OpenAiService(openaiApiKey);
        
        // 构建 Embedding 请求
        EmbeddingRequest request = EmbeddingRequest.builder()
                .model("text-embedding-ada-002") // OpenAI Embedding 模型
                .input(text)
                .build();
        
        // 获取向量结果
        List<Embedding> embeddings = service.createEmbeddings(request).getData();
        List<Double> vector = embeddings.get(0).getEmbedding();
        
        // 转换为 float 数组(Milvus 要求 float 类型)
        return vector.stream()
                .mapToFloat(Double::floatValue)
                .toArray();
    }
}

2. 文档入库(向量插入)

创建服务类,实现文档文本转向量并插入 Milvus:

import io.milvus.client.MilvusServiceClient;
import io.milvus.param.dml.InsertParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class DocumentService {

    @Autowired
    private MilvusServiceClient milvusClient;

    @Autowired
    private MilvusProperties milvusProperties;

    @Autowired
    private EmbeddingService embeddingService;

    // 插入文档(文本转向量后入库)
    public void insertDocument(Long id, String content) {
        // 1. 生成文本向量
        float[] vector = embeddingService.getEmbedding(content);

        // 2. 构建插入数据
        List<InsertParam.Field> fields = new ArrayList<>();
        // 主键 ID
        fields.add(new InsertParam.Field("id", List.of(id)));
        // 文本内容
        fields.add(new InsertParam.Field("content", List.of(content)));
        // 向量字段
        fields.add(new InsertParam.Field("vector", List.of(vector)));

        // 3. 插入 Milvus
        InsertParam insertParam = InsertParam.newBuilder()
                .withCollectionName(milvusProperties.getCollectionName())
                .addFields(fields)
                .build();

        milvusClient.insert(insertParam);
        log.info("文档插入成功,ID: {}, 内容: {}", id, content);
    }
}

3. 语义检索(向量类似度搜索)

实现核心检索功能,根据查询文本的向量匹配最类似的文档:

import io.milvus.client.MilvusServiceClient;
import io.milvus.param.dml.SearchParam;
import io.milvus.response.SearchResultsWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class SearchService {

    @Autowired
    private MilvusServiceClient milvusClient;

    @Autowired
    private MilvusProperties milvusProperties;

    @Autowired
    private EmbeddingService embeddingService;

    // 语义检索:返回 Top K 类似文档
    public List<Map<String, Object>> search(String queryText, int topK) {
        // 1. 生成查询文本的向量
        float[] queryVector = embeddingService.getEmbedding(queryText);

        // 2. 构建搜索参数
        List<String> outputFields = List.of("id", "content"); // 返回的字段
        SearchParam searchParam = SearchParam.newBuilder()
                .withCollectionName(milvusProperties.getCollectionName())
                .withMetricType(MetricType.COSINE) // 余弦类似度
                .withTopK(topK)
                .withVectors(List.of(queryVector))
                .withVectorFieldName("vector")
                .withOutputFields(outputFields)
                .withParams("{"nprobe": 10}") // 检索参数(nprobe 越大越准但越慢)
                .build();

        // 3. 执行搜索
        SearchResultsWrapper resultsWrapper = new SearchResultsWrapper(milvusClient.search(searchParam).getData());

        // 4. 解析结果
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (SearchResultsWrapper.IDScore idScore : resultsWrapper.getIDScore(0)) {
            // 获取文档字段和类似度分数
            Map<String, Object> fields = resultsWrapper.getFieldData(idScore.getLongID(), outputFields);
            fields.put("score", idScore.getScore()); // 类似度分数(余弦类似度:越接近 1 越类似)
            resultList.add(fields);
        }

        log.info("检索完成,查询文本: {}, 匹配结果数: {}", queryText, resultList.size());
        return resultList;
    }
}

四、接口封装与测试

1. 控制器层(REST API)

创建接口供外部调用:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/search")
public class SearchController {

    @Autowired
    private DocumentService documentService;

    @Autowired
    private SearchService searchService;

    // 插入文档
    @PostMapping("/insert")
    public String insertDocument(@RequestParam Long id, @RequestParam String content) {
        documentService.insertDocument(id, content);
        return "插入成功";
    }

    // 语义检索
    @GetMapping("/query")
    public List<Map<String, Object>> search(
            @RequestParam String query,
            @RequestParam(defaultValue = "5") int topK) {
        return searchService.search(query, topK);
    }
}

2. 测试验证

步骤 1:启动项目

确保 Milvus 已启动,运行 Spring Boot 应用。

步骤 2:插入测试文档

调用接口插入测试数据:

# 插入文档 1
curl -X POST "http://localhost:8080/api/search/insert?id=1&content=Java 是一种面向对象的编程语言,广泛用于后端开发"

# 插入文档 2
curl -X POST "http://localhost:8080/api/search/insert?id=2&content=Spring Boot 是基于 Spring 的快速开发框架,简化了配置"

# 插入文档 3
curl -X POST "http://localhost:8080/api/search/insert?id=3&content=Milvus 是开源的向量数据库,用于高效检索向量数据"

步骤 3:语义检索测试

调用检索接口,测试类似性搜索:

curl "http://localhost:8080/api/search/query?query=Spring Boot 简化 Java 开发&topK=2"

预期返回结果(包含类似度分数):

[
    {
        "id": 2,
        "content": "Spring Boot 是基于 Spring 的快速开发框架,简化了配置",
        "score": 0.98
    },
    {
        "id": 1,
        "content": "Java 是一种面向对象的编程语言,广泛用于后端开发",
        "score": 0.85
    }
]

五、进阶优化

1. 索引优化

  • 中小数据量:使用 IVF_FLAT 索引(精准但性能一般)
  • 大数据量:使用 HNSW 索引(高性能,参数 efConstruction 越大越准)
  • 调整 nlist/nprobe:nlist 是聚类数,nprobe 是检索的聚类数,需根据数据量调优

2. 批量操作

批量插入 / 检索可大幅提升性能,示例:

// 批量插入
List<Long> ids = List.of(1L, 2L, 3L);
List<String> contents = List.of("文本1", "文本2", "文本3");
List<float[]> vectors = contents.stream().map(embeddingService::getEmbedding).collect(Collectors.toList());

fields.add(new InsertParam.Field("id", ids));
fields.add(new InsertParam.Field("content", contents));
fields.add(new InsertParam.Field("vector", vectors));

3. 数据管理

  • 定期清理过期数据:使用 delete 接口
  • 集合分区:按业务维度分区,提升检索效率
  • 数据备份:Milvus 支持备份 / 恢复 API

4. 本地向量模型替换

如果不想用 OpenAI,可使用开源的 Sentence-BERT:

<!-- Sentence-BERT 依赖 -->
<dependency>
    <groupId>com.hankcs</groupId>
    <artifactId>sentence-transformers</artifactId>
    <version>1.0.0</version>
</dependency>

替换 EmbeddingService:

import com.hankcs.hanlp.mining.word2vec.Vector;
import com.hankcs.sentencetransformers.SentenceTransformer;

@Service
public class EmbeddingService {
    private final SentenceTransformer model;

    public EmbeddingService() {
        // 加载本地模型(需提前下载)
        model = new SentenceTransformer("all-MiniLM-L6-v2");
    }

    public float[] getEmbedding(String text) {
        Vector vector = model.encode(text);
        return vector.getValues();
    }
}

六、总结

本文实现了 Spring Boot 与 Milvus 的完整整合,核心流程为:

  1. 文本 → 向量(Embedding)
  2. 向量入库(Milvus 插入)
  3. 查询文本 → 向量 → 向量类似度检索 → 返回匹配文档

基于这个基础框架,你可以扩展:

  • 多模态检索(图片 / 音频转向量)
  • 检索结果排序优化
  • 分布式 Milvus 部署
  • 结合 RAG(检索增强生成)打造智能问答系统

Milvus 为向量检索提供了高效的底层支持,结合 Spring Boot 的快速开发能力,可快速落地各类智能搜索场景!

© 版权声明

相关文章

1 条评论

none
暂无评论...