静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
✨步子哥 @steper · 2025-11-07 04:56

JRediSearch 独立客户端集成指南

📋 概述

JRediSearch 是 RediSearch 的官方 Java 客户端,提供了完整的全文搜索功能。本文档详细介绍如何在智柴图项目中集成和使用 JRediSearch 独立客户端。

🔧 环境准备

1. Redis 服务器配置

确保 Redis 服务器已安装并启用了 RediSearch 模块:

# 检查 RediSearch 是否已安装
redis-cli MODULE LIST | grep search

# 如果未安装,安装 RediSearch
# Docker 方式
docker run -p 6379:6379 redislabs/redisearch:latest

# 或编译安装
git clone https://github.com/RediSearch/RediSearch.git
cd RediSearch
make setup
make build

2. 项目依赖配置

pom.xml 中添加 JRediSearch 依赖:

<dependencies>
    <!-- JRediSearch 核心依赖 -->
    <dependency>
        <groupId>com.redislabs</groupId>
        <artifactId>jredistimeseries</artifactId>
        <version>2.4.0</version>
    </dependency>
    
    <!-- JRediSearch 搜索模块 -->
    <dependency>
        <groupId>com.redislabs</groupId>
        <artifactId>jredisearch</artifactId>
        <version>2.4.0</version>
    </dependency>
    
    <!-- Jedis 连接池(JRediSearch 依赖) -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.3.1</version>
    </dependency>
</dependencies>

🚀 基础集成

1. 配置类

package com.zhichai.backend.config;

import io.redisearch.Client;
import io.redisearch.SearchResult;
import io.redisearch.Query;
import io.redisearch.Schema;
import io.redisearch.Document;
import io.redisearch.AggregationResult;
import io.redisearch.AggregationBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.HashMap;
import java.util.Map;

/**
 * RediSearch 配置类
 */
@Configuration
public class RediSearchConfig {
    
    @Value("${redis.host:localhost}")
    private String redisHost;
    
    @Value("${redis.port:6379}")
    private int redisPort;
    
    @Value("${redis.password:}")
    private String redisPassword;
    
    @Value("${redis.database:0}")
    private int redisDatabase;
    
    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);
        poolConfig.setTestOnBorrow(true);
        
        if (redisPassword.isEmpty()) {
            return new JedisPool(poolConfig, redisHost, redisPort, 2000, null, redisDatabase);
        } else {
            return new JedisPool(poolConfig, redisHost, redisPort, 2000, redisPassword, redisDatabase);
        }
    }
    
    @Bean
    public Client messageSearchClient(JedisPool jedisPool) {
        // 创建消息搜索索引客户端
        return new Client("message-index", jedisPool);
    }
    
    @Bean
    public Client userSearchClient(JedisPool jedisPool) {
        // 创建用户搜索索引客户端
        return new Client("user-index", jedisPool);
    }
}

2. 索引创建服务

package com.zhichai.backend.service;

import io.redisearch.Client;
import io.redisearch.Schema;
import io.redisearch.Schema.TextField;
import io.redisearch.Schema.NumericField;
import io.redisearch.Schema.TagField;
import io.redisearch.IndexOptions;
import io.redisearch.Client.IndexOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisPool;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

/**
 * RediSearch 索引管理服务
 */
@Service
public class RediSearchIndexService {
    
    @Autowired
    @Qualifier("messageSearchClient")
    private Client messageSearchClient;
    
    @Autowired
    @Qualifier("userSearchClient")
    private Client userSearchClient;
    
    @Autowired
    private JedisPool jedisPool;
    
    /**
     * 初始化搜索索引
     */
    @PostConstruct
    public void initializeIndexes() {
        try {
            createMessageIndex();
            createUserIndex();
            System.out.println("RediSearch 索引初始化完成");
        } catch (Exception e) {
            System.err.println("RediSearch 索引初始化失败: " + e.getMessage());
        }
    }
    
    /**
     * 创建消息搜索索引
     */
    private void createMessageIndex() {
        // 定义消息索引字段
        Schema schema = new Schema()
            .addTextField("content", 5.0)              // 消息内容,权重5.0
            .addTextField("authorName", 2.0)          // 作者名称,权重2.0
            .addNumericField("messageId")               // 消息ID
            .addNumericField("channelId")               // 频道ID
            .addNumericField("serverId")                // 服务器ID
            .addNumericField("authorId")                // 作者ID
            .addNumericField("createdAt")               // 创建时间
            .addTagField("messageType")                 // 消息类型标签
            .addTagField("isPinned")                    // 是否置顶标签
            .addTagField("isEdited")                    // 是否编辑标签
            .addTagField("language");                   // 语言标签
        
        // 设置索引选项
        IndexOptions options = IndexOptions.defaultOptions()
            .setNoOffsets(false)                        // 保存偏移量
            .setNoFields(false)                         // 保存字段
            .setStopwords("")                           // 不使用停用词
            .setLanguage("chinese");                     // 设置为中文分词
        
        try (var jedis = jedisPool.getResource()) {
            // 检查索引是否已存在
            boolean exists = jedis.exists("ft:message-index");
            if (exists) {
                // 删除旧索引
                messageSearchClient.dropIndex();
            }
            
            // 创建新索引
            messageSearchClient.createIndex(schema, options);
            System.out.println("消息搜索索引创建成功");
        }
    }
    
    /**
     * 创建用户搜索索引
     */
    private void createUserIndex() {
        Schema schema = new Schema()
            .addTextField("username", 5.0)              // 用户名,权重5.0
            .addTextField("nickname", 4.0)              // 昵称,权重4.0
            .addTextField("email", 2.0)                 // 邮箱,权重2.0
            .addNumericField("userId")                  // 用户ID
            .addNumericField("createdAt")               // 创建时间
            .addTagField("role")                        // 角色标签
            .addTagField("isOnline")                    // 在线状态标签
            .addTagField("language");                   // 语言标签
        
        IndexOptions options = IndexOptions.defaultOptions()
            .setLanguage("chinese");
        
        try (var jedis = jedisPool.getResource()) {
            boolean exists = jedis.exists("ft:user-index");
            if (exists) {
                userSearchClient.dropIndex();
            }
            
            userSearchClient.createIndex(schema, options);
            System.out.println("用户搜索索引创建成功");
        }
    }
}

🔍 搜索服务实现

1. 消息搜索服务

package com.zhichai.backend.service;

import io.redisearch.Client;
import io.redisearch.Query;
import io.redisearch.SearchResult;
import io.redisearch.Document;
import io.redisearch.AggregationResult;
import io.redisearch.AggregationBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.zhichai.backend.model.MessageNode;

import java.util.*;
import java.util.stream.Collectors;

/**
 * RediSearch 消息搜索服务
 */
@Service
public class RediSearchMessageService {
    
    @Autowired
    @Qualifier("messageSearchClient")
    private Client messageSearchClient;
    
    /**
     * 基础文本搜索
     */
    public List<Map<String, Object>> searchMessages(String keyword, int limit) {
        // 创建查询对象
        Query query = new Query(keyword)
            .setNoContent(false)                       // 返回内容
            .setWithScores(true)                        // 返回评分
            .setWithPayload(true)                       // 返回载荷
            .limit(0, limit);                          // 分页限制
        
        // 执行搜索
        SearchResult result = messageSearchClient.search(query);
        
        // 转换结果
        return result.docs.stream()
            .map(this::convertDocumentToMap)
            .collect(Collectors.toList());
    }
    
    /**
     * 高级搜索(带过滤条件)
     */
    public List<Map<String, Object>> searchMessagesAdvanced(
            String keyword, 
            Long channelId, 
            Long serverId, 
            String messageType,
            int limit) {
        
        // 构建查询字符串
        StringBuilder queryString = new StringBuilder();
        
        // 主搜索关键词
        if (keyword != null && !keyword.trim().isEmpty()) {
            queryString.append("@content:(").append(keyword).append(")");
        }
        
        // 频道过滤
        if (channelId != null) {
            if (queryString.length() > 0) queryString.append(" ");
            queryString.append("@channelId:[").append(channelId).append(" ").append(channelId).append("]");
        }
        
        // 服务器过滤
        if (serverId != null) {
            if (queryString.length() > 0) queryString.append(" ");
            queryString.append("@serverId:[").append(serverId).append(" ").append(serverId).append("]");
        }
        
        // 消息类型过滤
        if (messageType != null && !messageType.trim().isEmpty()) {
            if (queryString.length() > 0) queryString.append(" ");
            queryString.append("@messageType:{").append(messageType).append("}");
        }
        
        // 如果没有查询条件,返回空结果
        if (queryString.length() == 0) {
            return new ArrayList<>();
        }
        
        Query query = new Query(queryString.toString())
            .setNoContent(false)
            .setWithScores(true)
            .limit(0, limit);
        
        SearchResult result = messageSearchClient.search(query);
        
        return result.docs.stream()
            .map(this::convertDocumentToMap)
            .collect(Collectors.toList());
    }
    
    /**
     * 模糊搜索
     */
    public List<Map<String, Object>> fuzzySearch(String keyword, int limit) {
        // 使用模糊搜索语法
        String fuzzyQuery = "%" + keyword + "%";
        
        Query query = new Query(fuzzyQuery)
            .setNoContent(false)
            .setWithScores(true)
            .limit(0, limit);
        
        SearchResult result = messageSearchClient.search(query);
        
        return result.docs.stream()
            .map(this::convertDocumentToMap)
            .collect(Collectors.toList());
    }
    
    /**
     * 聚合搜索统计
     */
    public Map<String, Object> searchAggregation(String keyword, Long channelId) {
        AggregationBuilder builder = new AggregationBuilder()
            .groupBy("@channelId", 
                RediSearch reducers.count().as("messageCount"),
                RediSearch reducers.avg("@createdAt").as("avgTime"))
            .apply(RediSearch reducers.toString("@content").as("snippet"));
        
        // 添加过滤条件
        if (keyword != null && !keyword.trim().isEmpty()) {
            builder.filter("@content:(" + keyword + ")");
        }
        
        if (channelId != null) {
            builder.filter("@channelId:[" + channelId + " " + channelId + "]");
        }
        
        AggregationResult result = messageSearchClient.aggregate(builder);
        
        Map<String, Object> aggregationData = new HashMap<>();
        aggregationData.put("totalResults", result.totalResults);
        aggregationData.put("rows", result.rows);
        
        return aggregationData;
    }
    
    /**
     * 索引单个消息
     */
    public void indexMessage(MessageNode message) {
        Map<String, Object> fields = new HashMap<>();
        fields.put("content", message.getContent());
        fields.put("messageId", message.getMessageId());
        fields.put("channelId", message.getChannelId());
        fields.put("serverId", message.getServerId());
        fields.put("authorId", message.getAuthorId());
        fields.put("createdAt", message.getCreatedAt().getTime());
        fields.put("messageType", message.getMessageType());
        fields.put("isPinned", message.isPinned() ? "true" : "false");
        fields.put("isEdited", message.isEdited() ? "true" : "false");
        fields.put("language", "chinese");
        
        // 添加文档到索引
        String docId = "msg:" + message.getMessageId();
        messageSearchClient.addDocument(docId, 1.0, fields);
    }
    
    /**
     * 批量索引消息
     */
    public void indexMessages(List<MessageNode> messages) {
        for (MessageNode message : messages) {
            indexMessage(message);
        }
    }
    
    /**
     * 删除消息索引
     */
    public void deleteMessageIndex(Long messageId) {
        String docId = "msg:" + messageId;
        messageSearchClient.deleteDocument(docId);
    }
    
    /**
     * 更新消息索引
     */
    public void updateMessageIndex(MessageNode message) {
        // 删除旧索引
        deleteMessageIndex(message.getMessageId());
        // 添加新索引
        indexMessage(message);
    }
    
    /**
     * 转换文档为Map
     */
    private Map<String, Object> convertDocumentToMap(Document doc) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", doc.getId());
        result.put("score", doc.getScore());
        result.put("payload", doc.getPayload());
        
        // 添加所有字段
        for (Map.Entry<String, Object> entry : doc.getProperties().entrySet()) {
            result.put(entry.getKey(), entry.getValue());
        }
        
        return result;
    }
}

2. 搜索控制器

package com.zhichai.backend.controller;

import com.zhichai.backend.service.RediSearchMessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * RediSearch 搜索控制器
 */
@RestController
@RequestMapping("/api/search")
public class RediSearchController {
    
    @Autowired
    private RediSearchMessageService searchService;
    
    /**
     * 基础搜索
     */
    @GetMapping("/messages")
    public List<Map<String, Object>> searchMessages(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "10") int limit) {
        return searchService.searchMessages(keyword, limit);
    }
    
    /**
     * 高级搜索
     */
    @GetMapping("/messages/advanced")
    public List<Map<String, Object>> searchMessagesAdvanced(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) Long channelId,
            @RequestParam(required = false) Long serverId,
            @RequestParam(required = false) String messageType,
            @RequestParam(defaultValue = "10") int limit) {
        return searchService.searchMessagesAdvanced(keyword, channelId, serverId, messageType, limit);
    }
    
    /**
     * 模糊搜索
     */
    @GetMapping("/messages/fuzzy")
    public List<Map<String, Object>> fuzzySearch(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "10") int limit) {
        return searchService.fuzzySearch(keyword, limit);
    }
    
    /**
     * 搜索统计
     */
    @GetMapping("/messages/aggregation")
    public Map<String, Object> searchAggregation(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) Long channelId) {
        return searchService.searchAggregation(keyword, channelId);
    }
}

🧪 测试示例

1. 单元测试

package com.zhichai.backend.service;

import io.redisearch.Client;
import io.redisearch.SearchResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@TestPropertySource(properties = {
    "redis.host=localhost",
    "redis.port=6379"
})
public class RediSearchMessageServiceTest {
    
    @Autowired
    private RediSearchMessageService searchService;
    
    @Autowired
    private Client messageSearchClient;
    
    @BeforeEach
    void setUp() {
        // 清理测试数据
        try {
            messageSearchClient.dropIndex();
        } catch (Exception e) {
            // 索引不存在,忽略
        }
    }
    
    @Test
    void testBasicSearch() {
        // 准备测试数据
        MessageNode message = new MessageNode();
        message.setMessageId(1L);
        message.setContent("今天天气真好");
        message.setChannelId(100L);
        message.setServerId(10L);
        message.setAuthorId(1000L);
        
        // 索引消息
        searchService.indexMessage(message);
        
        // 搜索测试
        List<Map<String, Object>> results = searchService.searchMessages("天气", 10);
        
        // 验证结果
        assertFalse(results.isEmpty());
        assertTrue(results.get(0).get("content").toString().contains("天气"));
    }
    
    @Test
    void testFuzzySearch() {
        // 准备测试数据
        MessageNode message = new MessageNode();
        message.setMessageId(2L);
        message.setContent("智能手机产品");
        message.setChannelId(100L);
        message.setServerId(10L);
        message.setAuthorId(1000L);
        
        searchService.indexMessage(message);
        
        // 模糊搜索测试
        List<Map<String, Object>> results = searchService.fuzzySearch("手机", 10);
        
        // 验证结果
        assertFalse(results.isEmpty());
    }
}

2. 集成测试

package com.zhichai.backend.integration;

import com.zhichai.backend.service.RediSearchMessageService;
import com.zhichai.backend.model.MessageNode;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@SpringBootTest
public class RediSearchIntegrationTest {
    
    @Autowired
    private RediSearchMessageService searchService;
    
    @Test
    void testCompleteWorkflow() {
        // 1. 创建测试消息
        MessageNode message = createTestMessage();
        
        // 2. 索引消息
        searchService.indexMessage(message);
        
        // 3. 搜索消息
        List<Map<String, Object>> results = searchService.searchMessagesAdvanced(
            "测试", message.getChannelId(), message.getServerId(), "TEXT", 10
        );
        
        // 4. 验证结果
        assertFalse(results.isEmpty());
        
        // 5. 更新消息
        message.setContent("更新后的测试消息");
        searchService.updateMessageIndex(message);
        
        // 6. 再次搜索
        results = searchService.searchMessages("更新后", 10);
        assertFalse(results.isEmpty());
        
        // 7. 删除索引
        searchService.deleteMessageIndex(message.getMessageId());
        
        // 8. 验证删除
        results = searchService.searchMessages("更新后", 10);
        assertTrue(results.isEmpty());
    }
    
    private MessageNode createTestMessage() {
        MessageNode message = new MessageNode();
        message.setMessageId(System.currentTimeMillis());
        message.setContent("这是一条测试消息");
        message.setChannelId(100L);
        message.setServerId(10L);
        message.setAuthorId(1000L);
        message.setMessageType("TEXT");
        message.setCreatedAt(LocalDateTime.now());
        return message;
    }
}

🔧 高级配置

1. 连接池优化

@Configuration
public class RediSearchAdvancedConfig {
    
    @Bean
    public JedisPool optimizedJedisPool() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        
        // 连接池大小配置
        poolConfig.setMaxTotal(50);                    // 最大连接数
        poolConfig.setMaxIdle(20);                      // 最大空闲连接
        poolConfig.setMinIdle(10);                      // 最小空闲连接
        
        // 连接验证配置
        poolConfig.setTestOnBorrow(true);               // 借用时验证
        poolConfig.setTestOnReturn(true);               // 归还时验证
        poolConfig.setTestWhileIdle(true);              // 空闲时验证
        poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 30秒检查一次
        
        // 超时配置
        poolConfig.setMaxWaitMillis(5000);              // 最大等待时间5秒
        poolConfig.setMinEvictableIdleTimeMillis(60000); // 最小空闲时间1分钟
        
        return new JedisPool(poolConfig, "localhost", 6379, 5000);
    }
}

2. 性能监控

@Component
public class RediSearchMetrics {
    private final MeterRegistry meterRegistry;
    private final Timer searchTimer;
    private final Counter searchCounter;
    
    public RediSearchMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.searchTimer = Timer.builder("redisearch.search.duration")
            .description("RediSearch search duration")
            .register(meterRegistry);
        this.searchCounter = Counter.builder("redisearch.search.count")
            .description("RediSearch search count")
            .register(meterRegistry);
    }
    
    public <T> T recordSearch(String operation, Supplier<T> searchFunction) {
        searchCounter.increment(Tags.of("operation", operation));
        return Timer.Sample.start(meterRegistry)
            .stop(searchTimer)
            .recordCallable(() -> searchFunction.get());
    }
}

🚨 注意事项

1. 版本兼容性

  • Redis 版本: 需要 Redis 6.0+
  • RediSearch 版本: 建议 2.0+
  • JRediSearch 版本: 建议 2.4.0+

2. 内存管理

// 设置索引最大内存
FT.CONFIG SET MAXMEMORY_POLICY allkeys-lru

// 设置最大扩展数
FT.CONFIG SET MAX_EXPANSIONS 100

// 设置最小音译匹配长度
FT.CONFIG SET MIN_PHONETIC_MATCH_LEN 3

3. 错误处理

@Service
public class RediSearchService {
    
    public List<Map<String, Object>> safeSearch(String keyword) {
        try {
            return searchMessages(keyword, 10);
        } catch (io.redisearch.exceptions.RedisearchException e) {
            log.error("RediSearch 查询失败: {}", e.getMessage());
            // 降级到基础搜索
            return fallbackSearch(keyword);
        } catch (Exception e) {
            log.error("搜索服务异常: {}", e.getMessage());
            return Collections.emptyList();
        }
    }
    
    private List<Map<String, Object>> fallbackSearch(String keyword) {
        // 降级到当前项目的 Redis 原生搜索
        return messageSearchWriter.searchMessages(keyword, null, null, 10);
    }
}

📈 性能优化建议

1. 索引优化: 合理设置字段权重 2. 查询优化: 使用精确的查询语法 3. 连接池优化: 合理配置连接池参数 4. 监控告警: 建立性能监控和告警机制 5. 降级策略: 准备降级方案应对服务异常

---

文档版本: v1.0 创建时间: 2025-11-07 作者: 系统分析 相关文档: REDISEARCH_FEATURES_ANALYSIS.md

👍 1❤️ 1🚀 1✅ 1