案例专家:增加手动刷新索引功能

This commit is contained in:
liu.zixi
2025-10-11 12:55:25 +08:00
parent bad129a0a1
commit 16b2d90417
5 changed files with 258 additions and 166 deletions

View File

@@ -1,22 +1,12 @@
package com.xboe.config;
import com.xboe.module.boecase.service.IElasticSearchIndexService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* ElasticSearch索引初始化器
* 在Spring Boot启动完成并监听到配置文件加载完毕后检查并创建所需的ES索引
@@ -27,8 +17,8 @@ import java.io.IOException;
@Component
public class ElasticSearchIndexInitializer {
@Autowired(required = false)
private RestHighLevelClient elasticsearchClient;
@Autowired
private IElasticSearchIndexService elasticSearchIndexService;
/**
* 监听Spring Boot应用启动完成事件
@@ -36,158 +26,12 @@ public class ElasticSearchIndexInitializer {
*/
@EventListener(ApplicationReadyEvent.class)
public void initializeElasticSearchIndices() {
if (elasticsearchClient == null) {
log.warn("ElasticSearch客户端未配置跳过索引初始化");
return;
}
log.info("开始检查和初始化ElasticSearch索引...");
try {
// 检查并创建ai_chat_messages索引
checkAndCreateIndex("ai_chat_messages");
log.info("ElasticSearch索引初始化完成");
} catch (Exception e) {
log.error("ElasticSearch索引初始化失败", e);
}
}
/**
* 检查索引是否存在,如果不存在则创建
*
* @param indexName 索引名称
* @throws IOException IO异常
*/
private void checkAndCreateIndex(String indexName) throws IOException {
// 检查索引是否存在
GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
boolean exists = elasticsearchClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
if (exists) {
log.info("ElasticSearch索引 [{}] 已存在", indexName);
return;
}
log.info("ElasticSearch索引 [{}] 不存在,开始创建...", indexName);
// 创建索引
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
// 设置索引配置
createIndexRequest.settings(Settings.builder()
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put("index.analysis.analyzer.ik_max_word.tokenizer", "ik_max_word")
.put("index.analysis.analyzer.ik_smart.tokenizer", "ik_smart")
);
// 设置字段映射
String mapping = getAiChatMessagesMapping();
createIndexRequest.mapping(mapping, XContentType.JSON);
// 执行创建索引请求
CreateIndexResponse createIndexResponse = elasticsearchClient.indices()
.create(createIndexRequest, RequestOptions.DEFAULT);
if (createIndexResponse.isAcknowledged()) {
log.info("ElasticSearch索引 [{}] 创建成功", indexName);
String indexName = "ai_chat_messages";
if (elasticSearchIndexService.checkIndexExists(indexName)) {
log.info("ElasticSearch索引 ai_chat_messages 已存在");
} else {
log.warn("ElasticSearch索引 [{}] 创建可能失败,响应未确认", indexName);
log.info("ElasticSearch索引 ai_chat_messages 不存在,开始创建...");
elasticSearchIndexService.createIndex(indexName);
}
}
/**
* 获取ai_chat_messages索引的字段映射配置
* 根据项目中的会话消息数据结构规范定义映射
*
* @return JSON格式的映射配置
*/
private String getAiChatMessagesMapping() {
return "{\n" +
" \"properties\": {\n" +
" \"conversationId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"messageId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"messageType\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"query\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\",\n" +
" \"fields\": {\n" +
" \"keyword\": {\n" +
" \"type\": \"keyword\",\n" +
" \"ignore_above\": 256\n" +
" }\n" +
" }\n" +
" },\n" +
" \"answer\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"caseRefer\": {\n" +
" \"type\": \"nested\",\n" +
" \"properties\": {\n" +
" \"caseId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"title\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"authorName\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"keywords\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"content\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" },\n" +
" \"suggestions\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"userId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"userName\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"timestamp\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" },\n" +
" \"createTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" },\n" +
" \"updateTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" }\n" +
" }\n" +
"}";
}
}

View File

@@ -5,6 +5,7 @@ import com.xboe.core.JsonResponse;
import com.xboe.module.boecase.dto.CaseAiChatDto;
import com.xboe.module.boecase.service.ICaseAiChatService;
import com.xboe.module.boecase.service.ICaseAiPermissionService;
import com.xboe.module.boecase.service.IElasticSearchIndexService;
import com.xboe.module.boecase.vo.CaseAiMessageVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +37,9 @@ public class CaseAiChatApi extends ApiBaseController {
@Autowired
private ICaseAiPermissionService caseAiPermissionService;
@Autowired
private IElasticSearchIndexService elasticSearchIndexService;
/**
* 聊天
* @param caseAiChatDto
@@ -83,4 +87,23 @@ public class CaseAiChatApi extends ApiBaseController {
return error("判断失败", e.getMessage());
}
}
/**
* 手动刷新索引
* @return
*/
@PostMapping("/index/refresh")
public JsonResponse<String> deleteAndCreateEsIndex() {
String indexName = "ai_chat_messages";
if (elasticSearchIndexService.checkIndexExists(indexName)) {
boolean deleteResult = elasticSearchIndexService.deleteIndex(indexName);
if (deleteResult) {
elasticSearchIndexService.createIndex(indexName);
return success("刷新成功");
}
} else {
elasticSearchIndexService.createIndex(indexName);
}
return error("刷新失败");
}
}

View File

@@ -0,0 +1,27 @@
package com.xboe.module.boecase.service;
/**
* es索引
*/
public interface IElasticSearchIndexService {
/**
* 查看索引是否存在
* @param indexName
* @return
*/
boolean checkIndexExists(String indexName);
/**
* 创建索引
* @param indexName
*/
boolean createIndex(String indexName);
/**
* 删除索引
* @param indexName
* @return
*/
boolean deleteIndex(String indexName);
}

View File

@@ -217,8 +217,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
// 8. 执行HTTP请求
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(600, TimeUnit.SECONDS)
.readTimeout(600, TimeUnit.SECONDS)
.build();
EventSource.Factory factory = EventSources.createFactory(client);
factory.newEventSource(request, listener);

View File

@@ -0,0 +1,198 @@
package com.xboe.module.boecase.service.impl;
import com.xboe.module.boecase.service.IElasticSearchIndexService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
@Slf4j
public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService {
@Autowired(required = false)
private RestHighLevelClient elasticsearchClient;
@Override
public boolean checkIndexExists(String indexName) {
if (elasticsearchClient == null) {
log.warn("ElasticSearch客户端未配置");
return false;
}
// 检查索引是否存在
GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
try {
return elasticsearchClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("查询ElasticSearch索引时发生异常", e);
return false;
}
}
@Override
public boolean createIndex(String indexName) {
if (elasticsearchClient == null) {
log.warn("ElasticSearch客户端未配置");
return false;
}
// 创建索引
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
// 设置索引配置
createIndexRequest.settings(Settings.builder()
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 0)
.put("index.analysis.analyzer.ik_max_word.tokenizer", "ik_max_word")
.put("index.analysis.analyzer.ik_smart.tokenizer", "ik_smart")
);
// 设置字段映射
String mapping = getAiChatMessagesMapping();
createIndexRequest.mapping(mapping, XContentType.JSON);
// 执行创建索引请求
CreateIndexResponse createIndexResponse = null;
try {
createIndexResponse = elasticsearchClient.indices()
.create(createIndexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("创建ElasticSearch索引时发生异常", e);
return false;
}
if (createIndexResponse.isAcknowledged()) {
log.info("ElasticSearch索引 [{}] 创建成功", indexName);
return true;
} else {
log.warn("ElasticSearch索引 [{}] 创建可能失败,响应未确认", indexName);
return false;
}
}
@Override
public boolean deleteIndex(String indexName) {
if (elasticsearchClient == null) {
log.warn("ElasticSearch客户端未配置");
return false;
}
// 执行删除索引请求
DeleteIndexRequest deleteRequest = new DeleteIndexRequest(indexName);
try {
AcknowledgedResponse deleteResponse = elasticsearchClient.indices().delete(deleteRequest, RequestOptions.DEFAULT);
if (deleteResponse.isAcknowledged()) {
log.info("成功删除Elasticsearch索引: {}", indexName);
return true;
} else {
log.warn("删除索引 [{}] 未被确认(可能部分节点未响应)", indexName);
return false;
}
} catch (IOException e) {
log.error("删除ElasticSearch索引时发生异常", e);
return false;
}
}
/**
* 获取ai_chat_messages索引的字段映射配置
* 根据项目中的会话消息数据结构规范定义映射
*
* @return JSON格式的映射配置
*/
private String getAiChatMessagesMapping() {
return "{\n" +
" \"properties\": {\n" +
" \"conversationId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"messageId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"messageType\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"query\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\",\n" +
" \"fields\": {\n" +
" \"keyword\": {\n" +
" \"type\": \"keyword\",\n" +
" \"ignore_above\": 256\n" +
" }\n" +
" }\n" +
" },\n" +
" \"answer\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"caseRefer\": {\n" +
" \"type\": \"nested\",\n" +
" \"properties\": {\n" +
" \"caseId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"title\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"authorName\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"keywords\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"content\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" },\n" +
" \"suggestions\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"userId\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"userName\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true\n" +
" },\n" +
" \"timestamp\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" },\n" +
" \"createTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" },\n" +
" \"updateTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" }\n" +
" }\n" +
"}";
}
}