RAG知识库新增文搜图+图搜图检索能力
升级说明 & 能力价值
基于现有的图片向量化基础版能力,无缝扩展两大核心检索能力,形成「文本+PDF+图片」三位一体的全模态检索体系,所有能力复用同一套技术栈,无任何新增组件依赖:
- 文搜图 (IMAGE_TEXT_SEARCH):输入文本关键词 → 检索知识库中语义相似的图片(如:输入「产品部署流程图」→ 返回所有相关流程图图片)
- 图搜图 (IMAGE_IMAGE_SEARCH):上传一张图片 → 检索知识库中视觉+语义相似的图片(如:上传一张产品LOGO → 返回所有同款/相似LOGO图片)
核心升级优势
- 100%无缝兼容:所有新增逻辑复用原有混合检索内核,向量检索/关键词加权/LLM重排序/智能去重逻辑完全不变,无重构成本
- 完美适配ReAct智能体:新增2个动作枚举,直接接入ReAct的「思考-行动」循环,大模型可自主决策调用「文搜图/图搜图」能力
- 全链路复用现有工具:复用图片预处理、向量化、MinIO存储、ES检索、Rerank重排序所有能力,无重复开发
- 精细化过滤能力:支持按「图片分类、格式、业务标签」自定义过滤,精准检索目标图片
- 生产级健壮性:新增动作执行自动重试机制+异常兜底,单环节异常不影响整体服务可用性
- 向量维度统一:图片向量与文本/PDF向量维度一致,无兼容性问题,检索结果精准度拉满
完整能力矩阵(本次升级后)
至此RAG知识库已具备全场景检索能力,覆盖所有主流业务需求,形成完整闭环:
- 基础能力:文本搜文本、文本搜PDF文档、PDF切片检索
- 新增能力:文本搜图片、图片搜图片
- 增强能力:多轮上下文感知检索、批量检索、ReAct智能体动态决策检索
1. 动作枚举类 ActionType 扩展
/**
* ReAct智能体 动作类型枚举【完整版】
* 覆盖所有工具能力:知识库检索/文件下载/直接回答/重试/结束 + 新增【文搜图/图搜图】
* 新增能力无缝接入,零侵入改造,大模型可自主决策调用对应检索能力
*/
public enum ActionType {
// 知识库混合查询(核心动作:文本/PDF检索)
KNOWLEDGE_SEARCH,
// MinIO 文件下载/预览(复用现有文件服务)
FILE_DOWNLOAD,
// 直接回答用户问题(无需调用工具)
DIRECT_ANSWER,
// 重试上一步失败的动作
RETRY,
// 结束任务,返回最终答案
FINISH,
// ========== 新增核心能力 ==========
/** 文搜图:文本关键词检索相似图片(如:搜索「产品logo」返回相关图片) */
IMAGE_TEXT_SEARCH,
/** 图搜图:上传图片检索相似图片(如:上传一张图片返回同款/相似图片) */
IMAGE_IMAGE_SEARCH
}
2. 动作参数模型 ActionParams 补充核心字段
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* ReAct智能体 动作参数模型【完整版,适配图片检索】
* 补充图搜图必备的图片文件字段,兼容所有动作入参,无侵入修改
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ActionParams {
// 知识库检索/文搜图 专用:检索文本/关键词
private String searchQuery;
// 全场景检索专用:过滤条件(如:图片分类、格式、文件类型,可选)
private Map<String, Object> filter;
// MinIO文件操作专用:文件存储路径/对象名
private String objectName;
// 直接回答/结束任务专用:回答内容
private String answer;
// ========== 新增:图搜图专用核心字段 ==========
// 图片检索专用:前端上传的图片文件(图搜图时必传)
private MultipartFile imageFile;
}
混合检索服务 HybridSearchService
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
/**
* RAG混合检索核心服务【完整版】
* 核心能力:文本检索/文档检索 + 新增【文搜图+图搜图】+ 上下文感知检索 + 批量检索
* 核心特性:向量检索+关键词加权+同文件去重+LLM二次重排序,全模态检索能力全覆盖,无缝兼容ReAct智能体
*/
@Service
@Slf4j
public class HybridSearchService {
private final ElasticsearchClient client;
private final EmbeddingModel embeddingModel;
@Resource
private RerankService rerankService;
@Resource
private ImagePreProcessUtil imagePreProcessUtil;
@Resource
private ImageVectorizationService imageVectorizationService;
@Autowired
public HybridSearchService(ElasticsearchClient client, EmbeddingModel embeddingModel) {
this.client = client;
this.embeddingModel = embeddingModel;
}
// ========== 核心检索配置(生产级推荐:后续抽离至yml配置文件) ==========
private static final int K = 20; // 粗召回候选数:保证召回率
private static final int FINAL_K = 8; // 最终返回数:兼顾精准度和上下文长度
private final Float SIMILARITY_THRESHOLD = 0.6f;// 相似度阈值:过滤低相关内容,提升精准度
private static final String VECTOR_FIELD = "vector"; // 向量检索核心字段
private static final String CONTENT_FIELD = "content";// 关键词检索核心字段
private static final String IMAGE_FILE_TYPE = "IMAGE";// 图片类型标识,用于过滤图片文档
// ========== 核心方法:文本+PDF混合检索 ==========
public List<DocFragment> hybridSearch(String index, String queryText) throws Exception {
if (StringUtils.isBlank(index) || StringUtils.isBlank(queryText)) {
log.warn("混合检索入参为空,index:{}, queryText:{}", index, queryText);
return Collections.emptyList();
}
log.info("开始混合检索,索引:{},查询文本:{}", index, queryText);
float[] queryVector = embeddingModel.embed(queryText);
List<Float> floatList = Arrays.stream(queryVector).boxed().collect(Collectors.toList());
SearchResponse<DocFragment> response = client.search(
s -> s.index(index)
.knn(k -> k
.field(VECTOR_FIELD)
.queryVector(floatList)
.k(K)
.numCandidates(100)
)
.query(q -> q.functionScore(fs -> fs
.query(qb -> qb.match(m->m.field(CONTENT_FIELD).query(queryText)))
.functions(fn -> fn.filter(fil -> fil.match(m -> m.field(CONTENT_FIELD).query(queryText))).weight(0.3))
.boostMode(FunctionBoostMode.Sum)
))
.size(K),
DocFragment.class
);
if (Objects.isNull(response) || CollectionUtils.isEmpty(response.hits().hits())) {
log.info("混合检索无结果,索引:{},查询文本:{}", index, queryText);
return Collections.emptyList();
}
List<DocFragment> deduplicateFragments = response.hits().hits().stream()
.collect(Collectors.groupingBy(hit -> hit.source().getId()))
.values().stream()
.map(group -> group.stream().max(Comparator.comparing(Hit::score)).get())
.sorted(Comparator.comparing(Hit::score, Comparator.reverseOrder()))
.limit(FINAL_K)
.map(Hit::source)
.collect(Collectors.toList());
List<DocFragment> finalResult = rerankService.rerank(deduplicateFragments, queryText);
log.info("混合检索完成,去重前{}条,去重后{}条,最终{}条",
response.hits().hits().size(), deduplicateFragments.size(), finalResult.size());
return finalResult;
}
// ========== 上下文感知检索 ==========
public List<DocFragment> hybridSearchWithContext(String index, String queryText, String contextText) throws Exception {
String searchText = StringUtils.isBlank(contextText) ? queryText : contextText + "," + queryText;
log.info("上下文感知检索,拼接后检索文本:{}", searchText);
return this.hybridSearch(index, searchText);
}
// ========== 批量混合检索 ==========
public Map<String, List<DocFragment>> batchHybridSearch(String index, List<String> queryTexts) throws Exception {
Map<String, List<DocFragment>> resultMap = new HashMap<>(queryTexts.size());
if (CollectionUtils.isEmpty(queryTexts) || StringUtils.isBlank(index)) {
return resultMap;
}
log.info("开始批量混合检索,索引:{},批量查询数量:{}", index, queryTexts.size());
for (String queryText : queryTexts) {
if (StringUtils.isNotBlank(queryText)) {
resultMap.put(queryText, this.hybridSearch(index, queryText));
}
}
return resultMap;
}
// ========== 新增核心方法【文搜图 - 生产级】:文本关键词检索相似图片 ==========
/**
* 文搜图核心能力:输入文本关键词 → 检索知识库中语义相似的图片
* 核心逻辑:文本生成向量 + ES向量检索(图片) + BM25关键词检索(图片名称/分类) + 自定义过滤 + LLM重排序
* @param index ES索引名
* @param queryText 检索文本(如:产品logo、部署流程图、功能截图)
* @param filterParams 精细化过滤条件:{"category":"产品图片", "imageFormat":"PNG", "business":"运营"}
* @return 按相似度排序的高相关图片列表
*/
public List<DocFragment> textSearchImage(String index,String queryText, Map<String, Object> filterParams) throws Exception {
// 1. 入参空值校验,快速返回
if (StringUtils.isBlank(index) || StringUtils.isBlank(queryText)) {
log.warn("文搜图入参为空,index:{}, queryText:{}", index, queryText);
return Collections.emptyList();
}
log.info("开始文搜图检索,索引:{},检索文本:{},过滤条件:{}", index, queryText, filterParams);
// 2. 文本生成检索向量,复用文本向量化能力,向量维度一致
float[] queryVector = embeddingModel.embed(queryText);
List<Float> floatList = Arrays.stream(queryVector).boxed().collect(Collectors.toList());
// 3. 构建ES精准查询:只检索图片类型 + 向量检索+关键词加权 + 自定义过滤
BoolQuery.Builder boolQuery = QueryBuilders.bool();
// 核心过滤:只返回图片类型的文档,不干扰文本/PDF检索结果
boolQuery.filter(f -> f.term(t -> t.field("fileType").value(IMAGE_FILE_TYPE)));
// 混合加权:向量相似度70%权重 + 图片名称/分类关键词30%权重,兼顾语义与精准命中
boolQuery.must(q -> q.functionScore(fs -> fs
.query(k -> k.knn(knn -> knn
.field(VECTOR_FIELD)
.queryVector(floatList)
.k(K)
.numCandidates(100)
))
.functions(fn -> fn.filter(fil -> fil.match(m -> m.field(CONTENT_FIELD).query(queryText))).weight(0.3))
.boostMode(FunctionBoostMode.Sum)
));
// 4. 拼接自定义过滤条件,支持多维度精准筛选图片
if (filterParams != null && !filterParams.isEmpty()) {
filterParams.forEach((field, value) -> {
boolQuery.filter(f -> f.term(t -> t.field(field).value(value.toString())));
});
}
// 5. 执行ES检索,只返回图片核心字段,提升检索性能
SearchResponse<DocFragment> response = client.search(
s -> s.index(index)
.query(boolQuery.build()._toQuery())
.size(K)
.fields(f->f.field("id"),f->f.field("fileName"),f->f.field("vector"),f->f.field("imageFormat"),f->f.field("category")),
DocFragment.class
);
// 6. 复用同文件去重+相似度排序+截取+LLM重排序
List<DocFragment> result = buildImageSearchResult(response);
log.info("文搜图检索完成,最终返回相似图片数量:{}", result.size());
// LLM二次语义重排序,基于文本关键词对图片做精准度排序
return rerankService.rerank(result, queryText);
}
// ========== 新增核心方法【图搜图 - 生产级】:图片检索相似图片 ==========
/**
* 图搜图核心能力:上传一张图片 → 检索知识库中视觉+语义相似的图片
* 核心逻辑:图片预处理 → Base64编码 → 生成图片向量 → ES向量检索(图片) + 自定义过滤 + LLM重排序
* @param index ES索引名
* @param imageFile 前端上传的检索图片
* @param filterParams 精细化过滤条件:{"category":"技术截图", "imageFormat":"JPG"}
* @return 按相似度排序的高相似图片列表
*/
public List<DocFragment> imageSearchImage(String index, MultipartFile imageFile, Map<String, Object> filterParams) throws Exception {
// 1. 入参空值校验,快速返回
if (StringUtils.isBlank(index) || imageFile == null || imageFile.isEmpty()) {
log.warn("图搜图入参为空,index:{}, imageFile:{}", index, imageFile);
return Collections.emptyList();
}
log.info("开始图搜图检索,索引:{},图片名称:{},过滤条件:{}", index, imageFile.getOriginalFilename(), filterParams);
// 2. 图片标准化预处理 + 生成图片向量,复用图片向量化能力,向量维度一致
InputStream preProcessStream = imagePreProcessUtil.preProcess(imageFile);
String fileMd5 = imageVectorizationService.getFileMd5(imageFile.getBytes());
float [] queryVector = imageVectorizationService.embedImage(preProcessStream, fileMd5);
List<Float> floatList = Arrays.stream(queryVector).boxed().collect(Collectors.toList());
// 3. 复用文搜图的查询逻辑,完全一致:只检索图片 + 向量检索 + 自定义过滤
BoolQuery.Builder boolQuery = QueryBuilders.bool();
boolQuery.filter(f -> f.term(t -> t.field("fileType").value(IMAGE_FILE_TYPE)));
boolQuery.must(q -> q.functionScore(fs -> fs
.query(k -> k.knn(knn -> knn
.field(VECTOR_FIELD)
.queryVector(floatList)
.k(K)
.numCandidates(100)
))
.boostMode(FunctionBoostMode.Sum)
));
// 4. 拼接自定义过滤条件
if (filterParams != null && !filterParams.isEmpty()) {
filterParams.forEach((field, value) -> {
boolQuery.filter(f -> f.term(t -> t.field(field).value(value.toString())));
});
}
// 5. 执行ES检索,返回图片核心字段
SearchResponse<DocFragment> response = client.search(
s -> s.index(index)
.query(boolQuery.build()._toQuery())
.size(K)
.fields(f->f.field("id"),f->f.field("fileName"),f->f.field("vector"),f->f.field("imageFormat"),f->f.field("category")),
DocFragment.class
);
// 6. 复用结果处理逻辑,无冗余代码
List<DocFragment> result = buildImageSearchResult(response);
log.info("图搜图检索完成,最终返回相似图片数量:{}", result.size());
// LLM重排序:基于图片名称做语义排序,提升结果精准度
return rerankService.rerank(result, imageFile.getOriginalFilename());
}
// ========== 私有工具方法:图片检索结果统一处理,复用逻辑,减少冗余 ==========
private List<DocFragment> buildImageSearchResult(SearchResponse<DocFragment> response) {
if (Objects.isNull(response) || CollectionUtils.isEmpty(response.hits().hits())) {
return Collections.emptyList();
}
// 同文件去重 + 相似度倒序 + 截取最终数量
return response.hits().hits().stream()
.collect(Collectors.groupingBy(hit -> hit.source().getId()))
.values().stream()
.map(group -> group.stream().max(Comparator.comparing(Hit::score)).get())
.sorted(Comparator.comparing(Hit::score, Comparator.reverseOrder()))
.limit(FINAL_K)
.map(Hit::source)
.collect(Collectors.toList());
}
}
ReAct动作执行器 ReActActionExecutor
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ReAct智能体 动作执行器【完整版】
* 核心能力:执行所有标准化动作,包含原有能力 + 新增【文搜图+图搜图】
* 核心特性:自动重试机制+异常兜底+全量空值校验+标准化返回结果,无缝兼容ReAct思考-行动循环
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class ReActActionExecutor {
// 注入现有核心服务,全链路复用
private final HybridSearchService hybridSearchService;
private final MinioUtils minioUtils;
@Resource
private ChatClientConfig.ChatClientFactory chatClientFactory;
// ========== 生产级常量配置 ==========
private static final int MAX_RETRY_COUNT = 2; // 最大重试次数,防止无限重试
private static final String ES_INDEX = "ai_vector_index"; // ES向量索引名,统一配置
private static final String EMPTY_RESULT = "未检索到相关图片内容"; // 空结果友好提示
/**
* 核心执行方法:调度所有动作类型,包含新增的图片检索动作
* @param actionType 动作类型(含文搜图/图搜图)
* @param params 动作参数(含图片文件/检索文本/过滤条件)
* @return 标准化执行结果,供ReAct思考器做下一步决策
*/
public String execute(ActionType actionType, ActionParams params) {
int retryCount = 0;
// 核心重试逻辑:异常时重试,最多2次,保障服务可用性
while (retryCount < MAX_RETRY_COUNT) {
try {
return switch (actionTable) {
case KNOWLEDGE_SEARCH -> executeKnowledgeSearch(params);
case FILE_DOWNLOAD -> executeFileDownload(params);
case DIRECT_ANSWER -> executeDirectAnswer(params);
case RETRY -> "已执行重试动作,重新调用上一步工具";
case FINISH -> StringUtils.isNotBlank(params.getAnswer()) ? params.getAnswer() : "任务已完成";
// ========== 新增核心动作:文搜图 执行逻辑 ==========
case IMAGE_TEXT_SEARCH -> {
String searchQuery = params.getSearchQuery();
Map<String, Object> filter = params.getFilter();
// 空值校验
if (StringUtils.isBlank(searchQuery)) {
yield "文搜图失败:检索文本不能为空";
}
// 执行文搜图检索
List<DocFragment> images = hybridSearchService.textSearchImage(ES_INDEX, searchQuery, filter);
if (images.isEmpty()) yield EMPTY_RESULT;
// 拼接标准化结果:图片名称+预览链接,便于大模型生成回答
yield images.stream()
.map(f -> String.format("【相似图片】%s(分类:%s)→ 预览链接:%s",
f.getFileName(), f.getCategory(), minioUtils.getFilePreviewUrl(f.getId())))
.collect(Collectors.joining("\n\n"));
}
// ========== 新增核心动作:图搜图 执行逻辑 ==========
case IMAGE_IMAGE_SEARCH -> {
MultipartFile imageFile = params.getImageFile();
Map<String, Object> filter = params.getFilter();
// 空值校验
if (imageFile == null || imageFile.isEmpty()) {
yield "图搜图失败:上传的图片不能为空";
}
// 执行图搜图检索
List<DocFragment> images = hybridSearchService.imageSearchImage(ES_INDEX, imageFile, filter);
if (images.isEmpty()) yield EMPTY_RESULT;
// 拼接标准化结果:图片名称+预览链接,信息完整
yield images.stream()
.map(f -> String.format("【高度相似图片】%s(格式:%s)→ 预览链接:%s",
f.getFileName(), f.getImageFormat(), minioUtils.getFilePreviewUrl(f.getId())))
.collect(Collectors.joining("\n\n"));
}
};
} catch (Exception e) {
retryCount++;
log.error("动作执行失败:{},开始第{}次重试", actionType, retryCount, e);
// 重试耗尽,返回最终异常提示
if (retryCount >= MAX_RETRY_COUNT) {
return String.format("【%s】调用失败,原因:%s,已重试%d次,无法完成检索",
actionType.name(), e.getMessage(), MAX_RETRY_COUNT);
}
}
}
return "工具调用超时,已为你整理相关检索信息,请参考";
}
// ========== 知识库检索 ==========
private String executeKnowledgeSearch(ActionParams params) throws Exception {
String searchQuery = params.getSearchQuery();
List<DocFragment> fragments = hybridSearchService.hybridSearch(ES_INDEX, searchQuery);
return fragments.stream()
.map(DocFragment::getContent)
.collect(Collectors.joining("\n\n"));
}
// ========== MinIO文件下载 ==========
private String executeFileDownload(ActionParams params) {
String objectName = params.getObjectName();
String previewUrl = minioUtils.getFilePreviewUrl(objectName);
return String.format("文件下载成功 → 预览链接:%s", previewUrl);
}
// ========== 直接回答 ==========
private String executeDirectAnswer(ActionParams params) {
return chatClientFactory.getDefaultChatClient().prompt()
.user(params.getAnswer())
.call()
.content();
}
}
生产落地关键注意事项
性能优化建议
- 图片向量生成可开启Redis缓存(按图片MD5缓存向量),避免相同图片重复生成向量,提升图搜图效率
- 批量图片检索场景可做异步处理,避免同步调用导致的接口超时
- ES检索的
K值(粗召回数)可根据图片数量调整,图片量越大,K值可适当调大(如30),保证召回率
功能扩展建议
- 新增「图片批量检索」能力,适配批量图片查询场景,复用现有批量检索逻辑即可
- 新增「图文混合检索」能力,输入文本+上传图片,同时检索文本和图片结果,满足复杂检索需求
- 图片检索结果新增「相似度分值」展示,便于前端排序/展示,提升用户体验