1. 引言

本文介绍如何在 Spring Cloud 微服务架构中,使用**阿里云大模型(DashScope)**将文本转换为向量化数据,再通过 DeepSeek AI 的 Embedding 能力实现 Elasticsearch 向量查询,让系统具备语义搜索能力。

2. 技术架构

整体架构采用 Spring Cloud 微服务模式,核心组件包括:

  • Spring Cloud Gateway:统一网关入口
  • Nacos:服务注册与配置中心
  • 阿里云大模型(DashScope):负责将文本转换为向量(Embedding)
  • DeepSeek AI:负责将查询文本转换为向量(Embedding)
  • Elasticsearch:存储向量数据并执行相似度检索

客户端请求

Spring Cloud Gateway

业务服务

阿里云 DashScope Embedding API

生成文档向量并写入 ES

DeepSeek Embedding API

生成查询向量

Elasticsearch 向量检索

返回相似结果

3. 环境准备

引入 Maven 依赖,在 pom.xml 中添加以下依赖:

<dependency>
   <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>8.18.8</version>
</dependency>
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.18.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
    <groupId>jakarta.json</groupId>
    <artifactId>jakarta.json-api</artifactId>
    <version>2.0.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.json</artifactId>
    <version>2.0.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

4. 接入阿里云大模型生成向量化数据

4.1 配置阿里云客户端

application.yml 中配置自定义阿里云API 信息:

embedding:
	# 指定调用的嵌入模型名称
  model: qwen3.7-text-embedding
  # OpenAI接口规范的向量生成服务调用地址
  url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
  # 调用该服务的身份认证凭证
  api-key: 平台生成apikey
  # 指定模型输出的稠密向量维度
  dimensions: 768

4.2 创建阿里云 Embedding 调用服务

@Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

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

//提供向量化功能
@Service
public class EmbeddingService {

    //配置文件中的内容读取
    @Value("${embedding.model}")
    private String model;
    @Value("${embedding.dimensions}")
    private Integer dimensions;
    @Value("${embedding.api-key}")
    private String apiKey;
    @Value("${embedding.url}")
    private String embeddingUrl;

    @Autowired
    private RestTemplate restTemplate;

    public List<Float> getEmbedding(String text) {
        //组装请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Stream.of(MediaType.APPLICATION_JSON).collect(Collectors.toList()));
        headers.setBearerAuth(apiKey);
        //组装请求体
        EmbeddingReq req = new EmbeddingReq();
        req.setDimensions(dimensions);
        req.setInput(text);
        req.setModel(model);

        //生成entity对象
        HttpEntity<EmbeddingReq> request = new HttpEntity<>(req, headers);

        //发送请求 获取结果
        EmbeddingResp resp = restTemplate.postForObject(embeddingUrl, request, EmbeddingResp.class);
        if (resp == null || resp.getData() == null || resp.getData().isEmpty()) {
            throw new ServiceException("Embedding API returned null or empty=---向量化失败");
        }
        List<EmbeddingResp.EmbeddingData> data = resp.getData();
        if (data.get(0) == null || data.get(0).getEmbedding() == null) {
            throw new ServiceException("向量化失败");
        }
        return data.get(0).getEmbedding();
    }
}

4.3 定义响应实体

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

//做向量化的返回对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmbeddingResp {

    //向量模型允许一次发送多个字段,形成对应的向量
    private List<EmbeddingData> data;

    //真正生成向量的结构
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static  class EmbeddingData {
        private List<Float> embedding;
    }
}

5. 接入 DeepSeek AI 生成查询向量

5.1 配置 DeepSeek 客户端

application.yml 中配置 DeepSeek API 信息:

deepseek:
	# 设置为DeepSeek官方的API接入根地址 
  baseUrl: https://api.deepseek.com
  # 指定本次调用的模型
  model: deepseek-v4-pro
  # 取值为1,代表模型生成内容时的随机程度处于中等偏高水平
  temperature: 1
  # 配置为enabled即开启模型的深度思考模式
  thinking: enabled
  # 取值为1,代表模型在生成采样过程中会覆盖全部概率总和的候选token池,不会做内容裁剪,完整保留所有合法生成选项
  top_p: 1
  apiKey: 平台生成apikey

5.2 创建 DeepSeek 调用方法

返回ai生成内容

    @Value("${deepseek.baseUrl}")
    private String baseUrl;
    @Value("${deepseek.apiKey}")
    private String apiKey;
    @Value("${deepseek.model}")
    private String model;
    @Value("${deepseek.temperature}")
    private Double temperature;
    @Value("${deepseek.thinking}")
    private String thinking;
    @Value("${deepseek.top_p}")
    private Double top_p;

    @Autowired
    private ElasticsearchClient client;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private EmbeddingService embeddingService;

private String askAI(String question, List<StrategyEs> strategyEsList) {
        //将私库数据进行拼接
        StringJoiner joiner = new StringJoiner("\n\n");
        strategyEsList.forEach(strategyEs -> {joiner.add(JSON.toJSONString(strategyEs));});
        //攻略的字符串数据
        String strategyStr = joiner.toString();
        /*
            message role 分四种类型
                ① system :系统信息,存放前置规则和角色设定
                ② user : 用户信息,存放客户端输入的信息
                ③ assistant : 大模型数据自身生成的返回结果
                ④ tool : 存放外部工具,存放外部工具生成的返回结果
         */
        //私库数据
        OpenAIMessage ragMessage = new OpenAIMessage("system", strategyStr);
        //系统消息(角色定位)
        OpenAIMessage sysMessage = new OpenAIMessage("system", "你是一名资深导游,请根据用户问题,结合私库数据,并根据互联网信息,给出旅行建议,规划出路线图、避坑指南及旅行推荐。回答不多于200字");
        //用户消息
        OpenAIMessage userMessage = new OpenAIMessage("user", question);

        List<OpenAIMessage> messages = new ArrayList<>();
        messages.add(ragMessage);
        messages.add(sysMessage);
        messages.add(userMessage);
        OpenAIChatRequest requestBody = new OpenAIChatRequest();
        requestBody.setMessages(messages);
        requestBody.setModel(model);
        requestBody.setTemperature(temperature);
        requestBody.setTop_p(top_p);
        Map<String,Object> map = new HashMap<>();
        map.put("type",thinking);
        requestBody.setThinking(map);

        //header
        HttpHeaders headers = new HttpHeaders();
        //设置请求格式
        headers.setContentType(MediaType.APPLICATION_JSON);
        //设置相应格式
        List<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.APPLICATION_JSON);
        //接收的是json类型
        headers.setAccept(mediaTypes);

        //设置 Bearer Token
        headers.setBearerAuth(apiKey);
        HttpEntity<OpenAIChatRequest> request = new HttpEntity<>(requestBody, headers);
        String url = baseUrl + "/chat/completions";

        //发送请求
        OpenAIChatResponse response = restTemplate.postForObject(url, request, OpenAIChatResponse.class);

        if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
            return "AI没有返回合适的结果";
        }
        OpenAIChatResponse.Choice choice = response.getChoices().get(0);
        if (choice == null || choice.getMessage() == null || choice.getMessage().getContent() == null) {
            return "AI返回内容无结果";
        }
        return choice.getMessage().getContent();
    }

5.3 定义响应实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OpenAIChatRequest {
    private String model;
    //对话的消息列表 有四种类型,可以传递前置数据和规则
    private List<OpenAIMessage> messages;
    //采样温度,介于 0 和 2 之间
    // 更高的值会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定
    // 我们通常建议可以更改这个值或者更改 top_p,但不建议同时对两者进行修改
    private Double temperature;
    //Possible values: [enabled, disabled]
    //Default value: enabled
    //如果设为 enabled,则使用思考模式。如果设为 disabled,则使用非思考模式
    private Map<String, Object> thinking;
    //作为调节采样温度的替代方案,默认值1,模型会考虑前 top_p 概率的 token 的结果。所以 0.1 就意味着只有包括在最高 10% 概率中的 token 会被考虑
    private Double top_p;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OpenAIChatResponse {
    private String id;
    private List<Choice> choices;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Choice{
        private OpenAIMessage message;
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OpenAIMessage {
    private String role;
    private String content;
}

6. Elasticsearch 向量索引设计并实现向量写入

@Service
public class XxxxEsServiceImpl implements IXxxxEsService {

    @Autowired
    private RemoteXxxxService remoteXxxxService;
    @Autowired
    private ElasticsearchClient client;

    public static final String INDEX_NAME = "Xxxx";

    @Autowired
    private EmbeddingService embeddingService;

    @Value("${embedding.dimensions}")
    private Integer dimensions;

    @Override
    public void initXxxxEs() throws IOException {
        //删库
        boolean value = client.indices().exists(e -> e.index(INDEX_NAME)).value();
        //存在该库 则删除
        if (value){
            client.indices().delete(d->d.index(INDEX_NAME));
        }
        //建库 numberOfShards-分片数   建立映射
        CreateIndexRequest request = CreateIndexRequest.of(r -> r.index(INDEX_NAME)
                .settings(s -> s.numberOfShards("1").numberOfReplicas("1"))
                .mappings(m -> m.properties("id", p -> p.long_(l -> l))
                        .properties("title", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("subTitle", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("summary", p -> p.text(t -> t.analyzer("ik_max_word")))
                        .properties("embedding", p -> p.denseVector(
                                t -> t.dims(dimensions)
                                             .index(true)
                                             .similarity(DenseVectorSimilarity.Cosine))))
        );
        client.indices().create(request);
        //查询mysql
        List<Xxxx> Xxxxs = remoteXxxxService.list("inner").getData();
        //同步mysql数据到es
        for (Xxxx xxx : Xxxxs) {
            XxxxEs xxxEs = new XxxxEs();
            //相同名字的属性进行拷贝
            BeanUtils.copyProperties(xxx, xxxEs);
            //向量化
            StringJoiner sj = new StringJoiner(" ");
            sj.add(xxx.getTitle()).add(xxx.getSubTitle()).add(xxx.getSummary());
            List<Float> embedding = embeddingService.getEmbedding(sj.toString());
            strategyEs.setEmbedding(embedding);
            client.index(i->i.index(INDEX_NAME)
                    .id(xxxEs.getId().toString())
                    .document(xxxEs));
        }

    }
}

6.1 定义实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class XxxxEs {
    private Long id;
    private String title;
    private String subTitle;
    private String summary;
    private List<Float> embedding;
}

7. 查询服务

7.1 向量相似度查询服务(使用 DeepSeek AI 生成查询向量)


    @Autowired
    private ElasticsearchClient client;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private EmbeddingService embeddingService;

    @RequestMapping("/chat")
    public Object getAnswer(@RequestBody ChatRequest request) throws IOException {
        //精准匹配模式::查询es数据 根据关键字和每页显示条数 需要设置分词器和查询的字段
        /*SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("xxx")
                                                        .from(0)
                                                        .size(request.getTopK())
                                                        .query(q -> q.multiMatch(
                                                                m -> m.query(request.getQuestion())
                                                                        .fields("title", "subTitle", "summary")
                                                                        .analyzer("ik_max_word"))), XxxxEs.class);*/
        //向量模式查询
        //用户问题向量化
        List<Float> embedding = embeddingService.getEmbedding(request.getQuestion());
        //使用向量方式查询es
        SearchResponse<XxxxEs> resp = client.search(sh -> sh.index("xxx")
                .knn(k -> k.field("embedding")
                        .queryVector(embedding).k(request.getTopK())), XxxxEs.class);
        //私库数据
        HitsMetadata<XxxxEs> hits = resp.hits();
        List<Hit<XxxxEs>> hitList = hits.hits();

        //存储返回的攻略数据
        List<XxxxEs> xxxEsList = new ArrayList<>();

        //遍历将查询到的数据存储到list中
        hitList.forEach(hit -> xxxEsList.add(hit.source()));
        //发送请求到deepseek 通过askAI()方法实现
        String answer = askAI(request.getQuestion(),xxxEsList);
        //返回json格式数据 包含ai生成回答和私库数据
        return new ChatResponse(answer, xxxEsList);
    }

7.2 请求实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRequest {
    //查询关键字
    private String question;
    //每页显示条数
    private Integer topK;
}

更多推荐