Redis 不止缓存!从零到一吃透 Redis 向量数据库

0 阅读24分钟

前言

大模型时代,检索增强生成(RAG)、智能推荐、多模态检索等场景已成为业务创新的核心方向,而向量数据库正是支撑这些场景的底层基石。很多开发者提起向量数据库,第一反应是Milvus、Pinecone这类专业组件,却忽略了日常高频使用的Redis,已经具备了完整的企业级向量数据库能力。无需额外部署新组件,无需重构现有技术栈,就能基于Redis快速搭建低延迟、高可用的向量检索服务。

一、Redis向量数据库核心认知

1.1 核心定义

Redis向量数据库的能力,由Redis官方的RediSearch模块提供核心支撑,Redis Stack默认集成了该模块与RedisJSON等扩展组件。它在Redis原生的高性能KV存储能力之上,扩展了稠密向量的索引构建、存储、相似度检索与混合查询能力,能够将文本、图片、音频、用户画像等非结构化数据转换的向量,进行高效的近邻检索。

1.2 核心能力边界

Redis向量数据库的核心能力覆盖以下维度:

  • 支持FLOAT32类型的稠密向量,最大支持32768维向量
  • 提供两种核心索引类型:FLAT精确索引与HNSW近似索引
  • 支持3种主流相似度度量算法:L2欧几里得距离、内积(IP)、余弦相似度
  • 支持向量检索与标签、数值、文本关键词的混合过滤查询
  • 完全兼容Redis原生的主从、哨兵、集群高可用架构
  • 兼容Redis原生的持久化机制(RDB/AOF)、事务、过期策略等所有能力

1.3 选型对比:Redis向量库 vs 专业向量数据库

对比维度Redis向量数据库专业向量数据库(Milvus/Pinecone)
部署成本极低,基于现有Redis Stack即可部署,无额外组件较高,需独立部署分布式集群,依赖对象存储、协调服务等组件
学习成本极低,兼容Redis原生协议与Java客户端,复用现有技术栈较高,需学习独立的API、索引配置与集群运维体系
数据规模适配百万级到千万级向量数据适配亿级以上超大规模向量数据,支持分布式分片与水平扩展
检索延迟亚毫秒级,Redis原生内存级访问优势毫秒级,分布式架构带来一定的网络开销
高级特性满足基础向量检索与混合查询需求支持向量压缩、多租户、动态schema、多模态向量、增量索引等高级特性
业务适配中小规模数据量、快速迭代的业务,已有Redis技术栈的团队超大规模数据、复杂检索场景、企业级多租户需求的业务

二、底层原理深度拆解

2.1 向量的本质与高维空间语义

向量的本质,是将非结构化数据转换为一组固定长度的浮点数数组,数组中的每一个数值,都对应着数据的一个底层特征。比如一段文本“Java并发编程原理”,通过Embedding模型转换为1024维的float数组,每一个数值都代表了文本的语义特征。

在高维空间中,语义、特征越相似的数据,对应的向量点之间的距离就越近。向量检索的核心逻辑,就是计算查询向量与库中所有向量的相似度,返回距离最近的TopK个结果,也就是常说的K近邻(KNN)检索。

2.2 Redis向量的存储结构

Redis向量支持两种存储载体,均能被RediSearch索引识别与检索:

  1. Hash结构:向量字段以BLOB二进制格式存储在Hash的field中,是性能最优、内存占用最低的存储方式。FLOAT32类型的向量,每个维度占用4个字节,1024维的向量仅占用4KB内存,存储效率极高。
  2. JSON结构:向量以浮点数组的形式存储在RedisJSON文档中,适合需要同时存储向量与复杂结构化数据的场景,内存占用略高于Hash结构。

无论哪种存储方式,向量数据都完全兼容Redis原生的持久化、主从同步、集群分片机制,无需额外的存储适配。

2.3 核心索引类型与底层实现

索引是向量检索性能的核心,Redis提供两种索引类型,分别适配不同的业务场景。

2.3.1 FLAT索引(精确KNN检索)

FLAT索引是暴力搜索索引,底层采用全量扫描的方式,计算查询向量与库中所有向量的相似度,最终返回距离最近的TopK结果。

  • 核心优势:召回率100%,无任何精度损失,支持向量的实时增删改,数据更新后立即生效
  • 核心劣势:数据量超过10万条后,检索延迟会显著上升,不适合大数据量场景
  • 适用场景:万级以下小数据集、需要100%精确匹配的场景、数据更新频率极高的场景

2.3.2 HNSW索引(近似ANN检索)

HNSW全称Hierarchical Navigable Small World,即层次化导航小世界图,是目前工业界最主流的近似近邻检索算法。

  • 底层原理:将高维向量构建成多层有向图结构,最底层包含所有向量节点,越往上的层级节点数量越少。检索时从最顶层的入口节点开始,逐层向下查找距离最近的节点,最终在最底层找到TopK结果,避免了全量扫描。
  • 核心优势:检索延迟极低,百万级向量数据可实现亚毫秒级响应,适合大数据量高并发场景
  • 核心劣势:有轻微的召回率损失,索引构建有一定的资源开销,数据实时更新性能弱于FLAT索引
  • 适用场景:十万级以上大数据集、高并发检索场景、可接受轻微精度损失换取极致性能的业务

2.4 相似度度量算法全解析

Redis支持3种主流的相似度度量算法,分别适配不同的业务场景,算法的选择直接影响检索效果。

2.4.1 L2欧几里得距离

L2距离计算的是高维空间中两个向量点之间的直线距离,计算公式为:

  • 距离取值范围:[0, +∞),值越小,向量相似度越高
  • 适用场景:图像特征匹配、语音识别、用户行为特征匹配等对向量绝对值敏感的场景

2.4.2 内积(IP)

内积计算的是两个向量的点积,计算公式为:

  • 取值范围:(-∞, +∞),值越大,向量相似度越高
  • 核心特性:对向量的长度敏感,向量模长越大,内积值越高
  • 适用场景:已做L2归一化的向量检索、推荐系统中的用户-物品匹配场景

2.4.3 余弦相似度

余弦相似度计算的是两个向量夹角的余弦值,衡量的是向量的方向相似度,与向量长度无关,计算公式为:

  • 取值范围:[-1, 1],值越大,向量相似度越高,1代表完全相同
  • 核心特性:不受向量模长影响,仅关注向量的方向,完美适配文本语义匹配
  • 适用场景:文本语义检索、RAG场景、文档相似度匹配等NLP相关场景

关键知识点:若向量已完成L2归一化(模长为1),则余弦相似度与内积的计算结果完全一致,可直接使用内积算法减少计算开销。

三、环境搭建与项目整合

3.1 环境快速搭建

Redis向量能力依赖RediSearch模块,推荐使用Redis Stack官方镜像一键部署,Docker部署命令如下:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:7.4.0-v0
  • 6379端口:Redis原生服务端口
  • 8001端口:Redis Insight可视化管理界面,可直接查看向量索引与数据

3.2 核心基础命令

先通过Redis CLI掌握核心命令,理解向量操作的核心逻辑:

  1. 创建FLAT向量索引
FT.CREATE idx_doc_flat ON HASH PREFIX 1 doc: SCHEMA content TEXT tags TAG vector VECTOR FLAT 6 TYPE FLOAT32 DIM 1024 DISTANCE_METRIC COSINE

2. 创建HNSW向量索引

FT.CREATE idx_doc_hnsw ON HASH PREFIX 1 doc: SCHEMA content TEXT tags TAG vector VECTOR HNSW 10 TYPE FLOAT32 DIM 1024 DISTANCE_METRIC COSINE M 16 EF_CONSTRUCTION 200 EF_RUNTIME 10

3. 写入向量数据

HSET doc:001 content "Java并发编程核心原理" tags "Java,后端,并发" vector "<向量二进制字节数组>"

4. KNN向量检索

FT.SEARCH idx_doc_hnsw "*=>[KNN 5 @vector $query_vec]" PARAMS 2 query_vec "<查询向量二进制字节数组>" SORTBY __vector_score LIMIT 0 5

3.3 Java项目环境整合

基于Spring Boot 3.2.x搭建项目,pom.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.demo</groupId>
    <artifactId>redis-vector-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis-vector-demo</name>
    <properties>
        <java.version>17</java.version>
        <jedis.version>5.1.5</jedis.version>
        <fastjson2.version>2.0.52</fastjson2.version>
        <guava.version>33.1.0-jre</guava.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <springdoc.version>2.5.0</springdoc.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

application.yml配置文件:

spring:
  application:
    name: redis-vector-demo
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      database: 0
      timeout: 5000
      jedis:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5
          max-wait: 3000
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/vector_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: root
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    enabled: true
  api-docs:
    enabled: true
    path: /v3/api-docs

Redis配置类:

package com.jam.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis配置类
 * @author ken
 * @since 2026-04-03
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(stringRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public JedisPool jedisPool(JedisConnectionFactory jedisConnectionFactory) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);
        poolConfig.setMaxWaitMillis(3000);
        return new JedisPool(poolConfig,
                jedisConnectionFactory.getHostName(),
                jedisConnectionFactory.getPort(),
                jedisConnectionFactory.getTimeout(),
                jedisConnectionFactory.getPassword(),
                jedisConnectionFactory.getDatabase());
    }
}

向量工具类,核心解决Java大端序与Redis要求的小端序转换问题:

package com.jam.demo.util;

import org.springframework.util.ObjectUtils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * 向量处理工具类
 * @author ken
 * @since 2026-04-03
 */
public class VectorUtils {

    private VectorUtils() {}

    /**
     * float向量数组转换为Redis要求的小端序字节数组
     * @param vector 浮点向量数组
     * @return 小端序字节数组
     */
    public static byte[] floatVectorToLittleEndianBytes(float[] vector) {
        if (ObjectUtils.isEmpty(vector)) {
            return new byte[0];
        }
        ByteBuffer buffer = ByteBuffer.allocate(Float.BYTES * vector.length);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        for (float value : vector) {
            buffer.putFloat(value);
        }
        return buffer.array();
    }

    /**
     * Redis小端序字节数组转换为float向量数组
     * @param bytes 小端序字节数组
     * @return 浮点向量数组
     */
    public static float[] littleEndianBytesToFloatVector(byte[] bytes) {
        if (ObjectUtils.isEmpty(bytes)) {
            return new float[0];
        }
        int floatCount = bytes.length / Float.BYTES;
        float[] vector = new float[floatCount];
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        for (int i = 0; i < floatCount; i++) {
            vector[i] = buffer.getFloat();
        }
        return vector;
    }

    /**
     * 向量L2归一化处理
     * @param vector 原始向量
     * @return 归一化后的向量
     */
    public static float[] normalizeVector(float[] vector) {
        if (ObjectUtils.isEmpty(vector)) {
            return new float[0];
        }
        double sum = 0.0;
        for (float value : vector) {
            sum += Math.pow(value, 2);
        }
        float norm = (float) Math.sqrt(sum);
        if (norm == 0) {
            return vector;
        }
        float[] normalizedVector = new float[vector.length];
        for (int i = 0; i < vector.length; i++) {
            normalizedVector[i] = vector[i] / norm;
        }
        return normalizedVector;
    }
}

四、核心功能全实战

4.1 向量索引的创建与管理

实体类定义:

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 向量文档实体
 * @author ken
 * @since 2026-04-03
 */
@Data
@TableName("t_vector_document")
@Schema(description = "向量文档实体")
public class VectorDocument {

    @TableId(type = IdType.AUTO)
    @Schema(description = "主键ID", example = "1")
    private Long id;

    @Schema(description = "文档唯一ID", example = "doc:001")
    private String docId;

    @Schema(description = "文档内容", example = "Java并发编程核心原理")
    private String content;

    @Schema(description = "标签,逗号分隔", example = "Java,后端,并发")
    private String tags;

    @Schema(description = "向量维度", example = "1024")
    private Integer vectorDimension;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}

Mapper接口:

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.VectorDocument;
import org.apache.ibatis.annotations.Mapper;

/**
 * 向量文档Mapper
 * @author ken
 * @since 2026-04-03
 */
@Mapper
public interface VectorDocumentMapper extends BaseMapper<VectorDocument> {
}

索引管理Service接口:

package com.jam.demo.service;

import com.jam.demo.entity.VectorDocument;
import com.jam.demo.vo.VectorSearchResult;

import java.util.List;

/**
 * Redis向量服务接口
 * @author ken
 * @since 2026-04-03
 */
public interface RedisVectorService {

    boolean createFlatIndex(String indexName, int dimension, String distanceMetric);

    boolean createHnswIndex(String indexName, int dimension, String distanceMetric, int m, int efConstruction, int efRuntime);

    boolean deleteIndex(String indexName);

    boolean existIndex(String indexName);

    boolean saveVectorDocument(VectorDocument document, float[] vector);

    boolean deleteVectorDocument(String docId);

    List<VectorSearchResult> knnSearch(String indexName, float[] queryVector, int topK);

    List<VectorSearchResult> hybridSearch(String indexName, float[] queryVector, int topK, String tagFilter);
}

索引管理Service实现类核心方法:

package com.jam.demo.service.impl;

import com.jam.demo.entity.VectorDocument;
import com.jam.demo.mapper.VectorDocumentMapper;
import com.jam.demo.service.RedisVectorService;
import com.jam.demo.util.VectorUtils;
import com.jam.demo.vo.VectorSearchResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.search.Document;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;

import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Redis向量服务实现类
 * @author ken
 * @since 2026-04-03
 */
@Slf4j
@Service
public class RedisVectorServiceImpl implements RedisVectorService {

    private static final String DOC_KEY_PREFIX = "doc:";
    private static final String VECTOR_FIELD_NAME = "vector";
    private static final String CONTENT_FIELD_NAME = "content";
    private static final String TAGS_FIELD_NAME = "tags";
    private static final String SCORE_FIELD_NAME = "__vector_score";

    @Resource
    private JedisPool jedisPool;

    @Resource
    private VectorDocumentMapper vectorDocumentMapper;

    @Resource
    private TransactionTemplate transactionTemplate;

    @Override
    public boolean createFlatIndex(String indexName, int dimension, String distanceMetric) {
        if (!StringUtils.hasText(indexName) || dimension <= 0 || !StringUtils.hasText(distanceMetric)) {
            log.error("创建FLAT索引参数异常");
            return false;
        }
        try (Jedis jedis = jedisPool.getResource()) {
            String schema = String.format(
                    "ON HASH PREFIX 1 %s SCHEMA %s TEXT %s TAG %s VECTOR FLAT 6 TYPE FLOAT32 DIM %d DISTANCE_METRIC %s",
                    DOC_KEY_PREFIX, CONTENT_FIELD_NAME, TAGS_FIELD_NAME, VECTOR_FIELD_NAME, dimension, distanceMetric
            );
            jedis.ftCreate(indexName, schema.split(" "));
            log.info("FLAT索引创建成功,索引名:{}", indexName);
            return true;
        } catch (Exception e) {
            log.error("FLAT索引创建失败,索引名:{}", indexName, e);
            return false;
        }
    }

    @Override
    public boolean createHnswIndex(String indexName, int dimension, String distanceMetric, int m, int efConstruction, int efRuntime) {
        if (!StringUtils.hasText(indexName) || dimension <= 0 || !StringUtils.hasText(distanceMetric)) {
            log.error("创建HNSW索引参数异常");
            return false;
        }
        try (Jedis jedis = jedisPool.getResource()) {
            String schema = String.format(
                    "ON HASH PREFIX 1 %s SCHEMA %s TEXT %s TAG %s VECTOR HNSW 10 TYPE FLOAT32 DIM %d DISTANCE_METRIC %s M %d EF_CONSTRUCTION %d EF_RUNTIME %d",
                    DOC_KEY_PREFIX, CONTENT_FIELD_NAME, TAGS_FIELD_NAME, VECTOR_FIELD_NAME, dimension, distanceMetric, m, efConstruction, efRuntime
            );
            jedis.ftCreate(indexName, schema.split(" "));
            log.info("HNSW索引创建成功,索引名:{}", indexName);
            return true;
        } catch (Exception e) {
            log.error("HNSW索引创建失败,索引名:{}", indexName, e);
            return false;
        }
    }

    @Override
    public boolean deleteIndex(String indexName) {
        if (!StringUtils.hasText(indexName)) {
            return false;
        }
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.ftDropIndex(indexName);
            log.info("索引删除成功,索引名:{}", indexName);
            return true;
        } catch (Exception e) {
            log.error("索引删除失败,索引名:{}", indexName, e);
            return false;
        }
    }

    @Override
    public boolean existIndex(String indexName) {
        if (!StringUtils.hasText(indexName)) {
            return false;
        }
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.ftInfo(indexName);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

4.2 向量数据的增删改查

Service实现类新增增删改查核心方法:

@Override
public boolean saveVectorDocument(VectorDocument document, float[] vector) {
    if (ObjectUtils.isEmpty(document) || !StringUtils.hasText(document.getDocId()) || ObjectUtils.isEmpty(vector)) {
        log.error("保存向量文档参数异常");
        return false;
    }
    return transactionTemplate.execute(new TransactionCallback<Boolean>() {
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try {
                int dbResult;
                VectorDocument existDoc = vectorDocumentMapper.selectOne(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VectorDocument>()
                                .eq(VectorDocument::getDocId, document.getDocId())
                );
                if (ObjectUtils.isEmpty(existDoc)) {
                    dbResult = vectorDocumentMapper.insert(document);
                } else {
                    document.setId(existDoc.getId());
                    dbResult = vectorDocumentMapper.updateById(document);
                }
                if (dbResult <= 0) {
                    status.setRollbackOnly();
                    log.error("向量文档数据库保存失败,docId:{}"document.getDocId());
                    return false;
                }
                try (Jedis jedis = jedisPool.getResource()) {
                    byte[] vectorBytes = VectorUtils.floatVectorToLittleEndianBytes(vector);
                    Map<StringString> hashData = Map.of(
                            CONTENT_FIELD_NAMEdocument.getContent(),
                            TAGS_FIELD_NAMEObjectUtils.isEmpty(document.getTags()) ? "" : document.getTags()
                    );
                    jedis.hset(DOC_KEY_PREFIX + document.getDocId(), hashData);
                    jedis.hset(DOC_KEY_PREFIX + document.getDocId(), VECTOR_FIELD_NAME, vectorBytes);
                }
                log.info("向量文档保存成功,docId:{}"document.getDocId());
                return true;
            } catch (Exception e) {
                status.setRollbackOnly();
                log.error("向量文档保存失败,docId:{}"document.getDocId(), e);
                return false;
            }
        }
    });
}

@Override
public boolean deleteVectorDocument(String docId) {
    if (!StringUtils.hasText(docId)) {
        return false;
    }
    return transactionTemplate.execute(new TransactionCallback<Boolean>() {
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try {
                vectorDocumentMapper.delete(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VectorDocument>()
                                .eq(VectorDocument::getDocId, docId)
                );
                try (Jedis jedis = jedisPool.getResource()) {
                    jedis.del(DOC_KEY_PREFIX + docId);
                }
                log.info("向量文档删除成功,docId:{}", docId);
                return true;
            } catch (Exception e) {
                status.setRollbackOnly();
                log.error("向量文档删除失败,docId:{}", docId, e);
                return false;
            }
        }
    });
}

检索结果VO定义:

package com.jam.demo.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 向量检索结果VO
 * @author ken
 * @since 2026-04-03
 */
@Data
@Schema(description = "向量检索结果VO")
public class VectorSearchResult {

    @Schema(description = "文档ID", example = "doc:001")
    private String docId;

    @Schema(description = "文档内容", example = "Java并发编程核心原理")
    private String content;

    @Schema(description = "文档标签", example = "Java,后端,并发")
    private String tags;

    @Schema(description = "相似度得分", example = "0.92")
    private Double score;
}

4.3 精准KNN与近似ANN检索

Service实现类新增检索核心方法:

@Override
public List<VectorSearchResult> knnSearch(String indexName, float[] queryVector, int topK) {
    if (!StringUtils.hasText(indexName) || ObjectUtils.isEmpty(queryVector) || topK <= 0) {
        log.error("KNN检索参数异常");
        return new ArrayList<>();
    }
    try (Jedis jedis = jedisPool.getResource()) {
        byte[] queryVectorBytes = VectorUtils.floatVectorToLittleEndianBytes(queryVector);
        String queryString = String.format("*=>[KNN %d @%s $query_vec]", topK, VECTOR_FIELD_NAME);
        Query query = new Query(queryString)
                .addParam("query_vec", queryVectorBytes)
                .setSortBy(SCORE_FIELD_NAME, true)
                .limit(0, topK)
                .returnFields(CONTENT_FIELD_NAME, TAGS_FIELD_NAME, SCORE_FIELD_NAME);
        SearchResult searchResult = jedis.ftSearch(indexName, query);
        List<Document> documents = searchResult.getDocuments();
        if (CollectionUtils.isEmpty(documents)) {
            return new ArrayList<>();
        }
        List<VectorSearchResult> resultList = new ArrayList<>(documents.size());
        for (Document doc : documents) {
            VectorSearchResult result = new VectorSearchResult();
            result.setDocId(doc.getId().replace(DOC_KEY_PREFIX, ""));
            result.setContent(doc.getString(CONTENT_FIELD_NAME));
            result.setTags(doc.getString(TAGS_FIELD_NAME));
            result.setScore(1 - Double.parseDouble(doc.getString(SCORE_FIELD_NAME)));
            resultList.add(result);
        }
        return resultList;
    } catch (Exception e) {
        log.error("KNN检索失败,索引名:{}", indexName, e);
        return new ArrayList<>();
    }
}

@Override
public List<VectorSearchResult> hybridSearch(String indexName, float[] queryVector, int topK, String tagFilter) {
    if (!StringUtils.hasText(indexName) || ObjectUtils.isEmpty(queryVector) || topK <= 0 || !StringUtils.hasText(tagFilter)) {
        log.error("混合检索参数异常");
        return new ArrayList<>();
    }
    try (Jedis jedis = jedisPool.getResource()) {
        byte[] queryVectorBytes = VectorUtils.floatVectorToLittleEndianBytes(queryVector);
        String queryString = String.format("@%s:{%s} =>[KNN %d @%s $query_vec]", TAGS_FIELD_NAME, tagFilter, topK, VECTOR_FIELD_NAME);
        Query query = new Query(queryString)
                .addParam("query_vec", queryVectorBytes)
                .setSortBy(SCORE_FIELD_NAME, true)
                .limit(0, topK)
                .returnFields(CONTENT_FIELD_NAME, TAGS_FIELD_NAME, SCORE_FIELD_NAME);
        SearchResult searchResult = jedis.ftSearch(indexName, query);
        List<Document> documents = searchResult.getDocuments();
        if (CollectionUtils.isEmpty(documents)) {
            return new ArrayList<>();
        }
        List<VectorSearchResult> resultList = new ArrayList<>(documents.size());
        for (Document doc : documents) {
            VectorSearchResult result = new VectorSearchResult();
            result.setDocId(doc.getId().replace(DOC_KEY_PREFIX, ""));
            result.setContent(doc.getString(CONTENT_FIELD_NAME));
            result.setTags(doc.getString(TAGS_FIELD_NAME));
            result.setScore(1 - Double.parseDouble(doc.getString(SCORE_FIELD_NAME)));
            resultList.add(result);
        }
        return resultList;
    } catch (Exception e) {
        log.error("混合检索失败,索引名:{}", indexName, e);
        return new ArrayList<>();
    }
}

Controller层接口定义:

package com.jam.demo.controller;

import com.jam.demo.entity.VectorDocument;
import com.jam.demo.service.RedisVectorService;
import com.jam.demo.util.VectorUtils;
import com.jam.demo.vo.VectorSearchResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.annotation.Resource;
import java.util.List;
import java.util.Random;

/**
 * Redis向量数据库接口
 * @author ken
 * @since 2026-04-03
 */
@Slf4j
@RestController
@RequestMapping("/api/redis/vector")
@Tag(name = "Redis向量数据库接口", description = "向量索引管理、数据操作与检索接口")
public class RedisVectorController {

    @Resource
    private RedisVectorService redisVectorService;

    private static final int VECTOR_DIMENSION = 1024;
    private final Random random = new Random();

    @PostMapping("/index/flat")
    @Operation(summary = "创建FLAT精确索引", description = "创建FLAT类型的向量索引,支持100%召回率的精确检索")
    public ResponseEntity<BooleancreateFlatIndex(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_flat"@RequestParam String indexName,
            @Parameter(description = "距离度量算法", required = true, example = "COSINE"@RequestParam(defaultValue = "COSINE") String distanceMetric) {
        boolean result = redisVectorService.createFlatIndex(indexName, VECTOR_DIMENSION, distanceMetric);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/index/hnsw")
    @Operation(summary = "创建HNSW近似索引", description = "创建HNSW类型的向量索引,支持大数据量低延迟的近似检索")
    public ResponseEntity<BooleancreateHnswIndex(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_hnsw"@RequestParam String indexName,
            @Parameter(description = "距离度量算法", required = true, example = "COSINE"@RequestParam(defaultValue = "COSINE") String distanceMetric,
            @Parameter(description = "每层节点邻居数", example = "16"@RequestParam(defaultValue = "16") int m,
            @Parameter(description = "索引构建探索深度", example = "200"@RequestParam(defaultValue = "200") int efConstruction,
            @Parameter(description = "检索探索深度", example = "10"@RequestParam(defaultValue = "10") int efRuntime) {
        boolean result = redisVectorService.createHnswIndex(indexName, VECTOR_DIMENSION, distanceMetric, m, efConstruction, efRuntime);
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/index")
    @Operation(summary = "删除向量索引", description = "删除指定名称的向量索引")
    public ResponseEntity<BooleandeleteIndex(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_flat"@RequestParam String indexName) {
        boolean result = redisVectorService.deleteIndex(indexName);
        return ResponseEntity.ok(result);
    }

    @GetMapping("/index/exist")
    @Operation(summary = "判断索引是否存在", description = "检查指定名称的向量索引是否存在")
    public ResponseEntity<BooleanexistIndex(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_flat"@RequestParam String indexName) {
        boolean result = redisVectorService.existIndex(indexName);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/document")
    @Operation(summary = "保存向量文档", description = "保存向量文档与对应的向量数据,同时写入MySQL与Redis")
    public ResponseEntity<BooleansaveDocument(@RequestBody VectorDocument document) {
        float[] vector = new float[VECTOR_DIMENSION];
        for (int i = 0; i < VECTOR_DIMENSION; i++) {
            vector[i] = random.nextFloat() * 2 - 1;
        }
        vector = VectorUtils.normalizeVector(vector);
        boolean result = redisVectorService.saveVectorDocument(document, vector);
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/document")
    @Operation(summary = "删除向量文档", description = "删除指定docId的向量文档,同时删除MySQL与Redis中的数据")
    public ResponseEntity<BooleandeleteDocument(
            @Parameter(description = "文档ID", required = true, example = "001"@RequestParam String docId) {
        boolean result = redisVectorService.deleteVectorDocument(docId);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/search/knn")
    @Operation(summary = "KNN向量检索", description = "执行K近邻向量检索,返回TopK个相似结果")
    public ResponseEntity<List<VectorSearchResult>> knnSearch(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_hnsw"@RequestParam String indexName,
            @Parameter(description = "返回结果数量", required = true, example = "5"@RequestParam(defaultValue = "5") int topK) {
        float[] queryVector = new float[VECTOR_DIMENSION];
        for (int i = 0; i < VECTOR_DIMENSION; i++) {
            queryVector[i] = random.nextFloat() * 2 - 1;
        }
        queryVector = VectorUtils.normalizeVector(queryVector);
        List<VectorSearchResultresultList = redisVectorService.knnSearch(indexName, queryVector, topK);
        return ResponseEntity.ok(resultList);
    }

    @PostMapping("/search/hybrid")
    @Operation(summary = "混合检索", description = "执行标签过滤+向量检索的混合查询,先过滤再检索,保证性能")
    public ResponseEntity<List<VectorSearchResult>> hybridSearch(
            @Parameter(description = "索引名称", required = true, example = "idx_doc_hnsw"@RequestParam String indexName,
            @Parameter(description = "返回结果数量", required = true, example = "5"@RequestParam(defaultValue = "5") int topK,
            @Parameter(description = "标签过滤条件", required = true, example = "Java"@RequestParam String tagFilter) {
        float[] queryVector = new float[VECTOR_DIMENSION];
        for (int i = 0; i < VECTOR_DIMENSION; i++) {
            queryVector[i] = random.nextFloat() * 2 - 1;
        }
        queryVector = VectorUtils.normalizeVector(queryVector);
        List<VectorSearchResultresultList = redisVectorService.hybridSearch(indexName, queryVector, topK, tagFilter);
        return ResponseEntity.ok(resultList);
    }
}

五、落地实战

5.1 RAG检索增强生成全流程实现

RAG是目前大模型落地最核心的场景,Redis向量数据库可作为RAG的底层检索引擎,完整流程如下:

  1. 文档加载与分块:将PDF、Word、TXT等文档拆分为固定长度的文本块
  2. 文本向量化:通过Embedding模型将文本块转换为固定维度的向量
  3. 向量存储:将文本块与向量写入Redis,构建向量索引
  4. 查询向量化:将用户的提问转换为向量
  5. 相似检索:从Redis中检索与提问向量最相似的TopK个文本块
  6. Prompt构建:将检索到的文本块与用户提问拼接为Prompt,传入大模型
  7. 回答生成:大模型基于检索到的上下文,生成精准的回答

核心RAG服务实现:

package com.jam.demo.service;

import com.jam.demo.vo.VectorSearchResult;

import java.util.List;

/**
 * RAG服务接口
 * @author ken
 * @since 2026-04-03
 */
public interface RagService {

    void loadDocumentToVectorDb(String filePath);

    String generateAnswer(String userQuestion, int topK);
}
package com.jam.demo.service.impl;

import com.alibaba.fastjson2.JSON;
import com.jam.demo.entity.VectorDocument;
import com.jam.demo.service.RagService;
import com.jam.demo.service.RedisVectorService;
import com.jam.demo.util.VectorUtils;
import com.jam.demo.vo.VectorSearchResult;
import lombok.extern.slf4j.Slf4j;
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.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import jakarta.annotation.Resource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * RAG服务实现类
 * @author ken
 * @since 2026-04-03
 */
@Slf4j
@Service
public class RagServiceImpl implements RagService {

    private static final String INDEX_NAME = "idx_rag_doc";
    private static final int VECTOR_DIMENSION = 1024;
    private static final int CHUNK_SIZE = 500;
    private static final int CHUNK_OVERLAP = 50;

    @Value("${embedding.api.url}")
    private String embeddingApiUrl;

    @Value("${embedding.api.key}")
    private String embeddingApiKey;

    @Value("${llm.api.url}")
    private String llmApiUrl;

    @Value("${llm.api.key}")
    private String llmApiKey;

    @Resource
    private RedisVectorService redisVectorService;

    @Resource
    private RestTemplate restTemplate;

    @Override
    public void loadDocumentToVectorDb(String filePath) {
        if (!StringUtils.hasText(filePath)) {
            log.error("文档路径为空");
            return;
        }
        try {
            if (!redisVectorService.existIndex(INDEX_NAME)) {
                redisVectorService.createHnswIndex(INDEX_NAME, VECTOR_DIMENSION, "COSINE", 16, 200, 10);
            }
            String content = Files.readString(Paths.get(filePath));
            List<String> chunks = splitTextToChunks(content);
            for (String chunk : chunks) {
                float[] vector = getTextEmbedding(chunk);
                if (vector.length != VECTOR_DIMENSION) {
                    log.error("向量维度不匹配,跳过该文本块");
                    continue;
                }
                VectorDocument document = new VectorDocument();
                document.setDocId(UUID.randomUUID().toString().replace("-", ""));
                document.setContent(chunk);
                document.setTags("RAG");
                document.setVectorDimension(VECTOR_DIMENSION);
                document.setCreateTime(LocalDateTime.now());
                document.setUpdateTime(LocalDateTime.now());
                redisVectorService.saveVectorDocument(document, vector);
            }
            log.info("文档加载完成,共拆分{}个文本块", chunks.size());
        } catch (IOException e) {
            log.error("文档读取失败", e);
        }
    }

    @Override
    public String generateAnswer(String userQuestion, int topK) {
        if (!StringUtils.hasText(userQuestion)) {
            return "请输入有效的问题";
        }
        float[] queryVector = getTextEmbedding(userQuestion);
        List<VectorSearchResult> searchResults = redisVectorService.knnSearch(INDEX_NAME, queryVector, topK);
        if (CollectionUtils.isEmpty(searchResults)) {
            return "未检索到相关的上下文信息";
        }
        StringBuilder contextBuilder = new StringBuilder();
        for (VectorSearchResult result : searchResults) {
            contextBuilder.append(result.getContent()).append("\n\n");
        }
        String prompt = String.format(
                "请基于以下上下文信息回答用户的问题,若上下文中没有相关信息,请直接回答“未找到相关信息”,禁止编造内容。\n上下文信息:\n%s\n用户问题:%s",
                contextBuilder, userQuestion
        );
        return callLlm(prompt);
    }

    private List<String> splitTextToChunks(String text) {
        List<String> chunks = new ArrayList<>();
        if (!StringUtils.hasText(text)) {
            return chunks;
        }
        int length = text.length();
        int start = 0;
        while (start < length) {
            int end = Math.min(start + CHUNK_SIZE, length);
            if (end < length) {
                int lastPunctuation = Math.max(
                        text.lastIndexOf("。", end),
                        Math.max(text.lastIndexOf("!", end), text.lastIndexOf("?", end))
                );
                if (lastPunctuation > start) {
                    end = lastPunctuation + 1;
                }
            }
            chunks.add(text.substring(start, end).trim());
            start = end - CHUNK_OVERLAP;
            if (start >= length) {
                break;
            }
        }
        return chunks;
    }

    private float[] getTextEmbedding(String text) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Authorization", "Bearer " + embeddingApiKey);
            Map<String, Object> requestBody = Map.of("input", text, "model""text-embedding-v1");
            HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
            String response = restTemplate.postForObject(embeddingApiUrl, request, String.class);
            Map<String, Object> responseMap = JSON.parseObject(response);
            List<Float> embeddingList = (List<Float>) ((Map<String, Object>) ((List<Object>) responseMap.get("data")).get(0)).get("embedding");
            float[] embedding = new float[embeddingList.size()];
            for (int i = 0; i < embeddingList.size(); i++) {
                embedding[i] = embeddingList.get(i);
            }
            return VectorUtils.normalizeVector(embedding);
        } catch (Exception e) {
            log.error("文本向量化失败", e);
            return new float[VECTOR_DIMENSION];
        }
    }

    private String callLlm(String prompt) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Authorization", "Bearer " + llmApiKey);
            Map<String, Object> requestBody = Map.of(
                    "model", "qwen-max",
                    "messages", List.of(Map.of("role", "user", "content", prompt)),
                    "temperature", 0.1
            );
            HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
            String response = restTemplate.postForObject(llmApiUrl, request, String.class);
            Map<String, Object> responseMap = JSON.parseObject(response);
            return (String) ((Map<String, Object>) ((List<Object>) responseMap.get("choices")).get(0)).get("content");
        } catch (Exception e) {
            log.error("大模型调用失败", e);
            return "大模型调用异常,请稍后重试";
        }
    }
}

5.2 商品相似度推荐系统

基于商品的标题、描述、属性等信息生成商品特征向量,通过Redis向量检索实现相似商品推荐,同时支持品类、价格区间的过滤,实现精准的个性化推荐。核心逻辑与RAG场景一致,只需将商品信息作为文本块生成向量,检索时增加品类、价格等标签过滤条件,即可实现相似商品推荐。

5.3 用户画像相似度匹配

将用户的行为数据、偏好标签、消费特征等信息转换为用户画像向量,通过Redis向量检索实现相似用户匹配,可用于好友推荐、用户分群、精准营销等场景。针对百万级用户数据,HNSW索引可实现毫秒级的相似用户检索,完全满足高并发的业务需求。

六、性能调优与最佳实践

6.1 索引选型与参数调优

  1. 索引选型规则

    • 数据量<10万条,或需要100%召回率:选择FLAT索引
    • 数据量>10万条,或高并发检索场景:选择HNSW索引
    • 数据更新频率极高:优先选择FLAT索引,HNSW索引频繁更新会降低索引质量
  2. HNSW核心参数调优

    • M值:每层每个节点的邻居数量,推荐取值16-64。M值越大,索引占用内存越高,构建速度越慢,但召回率与检索速度越高。文本语义场景推荐16-32,图像特征场景推荐32-64。
    • EF_CONSTRUCTION:索引构建时的探索深度,推荐取值100-500。值越大,索引构建速度越慢,索引质量越高,召回率越高。生产环境推荐200-300。
    • EF_RUNTIME:检索时的探索深度,推荐取值10-100。值越大,检索速度越慢,召回率越高。可根据业务的延迟与召回率要求动态调整,生产环境默认10-50。

6.2 内存优化策略

  • 优先使用Hash结构+BLOB格式存储向量,相比JSON数组可节省30%以上的内存
  • 向量维度选择够用即可,文本语义场景1024维已能满足绝大多数需求,无需盲目使用4096等高维向量
  • 对向量进行L2归一化,可统一向量取值范围,减少计算开销,同时兼容内积与余弦相似度算法
  • 开启Redis的内存压缩功能,针对非向量字段进行压缩,进一步降低内存占用
  • 生产环境建议开启RDB+AOF混合持久化,平衡数据安全性与性能

6.3 高可用部署方案

Redis向量数据库完全兼容Redis原生的高可用架构,生产环境推荐两种部署方案:

  1. 哨兵模式:1主2从+3哨兵,适配中小规模数据量,保证故障自动切换,数据零丢失
  2. 集群模式:3主3从,适配百万级以上大数据量,通过分片水平扩展,支持更大的数据集与更高的并发

注意:向量索引会随Redis数据同步到从节点,检索请求可路由到从节点执行,实现读写分离,进一步提升集群的并发能力。

6.4 生产级最佳实践

  • 必须先创建索引,再写入向量数据,先写入数据再创建索引,不会对已有的数据构建索引
  • 混合检索场景,必须将过滤条件写在=>符号之前,实现先过滤后检索,避免全量检索后过滤导致的性能灾难
  • 向量写入前必须校验维度,确保与索引定义的维度一致,维度不一致的向量无法被索引识别
  • 批量写入向量数据时,使用Redis Pipeline批量执行命令,可提升5倍以上的写入性能
  • 生产环境禁止使用KEYS *等全量扫描命令,避免阻塞Redis主线程
  • 定期通过FT.INFO命令查看索引状态,监控索引构建进度、内存占用、检索延迟等指标

七、高频坑点与避坑指南

  1. 字节序错误导致检索完全失效

    • 问题:Java默认使用大端序存储浮点数,而Redis向量要求使用小端序的字节数组,字节序错误会导致向量完全无法匹配
    • 解决方案:必须通过ByteBuffer设置小端序进行向量与字节数组的转换,本文提供的VectorUtils工具类已解决该问题
  2. 向量未归一化导致余弦相似度检索结果异常

    • 问题:使用余弦相似度算法时,向量模长不一致会导致相似度计算结果不符合预期
    • 解决方案:向量写入与查询前,统一进行L2归一化处理,保证所有向量的模长为1
  3. 混合检索过滤条件位置错误导致性能极差

    • 问题:将过滤条件写在=>符号之后,会先执行全量向量检索,再对结果进行过滤,数据量大时延迟极高
    • 解决方案:必须将过滤条件写在=>符号之前,实现先过滤缩小数据集,再执行向量检索
  4. 索引创建顺序错误导致数据无法被检索

    • 问题:先写入向量数据,再创建索引,已写入的数据不会被索引构建,导致检索不到数据
    • 解决方案:严格遵循先创建索引,再写入数据的顺序;若已先写入数据,可删除索引后重新创建,重建索引会扫描全量数据
  5. HNSW索引频繁更新导致召回率下降

    • 问题:HNSW索引针对高频更新的场景,会出现图结构质量下降,导致召回率持续降低
    • 解决方案:高频更新场景优先使用FLAT索引;若必须使用HNSW索引,建议在低峰期定期重建索引,保证索引质量
  6. 向量维度不匹配导致写入失败

    • 问题:写入的向量维度与索引定义的维度不一致,导致向量无法被索引识别,检索不到该数据
    • 解决方案:写入前严格校验向量维度,确保与索引定义的维度完全一致
  7. Redis内存淘汰导致向量数据丢失

    • 问题:开启了allkeys-lru等内存淘汰策略,当内存满时,向量数据会被淘汰,导致索引失效
    • 解决方案:向量数据所在的Redis实例,必须将maxmemory-policy设置为noeviction,禁止内存淘汰;若需缓存与向量混用,建议分开部署实例

总结

Redis向量数据库,为Java开发者提供了一个低成本、高可用、易落地的向量检索方案。它无需额外部署新组件,完全复用现有Redis技术栈,即可快速实现企业级的向量检索能力,完美适配RAG、智能推荐、用户画像匹配等大模型时代的核心业务场景。

对于中小规模数据量、快速迭代的业务,Redis向量数据库是最优选择;对于超大规模数据、复杂检索场景,也可作为轻量级的验证方案,快速完成业务POC验证。