RAG知识库-走向多模态向量化-图片向量化基础版

12 阅读11分钟

知识库-走向多模态向量化-图片向量化基础版

核心设计说明 & 基础版定位

核心实现思路

本次图片向量化为轻量化基础版最优方案,核心逻辑无复杂依赖、低成本快速落地,完全复用现有知识库的全套技术栈,无任何新增组件引入:

前端上传图片 → 图片标准化预处理(压缩/格式统一/裁剪) → Base64字符串编码 → 复用原有文本Embedding向量模型生成向量 → MinIO存储原图 + Redis防重复入库 + ES向量入库

基础版核心优势

  1. 零新增大模型依赖:无需部署新的多模态向量模型,直接复用现有文本Embedding能力,开发/部署成本极低
  2. 100%无缝兼容:图片生成的向量维度与「文本/PDF切片」向量维度完全一致,原有混合检索逻辑一行不改即可兼容图片检索
  3. 全链路复用现有工具:完美复用MinIO文件存储、Redis重复校验、ES向量入库、Embedding模型等所有现有能力,无重复开发
  4. 轻量化易落地:基础版核心逻辑简单,无技术门槛,测试通过后可直接投产

适用场景

  1. 基础版适合快速落地图片知识库检索能力、对图片检索精准度要求中等、追求开发效率和维护成本的场景
  2. 后续可基于此版本无侵入升级为进阶版(多模态向量化/图文联合检索),无需重构现有代码。

前置依赖引入

轻量核心依赖,无冗余,仅引入图片处理+Base64编码必备包,兼容所有JDK版本

<!-- 图片处理核心依赖:轻量无侵入,支持压缩/裁剪/格式转换,性能优异 -->
<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.20</version>
</dependency>
<!-- Apache官方Base64编解码工具,前端上传图片Base64转换必备,兼容所有场景 -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.16.0</version>
</dependency>

一、图片标准化预处理工具类

核心能力

对上传图片做标准化预处理,解决图片大小不一、格式杂乱、体积过大等问题,为后续Base64编码和向量化做准备;工具类为通用组件,可独立复用,无业务耦合。

/**
 * 图片标准化预处理工具类【通用组件】
 * 核心能力:图片压缩/格式统一/Base64编码/流转换,适配所有主流图片格式(JPG/PNG/WEBP)
 * 特性:轻量高效、无业务耦合、可独立复用,预处理后的图片兼顾清晰度与处理速度
 */
@Component
public class ImagePreProcessUtil {
    // ========== 可配置常量(生产建议抽离至yml配置文件) ==========
    // 图片最大体积限制,超过则抛出异常,防止超大文件占用资源
    private static final long MAX_IMAGE_SIZE = 1024 * 1024 * 5; // 5MB
    // 图片压缩后宽高上限,保持宽高比不拉伸,兼顾清晰度和处理效率
    private static final int MAX_WIDTH = 1280;
    private static final int MAX_HEIGHT = 1280;
    // 图片压缩质量,0.8为最优平衡点(清晰度足够+体积减半)
    private static final float COMPRESS_QUALITY = 0.8f;

    /**
     * 核心预处理方法:图片标准化(大小校验+无损压缩+格式统一)
     * @param multipartFile 前端上传的图片文件
     * @return 预处理后的图片输入流,可直接用于Base64编码/存储
     * @throws Exception 文件超限/格式异常/处理失败等异常
     */
    public InputStream preProcess(MultipartFile multipartFile) throws Exception {
        // 1. 空值校验
        if (multipartFile == null || multipartFile.isEmpty()) {
            throw new IllegalArgumentException("图片文件不能为空");
        }
        // 2. 文件体积校验
        if (multipartFile.getSize() > MAX_IMAGE_SIZE) {
            throw new RuntimeException("图片文件大小超限,最大支持5MB");
        }
        // 3. 无损压缩图片(自动保持宽高比,不拉伸变形),自动关闭流
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            Thumbnails.of(multipartFile.getInputStream())
                    .size(MAX_WIDTH, MAX_HEIGHT)
                    .keepAspectRatio(true)
                    .outputQuality(COMPRESS_QUALITY)
                    .toOutputStream(bos);
            // 4. 返回预处理后的输入流
            return new ByteArrayInputStream(bos.toByteArray());
        }
    }

    /**
     * 安全获取图片格式,兼容所有主流格式,容错无后缀文件名
     * @param fileName 图片文件名
     * @return 标准化格式:JPG/PNG/WEBP/OTHER
     */
    public String getImageFormat(String fileName) {
        if (fileName == null || !fileName.contains(".")) {
            return "OTHER";
        }
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        return switch (suffix) {
            case "jpg", "jpeg" -> "JPG";
            case "png" -> "PNG";
            case "webp" -> "WEBP";
            default -> "OTHER";
        };
    }

    /**
     * 重载方法1:MultipartFile图片文件 → 标准Base64编码字符串
     * 最常用:前端上传图片直接调用此方法即可
     */
    public String imageToBase64(MultipartFile file) throws Exception {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("待编码的图片文件不能为空");
        }
        return imageInputStreamToBase64(file.getInputStream());
    }

    /**
     * 重载方法2:图片输入流 → 标准Base64编码字符串【核心】
     * 适配预处理后的图片流,复用性最强,Base64编码为通用标准格式,多模型兼容
     */
    public String imageInputStreamToBase64(InputStream inputStream) throws Exception {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             InputStream stream = inputStream) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = stream.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            byte[] imageBytes = bos.toByteArray();
            // 生成标准Base64字符串,无前缀,可直接传入Embedding模型
            return Base64.encodeBase64String(imageBytes);
        }
    }
}

二、图片向量化核心业务服务

/**
 * 知识库-图片向量化核心服务【基础版】
 * 核心链路:图片预处理 → Base64编码 → 复用文本Embedding生成向量 → 入库全流程
 * 核心特性:无缝复用现有技术栈、防重复入库、标准化向量存储、兼容原有检索体系
 */
@Service
@Slf4j
public class ImageVectorizationService {

    @Resource
    private ImagePreProcessUtil imagePreProcessUtil;
    @Resource
    private EmbeddingModel embeddingModel;
    @Resource
    private MinioUtils minioUtils;
    @Resource
    private ElasticsearchVectorService elasticsearchVectorService;
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    // ========== 常量配置(可抽离yml) ==========
    // Redis文件MD5去重Key前缀,复用原有文件查重逻辑,统一去重体系
    private static final String FILE_MD5_KEY = "file:md5:";
    // ES向量索引名称,与文本/PDF共用同一索引,无缝兼容混合检索
    private static final String ES_INDEX = "ai_vector_index";
    // Redis去重缓存有效期:7天,兼顾缓存效率与存储占用
    private static final long REDIS_EXPIRE_DAYS = 7;

    // ==========  核心:图片向量化核心方法【基础版核心逻辑】 ==========
    // 可选开启缓存:按图片MD5缓存向量,避免相同图片重复生成向量,提升性能
    // @Cacheable(value = "image2Vector", key = "#fileMd5", ttl = 604800)
    public float[] embedImage(InputStream inputStream, String fileMd5) throws Exception {
        // 基础版核心逻辑:图片流 → Base64字符串 → 复用文本Embedding模型生成向量
        String imageBase64 = imagePreProcessUtil.imageInputStreamToBase64(inputStream);
        // 关键:与文本向量生成用同一个方法,向量维度完全一致,检索时无兼容问题
        float[] imageVector = embeddingModel.embed(imageBase64);
        log.info("图片向量化完成,MD5={},向量维度={}", fileMd5, imageVector.length);
        return imageVector;
    }

    // ==========  复用原有工具方法:生成文件MD5,一行不改 ==========
    public String getFileMd5(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        return org.apache.commons.codec.digest.DigestUtils.md5Hex(bytes);
    }

    /**
     *  图片入库主方法【对外核心调用入口】
     * 一站式完成:图片预处理+去重+存储+向量化+ES入库全流程
     * @param fileId 业务文件ID(可选,业务侧传入)
     * @param file 前端上传的图片文件
     * @param category 图片分类(如:产品图片、技术截图、手册配图)
     * @param metadata 图片扩展元数据(可选,自定义业务字段)
     * @return 入库成功的MinIO文件唯一标识
     * @throws Exception 全流程异常统一抛出
     */
    public String vectorizeImage(String fileId, MultipartFile file, String category, Map<String,Object> metadata) throws Exception {
        // ========== 步骤1:基础参数校验 ==========
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("入库失败:图片文件不能为空");
        }
        String fileName = file.getOriginalFilename();
        if (fileName == null) {
            fileName = "unknown_image_" + System.currentTimeMillis();
        }
        String imageFormat = imagePreProcessUtil.getImageFormat(fileName);
        byte[] fileBytes = file.getBytes();

        // ========== 步骤2:Redis MD5查重 → 避免重复入库,核心性能优化 ==========
        String fileMd5 = getFileMd5(fileBytes);
        if (redisTemplate.hasKey(FILE_MD5_KEY + fileMd5)) {
            String existObjName = redisTemplate.opsForValue().get(FILE_MD5_KEY + fileMd5);
            log.info("图片已存在,跳过重复入库,文件名={},MD5={}", fileName, fileMd5);
            return existObjName;
        }

        // ========== 步骤3:MinIO存储原图 → 复用原有文件存储逻辑 ==========
        String dir = "image/" + System.currentTimeMillis() + "_" + fileName;
        String objectName = minioUtils.uploadFile(file, dir);
        String previewUrl = minioUtils.getFilePreviewUrl(objectName);
        log.info("图片成功上传至MinIO,存储路径={},预览链接={}", objectName, previewUrl);

        // ========== 步骤4:图片标准化预处理 + 生成图片向量【核心链路】 ==========
        InputStream preProcessStream = imagePreProcessUtil.preProcess(file);
        float[] imageVector = embedImage(preProcessStream, fileMd5);

        // ========== 步骤5:获取图片宽高(容错处理,避免空指针) ==========
        int imgWidth = 0;
        int imgHeight = 0;
        java.awt.Image img = java.awt.Toolkit.getDefaultToolkit().createImage(fileBytes);
        if (img != null) {
            imgWidth = img.getWidth(null);
            imgHeight = img.getHeight(null);
        }

        // ========== 步骤6:构建图片专属DocFragment实体 → 与文本/PDF结构统一 ==========
        DocFragment docFragment = new DocFragment();
        docFragment.setId(objectName);
        docFragment.setFileName(fileName);
        docFragment.setFileType("IMAGE"); // 标记为图片类型,便于检索过滤
        docFragment.setContent(fileName + " - 图片分类:" + category); // 用于BM25关键词检索
        docFragment.setVector(imageVector);
        docFragment.setPageNum(1); // 图片无页码,固定为1
        docFragment.setCategory(category);
        docFragment.setImageWidth(imgWidth);
        docFragment.setImageHeight(imgHeight);
        docFragment.setImageFormat(imageFormat);

        // ========== 步骤7:构建ES文档 → 复用原有批量入库逻辑 ==========
        Map<String, Object> metadatas = new java.util.HashMap<>(8);
        metadatas.put("id", fileId);
        metadatas.put("vector", imageVector);
        metadatas.put("docFragment", docFragment);
        // 扩展元数据可按需追加
        if (metadata != null && !metadata.isEmpty()) {
            metadatas.putAll(metadata);
        }
        Document document = new Document(fileId, "", metadatas);
        List<Document> documents = new ArrayList<>();
        documents.add(document);
        elasticsearchVectorService.writeBatchToEs(documents);

        // ========== 步骤8:Redis记录MD5 → 完成入库,标记已存在 ==========
        redisTemplate.opsForValue().set(FILE_MD5_KEY + fileMd5, objectName, REDIS_EXPIRE_DAYS, java.util.concurrent.TimeUnit.DAYS);
        log.info("图片向量入库完成,文件名={},ES索引={}", fileName, ES_INDEX);

        return objectName;
    }
}

三、核心设计亮点 & 基础版核心优势

核心优势

  1. 0成本兼容现有体系:图片向量与文本/PDF向量维度一致,原有hybridSearch混合检索逻辑一行代码不改,即可实现「文本+PDF+图片」的混合语义检索
  2. 全链路复用无重复开发:完美复用MinIO、Redis、ES、Embedding模型等所有现有组件,开发成本极低,投产速度快
  3. 防重复入库极致优化:基于文件MD5的Redis查重机制,杜绝相同图片多次入库,节省存储+向量计算资源
  4. 图片标准化预处理:统一压缩图片大小、格式,避免超大图片导致的处理超时/内存溢出问题
  5. 无新增技术依赖:基础版仅引入2个轻量Maven包,无多模态模型、无GPU依赖,低配服务器也能稳定运行

关键技术保障

  1. 向量一致性:图片向量由原有文本Embedding模型生成,维度完全一致,检索时无维度不匹配问题
  2. 存储一致性:图片与文档共用同一ES索引,检索时无需区分类型,统一返回结果
  3. 业务一致性:图片入库流程与PDF/文本完全一致,后续维护成本极低

四、生产落地必看 - 关键注意事项

核心注意点(重中之重)

  1. 向量维度一致性:务必保证图片向量化使用的EmbeddingModel与文本/文档向量化是同一个模型,否则向量维度不一致,ES向量检索会直接报错
  2. Base64编码兼容性:本方案生成的是纯Base64字符串(无前缀),所有主流Embedding模型均支持,无需额外处理
  3. 图片大小限制:建议保持5MB上限,过大的图片会导致Base64字符串过长,向量生成耗时增加,且无实际意义
  4. Redis缓存有效期:7天有效期为最优值,既可以避免短时间内重复上传相同图片,又能定期清理缓存,节省Redis内存
  5. ES字段兼容:确保ES索引中vector字段为浮点型数组,且维度足够,能容纳Embedding模型生成的向量

检索侧无改动说明

原有hybridSearch混合检索方法(向量KNN+BM25关键词检索)无需任何修改,即可对图片进行语义检索:

  • 向量检索:图片向量参与KNN相似度计算,返回语义相关的图片/文本/PDF
  • 关键词检索:图片的content字段存储了「文件名+分类」,可通过关键词匹配到相关图片

五、进阶版扩展方向

本次实现的是基础版图片向量化,适合快速落地;后续可基于此版本无侵入升级,无需重构现有代码,仅需扩展方法即可,推荐升级路径如下,优先级由高到低:

进阶1:图文联合向量化(推荐,低成本高收益)

图片Base64字符串 + 图片描述文本(如:产品截图-登录页)拼接后再向量化,向量同时包含「图片特征+文本语义」,检索精准度大幅提升,无需改动现有代码,仅需修改embedImage方法

进阶2:图片OCR文本提取+双向量入库

新增OCR工具提取图片中的文字内容,生成「图片Base64向量」+「OCR文本向量」双字段,检索时同时匹配,兼顾图片语义和文字内容,精准度拉满

进阶3:接入多模态向量模型(如CLIP)

部署专门的多模态向量模型,生成图片专属向量,图片检索精准度达到最优,适合对图片检索要求高的场景,仅需替换embedImage方法的实现逻辑,无其他改动

进阶4:纯图搜图能力

基于图片向量的KNN检索,实现「上传一张图片,检索知识库中相似图片」的纯图搜图功能,新增接口即可,无侵入现有检索体系