向量检索+Skywalking+Nacos配置中心+Sentinel持久化(8.25)
一、向量检索
我们之前用的是 multi_match + ik_max_word 关键词检索,本质是按字面搜,关键词没命中就搜不到;现在替换成向量检索,本质是按意思搜,通过 Embedding 模型将文本转为向量,用余弦相似度匹配语义最接近的文档,即使用户输入和文章内容字面不同,只要语义相关就能召回。
1、添加配置
在travel-modules-search模块的application.yml下添加向量的配置。
# Embedding 模型配置
embedding:
# 模型名称:使用阿里云的 qwen3.7-text-embedding 模型
model: qwen3.7-text-embedding
# 模型调用地址:阿里云 DashScope 兼容 OpenAI 格式的 API 端点
url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
# API 密钥:替换为你在阿里云 DashScope 控制台申请的真实 Key
api-Key: 你的Apikey
# 向量维度:该模型输出 768 维向量,ES 索引的 dims 配置必须与此一致
dimensions: 768
2、添加请求、应答和处理向量化的类
在travel-modules-search模块的business包下,新建embedding包,包含EmbeddingReq、EmbeddingResp、EmbeddingService。
(1)EmbeddingReq请求类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmbeddingReq {
// 模型名称
private String model;
// 要向量化的文本
private String input;
// 向量维度
private Integer dimensions;
}
(2)EmbeddingResp应答类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmbeddingResp {
// 响应数据列表,通常只取第一个元素 data.get(0)
// 接口返回多条是因为支持批量输入(一次传多段文本,返回多个向量)
private List<EmbeddingData> data;
/**
* 单条 Embedding 结果
* 包含模型输出的向量数组,如 [0.12, -0.34, 0.56, ...](768维)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class EmbeddingData {
// 模型输出的浮点数向量,维度与配置中的 dimensions 一致(768维)
private List<Float> embedding;
}
}
(3)EmbeddingService类
@Service
public class EmbeddingService {
// 从配置文件中读取 Embedding API 的调用地址
@Value("${embedding.url}")
private String embeddingUrl;
// 从配置文件中读取阿里云 DashScope 的 API 密钥
@Value("${embedding.api-key}")
private String apiKey;
// 从配置文件中读取使用的 Embedding 模型名称(如 qwen3.7-text-embedding)
@Value("${embedding.model}")
private String model;
// 从配置文件中读取向量维度(768),用于指定模型输出向量的长度
@Value("${embedding.dimensions}")
private Integer dimensions;
// 注入 RestTemplate,用于发送 HTTP POST 请求调用远程 Embedding API
@Autowired
private RestTemplate restTemplate;
/**
* 将输入文本通过 Embedding 模型转换为向量
*
* @param text 待向量化的文本(如用户问题或攻略文章内容)
* @return 768维的浮点数向量,可直接存入 ES 的 dense_vector 字段
*/
public List<Float> getEmbedding(String text) {
// ========== 1. 组装请求头 ==========
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); // 声明请求体格式为 JSON
headers.setAccept(Stream.of(MediaType.APPLICATION_JSON).collect(Collectors.toList())); // 声明期望响应格式为 JSON
headers.setBearerAuth(apiKey); // 设置 Bearer Token 鉴权,使用阿里云 API Key
// ========== 2. 组装请求体 ==========
EmbeddingReq req = new EmbeddingReq();
req.setDimensions(dimensions); // 指定输出向量维度为 768
req.setInput(text); // 设置待向量化的输入文本
req.setModel(model); // 指定使用的 Embedding 模型
// ========== 3. 封装 HTTP 请求实体(请求体 + 请求头) ==========
HttpEntity<EmbeddingReq> entity = new HttpEntity<>(req, headers);
// ========== 4. 发送 POST 请求,调用远程 Embedding API,并将响应自动反序列化为 EmbeddingResp ==========
EmbeddingResp resp = restTemplate.postForObject(embeddingUrl, entity, EmbeddingResp.class);
// ========== 5. 校验响应结果,防止空指针异常 ==========
// 校验整体响应是否为空(接口调用失败、网络异常等情况)
if(resp == null || resp.getData() == null || resp.getData().isEmpty()) {
throw new ServiceException("获取嵌入向量失败");
}
List<EmbeddingResp.EmbeddingData> data = resp.getData();
// 校验第一条数据及其向量是否为空(模型未正常返回结果)
if(data.get(0) == null || data.get(0).getEmbedding() == null) {
throw new ServiceException("向量化失败");
}
// ========== 6. 返回第一条向量结果 ==========
// 因为每次只传入一段文本,所以取 data 列表的第一个元素即可
return data.get(0).getEmbedding();
}
}
3、修改攻略数据初始化到Es库的initEs()方法
在StrategyEsServiceImpl中修改:
①映射中新增 embedding 向量字段
②调用 Embedding 模型生成向量,再将文本和向量一起写入 ES
@Override
public void initEs() throws IOException {
// 删库
BooleanResponse exists = client.indices().exists(e -> e.index(INDEX_NAME));
if(exists.value()){
client.indices().delete(e -> e.index(INDEX_NAME));
}
// 建库、建立映射
CreateIndexRequest createReq = 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")))
// 定义一个名为 "embedding" 的字段,类型为 dense_vector(密集向量)
.properties("embedding", p -> p.denseVector(
d -> d.dims(dimensions) // 指定向量维度(如 768),必须与 Embedding 模型输出维度严格一致
.index(true) // 启用向量索引(底层使用 HNSW 算法),开启后才能进行近似最近邻(kNN)检索
.similarity(DenseVectorSimilarity.Cosine) // 指定相似度计算方式为余弦相似度,值越接近 1 表示语义越相似
))))));
client.indices().create(createReq);
System.out.println("建库成功");
// 查询mysql
// 同步mysql数据到es库
List<Strategy> list = remoteStrategyService.list("inner").getData();
for (Strategy strategy : list) {
StrategyEs es = new StrategyEs();
// 同名属性拷贝
BeanUtils.copyProperties(strategy, es);
// ========== 向量化处理 ==========
// 使用 StringJoiner 将多个文本字段用空格拼接成一段完整文本
// 拼接后的文本将作为 Embedding 模型的输入,生成能代表这篇攻略整体语义的向量
StringJoiner joiner = new StringJoiner(" ");
joiner.add(strategy.getTitle()) // 拼接标题
.add(es.getSubTitle()) // 拼接副标题
.add(es.getSummary()); // 拼接摘要
// 调用 Embedding 服务,将拼接后的文本送入模型,返回 768 维浮点数向量
List<Float> embedding = embeddingService.getEmbedding(joiner.toString());
// 将生成的向量设置到 ES 文档对象的 embedding 字段中
es.setEmbedding(embedding);
// ========== 写入 ES 索引 ==========
// 将完整的 StrategyEs 文档(含文本字段 + 向量字段)写入 ES
// index:指定目标索引名称
// id:使用攻略主键作为 ES 文档的 _id,保证幂等性(重复执行不会产生重复数据)
// document:要写入的文档对象
client.index(i -> i.index(INDEX_NAME)
.id(es.getId().toString())
.document(es));
}
}
在StrategyEs中添加embedding字段。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StrategyEs {
private Long id;
private String title;
private String subTitle;
private String summary;
private List<Float> embedding;
}
4、修改 AIController 类,将用户输入的问题也通过 Embedding 模型转为向量,与 ES 中已存储的攻略向量进行语义匹配检索。
(1)在 travel-modules-ai 模块下添加 embedding 配置
embedding:
model: qwen3.7-text-embedding
url: https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings
api-Key: 你的Apikey
dimensions: 768
(2)拷贝 embedding 包
(3)修改 getAnswer 方法实现向量化 kNN 查询。
@RequestMapping("/chat")
public Object getAnswer(@RequestBody ChatRequest request) throws IOException {
// // 精准匹配
// SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
// .from(0).size(request.getTopK())
// .query(q -> q.multiMatch(m -> m.query(request.getQuestion())
// .fields("title", "subtitle", "summary")
// .analyzer("ik_max_word"))), StrategyEs.class);
// 将用户的问题向量化
List<Float> embedding = embeddingService.getEmbedding(request.getQuestion());
// 使用向量方式查询es
SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
.knn(k -> k.field("embedding")
.queryVector(embedding)
.k(request.getTopK())), StrategyEs.class);
HitsMetadata<StrategyEs> hits = resp.hits();
TotalHits total = hits.total();
List<Hit<StrategyEs>> hits2 = hits.hits();
// 私库数据
List<StrategyEs> strategies = new ArrayList<>();
for (Hit<StrategyEs> strategyEsHit : hits2) {
StrategyEs es = strategyEsHit.source();
strategies.add(es);
}
// 发送请求到deepseek处理
String answer = askAI(request.getQuestion(), strategies);
ChatResponse chatResponse = new ChatResponse(answer, strategies);
return chatResponse;
}
二、Skywalking集成
1、Skywalking简介
(1)Skywalking 和 Sleuth + Zipkin 对比
-
功能一致:SkyWalking 和 Sleuth + Zipkin 都是做链路追踪的。
-
维护方不同:Sleuth 已停止维护,SkyWalking 目前已由 Apache 软件基金会维护。
-
侵入性不同:Sleuth + Zipkin 有侵入,需要在代码中写日志来采集数据;SkyWalking 无侵入,通过 Agent 程序直接监控,不需要改业务代码。
(2)SkyWalking 特点
- 多语言支持:提供自动探针,支持 Java、.NET Core、Node.js 等多种语言。
- 多监控手段:同时支持语言探针和 Service Mesh(服务网格)两种监控方式。
- 轻量高效:架构轻量,不需要额外搭建复杂的大数据平台即可运行。
- 模块化架构:UI 界面和存储后端(如集群管理)均支持多种机制可选,灵活度高。
- 功能完善:内置支持告警功能,并具备优秀的可视化展示效果。
2、Skywalking集成
(1)双击bin下的startup.bat。
(2)浏览器访问localhost:8080
(3)在 IDEA 中配置 Spring Boot 启动项(VM options)。
点击编辑配置,找到对应的Spring Boot ,alt+v,把文件拷贝进去。
-
指定 Agent 路径
-
设置服务名称
-
指定后端服务地址
-javaagent:D:\ideaWorkSpace\work73\tools\tools\skywalking\apache-skywalking-apm-8.9.1\agent\skywalking-agent.jar
-Dskywalking.agent.service_name=travel-XXX
-Dskywalking.collector.backend_service=localhost:11800
三、Nacos配置中心
之前项目没有使用配置中心,所有配置都写在本地 application.yml 中。现在需要将这些配置迁移到 Nacos 配置中心统一管理。
以 travel-modules-strategy 模块为例:
1、将本地配置迁移到 Nacos
将 application.yml 中的内容拷贝到 Nacos 中已有的 travel-strategy-dev.yml 文件里,然后将本地 application.yml 中的配置注释掉。
server:
port: 8089
spring:
application:
name: travel-strategy
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wolf2w-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 1234
redis:
password: 123456
rabbitmq:
addresses: 192.168.222.129:5672,192.168.222.130:5672
username: root
password: root
virtual-host: /
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意:本次项目各模块共用一个 MySQL 数据库,正常项目中每个服务应拥有独立的数据库,不能通过 Mapper 直接访问其他服务的数据。
2、新建bootstrap.yml
Nacos 配置中心的加载优先于 application.yml,因此需要通过 bootstrap.yml 来指定 Nacos 配置中心地址及当前环境。
spring:
application:
name: travel-strategy
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
profiles:
active: dev
3、优化配置——抽取共享配置
观察 Nacos 中的配置可以发现,rabbitmq 和 mybatis-plus 的配置在多个服务中是相同的,可以抽取为共享配置,避免重复维护。
(1)在 Nacos 中新建两个共享配置文件
rabbitmq-shared-config.ymlmybatis-plus-config.yml
(2)将对应配置分别抽取到共享配置文件中
travel-strategy-dev.yml(精简后):
server:
port: 8089
spring:
application:
name: travel-strategy
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wolf2w-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 1234
redis:
password: 123456
rabbitmq-shared-config.yml:
spring:
rabbitmq:
addresses: 192.168.222.129:5672,192.168.222.130:5672
username: root
password: root
virtual-host: /
mybatis-plus-config.yml:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
(3)在 bootstrap.yml 中引入共享配置
spring:
application:
name: travel-strategy
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
shared-configs:
- data-id: rabbitmq-shared-config.yml
refresh: true
- data-id: mybatis-plus-config.yml
refresh: true
profiles:
active: dev
四、Sentinel持久化
Sentinel 的限流规则默认存储在内存中,服务重启后规则会丢失。因此需要将规则持久化到 Nacos 配置中心,实现规则的长期保存和动态生效。
在 sentinel-travel-gateway 中配置 Strategy 模块的限流规则:
{
"resource": "travel-strategy",
"count": 300,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
各字段含义如下:
| 字段 | 含义 | 当前值说明 |
|---|---|---|
resource |
资源名称,即限流规则作用的目标 | travel-strategy,表示对 Strategy 模块进行限流 |
count |
限流阈值 | 300,表示单机阈值上限为 300 |
grade |
阈值类型 | 1 表示按 QPS(每秒请求数)限流;0 表示按并发线程数限流 |
limitApp |
调用来源 | default 表示不区分调用来源,所有请求共享该阈值 |
strategy |
流控模式 | 0 直接模式(资源自身超阈值即限流);1 关联模式;2 链路模式 |
controlBehavior |
流控效果 | 0 快速失败(超阈值直接拒绝);1 Warm Up 预热;2 排队等待 |
实际项目中,限流阈值需要根据服务承载能力和业务重要性来设定,一般遵循核心服务阈值高、非核心服务阈值低的原则。以下是本项目各模块的限流配置参考:
| 模块 | 限流阈值(QPS) | 设定依据 |
|---|---|---|
| travel-gateway(网关) | 1000 | 网关是所有请求的统一入口,阈值最高,承担全局流量管控 |
| travel-strategy(攻略服务) | 300 | 核心业务模块,查询类接口为主,承载中等流量 |
| travel-order(订单服务) | 200 | 涉及写操作(下单),数据库压力较大,阈值适当降低 |
| travel-user(用户服务) | 500 | 用户信息查询频率高,但逻辑简单,可承受较高 QPS |
参考:网关层作为统一入口设置最高阈值 1000,做全局兜底限流;各微服务根据自身业务特点设置不同阈值,比如订单服务涉及写操作,阈值设为 200;攻略服务以查询为主,阈值设为 300。限流模式统一采用直接模式 + 快速失败,超阈值直接拒绝请求,避免请求堆积导致服务雪崩。
浙公网安备 33010602011771号