# JRediSearch 独立客户端集成指南
## 📋 概述
JRediSearch 是 RediSearch 的官方 Java 客户端,提供了完整的全文搜索功能。本文档详细介绍如何在智柴图项目中集成和使用 JRediSearch 独立客户端。
## 🔧 环境准备
### 1. Redis 服务器配置
确保 Redis 服务器已安装并启用了 RediSearch 模块:
```bash
# 检查 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 依赖:
```xml
<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. 配置类
```java
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. 索引创建服务
```java
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. 消息搜索服务
```java
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. 搜索控制器
```java
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. 单元测试
```java
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. 集成测试
```java
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. 连接池优化
```java
@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. 性能监控
```java
@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. 内存管理
```java
// 设置索引最大内存
FT.CONFIG SET MAXMEMORY_POLICY allkeys-lru
// 设置最大扩展数
FT.CONFIG SET MAX_EXPANSIONS 100
// 设置最小音译匹配长度
FT.CONFIG SET MIN_PHONETIC_MATCH_LEN 3
```
### 3. 错误处理
```java
@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](./REDISEARCH_FEATURES_ANALYSIS.md)