params);
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java
deleted file mode 100644
index 04d6f4be..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import dev.langchain4j.model.chat.StreamingChatModel;
-import dev.langchain4j.model.chat.response.ChatResponse;
-import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
-import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
-import org.ruoyi.service.graph.IGraphLLMService;
-import org.springframework.stereotype.Service;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-
-/**
- * DeepSeek 图谱LLM服务实现
- * 支持 DeepSeek 系列模型
- *
- * 注意:使用 langchain4j 的 OpenAiStreamingChatModel,通过 CompletableFuture 转换为同步调用
- * 参考 DeepSeekChatImpl 的实现,但改为同步模式
- *
- * @author ruoyi
- * @date 2025-10-13
- */
-@Slf4j
-@Service
-public class DeepSeekGraphLLMServiceImpl implements IGraphLLMService {
-
- @Override
- public String extractGraph(String prompt, ChatModelVo chatModel) {
- log.info("DeepSeek模型调用: model={}, apiHost={}, 提示词长度={}",
- chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
-
- try {
- // 使用 langchain4j 的 OpenAiStreamingChatModel(参考 DeepSeekChatImpl)
- StreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()
- .baseUrl(chatModel.getApiHost())
- .apiKey(chatModel.getApiKey())
- .modelName(chatModel.getModelName())
- .temperature(0.8)
- .logRequests(false)
- .logResponses(false)
- .build();
-
- // 用于收集完整响应
- StringBuilder fullResponse = new StringBuilder();
- CompletableFuture responseFuture = new CompletableFuture<>();
-
- // 发送流式消息,但通过 CompletableFuture 转换为同步
- long startTime = System.currentTimeMillis();
- streamingModel.chat(prompt, new StreamingChatResponseHandler() {
- @Override
- public void onPartialResponse(String partialResponse) {
- fullResponse.append(partialResponse);
- }
-
- @Override
- public void onCompleteResponse(ChatResponse completeResponse) {
- long duration = System.currentTimeMillis() - startTime;
- String responseText = fullResponse.toString();
- log.info("DeepSeek模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
- responseFuture.complete(responseText);
- }
-
- @Override
- public void onError(Throwable error) {
- log.error("DeepSeek模型调用错误: {}", error.getMessage());
- responseFuture.completeExceptionally(error);
- }
- });
-
- // 同步等待结果(最多2分钟)
- return responseFuture.get(2, TimeUnit.MINUTES);
-
- } catch (Exception e) {
- log.error("DeepSeek模型调用失败: {}", e.getMessage(), e);
- throw new RuntimeException("DeepSeek模型调用失败: " + e.getMessage(), e);
- }
- }
-
- @Override
- public String getCategory() {
- return "deepseek"; // 对应 ChatModel 表中的 category 字段
- }
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java
deleted file mode 100644
index 8204570c..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import io.github.imfangs.dify.client.DifyClient;
-import io.github.imfangs.dify.client.DifyClientFactory;
-import io.github.imfangs.dify.client.callback.ChatStreamCallback;
-import io.github.imfangs.dify.client.enums.ResponseMode;
-import io.github.imfangs.dify.client.event.ErrorEvent;
-import io.github.imfangs.dify.client.event.MessageEndEvent;
-import io.github.imfangs.dify.client.event.MessageEvent;
-import io.github.imfangs.dify.client.model.DifyConfig;
-import io.github.imfangs.dify.client.model.chat.ChatMessage;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
-import org.ruoyi.service.graph.IGraphLLMService;
-import org.springframework.stereotype.Service;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Dify 图谱LLM服务实现
- * 支持 Dify 平台的对话模型
- *
- * 注意:Dify 使用流式调用,通过 CompletableFuture 实现同步等待
- *
- * @author ruoyi
- * @date 2025-10-11
- */
-@Slf4j
-@Service
-public class DifyGraphLLMServiceImpl implements IGraphLLMService {
-
- @Override
- public String extractGraph(String prompt, ChatModelVo chatModel) {
- log.info("Dify模型调用: model={}, apiHost={}, 提示词长度={}",
- chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
-
- try {
- // 创建 Dify 客户端配置
- DifyConfig config = DifyConfig.builder()
- .baseUrl(chatModel.getApiHost())
- .apiKey(chatModel.getApiKey())
- .connectTimeout(5000)
- .readTimeout(120000) // 2分钟超时
- .writeTimeout(30000)
- .build();
-
- DifyClient chatClient = DifyClientFactory.createClient(config);
-
- // 创建聊天消息(使用流式模式)
- ChatMessage message = ChatMessage.builder()
- .query(prompt)
- .user("graph-system") // 图谱系统用户
- .responseMode(ResponseMode.STREAMING) // 流式模式
- .build();
-
- // 用于收集完整响应
- StringBuilder fullResponse = new StringBuilder();
- CompletableFuture responseFuture = new CompletableFuture<>();
-
- // 发送流式消息
- long startTime = System.currentTimeMillis();
- chatClient.sendChatMessageStream(message, new ChatStreamCallback() {
- @Override
- public void onMessage(MessageEvent event) {
- fullResponse.append(event.getAnswer());
- }
-
- @Override
- public void onMessageEnd(MessageEndEvent event) {
- long duration = System.currentTimeMillis() - startTime;
- String responseText = fullResponse.toString();
- log.info("Dify模型响应成功: 耗时={}ms, 响应长度={}, messageId={}",
- duration, responseText.length(), event.getMessageId());
- responseFuture.complete(responseText);
- }
-
- @Override
- public void onError(ErrorEvent event) {
- log.error("Dify模型调用错误: {}", event.getMessage());
- responseFuture.completeExceptionally(
- new RuntimeException("Dify调用错误: " + event.getMessage())
- );
- }
-
- @Override
- public void onException(Throwable throwable) {
- log.error("Dify模型调用异常: {}", throwable.getMessage(), throwable);
- responseFuture.completeExceptionally(throwable);
- }
- });
-
- // 同步等待结果(最多2分钟)
- return responseFuture.get(2, TimeUnit.MINUTES);
-
- } catch (Exception e) {
- log.error("Dify模型调用失败: {}", e.getMessage(), e);
- throw new RuntimeException("Dify模型调用失败: " + e.getMessage(), e);
- }
- }
-
- @Override
- public String getCategory() {
- return "dify"; // 对应 ChatModel 表中的 category 字段
- }
-}
-
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphBuildTaskServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphBuildTaskServiceImpl.java
deleted file mode 100644
index 96b2ea05..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphBuildTaskServiceImpl.java
+++ /dev/null
@@ -1,743 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.core.util.StrUtil;
-import com.alibaba.fastjson.JSON;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.domain.bo.graph.GraphBuildTask;
-import org.ruoyi.domain.bo.graph.GraphInstance;
-import org.ruoyi.domain.bo.knowledge.KnowledgeAttachBo;
-import org.ruoyi.domain.bo.knowledge.KnowledgeFragmentBo;
-import org.ruoyi.domain.dto.GraphExtractionResult;
-import org.ruoyi.domain.vo.knowledge.KnowledgeAttachVo;
-import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo;
-import org.ruoyi.mapper.graph.GraphBuildTaskMapper;
-import org.ruoyi.service.graph.IGraphBuildTaskService;
-import org.ruoyi.service.graph.IGraphInstanceService;
-import org.ruoyi.service.graph.IGraphRAGService;
-import org.ruoyi.service.knowledge.IKnowledgeAttachService;
-import org.ruoyi.service.knowledge.IKnowledgeFragmentService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 图谱构建任务服务实现
- *
- * @author ruoyi
- * @date 2025-09-30
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
-public class GraphBuildTaskServiceImpl implements IGraphBuildTaskService {
-
- private final GraphBuildTaskMapper taskMapper;
-
- private final IGraphRAGService graphRAGService;
-
- private final IGraphInstanceService graphInstanceService;
-
- private final IKnowledgeFragmentService knowledgeFragmentService;
-
- private final IKnowledgeAttachService knowledgeAttachService;
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public GraphBuildTask createTask(String graphUuid, String knowledgeId, String docId, Integer taskType) {
- GraphBuildTask task = new GraphBuildTask();
- task.setTaskUuid(IdUtil.fastSimpleUUID());
- task.setGraphUuid(graphUuid);
- task.setKnowledgeId(knowledgeId);
- task.setDocId(docId);
-
- // 设置任务类型和状态(使用整数)
- task.setTaskType(taskType != null ? taskType : 1);
- task.setTaskStatus(1); // 1-待处理
- task.setProgress(0);
-
- taskMapper.insert(task);
-
- log.info("创建图谱构建任务: taskId={}, taskUuid={}, graphUuid={}, knowledgeId={}, type={}",
- task.getId(), task.getTaskUuid(), graphUuid, knowledgeId, task.getTaskType());
-
- return task;
- }
-
- @Override
- @Async("graphBuildExecutor") // ⭐ 启用异步执行,使用专用线程池
- public void startTask(String taskUuid) {
- // 记录线程信息
- String threadName = Thread.currentThread().getName();
- log.info("🚀 图谱构建任务启动 - taskUuid: {}, 线程: {}", taskUuid, threadName);
-
- long startTime = System.currentTimeMillis();
-
- try {
- // 1. 验证任务存在性
- GraphBuildTask task = getByUuid(taskUuid);
- if (task == null) {
- log.error("❌ 任务不存在: taskUuid={}", taskUuid);
- return;
- }
-
- // 2. 检查任务状态(防止重复执行)
- if (task.getTaskStatus() != 1) { // 1-待处理
- log.warn("⚠️ 任务状态不允许执行: taskUuid={}, currentStatus={}",
- taskUuid, task.getTaskStatus());
- return;
- }
-
- // 3. 更新任务状态为运行中
- boolean statusUpdated = updateStatus(taskUuid, 2); // 2-运行中
- if (!statusUpdated) {
- log.error("❌ 更新任务状态失败: taskUuid={}", taskUuid);
- return;
- }
-
- log.info("✅ 任务状态已更新为运行中: taskUuid={}", taskUuid);
-
- // 4. 执行图谱构建逻辑
- try {
- executeTaskLogic(task);
-
- long duration = (System.currentTimeMillis() - startTime) / 1000;
- log.info("🎉 图谱构建任务完成: taskUuid={}, 耗时: {}秒, 线程: {}",
- taskUuid, duration, threadName);
-
- } catch (OutOfMemoryError oom) {
- // 特殊处理OOM错误
- log.error("💥 图谱构建任务内存溢出: taskUuid={}, 线程: {}", taskUuid, threadName, oom);
- markFailed(taskUuid, "内存溢出,请减少批处理文档数量或增加JVM内存");
-
- // 建议垃圾回收
- System.gc();
-
- } catch (InterruptedException ie) {
- // 特殊处理中断异常
- Thread.currentThread().interrupt();
- log.error("⚠️ 图谱构建任务被中断: taskUuid={}, 线程: {}", taskUuid, threadName, ie);
- markFailed(taskUuid, "任务被中断: " + ie.getMessage());
-
- } catch (Exception e) {
- // 处理其他业务异常
- log.error("❌ 图谱构建任务执行失败: taskUuid={}, 线程: {}", taskUuid, threadName, e);
-
- // 提取简洁的错误信息
- String errorMsg = extractErrorMessage(e);
- markFailed(taskUuid, errorMsg);
- }
-
- } catch (Exception e) {
- // 处理外层异常(如数据库访问异常)
- log.error("❌ 图谱构建任务启动失败: taskUuid={}, 线程: {}", taskUuid, threadName, e);
-
- try {
- String errorMsg = extractErrorMessage(e);
- markFailed(taskUuid, errorMsg);
- } catch (Exception markFailEx) {
- log.error("❌ 标记任务失败时出错: taskUuid={}", taskUuid, markFailEx);
- }
- }
- }
-
- /**
- * 提取简洁的错误信息(用于前端显示)
- *
- * @param e 异常对象
- * @return 简洁的错误信息
- */
- private String extractErrorMessage(Exception e) {
- // 1. 优先使用自定义异常消息
- String message = e.getMessage();
- if (StrUtil.isNotBlank(message) && message.length() < 200) {
- return message;
- }
-
- // 2. 检查原因链
- Throwable cause = e.getCause();
- if (cause != null && StrUtil.isNotBlank(cause.getMessage())) {
- String causeMsg = cause.getMessage();
- if (causeMsg.length() < 200) {
- return causeMsg;
- }
- }
-
- // 3. 使用异常类名
- return e.getClass().getSimpleName() + ": " +
- (message != null ? message.substring(0, Math.min(150, message.length())) : "未知错误");
- }
-
- @Override
- public GraphBuildTask getByUuid(String taskUuid) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- return taskMapper.selectOne(wrapper);
- }
-
- @Override
- public List listByGraphUuid(String graphUuid) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphBuildTask::getGraphUuid, graphUuid);
- wrapper.orderByDesc(GraphBuildTask::getCreateTime);
- return taskMapper.selectList(wrapper);
- }
-
- @Override
- public GraphBuildTask getLatestTask(String graphUuid) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphBuildTask::getGraphUuid, graphUuid);
- wrapper.orderByDesc(GraphBuildTask::getCreateTime);
- wrapper.last("LIMIT 1");
- return taskMapper.selectOne(wrapper);
- }
-
- @Override
- public List listByKnowledgeId(String knowledgeId) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphBuildTask::getKnowledgeId, knowledgeId);
- wrapper.orderByDesc(GraphBuildTask::getCreateTime);
- return taskMapper.selectList(wrapper);
- }
-
- @Override
- public List getPendingAndRunningTasks() {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.in(GraphBuildTask::getTaskStatus, 1, 2); // 1-待处理, 2-运行中
- wrapper.orderByAsc(GraphBuildTask::getCreateTime);
- return taskMapper.selectList(wrapper);
- }
-
- @Override
- public boolean updateProgress(String taskUuid, Integer progress, Integer processedDocs) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- wrapper.set(GraphBuildTask::getProgress, progress);
-
- if (processedDocs != null) {
- wrapper.set(GraphBuildTask::getProcessedDocs, processedDocs);
- }
-
- int rows = taskMapper.update(null, wrapper);
- log.info("📊 更新任务进度: taskUuid={}, progress={}%, processedDocs={}, rows={}",
- taskUuid, progress, processedDocs, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新任务进度失败: taskUuid={}, progress={}", taskUuid, progress, e);
- return false;
- }
- }
-
- @Override
- public boolean updateStatus(String taskUuid, Integer status) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- wrapper.set(GraphBuildTask::getTaskStatus, status);
-
- // 如果是开始运行,设置开始时间
- if (status == 2) {
- wrapper.set(GraphBuildTask::getStartTime, new Date());
- }
-
- // 如果是完成或失败,设置结束时间
- if (status == 3 || status == 4) {
- wrapper.set(GraphBuildTask::getEndTime, new Date());
- }
-
- int rows = taskMapper.update(null, wrapper);
-
- log.info("更新任务状态: taskUuid={}, status={}, rows={}", taskUuid, status, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新任务状态失败: taskUuid={}, status={}", taskUuid, status, e);
- return false;
- }
- }
-
- @Override
- public boolean updateExtractionStats(String taskUuid, Integer extractedEntities, Integer extractedRelations) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
-
- if (extractedEntities != null) {
- wrapper.set(GraphBuildTask::getExtractedEntities, extractedEntities);
- }
- if (extractedRelations != null) {
- wrapper.set(GraphBuildTask::getExtractedRelations, extractedRelations);
- }
-
- int rows = taskMapper.update(null, wrapper);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新提取统计失败: taskUuid={}", taskUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean markSuccess(String taskUuid, String resultSummary) {
- try {
- // 1. 更新任务状态
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- wrapper.set(GraphBuildTask::getTaskStatus, 3); // 3-已完成
- wrapper.set(GraphBuildTask::getProgress, 100);
- wrapper.set(GraphBuildTask::getEndTime, new Date());
- wrapper.set(GraphBuildTask::getResultSummary, resultSummary);
-
- int rows = taskMapper.update(null, wrapper);
-
- // 2. 更新图谱实例状态为"已完成"
- GraphBuildTask task = getByUuid(taskUuid);
- if (task != null && task.getGraphUuid() != null) {
- graphInstanceService.updateStatus(task.getGraphUuid(), 20); // 20-已完成
- log.info("更新图谱实例状态为已完成: graphUuid={}", task.getGraphUuid());
- }
-
- log.info("标记任务成功: taskUuid={}, rows={}", taskUuid, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("标记任务成功失败: taskUuid={}", taskUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean markFailed(String taskUuid, String errorMessage) {
- try {
- // 1. 更新任务状态
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- wrapper.set(GraphBuildTask::getTaskStatus, 4); // 4-失败
- wrapper.set(GraphBuildTask::getErrorMessage, errorMessage);
- wrapper.set(GraphBuildTask::getEndTime, new Date());
-
- int rows = taskMapper.update(null, wrapper);
-
- // 2. 更新图谱实例状态为"失败"
- GraphBuildTask task = getByUuid(taskUuid);
- if (task != null && task.getGraphUuid() != null) {
- graphInstanceService.updateStatus(task.getGraphUuid(), 30); // 30-失败
- log.info("更新图谱实例状态为失败: graphUuid={}", task.getGraphUuid());
- }
-
- log.error("标记任务失败: taskUuid={}, error={}, rows={}", taskUuid, errorMessage, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("标记任务失败失败: taskUuid={}", taskUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean cancelTask(String taskUuid) {
- try {
- GraphBuildTask task = getByUuid(taskUuid);
- if (task == null) {
- log.error("任务不存在: taskUuid={}", taskUuid);
- return false;
- }
-
- // 只能取消待处理或运行中的任务
- if (task.getTaskStatus() != 1 && task.getTaskStatus() != 2) {
- log.warn("任务状态不允许取消: taskUuid={}, status={}", taskUuid, task.getTaskStatus());
- return false;
- }
-
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- wrapper.set(GraphBuildTask::getTaskStatus, 4); // 4-失败
- wrapper.set(GraphBuildTask::getErrorMessage, "任务已取消");
- wrapper.set(GraphBuildTask::getEndTime, new Date());
-
- int rows = taskMapper.update(null, wrapper);
-
- log.info("取消任务: taskUuid={}, rows={}", taskUuid, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("取消任务失败: taskUuid={}", taskUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public String retryTask(String taskUuid) {
- try {
- GraphBuildTask oldTask = getByUuid(taskUuid);
- if (oldTask == null) {
- log.error("任务不存在: taskUuid={}", taskUuid);
- return null;
- }
-
- // 创建新任务
- GraphBuildTask newTask = createTask(
- oldTask.getGraphUuid(),
- oldTask.getKnowledgeId(),
- oldTask.getDocId(),
- oldTask.getTaskType()
- );
-
- log.info("重试任务: oldTaskUuid={}, newTaskUuid={}", taskUuid, newTask.getTaskUuid());
- return newTask.getTaskUuid();
- } catch (Exception e) {
- log.error("重试任务失败: taskUuid={}", taskUuid, e);
- return null;
- }
- }
-
- /**
- * 执行图谱构建任务的核心逻辑
- *
- * @param task 构建任务
- * @throws Exception 执行过程中的异常
- */
- private void executeTaskLogic(GraphBuildTask task) throws Exception {
- String taskUuid = task.getTaskUuid();
- String graphUuid = task.getGraphUuid();
- String knowledgeId = task.getKnowledgeId();
- String docId = task.getDocId();
- Integer taskType = task.getTaskType();
-
- long startTime = System.currentTimeMillis();
- int totalDocs = 0;
- int processedDocs = 0;
- int successDocs = 0; // ⭐ 新增:成功处理的文档数
- int failedDocs = 0; // ⭐ 新增:失败的文档数
- int totalEntities = 0;
- int totalRelations = 0;
-
- // ⭐ 记录初始内存状态
- Runtime runtime = Runtime.getRuntime();
- long initialMemory = runtime.totalMemory() - runtime.freeMemory();
- log.info("📊 初始内存使用: {} MB / {} MB",
- initialMemory / 1024 / 1024,
- runtime.maxMemory() / 1024 / 1024);
-
- try {
- // 0. 获取图谱实例配置(包括LLM模型)
- String modelName = null;
- if (StrUtil.isNotBlank(graphUuid)) {
- GraphInstance graphInstance = graphInstanceService.getByUuid(graphUuid);
- if (graphInstance != null && StrUtil.isNotBlank(graphInstance.getModelName())) {
- modelName = graphInstance.getModelName();
- log.info("使用图谱实例配置的模型: {}", modelName);
- }
- }
-
- // 1. 获取需要处理的文档列表
- List documents;
-
- if (taskType == 1) {
- // 类型1: 全量构建(知识库所有文档)
- if (StrUtil.isBlank(knowledgeId)) {
- throw new RuntimeException("知识库构建任务缺少知识库ID");
- }
-
- // 查询知识库下的所有文档(目前只处理单文档)
- KnowledgeFragmentBo bo = new KnowledgeFragmentBo();
- bo.setDocId(docId);
- log.info("🔍 准备查询文档: knowledgeId={}", knowledgeId);
- documents = knowledgeFragmentService.queryList(bo);
- log.info("📋 查询返回文档数: {}", documents != null ? documents.size() : "null");
-
- } else if (taskType == 2) {
- // 类型2: 重建(清空后全量重建)
- if (StrUtil.isBlank(knowledgeId)) {
- throw new RuntimeException("知识库构建任务缺少知识库ID");
- }
-
- // ⭐ 先清空该知识库的旧图谱数据
- log.info("🗑️ 重建模式:先清空知识库的旧图谱数据,knowledgeId: {}", knowledgeId);
- boolean deleted = graphRAGService.deleteGraphData(knowledgeId);
- if (deleted) {
- log.info("✅ 旧图谱数据清空成功");
- } else {
- log.warn("⚠️ 旧图谱数据清空失败(可能是没有旧数据)");
- }
-
- // 查询知识库下的所有文档
- KnowledgeFragmentBo bo = new KnowledgeFragmentBo();
- bo.setDocId(docId);
- log.info("🔍 准备查询文档: knowledgeId={}, bo.getKid()={}", knowledgeId);
- documents = knowledgeFragmentService.queryList(bo);
- log.info("📋 查询返回文档数: {}", documents != null ? documents.size() : "null");
-
- } else if (taskType == 3) {
- // 类型3: 单文档增量构建
- if (StrUtil.isBlank(docId)) {
- throw new RuntimeException("单文档构建任务缺少文档ID");
- }
-
- // 根据docId查询单个文档
- KnowledgeFragmentBo bo = new KnowledgeFragmentBo();
- bo.setDocId(docId);
- log.info("🔍 准备查询单个文档: docId={}, bo.getDocId()={}", docId, bo.getDocId());
- documents = knowledgeFragmentService.queryList(bo);
- log.info("📋 查询返回文档数: {}", documents != null ? documents.size() : "null");
-
- } else {
- throw new RuntimeException("未知的任务类型: " + taskType);
- }
-
- if (documents == null || documents.isEmpty()) {
- String errorMsg = String.format(
- "❌ 没有找到需要处理的文档!\n" +
- " taskUuid: %s\n" +
- " knowledgeId: %s\n" +
- " docId: %s\n" +
- " taskType: %d\n" +
- " documents: %s\n" +
- "请检查:\n" +
- " 1. knowledge_attach 表中是否有 kid='%s' 的记录\n" +
- " 2. knowledgeId 是否正确传递\n" +
- " 3. KnowledgeAttachService.queryList() 是否正确执行",
- taskUuid, knowledgeId, docId, taskType,
- documents == null ? "null" : "empty list",
- knowledgeId
- );
- log.warn(errorMsg);
-
- Map summary = new HashMap<>();
- summary.put("message", "没有找到需要处理的文档");
- summary.put("totalDocs", 0);
- summary.put("knowledgeId", knowledgeId);
- summary.put("taskType", taskType);
- markSuccess(taskUuid, JSON.toJSONString(summary)); // ⭐ 使用 JSON 序列化
- return;
- }
-
- totalDocs = documents.size();
- log.info("开始构建图谱,共 {} 个文档", totalDocs);
-
- // ⭐ 更新任务的 total_docs 字段
- LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>();
- updateWrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
- updateWrapper.set(GraphBuildTask::getTotalDocs, totalDocs);
- taskMapper.update(null, updateWrapper);
- log.info("📊 更新任务total_docs: {}", totalDocs);
-
- // 限制处理文档数量,避免内存溢出
- int maxDocsPerBatch = 50; // 每批最多处理50个文档
- if (totalDocs > maxDocsPerBatch) {
- log.warn("文档数量较多({}个),建议分批处理,当前批次限制为{}个", totalDocs, maxDocsPerBatch);
- documents = documents.subList(0, Math.min(maxDocsPerBatch, totalDocs));
- totalDocs = documents.size();
-
- // ⭐ 重新更新 total_docs(因为被限制了)
- LambdaUpdateWrapper updateWrapper2 = new LambdaUpdateWrapper<>();
- updateWrapper2.eq(GraphBuildTask::getTaskUuid, taskUuid);
- updateWrapper2.set(GraphBuildTask::getTotalDocs, totalDocs);
- taskMapper.update(null, updateWrapper2);
- log.info("📊 更新限制后的total_docs: {}", totalDocs);
- }
-
- // 2. 逐个处理文档(带内存管理和错误恢复)
- for (int i = 0; i < documents.size(); i++) {
- KnowledgeFragmentVo doc = documents.get(i);
- long docStartTime = System.currentTimeMillis();
-
- try {
- // ⭐ 检查内存状态
- long usedMemory = runtime.totalMemory() - runtime.freeMemory();
- long maxMemory = runtime.maxMemory();
- double memoryUsage = (double) usedMemory / maxMemory * 100;
-
- if (memoryUsage > 80) {
- log.warn("⚠️ 内存使用率过高: {}/{}MB ({}%), 建议垃圾回收",
- usedMemory / 1024 / 1024,
- maxMemory / 1024 / 1024,
- String.format("%.2f", memoryUsage));
- System.gc();
- try {
- Thread.sleep(1000); // 等待GC完成
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- log.warn("⚠️ 等待GC时被中断");
- }
- }
-
- // 2.1 获取文档内容
- String content = doc.getContent();
- if (StrUtil.isBlank(content)) {
- log.warn("⚠️ 文档内容为空,跳过: docId={}", doc.getDocId());
- processedDocs++;
- failedDocs++;
- continue;
- }
-
- // 限制单个文档内容大小,避免内存溢出
- if (content.length() > 50000) {
- log.warn("⚠️ 文档内容过大({} 字符),截断处理: docId={}",
- content.length(), doc.getDocId());
- content = content.substring(0, 50000);
- }
- KnowledgeAttachBo bo = new KnowledgeAttachBo();
- bo.setDocId(docId);
- KnowledgeAttachVo docInfo = knowledgeAttachService.queryList(bo).get(0);
-
- // 2.2 准备元数据(不包含大字段)
- Map metadata = new HashMap<>();
- metadata.put("docId", doc.getDocId());
- metadata.put("docName", docInfo.getName());
- metadata.put("docType", docInfo.getType());
- metadata.put("kid", knowledgeId);
-
- // 2.3 调用GraphRAG服务进行图谱入库(使用图谱实例配置的模型)
- GraphExtractionResult result = null;
- try {
- if (content.length() > 2000) {
- // 长文档,使用分片处理
- result = graphRAGService.ingestDocumentWithModel(
- content, knowledgeId, metadata, modelName);
- } else {
- // 短文档,直接处理
- result = graphRAGService.ingestTextWithModel(
- content, knowledgeId, metadata, modelName);
- }
- } catch (OutOfMemoryError oom) {
- // OOM单独处理:强制GC后继续
- log.error("💥 处理文档时OOM,强制垃圾回收: docId={}", doc.getDocId());
- System.gc();
- try {
- Thread.sleep(2000); // 等待GC完成
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- log.warn("⚠️ 等待GC时被中断");
- // 中断时不继续处理,跳出循环
- throw ie;
- }
- processedDocs++;
- failedDocs++;
- continue;
- } catch (Exception e) {
- log.error("❌ LLM调用失败,跳过文档: docId={}, error={}",
- doc.getDocId(), e.getMessage());
- processedDocs++;
- failedDocs++;
- continue;
- }
-
- // 2.4 统计结果
- if (result != null && result.getSuccess()) {
- int entities = result.getEntities().size();
- int relations = result.getRelations().size();
- totalEntities += entities;
- totalRelations += relations;
- successDocs++;
-
- long docDuration = System.currentTimeMillis() - docStartTime;
- log.info("✅ 文档处理成功: docId={}, 实体数={}, 关系数={}, 耗时={}ms",
- doc.getDocId(), entities, relations, docDuration);
- } else {
- failedDocs++;
- log.warn("⚠️ 文档处理失败: docId={}, error={}",
- doc.getDocId(), result != null ? result.getErrorMessage() : "unknown");
- }
-
- // 2.5 更新进度
- processedDocs++;
- int progress = (processedDocs * 100) / totalDocs;
- log.info("📈 文档进度: {}/{}, 进度={}%", processedDocs, totalDocs, progress);
- boolean updated = updateProgress(taskUuid, progress, processedDocs);
- if (!updated) {
- log.warn("⚠️ 进度更新失败: taskUuid={}, progress={}", taskUuid, progress);
- }
-
- // 2.6 定期进行垃圾回收和内存检查
- if ((i + 1) % 10 == 0) {
- long currentMemory = runtime.totalMemory() - runtime.freeMemory();
- log.info("📊 已处理{}/{}个文档, 内存使用: {} MB",
- i + 1, totalDocs, currentMemory / 1024 / 1024);
- System.gc();
- try {
- Thread.sleep(500); // 短暂等待GC
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- log.warn("⚠️ 等待GC时被中断");
- }
- }
-
- } catch (InterruptedException ie) {
- // 中断异常:重新抛出,终止任务
- Thread.currentThread().interrupt();
- log.error("⚠️ 任务被中断,停止处理文档: docId={}", doc.getDocId());
- throw ie;
- } catch (Exception e) {
- log.error("❌ 处理文档时发生异常: docId={}, error={}",
- doc.getDocId(), e.getMessage(), e);
- processedDocs++;
- failedDocs++;
- // 继续处理下一个文档(不中断整个任务)
- } finally {
- // 释放文档引用,帮助GC
- documents.set(i, null);
- }
- }
-
- // 3. 构建完成,生成详细摘要
- long duration = (System.currentTimeMillis() - startTime) / 1000;
- long finalMemory = runtime.totalMemory() - runtime.freeMemory();
-
- Map summary = new HashMap<>();
- summary.put("totalDocs", totalDocs);
- summary.put("processedDocs", processedDocs);
- summary.put("successDocs", successDocs); // ⭐ 成功文档数
- summary.put("failedDocs", failedDocs); // ⭐ 失败文档数
- summary.put("totalEntities", totalEntities);
- summary.put("totalRelations", totalRelations);
- summary.put("duration", duration + "秒");
- summary.put("avgTimePerDoc", totalDocs > 0 ? (duration * 1000 / totalDocs) + "ms" : "N/A"); // ⭐ 平均处理时间
- summary.put("memoryUsed", (finalMemory - initialMemory) / 1024 / 1024 + "MB"); // ⭐ 内存增量
- summary.put("status", "completed");
- summary.put("modelName", modelName != null ? modelName : "default"); // ⭐ 使用的模型
-
- // 更新统计信息到任务
- updateExtractionStats(taskUuid, totalEntities, totalRelations);
-
- markSuccess(taskUuid, JSON.toJSONString(summary));
-
- log.info("🎉 图谱构建任务完成汇总:");
- log.info(" - taskUuid: {}", taskUuid);
- log.info(" - 文档总数: {}", totalDocs);
- log.info(" - 成功处理: {} 个", successDocs);
- log.info(" - 失败文档: {} 个", failedDocs);
- log.info(" - 实体总数: {}", totalEntities);
- log.info(" - 关系总数: {}", totalRelations);
- log.info(" - 总耗时: {} 秒", duration);
- log.info(" - 平均耗时: {} ms/文档", totalDocs > 0 ? duration * 1000 / totalDocs : 0);
- log.info(" - 内存增量: {} MB", (finalMemory - initialMemory) / 1024 / 1024);
-
- } catch (InterruptedException ie) {
- // 中断异常:向上抛出
- Thread.currentThread().interrupt();
- log.error("⚠️ 图谱构建任务被中断: taskUuid={}", taskUuid, ie);
- throw ie;
- } catch (Exception e) {
- log.error("❌ 图谱构建任务执行失败: taskUuid={}", taskUuid, e);
- throw e;
- } finally {
- // 清理资源,帮助GC
- System.gc();
- log.info("📊 最终内存状态: {} MB / {} MB",
- (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024,
- runtime.maxMemory() / 1024 / 1024);
- }
- }
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java
deleted file mode 100644
index bdceb35f..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java
+++ /dev/null
@@ -1,367 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import cn.hutool.core.util.StrUtil;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.common.chat.service.chat.IChatModelService;
-import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
-import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
-import org.ruoyi.config.GraphExtractPrompt;
-import org.ruoyi.constant.GraphConstants;
-import org.ruoyi.domain.dto.ExtractedEntity;
-import org.ruoyi.domain.dto.ExtractedRelation;
-import org.ruoyi.domain.dto.GraphExtractionResult;
-import org.ruoyi.factory.GraphLLMServiceFactory;
-import org.ruoyi.service.graph.IGraphExtractionService;
-import org.ruoyi.service.graph.IGraphLLMService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-
-/**
- * 图谱实体关系抽取服务实现
- * 使用工厂模式支持多种LLM模型(参考 ruoyi-chat 设计)
- *
- * @author ruoyi
- * @date 2025-09-30
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
-public class GraphExtractionServiceImpl implements IGraphExtractionService {
-
- private final IChatModelService chatModelService;
- private final GraphLLMServiceFactory llmServiceFactory;
-
- /**
- * 实体匹配正则表达式
- * 格式: ("entity"<|>ENTITY_NAME<|>ENTITY_TYPE<|>ENTITY_DESCRIPTION)
- */
- private static final Pattern ENTITY_PATTERN = Pattern.compile(
- "\\(\"entity\"" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^)]+)\\)"
- );
- /**
- * 关系匹配正则表达式
- * 格式: ("relationship"<|>SOURCE<|>TARGET<|>DESCRIPTION<|>STRENGTH)
- */
- private static final Pattern RELATION_PATTERN = Pattern.compile(
- "\\(\"relationship\"" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
- Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
- "([^)]+)\\)"
- );
-
-
- @Override
- public GraphExtractionResult extractFromText(String text) {
- return extractFromText(text, GraphConstants.DEFAULT_ENTITY_TYPES);
- }
-
- @Override
- public GraphExtractionResult extractFromText(String text, String[] entityTypes) {
- log.info("开始从文本中抽取实体和关系,文本长度: {}", text.length());
-
- try {
- // 1. 构建提示词
- String prompt = GraphExtractPrompt.buildExtractionPrompt(text, entityTypes);
-
- // 2. 调用LLM(使用默认模型)
- String llmResponse = callLLM(prompt);
-
- // 3. 解析响应
- GraphExtractionResult result = parseGraphResponse(llmResponse);
- result.setRawResponse(llmResponse);
- result.setSuccess(true);
-
- log.info("抽取完成,实体数: {}, 关系数: {}",
- result.getEntities().size(), result.getRelations().size());
-
- return result;
-
- } catch (Exception e) {
- log.error("实体关系抽取失败", e);
- return GraphExtractionResult.builder()
- .entities(new ArrayList<>())
- .relations(new ArrayList<>())
- .success(false)
- .errorMessage(e.getMessage())
- .build();
- }
- }
-
- @Override
- public GraphExtractionResult extractFromTextWithModel(String text, String modelName) {
- log.info("开始从文本中抽取实体和关系,使用模型: {}, 文本长度: {}", modelName, text.length());
-
- try {
- // 1. 获取模型配置
- ChatModelVo chatModel = chatModelService.selectModelByName(modelName);
- if (chatModel == null) {
- log.warn("未找到模型: {}, 使用默认模型", modelName);
- return extractFromText(text);
- }
-
- // 2. 构建提示词
- String prompt = GraphExtractPrompt.buildExtractionPrompt(text, GraphConstants.DEFAULT_ENTITY_TYPES);
-
- // 3. 调用LLM(使用指定模型)
- String llmResponse = callLLMWithModel(prompt, chatModel);
-
- // 4. 解析响应
- GraphExtractionResult result = parseGraphResponse(llmResponse);
- result.setRawResponse(llmResponse);
- result.setSuccess(true);
-
- log.info("抽取完成,实体数: {}, 关系数: {}, 使用模型: {}",
- result.getEntities().size(), result.getRelations().size(), modelName);
-
- // ⭐ 调试:如果没有关系,记录原始响应(便于诊断)
- if (result.getRelations().isEmpty() && !result.getEntities().isEmpty()) {
- log.warn("⚠️ LLM 提取到 {} 个实体,但没有提取到任何关系!", result.getEntities().size());
- log.warn("LLM 原始响应预览(前500字符): {}",
- llmResponse.length() > 500 ? llmResponse.substring(0, 500) + "..." : llmResponse);
- }
-
- return result;
-
- } catch (Exception e) {
- log.error("实体关系抽取失败,模型: {}", modelName, e);
- return GraphExtractionResult.builder()
- .entities(new ArrayList<>())
- .relations(new ArrayList<>())
- .success(false)
- .errorMessage(e.getMessage())
- .build();
- }
- }
-
- @Override
- public GraphExtractionResult parseGraphResponse(String response) {
- log.debug("开始解析图谱响应,响应长度: {}", response != null ? response.length() : 0);
-
- List entities = new ArrayList<>();
- List relations = new ArrayList<>();
-
- if (StrUtil.isBlank(response)) {
- log.warn("响应为空,无法解析");
- return GraphExtractionResult.builder()
- .entities(entities)
- .relations(relations)
- .success(false)
- .errorMessage("LLM响应为空")
- .build();
- }
-
- try {
- // 1. 解析实体
- Matcher entityMatcher = ENTITY_PATTERN.matcher(response);
- while (entityMatcher.find()) {
- String name = entityMatcher.group(1).trim();
- String type = entityMatcher.group(2).trim();
- String description = entityMatcher.group(3).trim();
-
- // ⭐ 过滤无效实体(N/A 或包含特殊字符)
- if (isInvalidEntity(name, type)) {
- log.debug("跳过无效实体: name={}, type={}", name, type);
- continue;
- }
-
- ExtractedEntity entity = ExtractedEntity.builder()
- .name(name)
- .type(type)
- .description(description)
- .build();
-
- entities.add(entity);
- log.debug("解析到实体: name={}, type={}", name, type);
- }
-
- // 2. 解析关系
- Matcher relationMatcher = RELATION_PATTERN.matcher(response);
- while (relationMatcher.find()) {
- String sourceEntity = relationMatcher.group(1).trim();
- String targetEntity = relationMatcher.group(2).trim();
- String description = relationMatcher.group(3).trim();
- String strengthStr = relationMatcher.group(4).trim();
-
- Integer strength = parseStrength(strengthStr);
- Double confidence = calculateConfidence(strength);
-
- ExtractedRelation relation = ExtractedRelation.builder()
- .sourceEntity(sourceEntity)
- .targetEntity(targetEntity)
- .description(description)
- .strength(strength)
- .confidence(confidence)
- .build();
-
- relations.add(relation);
- log.debug("解析到关系: sourceEntity={}, targetEntity={}, strength={}",
- sourceEntity, targetEntity, strength);
- }
-
- log.info("解析完成,实体数: {}, 关系数: {}", entities.size(), relations.size());
-
- return GraphExtractionResult.builder()
- .entities(entities)
- .relations(relations)
- .success(true)
- .build();
-
- } catch (Exception e) {
- log.error("解析图谱响应失败", e);
- return GraphExtractionResult.builder()
- .entities(entities)
- .relations(relations)
- .success(false)
- .errorMessage("解析失败: " + e.getMessage())
- .build();
- }
- }
-
- /**
- * 调用LLM获取响应(使用默认模型)
- *
- * @param prompt 提示词
- * @return LLM响应
- */
- private String callLLM(String prompt) {
- // 获取聊天分类的最高优先级模型作为默认模型
- // 如果没有chat分类的模型,尝试查询任意可用模型
- ChatModelVo defaultModel = chatModelService.queryList(new ChatModelBo()).get(0);
-
- if (defaultModel == null) {
- log.error("未找到可用的LLM模型");
- throw new RuntimeException("未找到可用的LLM模型,请先配置聊天模型");
- }
-
- log.info("使用默认模型: {}", defaultModel.getModelName());
- return callLLMWithModel(prompt, defaultModel);
- }
-
- /**
- * 使用指定模型调用LLM获取响应(使用工厂模式,支持多种LLM)
- *
- * @param prompt 提示词
- * @param chatModel 模型配置
- * @return LLM响应
- */
- private String callLLMWithModel(String prompt, ChatModelVo chatModel) {
- log.info("调用LLM模型: model={}, category={}, 提示词长度={}",
- chatModel.getModelName(), chatModel.getCategory(), prompt.length());
-
- try {
- // 根据模型类别获取对应的LLM服务实现
- IGraphLLMService llmService = llmServiceFactory.getLLMService(chatModel.getCategory());
-
- // 调用LLM进行图谱抽取
- String responseText = llmService.extractGraph(prompt, chatModel);
-
- log.info("LLM调用成功: model={}, category={}, 响应长度={}",
- chatModel.getModelName(), chatModel.getCategory(), responseText.length());
-
- return responseText;
-
- } catch (IllegalArgumentException e) {
- // 不支持的模型类别,降级到默认实现
- log.warn("不支持的模型类别: {}, 尝试使用OpenAI兼容模式", chatModel.getCategory());
-
- try {
- IGraphLLMService openAiService = llmServiceFactory.getLLMService("openai");
- return openAiService.extractGraph(prompt, chatModel);
- } catch (Exception fallbackEx) {
- log.error("降级调用也失败: {}", fallbackEx.getMessage(), fallbackEx);
- throw new RuntimeException("LLM调用失败: " + fallbackEx.getMessage(), fallbackEx);
- }
-
- } catch (Exception e) {
- log.error("LLM调用失败: {}", e.getMessage(), e);
- throw new RuntimeException("LLM调用失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 解析关系强度
- *
- * @param strengthStr 强度字符串
- * @return 强度值(0-10)
- */
- private Integer parseStrength(String strengthStr) {
- try {
- // 尝试解析为整数
- int strength = Integer.parseInt(strengthStr);
- // 限制在0-10范围内
- return Math.max(0, Math.min(10, strength));
- } catch (NumberFormatException e) {
- log.debug("无法解析关系强度: {}, 使用默认值5", strengthStr);
- return 5; // 默认中等强度
- }
- }
-
- /**
- * 验证实体是否有效
- * 过滤 N/A 以及包含 Neo4j 不支持的特殊字符的实体
- *
- * @param name 实体名称
- * @param type 实体类型
- * @return true=无效,false=有效
- */
- private boolean isInvalidEntity(String name, String type) {
- // 1. 检查是否为 N/A
- if ("N/A".equalsIgnoreCase(name) || "N/A".equalsIgnoreCase(type)) {
- return true;
- }
-
- // 2. 检查是否为空或纯空格
- if (StrUtil.isBlank(name) || StrUtil.isBlank(type)) {
- return true;
- }
-
- // 3. 检查类型是否包含 Neo4j Label 不支持的字符
- // Neo4j Label 规则:不能包含 / : & | 等特殊字符
- if (type.matches(".*[/:&|\\\\].*")) {
- log.warn("⚠️ 实体类型包含非法字符,将被过滤: type={}", type);
- return true;
- }
-
- // 4. 检查名称是否过长(Neo4j 建议 < 256)
- if (name.length() > 255 || type.length() > 64) {
- log.warn("⚠️ 实体名称或类型过长,将被过滤: name.length={}, type.length={}",
- name.length(), type.length());
- return true;
- }
-
- return false;
- }
-
- /**
- * 根据关系强度计算置信度
- *
- * @param strength 关系强度(0-10)
- * @return 置信度(0.0-1.0)
- */
- private Double calculateConfidence(Integer strength) {
- if (strength == null) {
- return 0.5;
- }
- // 将0-10的强度映射到0.3-1.0的置信度
- return 0.3 + (strength / 10.0) * 0.7;
- }
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphInstanceServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphInstanceServiceImpl.java
deleted file mode 100644
index 33d7843b..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphInstanceServiceImpl.java
+++ /dev/null
@@ -1,284 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.common.core.utils.StringUtils;
-import org.ruoyi.domain.bo.graph.GraphInstance;
-import org.ruoyi.mapper.graph.GraphInstanceMapper;
-import org.ruoyi.service.graph.IGraphInstanceService;
-import org.ruoyi.service.graph.IGraphStoreService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 图谱实例服务实现
- *
- * @author ruoyi
- * @date 2025-09-30
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
-public class GraphInstanceServiceImpl implements IGraphInstanceService {
-
- private final GraphInstanceMapper graphInstanceMapper;
- private final IGraphStoreService graphStoreService;
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public GraphInstance createInstance(String knowledgeId, String graphName, String config) {
- // 检查是否已存在
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
- GraphInstance existing = graphInstanceMapper.selectOne(wrapper);
-
- if (existing != null) {
- log.warn("知识库 {} 已存在图谱实例", knowledgeId);
- return existing;
- }
-
- // 创建新实例
- GraphInstance instance = new GraphInstance();
- instance.setGraphUuid(String.valueOf(IdUtil.getSnowflake().nextId())); // UUID
- instance.setKnowledgeId(knowledgeId);
- instance.setGraphName(StringUtils.isNotBlank(graphName) ? graphName : "知识图谱-" + knowledgeId);
- instance.setGraphStatus(0); // 0-未构建(新建时状态为未构建,需手动点击"构建"按钮)
- instance.setNodeCount(0);
- instance.setRelationshipCount(0);
-
- // 解析配置
- if (StringUtils.isNotBlank(config)) {
- instance.setConfig(config);
- }
-
- graphInstanceMapper.insert(instance);
-
- // 创建 Neo4j Schema
- graphStoreService.createGraphSchema(knowledgeId);
-
- log.info("创建图谱实例成功: knowledgeId={}, instanceId={}", knowledgeId, instance.getId());
- return instance;
- }
-
- @Override
- public GraphInstance getById(Long id) {
- return graphInstanceMapper.selectById(id);
- }
-
- @Override
- public GraphInstance getByUuid(String graphUuid) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
- return graphInstanceMapper.selectOne(wrapper);
- }
-
- @Override
- public boolean updateInstance(GraphInstance instance) {
- try {
- int rows = graphInstanceMapper.updateById(instance);
- log.info("更新图谱实例: id={}, rows={}", instance.getId(), rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新图谱实例失败: id={}", instance.getId(), e);
- return false;
- }
- }
-
- @Override
- public List listByKnowledgeId(String knowledgeId) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
- wrapper.orderByDesc(GraphInstance::getCreateTime);
- return graphInstanceMapper.selectList(wrapper);
- }
-
- @Override
- public Page queryPage(Page page, String instanceName, String knowledgeId, Integer graphStatus) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
-
- // 图谱名称模糊查询
- if (StringUtils.isNotBlank(instanceName)) {
- wrapper.like(GraphInstance::getGraphName, instanceName.trim());
- }
-
- // 知识库ID精确查询
- if (StringUtils.isNotBlank(knowledgeId)) {
- wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId.trim());
- }
-
- // 状态精确查询
- if (graphStatus != null) {
- wrapper.eq(GraphInstance::getGraphStatus, graphStatus);
- }
-
- // 只查询未删除的记录
- wrapper.eq(GraphInstance::getDelFlag, "0");
-
- // 按创建时间倒序
- wrapper.orderByDesc(GraphInstance::getCreateTime);
-
- return graphInstanceMapper.selectPage(page, wrapper);
- }
-
- @Override
- public boolean updateStatus(String graphUuid, Integer status) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
- wrapper.set(GraphInstance::getGraphStatus, status);
-
- int rows = graphInstanceMapper.update(null, wrapper);
-
- log.info("更新图谱状态: graphUuid={}, status={}, rows={}", graphUuid, status, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新图谱状态失败: graphUuid={}, status={}", graphUuid, status, e);
- return false;
- }
- }
-
- @Override
- public boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
-
- if (nodeCount != null) {
- wrapper.set(GraphInstance::getNodeCount, nodeCount);
- }
- if (relationshipCount != null) {
- wrapper.set(GraphInstance::getRelationshipCount, relationshipCount);
- }
-
- int rows = graphInstanceMapper.update(null, wrapper);
-
- log.info("更新图谱统计: graphUuid={}, nodeCount={}, relationshipCount={}, rows={}",
- graphUuid, nodeCount, relationshipCount, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新图谱统计失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- public boolean updateConfig(String graphUuid, String config) {
- try {
- LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
- wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
- wrapper.set(GraphInstance::getConfig, config);
-
- int rows = graphInstanceMapper.update(null, wrapper);
-
- log.info("更新图谱配置: graphUuid={}, rows={}", graphUuid, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("更新图谱配置失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean deleteInstance(String graphUuid) {
- try {
- log.info("🗑️ 开始删除图谱实例及数据,graphUuid: {}", graphUuid);
-
- // ⭐ 1. 先获取实例信息(获取knowledgeId)
- GraphInstance instance = getByUuid(graphUuid);
- if (instance == null) {
- log.warn("⚠️ 图谱实例不存在: graphUuid={}", graphUuid);
- return false;
- }
-
- String knowledgeId = instance.getKnowledgeId();
-
- // ⭐ 2. 删除Neo4j中的图数据(通过knowledgeId)
- if (StrUtil.isNotBlank(knowledgeId)) {
- log.info("删除Neo4j图数据,knowledgeId: {}", knowledgeId);
- boolean neo4jDeleted = graphStoreService.deleteByKnowledgeId(knowledgeId);
- if (neo4jDeleted) {
- log.info("✅ Neo4j图数据删除成功");
- } else {
- log.warn("⚠️ Neo4j图数据删除失败(可能是没有数据)");
- }
- } else {
- log.warn("⚠️ 实例没有关联知识库ID,跳过Neo4j数据删除");
- }
-
- // 3. 删除MySQL中的实例记录
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
- int rows = graphInstanceMapper.delete(wrapper);
-
- log.info("✅ 删除图谱实例成功: graphUuid={}, knowledgeId={}, rows={}",
- graphUuid, knowledgeId, rows);
- return rows > 0;
- } catch (Exception e) {
- log.error("❌ 删除图谱实例失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean deleteInstanceAndData(String graphUuid) {
- try {
- // 1. 删除 Neo4j 中的图谱数据
- boolean graphDeleted = graphStoreService.deleteGraph(graphUuid);
-
- // 2. 删除 MySQL 中的实例记录
- boolean instanceDeleted = deleteInstance(graphUuid);
-
- log.info("删除图谱实例及数据: graphUuid={}, graphDeleted={}, instanceDeleted={}",
- graphUuid, graphDeleted, instanceDeleted);
-
- return graphDeleted && instanceDeleted;
- } catch (Exception e) {
- log.error("删除图谱实例及数据失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- public Map getStatistics(String graphUuid) {
- try {
- // 从 Neo4j 获取实时统计
- Map stats = graphStoreService.getGraphStatistics(graphUuid);
-
- // 更新到 MySQL(异步)
- if (stats.containsKey("nodeCount") && stats.containsKey("relationshipCount")) {
- updateCounts(
- graphUuid,
- (Integer) stats.get("nodeCount"),
- (Integer) stats.get("relationshipCount")
- );
- }
-
- // 添加实例信息
- GraphInstance instance = getByUuid(graphUuid);
- if (instance != null) {
- stats.put("graphName", instance.getGraphName());
- stats.put("status", instance.getGraphStatus());
- stats.put("createTime", instance.getCreateTime());
- stats.put("updateTime", instance.getUpdateTime());
- }
-
- return stats;
- } catch (Exception e) {
- log.error("获取图谱统计信息失败: graphUuid={}", graphUuid, e);
- return new HashMap<>();
- }
- }
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphRAGServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphRAGServiceImpl.java
deleted file mode 100644
index 411d6730..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphRAGServiceImpl.java
+++ /dev/null
@@ -1,462 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.core.util.StrUtil;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.ruoyi.constant.GraphConstants;
-import org.ruoyi.domain.bo.graph.GraphEdge;
-import org.ruoyi.domain.bo.graph.GraphVertex;
-import org.ruoyi.domain.dto.ExtractedEntity;
-import org.ruoyi.domain.dto.ExtractedRelation;
-import org.ruoyi.domain.dto.GraphExtractionResult;
-import org.ruoyi.service.graph.IGraphExtractionService;
-import org.ruoyi.service.graph.IGraphRAGService;
-import org.ruoyi.service.graph.IGraphStoreService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-
-/**
- * GraphRAG服务实现
- *
- * @author ruoyi
- * @date 2025-09-30
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
-public class GraphRAGServiceImpl implements IGraphRAGService {
-
- private final IGraphExtractionService graphExtractionService;
- private final IGraphStoreService graphStoreService;
-
- @Override
- public GraphExtractionResult ingestText(String text, String knowledgeId, Map metadata) {
- return ingestTextWithModel(text, knowledgeId, metadata, null);
- }
-
- @Override
- public GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map metadata, String modelName) {
- log.info("开始将文本入库到图谱,知识库ID: {}, 模型: {}, 文本长度: {}",
- knowledgeId, modelName != null ? modelName : "默认", text.length());
-
- try {
- // 1. 从文本中抽取实体和关系
- GraphExtractionResult extractionResult;
- if (StrUtil.isNotBlank(modelName)) {
- extractionResult = graphExtractionService.extractFromTextWithModel(text, modelName);
- } else {
- extractionResult = graphExtractionService.extractFromText(text);
- }
-
- if (!extractionResult.getSuccess()) {
- log.error("实体抽取失败: {}", extractionResult.getErrorMessage());
- return extractionResult;
- }
-
- // 2. 将抽取的实体转换为图节点
- List vertices = convertEntitiesToVertices(
- extractionResult.getEntities(),
- knowledgeId,
- metadata
- );
-
- // 3. 批量添加节点到Neo4j,并建立实体名称→nodeId的映射
- Map entityNameToNodeIdMap = new HashMap<>();
- if (!vertices.isEmpty()) {
- int addedCount = graphStoreService.addVertices(vertices);
- log.info("成功添加 {} 个节点到图谱", addedCount);
-
- // ⭐ 建立映射:实体名称 → nodeId
- for (GraphVertex vertex : vertices) {
- entityNameToNodeIdMap.put(vertex.getName(), vertex.getNodeId());
- }
- log.debug("建立实体名称映射: {} 个实体", entityNameToNodeIdMap.size());
- }
-
- // 4. 将抽取的关系转换为图边,使用映射填充nodeId
- List edges = convertRelationsToEdges(
- extractionResult.getRelations(),
- knowledgeId,
- metadata,
- entityNameToNodeIdMap // ⭐ 传入映射
- );
-
- // 5. 批量添加关系到Neo4j
- if (!edges.isEmpty()) {
- int addedCount = graphStoreService.addEdges(edges);
- log.info("成功添加 {} 个关系到图谱", addedCount);
- }
-
- return extractionResult;
-
- } catch (Exception e) {
- log.error("文本入库失败", e);
- return GraphExtractionResult.builder()
- .entities(new ArrayList<>())
- .relations(new ArrayList<>())
- .success(false)
- .errorMessage(e.getMessage())
- .build();
- }
- }
-
- @Override
- public GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map metadata) {
- return ingestDocumentWithModel(documentText, knowledgeId, metadata, null);
- }
-
- @Override
- public GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map metadata, String modelName) {
- log.info("开始将文档入库到图谱,知识库ID: {}, 模型: {}, 文档长度: {}",
- knowledgeId, modelName != null ? modelName : "默认", documentText.length());
-
- // 如果文档较短,直接处理
- if (documentText.length() < GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4) {
- return ingestTextWithModel(documentText, knowledgeId, metadata, modelName);
- }
-
- // 文档较长,需要分片处理
- List chunks = splitDocument(documentText);
- log.info("文档已分割为 {} 个片段", chunks.size());
-
- // 合并结果
- List allEntities = new ArrayList<>();
- List allRelations = new ArrayList<>();
- int totalTokenUsed = 0;
-
- for (int i = 0; i < chunks.size(); i++) {
- String chunk = chunks.get(i);
- log.debug("处理第 {}/{} 个片段", i + 1, chunks.size());
-
- // 为每个片段添加序号元数据
- Map chunkMetadata = new HashMap<>(metadata);
- chunkMetadata.put("chunk_index", i);
- chunkMetadata.put("total_chunks", chunks.size());
-
- GraphExtractionResult result = ingestTextWithModel(chunk, knowledgeId, chunkMetadata, modelName);
-
- if (result.getSuccess()) {
- allEntities.addAll(result.getEntities());
- allRelations.addAll(result.getRelations());
- if (result.getTokenUsed() != null) {
- totalTokenUsed += result.getTokenUsed();
- }
- }
- }
-
- // 去重实体(基于名称和类型)
- List uniqueEntities = deduplicateEntities(allEntities);
- log.info("去重后实体数: {} -> {}", allEntities.size(), uniqueEntities.size());
-
- return GraphExtractionResult.builder()
- .entities(uniqueEntities)
- .relations(allRelations)
- .tokenUsed(totalTokenUsed)
- .success(true)
- .build();
- }
-
- @Override
- public String retrieveFromGraph(String query, String knowledgeId, int maxResults) {
- log.info("从图谱检索相关内容,查询: {}, 知识库ID: {}", query, knowledgeId);
-
- try {
- // 1. 从查询中抽取关键词(简单分词)
- List keywords = extractKeywords(query);
- log.debug("提取的关键词: {}", keywords);
-
- if (keywords.isEmpty()) {
- return "未能从查询中提取关键信息";
- }
-
- // 2. 在图谱中搜索相关实体节点
- List matchedNodes = new ArrayList<>();
- for (String keyword : keywords) {
- List nodes = graphStoreService.searchVerticesByName(
- keyword, knowledgeId, Math.min(5, maxResults)
- );
- matchedNodes.addAll(nodes);
- }
-
- if (matchedNodes.isEmpty()) {
- return "图谱中未找到相关实体";
- }
-
- log.info("找到 {} 个匹配的实体节点", matchedNodes.size());
-
- // 3. 去重(按nodeId)
- Map uniqueNodes = new HashMap<>();
- for (GraphVertex node : matchedNodes) {
- uniqueNodes.putIfAbsent(node.getNodeId(), node);
- }
- matchedNodes = new ArrayList<>(uniqueNodes.values());
-
- // 限制结果数量
- if (matchedNodes.size() > maxResults) {
- matchedNodes = matchedNodes.subList(0, maxResults);
- }
-
- // 4. 为每个匹配节点获取邻居,构建子图上下文
- StringBuilder result = new StringBuilder();
- result.append("### 图谱检索结果\n\n");
- result.append(String.format("查询: %s\n", query));
- result.append(String.format("找到 %d 个相关实体:\n\n", matchedNodes.size()));
-
- for (int i = 0; i < matchedNodes.size(); i++) {
- GraphVertex node = matchedNodes.get(i);
- result.append(String.format("**%d. %s** (%s)\n", i + 1, node.getName(), node.getLabel()));
-
- if (StrUtil.isNotBlank(node.getDescription())) {
- result.append(String.format(" 描述: %s\n", node.getDescription()));
- }
-
- // 获取邻居节点(1跳)
- List neighbors = graphStoreService.getNeighbors(
- node.getNodeId(), knowledgeId, 5
- );
-
- if (!neighbors.isEmpty()) {
- result.append(" 关联实体: ");
- List neighborNames = neighbors.stream()
- .map(GraphVertex::getName)
- .limit(5)
- .collect(java.util.stream.Collectors.toList());
- result.append(String.join(", ", neighborNames));
- result.append("\n");
- }
-
- result.append("\n");
- }
-
- // 5. 添加统计信息
- result.append("---\n");
- result.append(String.format("总计: %d 个实体节点\n", matchedNodes.size()));
-
- return result.toString();
-
- } catch (Exception e) {
- log.error("图谱检索失败", e);
- return "检索失败: " + e.getMessage();
- }
- }
-
- /**
- * 从查询中提取关键词
- *
- * @param query 查询文本
- * @return 关键词列表
- */
- private List extractKeywords(String query) {
- List keywords = new ArrayList<>();
-
- // 简单的中文分词策略
- // 1. 去除标点符号
- String cleaned = query.replaceAll("[\\p{Punct}\\s]+", " ");
-
- // 2. 按空格分割
- String[] words = cleaned.split("\\s+");
-
- // 3. 过滤停用词和短词
- Set stopWords = new HashSet<>(Arrays.asList(
- "的", "了", "和", "是", "在", "我", "有", "个", "这", "那", "为",
- "与", "或", "但", "等", "及", "而", "中", "如", "一", "二", "三"
- ));
-
- for (String word : words) {
- word = word.trim();
- if (word.length() >= 2 && !stopWords.contains(word)) {
- keywords.add(word);
- }
- }
-
- // 4. 如果没有提取到关键词,尝试按2-3字切分
- if (keywords.isEmpty() && query.length() >= 2) {
- for (int i = 0; i <= query.length() - 2; i++) {
- String chunk = query.substring(i, Math.min(i + 3, query.length()));
- if (chunk.length() >= 2 && !stopWords.contains(chunk)) {
- keywords.add(chunk);
- }
- }
- }
-
- // 去重并限制数量
- return keywords.stream()
- .distinct()
- .limit(5)
- .collect(java.util.stream.Collectors.toList());
- }
-
- @Override
- public boolean deleteGraphData(String knowledgeId) {
- log.info("删除知识库图谱数据,知识库ID: {}", knowledgeId);
-
- try {
- // 删除该知识库的所有节点和关系
- return graphStoreService.deleteByKnowledgeId(knowledgeId);
- } catch (Exception e) {
- log.error("删除图谱数据失败", e);
- return false;
- }
- }
-
- /**
- * 将抽取的实体转换为图节点
- */
- private List convertEntitiesToVertices(
- List entities,
- String knowledgeId,
- Map metadata) {
-
- List vertices = new ArrayList<>();
-
- for (ExtractedEntity entity : entities) {
- GraphVertex vertex = new GraphVertex();
- vertex.setNodeId(IdUtil.simpleUUID()); // 生成唯一ID
- vertex.setName(entity.getName());
- vertex.setLabel(entity.getType());
- vertex.setDescription(entity.getDescription());
- vertex.setKnowledgeId(knowledgeId);
- vertex.setConfidence(entity.getConfidence() != null ? entity.getConfidence() : 1.0);
-
- // 添加元数据
- if (metadata != null && !metadata.isEmpty()) {
- vertex.setMetadata(metadata);
- }
-
- vertices.add(vertex);
- }
-
- return vertices;
- }
-
- /**
- * 将抽取的关系转换为图边
- *
- * @param relations 抽取的关系列表
- * @param knowledgeId 知识库ID
- * @param metadata 元数据
- * @param entityNameToNodeIdMap 实体名称到节点ID的映射
- * @return 图边列表
- */
- private List convertRelationsToEdges(
- List relations,
- String knowledgeId,
- Map metadata,
- Map entityNameToNodeIdMap) {
-
- List edges = new ArrayList<>();
- int skippedCount = 0;
-
- for (ExtractedRelation relation : relations) {
- // ⭐ 通过实体名称查找对应的nodeId
- String sourceNodeId = entityNameToNodeIdMap.get(relation.getSourceEntity());
- String targetNodeId = entityNameToNodeIdMap.get(relation.getTargetEntity());
-
- // 如果找不到对应的节点ID,跳过这个关系
- if (sourceNodeId == null || targetNodeId == null) {
- log.warn("⚠️ 跳过关系(节点未找到): {} -> {}",
- relation.getSourceEntity(), relation.getTargetEntity());
- skippedCount++;
- continue;
- }
-
- GraphEdge edge = new GraphEdge();
- edge.setEdgeId(IdUtil.simpleUUID());
- edge.setSourceNodeId(sourceNodeId); // ⭐ 设置源节点ID
- edge.setTargetNodeId(targetNodeId); // ⭐ 设置目标节点ID
- edge.setSourceName(relation.getSourceEntity());
- edge.setTargetName(relation.getTargetEntity());
- edge.setLabel("RELATED_TO"); // 默认关系类型
- edge.setDescription(relation.getDescription());
- edge.setWeight(relation.getStrength() / 10.0); // 转换为0-1的权重
- edge.setKnowledgeId(knowledgeId);
- edge.setConfidence(relation.getConfidence() != null ? relation.getConfidence() : 1.0);
-
- // 添加元数据
- if (metadata != null && !metadata.isEmpty()) {
- edge.setMetadata(metadata);
- }
-
- edges.add(edge);
- }
-
- if (skippedCount > 0) {
- log.warn("⚠️ 共跳过 {} 个关系(对应的实体节点未找到)", skippedCount);
- }
-
- return edges;
- }
-
- /**
- * 分割文档为多个片段
- */
- private List splitDocument(String text) {
- List chunks = new ArrayList<>();
- int chunkSize = GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4; // 简单估算字符数
- int overlap = GraphConstants.RAG_SEGMENT_OVERLAP_IN_TOKENS * 4;
-
- int start = 0;
- while (start < text.length()) {
- int end = Math.min(start + chunkSize, text.length());
-
- // 尝试在句子边界分割
- if (end < text.length()) {
- int lastPeriod = text.lastIndexOf('。', end);
- int lastNewline = text.lastIndexOf('\n', end);
- int boundary = Math.max(lastPeriod, lastNewline);
-
- if (boundary > start) {
- end = boundary + 1;
- }
- }
-
- chunks.add(text.substring(start, end));
-
- // ⭐ 修复死循环:确保 start 一定会增加
- // 如果已经到达文本末尾,直接退出
- if (end >= text.length()) {
- break;
- }
-
- // 计算下一个起始位置,确保至少前进1个字符
- int nextStart = end - overlap;
- if (nextStart <= start) {
- // 如果 overlap 太大导致无法前进,强制前进到 end
- start = end;
- } else {
- start = nextStart;
- }
- }
-
- return chunks;
- }
-
- /**
- * 去重实体
- */
- private List deduplicateEntities(List entities) {
- Map entityMap = new HashMap<>();
-
- for (ExtractedEntity entity : entities) {
- String key = entity.getName() + "|" + entity.getType();
-
- if (!entityMap.containsKey(key)) {
- entityMap.put(key, entity);
- } else {
- // 如果已存在,保留置信度更高的
- ExtractedEntity existing = entityMap.get(key);
- double entityConf = entity.getConfidence() != null ? entity.getConfidence() : 1.0;
- double existingConf = existing.getConfidence() != null ? existing.getConfidence() : 1.0;
- if (entityConf > existingConf) {
- entityMap.put(key, entity);
- }
- }
- }
-
- return new ArrayList<>(entityMap.values());
- }
-}
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphStoreServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphStoreServiceImpl.java
deleted file mode 100644
index 26d0e4d4..00000000
--- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphStoreServiceImpl.java
+++ /dev/null
@@ -1,929 +0,0 @@
-package org.ruoyi.service.graph.impl;
-
-import com.alibaba.fastjson.JSON;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.neo4j.driver.Driver;
-import org.neo4j.driver.Record;
-import org.neo4j.driver.Result;
-import org.neo4j.driver.Session;
-import org.neo4j.driver.types.Node;
-import org.neo4j.driver.types.Relationship;
-import org.ruoyi.config.GraphProperties;
-import org.ruoyi.domain.bo.graph.GraphEdge;
-import org.ruoyi.domain.bo.graph.GraphVertex;
-import org.ruoyi.service.graph.IGraphStoreService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-import static org.neo4j.driver.Values.parameters;
-
-/**
- * 图存储服务实现
- * 负责与 Neo4j 图数据库交互
- *
- * @author ruoyi
- * @date 2025-09-30
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
-public class GraphStoreServiceImpl implements IGraphStoreService {
-
- private final Driver neo4jDriver;
- private final GraphProperties graphProperties;
-
- // ==================== 节点操作 ====================
-
- @Override
- public boolean addVertex(GraphVertex vertex) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "CREATE (n:" + vertex.getLabel() + " {" +
- "id: $id, " +
- "name: $name, " +
- "description: $description, " +
- "knowledgeId: $knowledgeId, " +
- "docIds: $docIds, " +
- "properties: $properties, " +
- "confidence: $confidence" +
- "}) RETURN n";
-
- Result result = session.run(cypher, parameters(
- "id", vertex.getNodeId(), // ⭐ 修复:使用 nodeId 而不是 id
- "name", vertex.getName(),
- "description", vertex.getDescription(),
- "knowledgeId", vertex.getKnowledgeId(),
- "docIds", vertex.getDocIds(),
- "properties", vertex.getProperties(),
- "confidence", vertex.getConfidence()
- ));
-
- return result.hasNext();
- } catch (Exception e) {
- log.error("添加节点失败: {}", vertex, e);
- return false;
- }
- }
-
- @Override
- public int addVertices(List vertices) {
- if (vertices == null || vertices.isEmpty()) {
- return 0;
- }
-
- int successCount = 0;
- int batchSize = graphProperties.getBatchSize();
-
- try (Session session = neo4jDriver.session()) {
- // 分批处理
- for (int i = 0; i < vertices.size(); i += batchSize) {
- List batch = vertices.subList(
- i, Math.min(i + batchSize, vertices.size())
- );
-
- successCount += session.writeTransaction(tx -> {
- int count = 0;
- for (GraphVertex vertex : batch) {
- String cypher = "CREATE (n:" + vertex.getLabel() + " {" +
- "id: $id, name: $name, description: $description, " +
- "knowledgeId: $knowledgeId, docIds: $docIds, " +
- "properties: $properties, confidence: $confidence})";
-
- tx.run(cypher, parameters(
- "id", vertex.getNodeId(), // ⭐ 修复:使用 nodeId 而不是 id
- "name", vertex.getName(),
- "description", vertex.getDescription(),
- "knowledgeId", vertex.getKnowledgeId(),
- "docIds", vertex.getDocIds(),
- "properties", vertex.getProperties(),
- "confidence", vertex.getConfidence()
- ));
- count++;
- }
- return count;
- });
- }
- } catch (Exception e) {
- log.error("批量添加节点失败", e);
- }
-
- return successCount;
- }
-
- @Override
- public GraphVertex getVertex(String nodeId, String graphUuid) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (n) WHERE n.id = $nodeId AND n.knowledgeId = $graphUuid RETURN n";
-
- Result result = session.run(cypher, parameters(
- "nodeId", nodeId,
- "graphUuid", graphUuid
- ));
-
- if (result.hasNext()) {
- Record record = result.single();
- return nodeToVertex(record.get("n").asNode());
- }
- return null;
- } catch (Exception e) {
- log.error("获取节点失败: nodeId={}, graphUuid={}", nodeId, graphUuid, e);
- return null;
- }
- }
-
- @Override
- public List searchVertices(String graphUuid, String label, Integer limit) {
- try (Session session = neo4jDriver.session()) {
- StringBuilder cypher = new StringBuilder("MATCH (n");
- if (label != null && !label.isEmpty()) {
- cypher.append(":").append(label);
- }
- cypher.append(") WHERE n.knowledgeId = $graphUuid RETURN n");
-
- if (limit != null && limit > 0) {
- cypher.append(" LIMIT $limit");
- }
-
- Map params = new HashMap<>();
- params.put("graphUuid", graphUuid);
- if (limit != null && limit > 0) {
- params.put("limit", limit);
- }
-
- Result result = session.run(cypher.toString(), params);
-
- return result.stream()
- .map(record -> nodeToVertex(record.get("n").asNode()))
- .collect(Collectors.toList());
- } catch (Exception e) {
- log.error("搜索节点失败: graphUuid={}, label={}", graphUuid, label, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public List searchVerticesByName(String graphUuid, String name) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (n) WHERE n.knowledgeId = $graphUuid AND n.name CONTAINS $name RETURN n";
-
- Result result = session.run(cypher, parameters(
- "graphUuid", graphUuid,
- "name", name
- ));
-
- return result.stream()
- .map(record -> nodeToVertex(record.get("n").asNode()))
- .collect(Collectors.toList());
- } catch (Exception e) {
- log.error("按名称搜索节点失败: graphUuid={}, name={}", graphUuid, name, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public List searchVerticesByName(String keyword, String knowledgeId, Integer limit) {
- List vertices = new ArrayList<>();
-
- try (Session session = neo4jDriver.session()) {
- String cypher;
- Map params = new HashMap<>();
- params.put("keyword", keyword);
- params.put("limit", limit);
-
- if (knowledgeId != null && !knowledgeId.isEmpty()) {
- cypher = "MATCH (n {knowledgeId: $knowledgeId}) " +
- "WHERE n.name CONTAINS $keyword " +
- "RETURN n LIMIT $limit";
- params.put("knowledgeId", knowledgeId);
- } else {
- cypher = "MATCH (n) " +
- "WHERE n.name CONTAINS $keyword " +
- "RETURN n LIMIT $limit";
- }
-
- Result result = session.run(cypher, params);
-
- result.stream().forEach(record -> {
- Node node = record.get("n").asNode();
- vertices.add(nodeToVertex(node));
- });
-
- log.info("搜索到 {} 个节点,关键词: {}", vertices.size(), keyword);
- return vertices;
- } catch (Exception e) {
- log.error("按关键词搜索节点失败: keyword={}, knowledgeId={}", keyword, knowledgeId, e);
- return vertices;
- }
- }
-
- @Override
- public boolean updateVertex(GraphVertex vertex) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (n {id: $id, knowledgeId: $knowledgeId}) " +
- "SET n.name = $name, n.description = $description, " +
- "n.properties = $properties, n.confidence = $confidence " +
- "RETURN n";
-
- Result result = session.run(cypher, parameters(
- "id", vertex.getId(),
- "knowledgeId", vertex.getKnowledgeId(),
- "name", vertex.getName(),
- "description", vertex.getDescription(),
- "properties", vertex.getProperties(),
- "confidence", vertex.getConfidence()
- ));
-
- return result.hasNext();
- } catch (Exception e) {
- log.error("更新节点失败: {}", vertex, e);
- return false;
- }
- }
-
- @Override
- public boolean deleteVertex(String nodeId, String graphUuid, boolean includeEdges) {
- try (Session session = neo4jDriver.session()) {
- String cypher;
- if (includeEdges) {
- cypher = "MATCH (n {id: $nodeId, knowledgeId: $graphUuid}) DETACH DELETE n";
- } else {
- cypher = "MATCH (n {id: $nodeId, knowledgeId: $graphUuid}) DELETE n";
- }
-
- session.run(cypher, parameters(
- "nodeId", nodeId,
- "graphUuid", graphUuid
- ));
-
- return true;
- } catch (Exception e) {
- log.error("删除节点失败: nodeId={}, graphUuid={}", nodeId, graphUuid, e);
- return false;
- }
- }
-
- // ==================== 关系操作 ====================
-
- @Override
- public boolean addEdge(GraphEdge edge) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (s {id: $startNodeId, knowledgeId: $knowledgeId}) " +
- "MATCH (t {id: $endNodeId, knowledgeId: $knowledgeId}) " +
- "CREATE (s)-[r:" + edge.getLabel() + " {" +
- "id: $id, description: $description, weight: $weight, " +
- "docIds: $docIds, properties: $properties, confidence: $confidence" +
- "}]->(t) RETURN r";
-
- Result result = session.run(cypher, parameters(
- "startNodeId", edge.getSourceNodeId(),
- "endNodeId", edge.getTargetNodeId(),
- "knowledgeId", edge.getKnowledgeId(),
- "id", edge.getEdgeId(),
- "description", edge.getDescription(),
- "weight", edge.getWeight(),
- "docIds", edge.getDocIds(),
- "properties", edge.getProperties(),
- "confidence", edge.getConfidence()
- ));
-
- return result.hasNext();
- } catch (Exception e) {
- log.error("添加关系失败: {}", edge, e);
- return false;
- }
- }
-
- @Override
- public int addEdges(List edges) {
- if (edges == null || edges.isEmpty()) {
- return 0;
- }
-
- log.info("🔄 开始批量添加 {} 个关系到Neo4j", edges.size());
- int successCount = 0;
- int failedCount = 0;
- int batchSize = graphProperties.getBatchSize();
-
- try (Session session = neo4jDriver.session()) {
- for (int i = 0; i < edges.size(); i += batchSize) {
- List batch = edges.subList(
- i, Math.min(i + batchSize, edges.size())
- );
-
- int batchIndex = i / batchSize + 1;
- log.debug("处理第 {}/{} 批,本批 {} 个关系",
- batchIndex, (edges.size() + batchSize - 1) / batchSize, batch.size());
-
- successCount += session.writeTransaction(tx -> {
- int count = 0;
- for (GraphEdge edge : batch) {
- try {
- String cypher = "MATCH (s {id: $startNodeId, knowledgeId: $knowledgeId}) " +
- "MATCH (t {id: $endNodeId, knowledgeId: $knowledgeId}) " +
- "CREATE (s)-[r:" + edge.getLabel() + " {" +
- "id: $id, knowledgeId: $knowledgeId, description: $description, weight: $weight, " +
- "docIds: $docIds, properties: $properties, confidence: $confidence" +
- "}]->(t)";
-
- Result result = tx.run(cypher, parameters(
- "startNodeId", edge.getSourceNodeId(),
- "endNodeId", edge.getTargetNodeId(),
- "knowledgeId", edge.getKnowledgeId(),
- "id", edge.getEdgeId(),
- "description", edge.getDescription(),
- "weight", edge.getWeight(),
- "docIds", edge.getDocIds(),
- "properties", edge.getProperties(),
- "confidence", edge.getConfidence()
- ));
-
- // ⭐ 检查是否真的创建了关系
- if (result.consume().counters().relationshipsCreated() > 0) {
- count++;
- } else {
- log.warn("⚠️ 关系创建失败(节点未找到): {} -> {} (knowledgeId: {})",
- edge.getSourceNodeId(), edge.getTargetNodeId(), edge.getKnowledgeId());
- }
- } catch (Exception e) {
- log.error("❌ 添加单个关系失败: {} -> {}, 错误: {}",
- edge.getSourceNodeId(), edge.getTargetNodeId(), e.getMessage());
- }
- }
- return count;
- });
- }
- } catch (Exception e) {
- log.error("❌ 批量添加关系失败", e);
- }
-
- failedCount = edges.size() - successCount;
- log.info("✅ 关系添加完成: 成功 {}/{}, 失败 {}", successCount, edges.size(), failedCount);
-
- if (failedCount > 0) {
- log.warn("⚠️ 有 {} 个关系添加失败,可能原因:", failedCount);
- log.warn(" 1. 源节点或目标节点不存在");
- log.warn(" 2. sourceNodeId/targetNodeId 不匹配");
- log.warn(" 3. knowledgeId 不匹配");
- }
-
- return successCount;
- }
-
- @Override
- public GraphEdge getEdge(String edgeId, String graphUuid) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (s)-[r]->(t) " +
- "WHERE r.id = $edgeId AND r.knowledgeId = $graphUuid " +
- "RETURN s, r, t";
-
- Result result = session.run(cypher, parameters(
- "edgeId", edgeId,
- "graphUuid", graphUuid
- ));
-
- if (result.hasNext()) {
- Record record = result.single();
- return relationshipToEdge(
- record.get("s").asNode(),
- record.get("r").asRelationship(),
- record.get("t").asNode()
- );
- }
- return null;
- } catch (Exception e) {
- log.error("获取关系失败: edgeId={}, graphUuid={}", edgeId, graphUuid, e);
- return null;
- }
- }
-
- @Override
- public List searchEdges(String graphUuid, String sourceNodeId, String targetNodeId, Integer limit) {
- try (Session session = neo4jDriver.session()) {
- StringBuilder cypher = new StringBuilder("MATCH (s)-[r]->(t) WHERE r.knowledgeId = $graphUuid");
-
- Map params = new HashMap<>();
- params.put("graphUuid", graphUuid);
-
- if (sourceNodeId != null && !sourceNodeId.isEmpty()) {
- cypher.append(" AND s.id = $sourceNodeId");
- params.put("sourceNodeId", sourceNodeId);
- }
-
- if (targetNodeId != null && !targetNodeId.isEmpty()) {
- cypher.append(" AND t.id = $targetNodeId");
- params.put("targetNodeId", targetNodeId);
- }
-
- cypher.append(" RETURN s, r, t");
-
- if (limit != null && limit > 0) {
- cypher.append(" LIMIT $limit");
- params.put("limit", limit);
- }
-
- Result result = session.run(cypher.toString(), params);
-
- return result.stream()
- .map(record -> relationshipToEdge(
- record.get("s").asNode(),
- record.get("r").asRelationship(),
- record.get("t").asNode()
- ))
- .collect(Collectors.toList());
- } catch (Exception e) {
- log.error("搜索关系失败: graphUuid={}", graphUuid, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public List getNodeEdges(String nodeId, String graphUuid, String direction) {
- try (Session session = neo4jDriver.session()) {
- String cypher;
- switch (direction.toUpperCase()) {
- case "IN":
- cypher = "MATCH (s)-[r]->(t {id: $nodeId, knowledgeId: $graphUuid}) RETURN s, r, t";
- break;
- case "OUT":
- cypher = "MATCH (s {id: $nodeId, knowledgeId: $graphUuid})-[r]->(t) RETURN s, r, t";
- break;
- case "BOTH":
- default:
- cypher = "MATCH (s)-[r]-(t {id: $nodeId, knowledgeId: $graphUuid}) RETURN s, r, t";
- break;
- }
-
- Result result = session.run(cypher, parameters(
- "nodeId", nodeId,
- "graphUuid", graphUuid
- ));
-
- return result.stream()
- .map(record -> relationshipToEdge(
- record.get("s").asNode(),
- record.get("r").asRelationship(),
- record.get("t").asNode()
- ))
- .collect(Collectors.toList());
- } catch (Exception e) {
- log.error("获取节点关系失败: nodeId={}, graphUuid={}", nodeId, graphUuid, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public boolean updateEdge(GraphEdge edge) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH ()-[r {id: $id, knowledgeId: $knowledgeId}]->() " +
- "SET r.description = $description, r.weight = $weight, " +
- "r.properties = $properties, r.confidence = $confidence " +
- "RETURN r";
-
- Result result = session.run(cypher, parameters(
- "id", edge.getEdgeId(),
- "knowledgeId", edge.getKnowledgeId(),
- "description", edge.getDescription(),
- "weight", edge.getWeight(),
- "properties", edge.getProperties(),
- "confidence", edge.getConfidence()
- ));
-
- return result.hasNext();
- } catch (Exception e) {
- log.error("更新关系失败: {}", edge, e);
- return false;
- }
- }
-
- @Override
- public boolean deleteEdge(String edgeId, String graphUuid) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH ()-[r {id: $edgeId, knowledgeId: $graphUuid}]->() DELETE r";
-
- session.run(cypher, parameters(
- "edgeId", edgeId,
- "graphUuid", graphUuid
- ));
-
- return true;
- } catch (Exception e) {
- log.error("删除关系失败: edgeId={}, graphUuid={}", edgeId, graphUuid, e);
- return false;
- }
- }
-
- // ==================== 图谱管理 ====================
-
- @Override
- public boolean createGraphSchema(String graphUuid) {
- try (Session session = neo4jDriver.session()) {
- // 创建索引以提高查询性能 - 使用正确的Neo4j 4.x/5.x语法
- session.run("CREATE INDEX entity_id_index IF NOT EXISTS FOR (n:Entity) ON (n.id)");
- session.run("CREATE INDEX entity_knowledge_id_index IF NOT EXISTS FOR (n:Entity) ON (n.knowledgeId)");
- session.run("CREATE INDEX entity_name_index IF NOT EXISTS FOR (n:Entity) ON (n.name)");
-
- // 为关系也创建索引
- session.run("CREATE INDEX relation_id_index IF NOT EXISTS FOR ()-[r:RELATION]-() ON (r.id)");
- session.run("CREATE INDEX relation_type_index IF NOT EXISTS FOR ()-[r:RELATION]-() ON (r.type)");
-
- log.info("图谱Schema创建成功: graphUuid={}", graphUuid);
- return true;
- } catch (Exception e) {
- log.error("创建图谱Schema失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- public boolean deleteGraph(String graphUuid) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (n {knowledgeId: $graphUuid}) DETACH DELETE n";
-
- session.run(cypher, parameters("graphUuid", graphUuid));
-
- log.info("图谱数据删除成功: graphUuid={}", graphUuid);
- return true;
- } catch (Exception e) {
- log.error("删除图谱数据失败: graphUuid={}", graphUuid, e);
- return false;
- }
- }
-
- @Override
- public Map getGraphStatistics(String graphUuid) {
- Map stats = new HashMap<>();
-
- try (Session session = neo4jDriver.session()) {
- // 统计节点数
- Result nodeResult = session.run(
- "MATCH (n {knowledgeId: $graphUuid}) RETURN count(n) as count",
- parameters("graphUuid", graphUuid)
- );
- stats.put("nodeCount", nodeResult.single().get("count").asInt());
-
- // 统计关系数
- Result relResult = session.run(
- "MATCH ()-[r {knowledgeId: $graphUuid}]->() RETURN count(r) as count",
- parameters("graphUuid", graphUuid)
- );
- stats.put("relationshipCount", relResult.single().get("count").asInt());
-
- } catch (Exception e) {
- log.error("获取图谱统计信息失败: graphUuid={}", graphUuid, e);
- }
-
- return stats;
- }
-
- // ==================== 高级查询 ====================
-
- @Override
- public List> findPaths(String sourceNodeId, String targetNodeId, String graphUuid, Integer maxDepth) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH path = (s {id: $sourceNodeId, knowledgeId: $graphUuid})" +
- "-[*1.." + (maxDepth != null ? maxDepth : 5) + "]->" +
- "(t {id: $targetNodeId, knowledgeId: $graphUuid}) " +
- "RETURN nodes(path) as path LIMIT 10";
-
- Result result = session.run(cypher, parameters(
- "sourceNodeId", sourceNodeId,
- "targetNodeId", targetNodeId,
- "graphUuid", graphUuid
- ));
-
- List> paths = new ArrayList<>();
- result.stream().forEach(record -> {
- List path = record.get("path").asList(
- value -> nodeToVertex(value.asNode())
- );
- paths.add(path);
- });
-
- return paths;
- } catch (Exception e) {
- log.error("查找路径失败: source={}, target={}, graphUuid={}", sourceNodeId, targetNodeId, graphUuid, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public List findNeighbors(String nodeId, String graphUuid, Integer depth) {
- try (Session session = neo4jDriver.session()) {
- String cypher = "MATCH (s {id: $nodeId, knowledgeId: $graphUuid})" +
- "-[*1.." + (depth != null ? depth : 1) + "]-(neighbor) " +
- "RETURN DISTINCT neighbor";
-
- Result result = session.run(cypher, parameters(
- "nodeId", nodeId,
- "graphUuid", graphUuid
- ));
-
- return result.stream()
- .map(record -> nodeToVertex(record.get("neighbor").asNode()))
- .collect(Collectors.toList());
- } catch (Exception e) {
- log.error("查找邻居节点失败: nodeId={}, graphUuid={}", nodeId, graphUuid, e);
- return Collections.emptyList();
- }
- }
-
- @Override
- public List