fix(billing): 新增知识图谱构

1. 从非结构化文本中自动抽取实体和关系
2. 构建和管理知识图谱
3. 基于图谱的检索增强生成(GraphRAG)
4. 交互式图谱可视化
This commit is contained in:
Administrator
2025-10-23 09:48:49 +08:00
parent 827ac48826
commit 3610899f2b
56 changed files with 7876 additions and 2 deletions

View File

@@ -21,7 +21,8 @@
<module>ruoyi-chat</module>
<module>ruoyi-system</module>
<module>ruoyi-generator</module>
<module>ruoyi-wechat</module>
<module>ruoyi-wechat</module>
<module>ruoyi-graph</module>
</modules>
<properties>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>ruoyi-graph</artifactId>
<description>
知识图谱模块
</description>
<properties>
<langchain4j.version>1.0.0-beta4</langchain4j.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Langchain4j BOM 版本管理 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 通用核心模块 -->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<!-- 知识库API模块 -->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-knowledge-api</artifactId>
</dependency>
<!-- Chat API模块用于模型选择 -->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-chat-api</artifactId>
</dependency>
<!-- Neo4j Driver -->
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
</dependency>
<!-- Neo4j Cypher DSL -->
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-chat</artifactId>
</dependency>
<!-- Langchain4j 依赖(用于 DeepSeek 图谱构建) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,67 @@
package org.ruoyi.graph.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 图谱构建异步任务配置
*
* @author ruoyi
* @date 2025-10-11
*/
@Slf4j
@EnableAsync
@Configuration
public class GraphAsyncConfig {
/**
* 图谱构建专用线程池
* 用于执行图谱构建任务,避免阻塞主线程池
*/
@Bean("graphBuildExecutor")
public Executor graphBuildExecutor() {
log.info("初始化图谱构建线程池...");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数CPU核心数
int processors = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(processors);
// 最大线程数CPU核心数 * 2
executor.setMaxPoolSize(processors * 2);
// 队列容量100个任务
executor.setQueueCapacity(100);
// 线程空闲时间60秒
executor.setKeepAliveSeconds(60);
// 线程名称前缀
executor.setThreadNamePrefix("graph-build-");
// 拒绝策略:由调用线程处理
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间60秒
executor.setAwaitTerminationSeconds(60);
// 初始化
executor.initialize();
log.info("图谱构建线程池初始化完成: corePoolSize={}, maxPoolSize={}, queueCapacity={}",
processors, processors * 2, 100);
return executor;
}
}

View File

@@ -0,0 +1,113 @@
package org.ruoyi.graph.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 知识图谱配置属性
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Component
@ConfigurationProperties(prefix = "knowledge.graph")
public class GraphProperties {
/**
* 是否启用知识图谱功能
*/
private Boolean enabled = true;
/**
* 图数据库类型: neo4j 或 apache-age
*/
private String databaseType = "neo4j";
/**
* 是否自动创建索引
*/
private Boolean autoCreateIndex = true;
/**
* 批量处理大小
*/
private Integer batchSize = 1000;
/**
* 最大重试次数
*/
private Integer maxRetryCount = 3;
/**
* 实体抽取配置
*/
private ExtractionConfig extraction = new ExtractionConfig();
/**
* 查询配置
*/
private QueryConfig query = new QueryConfig();
@Data
public static class ExtractionConfig {
/**
* 置信度阈值(低于此值的实体将被过滤)
*/
private Double confidenceThreshold = 0.7;
/**
* 最大实体数量(每个文档)
*/
private Integer maxEntitiesPerDoc = 100;
/**
* 最大关系数量(每个文档)
*/
private Integer maxRelationsPerDoc = 200;
/**
* 文本分片大小(用于长文档)
*/
private Integer chunkSize = 2000;
/**
* 分片重叠大小
*/
private Integer chunkOverlap = 200;
}
@Data
public static class QueryConfig {
/**
* 默认查询限制数量
*/
private Integer defaultLimit = 100;
/**
* 最大查询限制数量
*/
private Integer maxLimit = 1000;
/**
* 路径查询最大深度
*/
private Integer maxPathDepth = 5;
/**
* 查询超时时间(秒)
*/
private Integer timeoutSeconds = 30;
/**
* 是否启用查询缓存
*/
private Boolean cacheEnabled = true;
/**
* 缓存过期时间(分钟)
*/
private Integer cacheExpireMinutes = 60;
}
}

View File

@@ -0,0 +1,70 @@
package org.ruoyi.graph.config;
import lombok.Data;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Neo4j配置类
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "neo4j")
public class Neo4jConfig {
/**
* Neo4j连接URI
* 例如: bolt://localhost:7687
*/
private String uri;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 数据库名称Neo4j 4.0+支持多数据库)
* 默认: neo4j
*/
private String database = "neo4j";
/**
* 最大连接池大小
*/
private Integer maxConnectionPoolSize = 50;
/**
* 连接超时时间(秒)
*/
private Integer connectionTimeoutSeconds = 30;
/**
* 创建Neo4j Driver Bean
*
* @return Neo4j Driver
*/
@Bean
public Driver neo4jDriver() {
return GraphDatabase.driver(
uri,
AuthTokens.basic(username, password),
org.neo4j.driver.Config.builder()
.withMaxConnectionPoolSize(maxConnectionPoolSize)
.withConnectionTimeout(connectionTimeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
.build()
);
}
}

View File

@@ -0,0 +1,103 @@
package org.ruoyi.graph.constants;
/**
* 知识图谱常量
*
* @author ruoyi
* @date 2025-09-30
*/
public class GraphConstants {
/**
* 图谱记录分隔符
*/
public static final String GRAPH_RECORD_DELIMITER = "##";
/**
* 图谱元组分隔符
*/
public static final String GRAPH_TUPLE_DELIMITER = "<|>";
/**
* 图谱完成标记
*/
public static final String GRAPH_COMPLETION_DELIMITER = "<|COMPLETE|>";
/**
* 实体类型:人物
*/
public static final String ENTITY_TYPE_PERSON = "PERSON";
/**
* 实体类型:组织机构
*/
public static final String ENTITY_TYPE_ORGANIZATION = "ORGANIZATION";
/**
* 实体类型:地点
*/
public static final String ENTITY_TYPE_LOCATION = "LOCATION";
/**
* 实体类型:概念
*/
public static final String ENTITY_TYPE_CONCEPT = "CONCEPT";
/**
* 实体类型:事件
*/
public static final String ENTITY_TYPE_EVENT = "EVENT";
/**
* 实体类型:产品
*/
public static final String ENTITY_TYPE_PRODUCT = "PRODUCT";
/**
* 实体类型:技术
*/
public static final String ENTITY_TYPE_TECHNOLOGY = "TECHNOLOGY";
/**
* 默认实体抽取类型列表
*/
public static final String[] DEFAULT_ENTITY_TYPES = {
ENTITY_TYPE_PERSON,
ENTITY_TYPE_ORGANIZATION,
ENTITY_TYPE_LOCATION,
ENTITY_TYPE_CONCEPT,
ENTITY_TYPE_EVENT,
ENTITY_TYPE_PRODUCT,
ENTITY_TYPE_TECHNOLOGY
};
/**
* 元数据键知识库UUID
*/
public static final String METADATA_KB_UUID = "kb_uuid";
/**
* 元数据键知识库条目UUID
*/
public static final String METADATA_KB_ITEM_UUID = "kb_item_uuid";
/**
* 元数据键文档UUID
*/
public static final String METADATA_DOC_UUID = "doc_uuid";
/**
* 元数据键片段UUID
*/
public static final String METADATA_SEGMENT_UUID = "segment_uuid";
/**
* RAG最大片段大小token数
*/
public static final int RAG_MAX_SEGMENT_SIZE_IN_TOKENS = 512;
/**
* RAG片段重叠大小token数
*/
public static final int RAG_SEGMENT_OVERLAP_IN_TOKENS = 50;
}

View File

@@ -0,0 +1,416 @@
package org.ruoyi.graph.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.graph.domain.GraphBuildTask;
import org.ruoyi.graph.domain.GraphInstance;
import org.ruoyi.graph.enums.GraphStatusEnum;
import org.ruoyi.graph.service.IGraphBuildTaskService;
import org.ruoyi.graph.service.IGraphInstanceService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 图谱实例管理控制器
*
* @author ruoyi
* @date 2025-09-30
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/graph/instance")
@Tag(name = "图谱实例管理", description = "知识图谱实例的创建、查询、更新、删除")
public class GraphInstanceController extends BaseController {
private final IGraphInstanceService graphInstanceService;
private final IGraphBuildTaskService buildTaskService;
/**
* 辅助方法根据ID或UUID获取图谱实例
*/
private GraphInstance getInstanceByIdOrUuid(String id) {
GraphInstance instance = null;
// 尝试作为数字ID查询
try {
Long numericId = Long.parseLong(id);
instance = graphInstanceService.getById(numericId);
} catch (NumberFormatException e) {
// 不是数字尝试作为UUID查询
instance = graphInstanceService.getByUuid(id);
}
return instance;
}
/**
* 创建图谱实例
*/
@Operation(summary = "创建图谱实例")
@PostMapping
public R<GraphInstance> createInstance(@RequestBody GraphInstance graphInstance) {
try {
if (graphInstance.getKnowledgeId() == null || graphInstance.getKnowledgeId().trim().isEmpty()) {
return R.fail("知识库ID不能为空");
}
if (graphInstance.getInstanceName() == null || graphInstance.getInstanceName().trim().isEmpty()) {
return R.fail("图谱名称不能为空");
}
// 创建基础实例
GraphInstance instance = graphInstanceService.createInstance(
graphInstance.getKnowledgeId(),
graphInstance.getInstanceName(),
graphInstance.getConfig()
);
// 设置扩展属性
boolean needUpdate = false;
if (graphInstance.getModelName() != null) {
instance.setModelName(graphInstance.getModelName());
needUpdate = true;
}
if (graphInstance.getEntityTypes() != null) {
instance.setEntityTypes(graphInstance.getEntityTypes());
needUpdate = true;
}
if (graphInstance.getRelationTypes() != null) {
instance.setRelationTypes(graphInstance.getRelationTypes());
needUpdate = true;
}
if (graphInstance.getRemark() != null) {
instance.setRemark(graphInstance.getRemark());
needUpdate = true;
}
// 如果有扩展属性,更新到数据库
if (needUpdate) {
graphInstanceService.updateInstance(instance);
}
return R.ok(instance);
} catch (Exception e) {
return R.fail("创建图谱实例失败: " + e.getMessage());
}
}
/**
* 更新图谱实例
*/
@Operation(summary = "更新图谱实例")
@PutMapping
public R<GraphInstance> updateInstance(@RequestBody GraphInstance graphInstance) {
try {
if (graphInstance.getId() == null && (graphInstance.getGraphUuid() == null || graphInstance.getGraphUuid().trim().isEmpty())) {
return R.fail("图谱ID不能为空");
}
// 如果有 instanceName更新基本信息
if (graphInstance.getInstanceName() != null) {
// 这里可以添加更新实例名称的逻辑
}
// 更新配置
if (graphInstance.getConfig() != null) {
graphInstanceService.updateConfig(graphInstance.getGraphUuid(), graphInstance.getConfig());
}
// 更新模型名称、实体类型、关系类型等
// 注意:这里需要在 Service 层实现完整的更新逻辑
GraphInstance instance = graphInstanceService.getByUuid(graphInstance.getGraphUuid());
return R.ok(instance);
} catch (Exception e) {
return R.fail("更新图谱实例失败: " + e.getMessage());
}
}
/**
* 根据ID或UUID获取图谱实例
*/
@Operation(summary = "获取图谱实例")
@GetMapping("/{id}")
public R<GraphInstance> getByUuid(@PathVariable String id) {
try {
GraphInstance instance = getInstanceByIdOrUuid(id);
if (instance == null) {
return R.fail("图谱实例不存在");
}
return R.ok(instance);
} catch (Exception e) {
return R.fail("获取图谱实例失败: " + e.getMessage());
}
}
/**
* 获取图谱实例列表(支持分页和条件查询)
*/
@Operation(summary = "获取图谱实例列表")
@GetMapping("/list")
public R<Map<String, Object>> list(
@RequestParam(required = false) String instanceName,
@RequestParam(required = false) String knowledgeId,
@RequestParam(required = false) String status,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
try {
// 使用枚举转换前端状态字符串为数字状态码
Integer graphStatus = GraphStatusEnum.getCodeByStatusKey(status);
// 创建分页对象
Page<GraphInstance> page = new Page<>(pageNum, pageSize);
// 调用 Service 层分页查询
Page<GraphInstance> result = graphInstanceService.queryPage(page, instanceName, knowledgeId, graphStatus);
// 构造返回结果
Map<String, Object> data = new HashMap<>();
data.put("rows", result.getRecords());
data.put("total", result.getTotal());
return R.ok(data);
} catch (Exception e) {
return R.fail("获取图谱列表失败: " + e.getMessage());
}
}
/**
* 根据知识库ID获取图谱列表
*/
@Operation(summary = "获取知识库的图谱列表")
@GetMapping("/knowledge/{knowledgeId}")
public R<List<GraphInstance>> listByKnowledge(@PathVariable String knowledgeId) {
try {
List<GraphInstance> instances = graphInstanceService.listByKnowledgeId(knowledgeId);
return R.ok(instances);
} catch (Exception e) {
return R.fail("获取图谱列表失败: " + e.getMessage());
}
}
/**
* 更新图谱状态
*/
@Operation(summary = "更新图谱状态")
@PutMapping("/status/{graphUuid}")
public R<Void> updateStatus(
@PathVariable String graphUuid,
@RequestParam Integer status) {
try {
boolean success = graphInstanceService.updateStatus(graphUuid, status);
return success ? R.ok() : R.fail("更新状态失败");
} catch (Exception e) {
return R.fail("更新状态失败: " + e.getMessage());
}
}
/**
* 更新图谱统计信息
*/
@Operation(summary = "更新图谱统计")
@PutMapping("/counts/{graphUuid}")
public R<Void> updateCounts(
@PathVariable String graphUuid,
@RequestParam Integer nodeCount,
@RequestParam Integer relationshipCount) {
try {
boolean success = graphInstanceService.updateCounts(graphUuid, nodeCount, relationshipCount);
return success ? R.ok() : R.fail("更新统计失败");
} catch (Exception e) {
return R.fail("更新统计失败: " + e.getMessage());
}
}
/**
* 更新图谱配置
*/
@Operation(summary = "更新图谱配置")
@PutMapping("/config/{graphUuid}")
public R<Void> updateConfig(
@PathVariable String graphUuid,
@RequestBody Map<String, String> request) {
try {
String config = request.get("config");
boolean success = graphInstanceService.updateConfig(graphUuid, config);
return success ? R.ok() : R.fail("更新配置失败");
} catch (Exception e) {
return R.fail("更新配置失败: " + e.getMessage());
}
}
/**
* 删除图谱实例(软删除)
*/
@Operation(summary = "删除图谱实例")
@DeleteMapping("/{id}")
public R<Void> deleteInstance(@PathVariable String id) {
try {
// 获取图谱实例
GraphInstance instance = getInstanceByIdOrUuid(id);
if (instance == null) {
return R.fail("图谱实例不存在");
}
boolean success = graphInstanceService.deleteInstance(instance.getGraphUuid());
return success ? R.ok() : R.fail("删除失败");
} catch (Exception e) {
return R.fail("删除图谱实例失败: " + e.getMessage());
}
}
/**
* 物理删除图谱实例及其数据
*/
@Operation(summary = "彻底删除图谱")
@DeleteMapping("/permanent/{graphUuid}")
public R<Void> deleteInstanceAndData(@PathVariable String graphUuid) {
try {
boolean success = graphInstanceService.deleteInstanceAndData(graphUuid);
return success ? R.ok() : R.fail("删除失败");
} catch (Exception e) {
return R.fail("彻底删除图谱失败: " + e.getMessage());
}
}
/**
* 获取图谱统计信息
*/
@Operation(summary = "获取图谱统计")
@GetMapping("/stats/{graphUuid}")
public R<Map<String, Object>> getStatistics(@PathVariable String graphUuid) {
try {
Map<String, Object> stats = graphInstanceService.getStatistics(graphUuid);
return R.ok(stats);
} catch (Exception e) {
return R.fail("获取统计信息失败: " + e.getMessage());
}
}
/**
* 构建图谱(全量构建知识库)
*/
@Operation(summary = "构建图谱")
@PostMapping("/build/{id}")
public R<GraphBuildTask> buildGraph(@PathVariable String id) {
try {
// 获取图谱实例
GraphInstance instance = getInstanceByIdOrUuid(id);
if (instance == null) {
return R.fail("图谱实例不存在");
}
// 更新状态为构建中
graphInstanceService.updateStatus(instance.getGraphUuid(), 10); // 10=构建中
// 创建构建任务(全量构建)
GraphBuildTask task = buildTaskService.createTask(
instance.getGraphUuid(),
instance.getKnowledgeId(),
null, // docId=null 表示全量构建
1 // taskType=1 全量构建
);
// 异步启动任务
buildTaskService.startTask(task.getTaskUuid());
return R.ok(task);
} catch (Exception e) {
return R.fail("启动构建任务失败: " + e.getMessage());
}
}
/**
* 重建图谱(清空后重新构建)
*/
@Operation(summary = "重建图谱")
@PostMapping("/rebuild/{id}")
public R<GraphBuildTask> rebuildGraph(@PathVariable String id) {
try {
// 获取图谱实例
GraphInstance instance = getInstanceByIdOrUuid(id);
if (instance == null) {
return R.fail("图谱实例不存在");
}
// 更新状态为构建中
graphInstanceService.updateStatus(instance.getGraphUuid(), 10); // 10=构建中
// 创建重建任务
GraphBuildTask task = buildTaskService.createTask(
instance.getGraphUuid(),
instance.getKnowledgeId(),
null, // docId=null 表示全量
2 // taskType=2 重建
);
// 异步启动任务
buildTaskService.startTask(task.getTaskUuid());
return R.ok(task);
} catch (Exception e) {
return R.fail("启动重建任务失败: " + e.getMessage());
}
}
/**
* 获取构建状态
*/
@Operation(summary = "获取构建状态")
@GetMapping("/status/{id}")
public R<Map<String, Object>> getBuildStatus(@PathVariable String id) {
try {
// 获取图谱实例
GraphInstance instance = getInstanceByIdOrUuid(id);
if (instance == null) {
return R.fail("图谱实例不存在");
}
// 获取最新的构建任务
GraphBuildTask latestTask = buildTaskService.getLatestTask(instance.getGraphUuid());
Map<String, Object> result = new HashMap<>();
result.put("graphStatus", instance.getGraphStatus());
result.put("nodeCount", instance.getNodeCount());
result.put("relationshipCount", instance.getRelationshipCount());
if (latestTask != null) {
result.put("taskStatus", latestTask.getTaskStatus());
// ⭐ 确保 progress 不为 null前端期望是 number 类型
Integer progress = latestTask.getProgress();
result.put("progress", progress != null ? progress : 0);
result.put("errorMessage", latestTask.getErrorMessage());
// 转换状态字符串(兼容前端)
String status = "NOT_BUILT";
if (instance.getGraphStatus() == 10) status = "BUILDING";
else if (instance.getGraphStatus() == 20) status = "COMPLETED";
else if (instance.getGraphStatus() == 30) status = "FAILED";
result.put("status", status);
} else {
// ⭐ 如果没有任务,也返回默认值
result.put("taskStatus", null);
result.put("progress", 0);
result.put("errorMessage", null);
result.put("status", "NOT_BUILT");
}
return R.ok(result);
} catch (Exception e) {
return R.fail("获取构建状态失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,225 @@
package org.ruoyi.graph.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.graph.domain.GraphEdge;
import org.ruoyi.graph.domain.GraphVertex;
import org.ruoyi.graph.dto.GraphExtractionResult;
import org.ruoyi.graph.service.IGraphExtractionService;
import org.ruoyi.graph.service.IGraphRAGService;
import org.ruoyi.graph.service.IGraphStoreService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 图谱查询控制器
*
* @author ruoyi
* @date 2025-09-30
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/graph/query")
@Tag(name = "图谱查询", description = "知识图谱查询相关接口")
public class GraphQueryController extends BaseController {
private final IGraphStoreService graphStoreService;
private final IGraphExtractionService graphExtractionService;
private final IGraphRAGService graphRAGService;
/**
* 获取知识库的图谱数据
*/
@Operation(summary = "获取知识库图谱")
@GetMapping("/knowledge/{knowledgeId}")
public R<Map<String, Object>> getGraphByKnowledge(
@PathVariable String knowledgeId,
@RequestParam(defaultValue = "100") Integer limit) {
try {
// 查询节点
List<GraphVertex> vertices = graphStoreService.queryVerticesByKnowledgeId(knowledgeId, limit);
// 查询关系
List<GraphEdge> edges = graphStoreService.queryEdgesByKnowledgeId(knowledgeId, limit);
Map<String, Object> result = new HashMap<>();
result.put("vertices", vertices);
result.put("edges", edges);
result.put("vertexCount", vertices.size());
result.put("edgeCount", edges.size());
return R.ok(result);
} catch (Exception e) {
return R.fail("获取图谱数据失败: " + e.getMessage());
}
}
/**
* 搜索实体节点
*/
@Operation(summary = "搜索实体")
@GetMapping("/search/entity")
public R<List<GraphVertex>> searchEntity(
@RequestParam String keyword,
@RequestParam(required = false) String knowledgeId,
@RequestParam(defaultValue = "20") Integer limit) {
try {
List<GraphVertex> vertices = graphStoreService.searchVerticesByName(keyword, knowledgeId, limit);
return R.ok(vertices);
} catch (Exception e) {
return R.fail("搜索实体失败: " + e.getMessage());
}
}
/**
* 查询实体的邻居节点
*/
@Operation(summary = "查询邻居节点")
@GetMapping("/neighbors/{nodeId}")
public R<List<GraphVertex>> getNeighbors(
@PathVariable String nodeId,
@RequestParam(required = false) String knowledgeId,
@RequestParam(defaultValue = "20") Integer limit) {
try {
List<GraphVertex> neighbors = graphStoreService.getNeighbors(nodeId, knowledgeId, limit);
return R.ok(neighbors);
} catch (Exception e) {
return R.fail("查询邻居节点失败: " + e.getMessage());
}
}
/**
* 查询两个实体之间的路径
*/
@Operation(summary = "查询实体路径")
@GetMapping("/path")
public R<List<List<GraphVertex>>> findPath(
@RequestParam String startNodeId,
@RequestParam String endNodeId,
@RequestParam(defaultValue = "5") Integer maxDepth) {
try {
List<List<GraphVertex>> paths = graphStoreService.findPaths(startNodeId, endNodeId, maxDepth);
return R.ok(paths);
} catch (Exception e) {
return R.fail("查询路径失败: " + e.getMessage());
}
}
/**
* 从文本抽取实体和关系(测试用)
*/
@Operation(summary = "文本实体抽取")
@PostMapping("/extract")
public R<GraphExtractionResult> extractFromText(@RequestBody Map<String, String> request) {
try {
String text = request.get("text");
if (text == null || text.trim().isEmpty()) {
return R.fail("文本不能为空");
}
String modelName = request.get("modelName");
GraphExtractionResult result;
if (modelName != null && !modelName.trim().isEmpty()) {
result = graphExtractionService.extractFromTextWithModel(text, modelName);
} else {
result = graphExtractionService.extractFromText(text);
}
return R.ok(result);
} catch (Exception e) {
return R.fail("实体抽取失败: " + e.getMessage());
}
}
/**
* 将文本入库到图谱
*/
@Operation(summary = "文本入库")
@PostMapping("/ingest")
public R<GraphExtractionResult> ingestText(@RequestBody Map<String, Object> request) {
try {
String text = (String) request.get("text");
String knowledgeId = (String) request.get("knowledgeId");
String modelName = (String) request.get("modelName");
if (text == null || text.trim().isEmpty()) {
return R.fail("文本不能为空");
}
if (knowledgeId == null || knowledgeId.trim().isEmpty()) {
return R.fail("知识库ID不能为空");
}
@SuppressWarnings("unchecked")
Map<String, Object> metadata = (Map<String, Object>) request.get("metadata");
GraphExtractionResult result;
if (modelName != null && !modelName.trim().isEmpty()) {
result = graphRAGService.ingestTextWithModel(text, knowledgeId, metadata, modelName);
} else {
result = graphRAGService.ingestText(text, knowledgeId, metadata);
}
return R.ok(result);
} catch (Exception e) {
return R.fail("文本入库失败: " + e.getMessage());
}
}
/**
* 基于图谱检索
*/
@Operation(summary = "图谱检索")
@GetMapping("/retrieve")
public R<String> retrieveFromGraph(
@RequestParam String query,
@RequestParam String knowledgeId,
@RequestParam(defaultValue = "10") Integer maxResults) {
try {
String result = graphRAGService.retrieveFromGraph(query, knowledgeId, maxResults);
return R.ok(result);
} catch (Exception e) {
return R.fail("图谱检索失败: " + e.getMessage());
}
}
/**
* 删除知识库的图谱数据
*/
@Operation(summary = "删除图谱数据")
@DeleteMapping("/knowledge/{knowledgeId}")
public R<Void> deleteGraphData(@PathVariable String knowledgeId) {
try {
boolean success = graphRAGService.deleteGraphData(knowledgeId);
return success ? R.ok() : R.fail("删除失败");
} catch (Exception e) {
return R.fail("删除图谱数据失败: " + e.getMessage());
}
}
/**
* 获取图谱统计信息
*/
@Operation(summary = "图谱统计")
@GetMapping("/stats/{knowledgeId}")
public R<Map<String, Object>> getGraphStats(@PathVariable String knowledgeId) {
try {
Map<String, Object> stats = graphStoreService.getStatistics(knowledgeId);
return R.ok(stats);
} catch (Exception e) {
return R.fail("获取统计信息失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,145 @@
package org.ruoyi.graph.controller;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.graph.config.GraphProperties;
import org.ruoyi.graph.config.Neo4jConfig;
import org.ruoyi.graph.util.Neo4jTestUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* Neo4j测试控制器
* 仅用于开发环境测试Neo4j连接
*
* @author ruoyi
* @date 2025-09-30
*/
@RestController
@RequestMapping("/graph/test")
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
public class Neo4jTestController {
private final Neo4jTestUtil neo4jTestUtil;
private final Neo4jConfig neo4jConfig;
private final GraphProperties graphProperties;
@GetMapping("/connection")
public R<Map<String, Object>> testConnection() {
Map<String, Object> result = neo4jTestUtil.testConnection();
if ("SUCCESS".equals(result.get("connection"))) {
return R.ok("Neo4j连接成功", result);
} else {
return R.fail(result.get("error").toString());
}
}
@GetMapping("/config")
public R<Map<String, Object>> getConfig() {
Map<String, Object> config = new HashMap<>();
config.put("neo4j", Map.of(
"uri", neo4jConfig.getUri(),
"username", neo4jConfig.getUsername(),
"database", neo4jConfig.getDatabase(),
"maxConnectionPoolSize", neo4jConfig.getMaxConnectionPoolSize()
));
config.put("graph", Map.of(
"enabled", graphProperties.getEnabled(),
"databaseType", graphProperties.getDatabaseType(),
"batchSize", graphProperties.getBatchSize(),
"extraction", graphProperties.getExtraction(),
"query", graphProperties.getQuery()
));
return R.ok(config);
}
@PostMapping("/node")
public R<Map<String, Object>> createTestNode(@RequestParam String name) {
Map<String, Object> result = neo4jTestUtil.createTestNode(name);
if (Boolean.TRUE.equals(result.get("success"))) {
return R.ok("测试节点创建成功", result);
} else {
return R.fail(result.get("error").toString());
}
}
@GetMapping("/node/{name}")
public R<Map<String, Object>> queryTestNode(@PathVariable String name) {
Map<String, Object> result = neo4jTestUtil.queryTestNode(name);
if (Boolean.TRUE.equals(result.get("found"))) {
return R.ok("节点查询成功", result);
} else {
return R.ok("未找到节点", result);
}
}
@PostMapping("/relationship")
public R<Map<String, Object>> createTestRelationship(
@RequestParam String source,
@RequestParam String target) {
Map<String, Object> result = neo4jTestUtil.createTestRelationship(source, target);
if (Boolean.TRUE.equals(result.get("success"))) {
return R.ok("测试关系创建成功", result);
} else {
return R.fail(result.get("error").toString());
}
}
@DeleteMapping("/nodes")
public R<Map<String, Object>> deleteAllTestNodes() {
Map<String, Object> result = neo4jTestUtil.deleteAllTestNodes();
if (Boolean.TRUE.equals(result.get("success"))) {
return R.ok("测试节点清理完成", result);
} else {
return R.fail(result.get("error").toString());
}
}
@GetMapping("/statistics")
public R<Map<String, Object>> getStatistics() {
Map<String, Object> result = neo4jTestUtil.getStatistics();
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
@GetMapping("/health")
public R<String> health() {
Map<String, Object> result = neo4jTestUtil.testConnection();
if ("SUCCESS".equals(result.get("connection"))) {
return R.ok("Neo4j服务正常");
} else {
return R.fail("Neo4j服务异常: " + result.get("error"));
}
}
/**
* 调试关系查询 - 查看指定知识库的所有关系
*/
@GetMapping("/debug/relationships/{knowledgeId}")
public R<Map<String, Object>> debugRelationships(@PathVariable String knowledgeId) {
Map<String, Object> result = neo4jTestUtil.debugRelationships(knowledgeId);
return R.ok(result);
}
}

View File

@@ -0,0 +1,107 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.ruoyi.core.domain.BaseEntity;
import java.io.Serial;
import java.util.Date;
/**
* 图谱构建任务对象 graph_build_task
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@TableName("graph_build_task")
public class GraphBuildTask extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 任务UUID
*/
private String taskUuid;
/**
* 图谱UUID
*/
private String graphUuid;
/**
* 知识库ID
*/
private String knowledgeId;
/**
* 文档ID可选null表示全量构建
*/
private String docId;
/**
* 任务类型1全量构建、2增量更新、3重建
*/
private Integer taskType;
/**
* 任务状态1待执行、2执行中、3成功、4失败
*/
private Integer taskStatus;
/**
* 进度百分比0-100
*/
private Integer progress;
/**
* 总文档数
*/
private Integer totalDocs;
/**
* 已处理文档数
*/
private Integer processedDocs;
/**
* 提取的实体数
*/
private Integer extractedEntities;
/**
* 提取的关系数
*/
private Integer extractedRelations;
/**
* 错误信息
*/
private String errorMessage;
/**
* 结果摘要(JSON格式)
*/
private String resultSummary;
/**
* 开始时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/**
* 结束时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
}

View File

@@ -0,0 +1,137 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 图关系实体
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("graph_edge")
public class GraphEdge implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 图谱UUID
*/
private String graphUuid;
/**
* 关系唯一标识Neo4j中的关系ID
*/
private String edgeId;
/**
* 关系标签(类型)
*/
private String label;
/**
* 源节点ID
*/
private String sourceNodeId;
/**
* 源节点名称
*/
private String sourceName;
/**
* 目标节点ID
*/
private String targetNodeId;
/**
* 目标节点名称
*/
private String targetName;
/**
* 关系类型编码
*/
private String relationType;
/**
* 关系描述
*/
private String description;
/**
* 关系权重0.0-1.0
*/
private Double weight;
/**
* 置信度0.0-1.0
*/
private Double confidence;
/**
* 来源知识库ID
*/
private String knowledgeId;
/**
* 来源文档ID列表JSON格式
*/
private String docIds;
/**
* 来源片段ID列表JSON格式
*/
private String fragmentIds;
/**
* 文本段ID关联到具体的文本段
*/
@JsonProperty("text_segment_id")
private String textSegmentId;
/**
* 其他属性JSON格式
*/
private String properties;
/**
* 元数据JSON格式
*/
private Map<String, Object> metadata;
/**
* 源节点元数据
*/
private Map<String, Object> sourceMetadata;
/**
* 目标节点元数据
*/
private Map<String, Object> targetMetadata;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,65 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.core.domain.BaseEntity;
import java.io.Serial;
/**
* 图谱实体类型定义对象 graph_entity_type
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("graph_entity_type")
public class GraphEntityType extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 实体类型名称
*/
private String typeName;
/**
* 类型编码
*/
private String typeCode;
/**
* 描述
*/
private String description;
/**
* 可视化颜色
*/
private String color;
/**
* 图标
*/
private String icon;
/**
* 显示顺序
*/
private Integer sort;
/**
* 是否启用0否 1是
*/
private Integer isEnable;
}

View File

@@ -0,0 +1,117 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.core.domain.BaseEntity;
import java.io.Serial;
/**
* 知识图谱实例对象 knowledge_graph_instance
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("knowledge_graph_instance")
public class GraphInstance extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 图谱UUID
*/
private String graphUuid;
/**
* 关联knowledge_info.kid
*/
private String knowledgeId;
/**
* 图谱名称
*/
private String graphName;
/**
* 图谱实例名称(前端使用,不映射到数据库)
*/
@TableField(exist = false)
private String instanceName;
/**
* 构建状态10构建中、20已完成、30失败
*/
private Integer graphStatus;
/**
* 节点数量
*/
private Integer nodeCount;
/**
* 关系数量
*/
private Integer relationshipCount;
/**
* 图谱配置(JSON格式)
*/
private String config;
/**
* LLM模型名称
*/
private String modelName;
/**
* 实体类型(逗号分隔)
*/
private String entityTypes;
/**
* 关系类型(逗号分隔)
*/
private String relationTypes;
/**
* 错误信息
*/
private String errorMessage;
/**
* 删除标志0代表存在 1代表删除
*/
private String delFlag;
/**
* 备注
*/
private String remark;
/**
* 获取实例名称(兼容前端)
*/
public String getInstanceName() {
return instanceName != null ? instanceName : graphName;
}
/**
* 设置实例名称同步到graphName
*/
public void setInstanceName(String instanceName) {
this.instanceName = instanceName;
this.graphName = instanceName;
}
}

View File

@@ -0,0 +1,65 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.core.domain.BaseEntity;
import java.io.Serial;
/**
* 图谱关系类型定义对象 graph_relation_type
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("graph_relation_type")
public class GraphRelationType extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 关系名称
*/
private String relationName;
/**
* 关系编码
*/
private String relationCode;
/**
* 描述
*/
private String description;
/**
* 关系方向0双向、1单向
*/
private Integer direction;
/**
* 可视化样式(JSON格式)
*/
private String style;
/**
* 显示顺序
*/
private Integer sort;
/**
* 是否启用0否 1是
*/
private Integer isEnable;
}

View File

@@ -0,0 +1,112 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 图节点实体
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("graph_vertex")
public class GraphVertex implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 图谱UUID
*/
private String graphUuid;
/**
* 节点唯一标识Neo4j中的节点ID
*/
private String nodeId;
/**
* 节点名称
*/
private String name;
/**
* 节点标签(类型)
*/
private String label;
/**
* 节点类型编码
*/
private String type;
/**
* 描述信息
*/
private String description;
/**
* 置信度0.0-1.0
*/
private Double confidence;
/**
* 来源知识库ID
*/
private String knowledgeId;
/**
* 来源文档ID列表JSON格式
*/
private String docIds;
/**
* 来源片段ID列表JSON格式
*/
private String fragmentIds;
/**
* 文本段ID关联到具体的文本段
*/
@JsonProperty("text_segment_id")
private String textSegmentId;
/**
* 别名列表JSON格式
*/
private String aliases;
/**
* 其他属性JSON格式
*/
private String properties;
/**
* 元数据JSON格式
*/
private Map<String, Object> metadata;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,97 @@
package org.ruoyi.graph.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.core.domain.BaseEntity;
import java.io.Serial;
/**
* 知识图谱片段实体
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("knowledge_base_graph_segment")
public class KnowledgeBaseGraphSegment extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 片段UUID
*/
private String uuid;
/**
* 知识库UUID
*/
private String kbUuid;
/**
* 知识库条目UUID
*/
private String kbItemUuid;
/**
* 文档UUID
*/
private String docUuid;
/**
* 片段文本内容
*/
private String segmentText;
/**
* 片段索引(第几个片段)
*/
private Integer chunkIndex;
/**
* 总片段数
*/
private Integer totalChunks;
/**
* 抽取状态0-待处理 1-处理中 2-已完成 3-失败
*/
private Integer extractionStatus;
/**
* 抽取的实体数量
*/
private Integer entityCount;
/**
* 抽取的关系数量
*/
private Integer relationCount;
/**
* 消耗的token数
*/
private Integer tokenUsed;
/**
* 错误信息
*/
private String errorMessage;
/**
* 用户ID
*/
private Long userId;
}

View File

@@ -0,0 +1,45 @@
package org.ruoyi.graph.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 抽取的实体
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExtractedEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实体名称
*/
private String name;
/**
* 实体类型
*/
private String type;
/**
* 实体描述
*/
private String description;
/**
* 置信度0.0-1.0
*/
private Double confidence;
}

View File

@@ -0,0 +1,50 @@
package org.ruoyi.graph.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 抽取的关系
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExtractedRelation implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 源实体名称
*/
private String sourceEntity;
/**
* 目标实体名称
*/
private String targetEntity;
/**
* 关系描述
*/
private String description;
/**
* 关系强度0-10
*/
private Integer strength;
/**
* 置信度0.0-1.0
*/
private Double confidence;
}

View File

@@ -0,0 +1,56 @@
package org.ruoyi.graph.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 图谱抽取结果
*
* @author ruoyi
* @date 2025-09-30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GraphExtractionResult implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 抽取的实体列表
*/
private List<ExtractedEntity> entities;
/**
* 抽取的关系列表
*/
private List<ExtractedRelation> relations;
/**
* 原始LLM响应
*/
private String rawResponse;
/**
* 消耗的token数
*/
private Integer tokenUsed;
/**
* 是否成功
*/
private Boolean success;
/**
* 错误信息
*/
private String errorMessage;
}

View File

@@ -0,0 +1,78 @@
package org.ruoyi.graph.enums;
import lombok.Getter;
/**
* 图谱构建状态枚举
*
* @author ruoyi
* @date 2025-09-30
*/
@Getter
public enum GraphStatusEnum {
/**
* 未构建
*/
NOT_BUILT(0, "未构建", "NOT_BUILT"),
/**
* 构建中
*/
BUILDING(10, "构建中", "BUILDING"),
/**
* 已完成
*/
COMPLETED(20, "已完成", "COMPLETED"),
/**
* 失败
*/
FAILED(30, "失败", "FAILED");
private final Integer code;
private final String description;
private final String statusKey;
GraphStatusEnum(Integer code, String description, String statusKey) {
this.code = code;
this.description = description;
this.statusKey = statusKey;
}
/**
* 根据code获取枚举
*/
public static GraphStatusEnum getByCode(Integer code) {
for (GraphStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
/**
* 根据前端状态字符串获取状态码
*/
public static Integer getCodeByStatusKey(String statusKey) {
if (statusKey == null || statusKey.trim().isEmpty()) {
return null;
}
for (GraphStatusEnum status : values()) {
if (status.getStatusKey().equals(statusKey)) {
return status.getCode();
}
}
return null;
}
/**
* 根据状态码获取前端状态字符串
*/
public static String getStatusKeyByCode(Integer code) {
GraphStatusEnum status = getByCode(code);
return status != null ? status.getStatusKey() : "NOT_BUILT";
}
}

View File

@@ -0,0 +1,53 @@
package org.ruoyi.graph.enums;
import lombok.Getter;
/**
* 构建任务状态枚举
*
* @author ruoyi
* @date 2025-09-30
*/
@Getter
public enum TaskStatusEnum {
/**
* 待执行
*/
PENDING(1, "待执行"),
/**
* 执行中
*/
RUNNING(2, "执行中"),
/**
* 成功
*/
SUCCESS(3, "成功"),
/**
* 失败
*/
FAILED(4, "失败");
private final Integer code;
private final String description;
TaskStatusEnum(Integer code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据code获取枚举
*/
public static TaskStatusEnum getByCode(Integer code) {
for (TaskStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
}

View File

@@ -0,0 +1,48 @@
package org.ruoyi.graph.enums;
import lombok.Getter;
/**
* 构建任务类型枚举
*
* @author ruoyi
* @date 2025-09-30
*/
@Getter
public enum TaskTypeEnum {
/**
* 全量构建
*/
FULL_BUILD(1, "全量构建"),
/**
* 增量更新
*/
INCREMENTAL_UPDATE(2, "增量更新"),
/**
* 重建
*/
REBUILD(3, "重建");
private final Integer code;
private final String description;
TaskTypeEnum(Integer code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据code获取枚举
*/
public static TaskTypeEnum getByCode(Integer code) {
for (TaskTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
package org.ruoyi.graph.factory;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.graph.service.llm.IGraphLLMService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 图谱LLM服务工厂类
* 参考 ruoyi-chat 的 ChatServiceFactory 设计
* 根据模型类别自动选择对应的LLM服务实现
*
* @author ruoyi
* @date 2025-10-11
*/
@Slf4j
@Component
public class GraphLLMServiceFactory implements ApplicationContextAware {
private final Map<String, IGraphLLMService> llmServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 初始化时收集所有 IGraphLLMService 的实现
Map<String, IGraphLLMService> serviceMap = applicationContext.getBeansOfType(IGraphLLMService.class);
for (IGraphLLMService service : serviceMap.values()) {
if (service != null) {
String category = service.getCategory();
llmServiceMap.put(category, service);
log.info("注册图谱LLM服务: category={}, service={}",
category, service.getClass().getSimpleName());
}
}
log.info("图谱LLM服务工厂初始化完成共注册 {} 个服务", llmServiceMap.size());
}
/**
* 根据模型类别获取对应的LLM服务实现
*
* @param category 模型类别(如: openai, qwen, zhipu
* @return LLM服务实现
* @throws IllegalArgumentException 如果不支持该类别
*/
public IGraphLLMService getLLMService(String category) {
IGraphLLMService service = llmServiceMap.get(category);
if (service == null) {
log.error("不支持的模型类别: {}, 可用类别: {}", category, llmServiceMap.keySet());
throw new IllegalArgumentException("不支持的模型类别: " + category +
", 可用类别: " + llmServiceMap.keySet());
}
return service;
}
/**
* 获取所有支持的模型类别
*
* @return 模型类别集合
*/
public java.util.Set<String> getSupportedCategories() {
return llmServiceMap.keySet();
}
/**
* 检查是否支持指定的模型类别
*
* @param category 模型类别
* @return 是否支持
*/
public boolean isSupported(String category) {
return llmServiceMap.containsKey(category);
}
}

View File

@@ -0,0 +1,85 @@
package org.ruoyi.graph.listener;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import java.util.concurrent.CompletableFuture;
/**
* 图谱抽取LLM响应监听器
* 用于收集完整的LLM响应非流式
*
* @author ruoyi-ai
*/
@Slf4j
public class GraphExtractionListener extends EventSourceListener {
private final StringBuilder responseBuilder = new StringBuilder();
private final CompletableFuture<String> responseFuture;
private final ObjectMapper objectMapper = new ObjectMapper();
public GraphExtractionListener(CompletableFuture<String> responseFuture) {
this.responseFuture = responseFuture;
}
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
log.debug("LLM连接已建立");
}
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, @NotNull String data) {
try {
if ("[DONE]".equals(data)) {
// 响应完成,返回完整内容
responseFuture.complete(responseBuilder.toString());
return;
}
// 解析响应
ChatCompletionResponse completionResponse = objectMapper.readValue(data, ChatCompletionResponse.class);
if (completionResponse != null &&
completionResponse.getChoices() != null &&
!completionResponse.getChoices().isEmpty()) {
Object content = completionResponse.getChoices().get(0).getDelta().getContent();
if (content != null) {
responseBuilder.append(content);
}
}
} catch (Exception e) {
log.error("解析LLM响应失败: {}", e.getMessage(), e);
responseFuture.completeExceptionally(e);
}
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
log.debug("LLM连接已关闭");
// 如果还没有完成,就用当前内容完成
if (!responseFuture.isDone()) {
responseFuture.complete(responseBuilder.toString());
}
}
@Override
public void onFailure(@NotNull EventSource eventSource, Throwable t, Response response) {
String errorMsg = "LLM调用失败";
if (response != null && response.body() != null) {
try {
errorMsg = response.body().string();
} catch (Exception e) {
errorMsg = response.toString();
}
}
log.error("LLM调用失败: {}", errorMsg, t);
responseFuture.completeExceptionally(
new RuntimeException(errorMsg, t)
);
}
}

View File

@@ -0,0 +1,65 @@
package org.ruoyi.graph.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.ruoyi.graph.domain.GraphBuildTask;
import java.util.List;
/**
* 图谱构建任务Mapper接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface GraphBuildTaskMapper extends BaseMapper<GraphBuildTask> {
/**
* 根据任务UUID查询
*
* @param taskUuid 任务UUID
* @return 构建任务
*/
GraphBuildTask selectByTaskUuid(String taskUuid);
/**
* 根据图谱UUID查询任务列表
*
* @param graphUuid 图谱UUID
* @return 任务列表
*/
List<GraphBuildTask> selectByGraphUuid(String graphUuid);
/**
* 根据知识库ID查询任务列表
*
* @param knowledgeId 知识库ID
* @return 任务列表
*/
List<GraphBuildTask> selectByKnowledgeId(String knowledgeId);
/**
* 查询待执行和执行中的任务
*
* @return 任务列表
*/
List<GraphBuildTask> selectPendingAndRunningTasks();
/**
* 更新任务进度
*
* @param taskUuid 任务UUID
* @param progress 进度
* @param processedDocs 已处理文档数
* @return 影响行数
*/
int updateProgress(String taskUuid, Integer progress, Integer processedDocs);
/**
* 更新任务状态
*
* @param taskUuid 任务UUID
* @param status 状态
* @return 影响行数
*/
int updateStatus(String taskUuid, Integer status);
}

View File

@@ -0,0 +1,38 @@
package org.ruoyi.graph.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.ruoyi.graph.domain.GraphEntityType;
import java.util.List;
/**
* 图谱实体类型Mapper接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface GraphEntityTypeMapper extends BaseMapper<GraphEntityType> {
/**
* 根据类型编码查询
*
* @param typeCode 类型编码
* @return 实体类型
*/
GraphEntityType selectByTypeCode(String typeCode);
/**
* 查询所有启用的实体类型
*
* @return 实体类型列表
*/
List<GraphEntityType> selectEnabledTypes();
/**
* 批量查询实体类型
*
* @param typeCodes 类型编码列表
* @return 实体类型列表
*/
List<GraphEntityType> selectByTypeCodes(List<String> typeCodes);
}

View File

@@ -0,0 +1,48 @@
package org.ruoyi.graph.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.ruoyi.graph.domain.GraphInstance;
/**
* 知识图谱实例Mapper接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface GraphInstanceMapper extends BaseMapper<GraphInstance> {
/**
* 根据图谱UUID查询
*
* @param graphUuid 图谱UUID
* @return 图谱实例
*/
GraphInstance selectByGraphUuid(String graphUuid);
/**
* 根据知识库ID查询图谱列表
*
* @param knowledgeId 知识库ID
* @return 图谱实例列表
*/
java.util.List<GraphInstance> selectByKnowledgeId(String knowledgeId);
/**
* 更新节点和关系数量
*
* @param graphUuid 图谱UUID
* @param nodeCount 节点数量
* @param relationshipCount 关系数量
* @return 影响行数
*/
int updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount);
/**
* 更新图谱状态
*
* @param graphUuid 图谱UUID
* @param status 状态
* @return 影响行数
*/
int updateStatus(String graphUuid, Integer status);
}

View File

@@ -0,0 +1,38 @@
package org.ruoyi.graph.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.ruoyi.graph.domain.GraphRelationType;
import java.util.List;
/**
* 图谱关系类型Mapper接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface GraphRelationTypeMapper extends BaseMapper<GraphRelationType> {
/**
* 根据关系编码查询
*
* @param relationCode 关系编码
* @return 关系类型
*/
GraphRelationType selectByRelationCode(String relationCode);
/**
* 查询所有启用的关系类型
*
* @return 关系类型列表
*/
List<GraphRelationType> selectEnabledTypes();
/**
* 批量查询关系类型
*
* @param relationCodes 关系编码列表
* @return 关系类型列表
*/
List<GraphRelationType> selectByRelationCodes(List<String> relationCodes);
}

View File

@@ -0,0 +1,174 @@
package org.ruoyi.graph.prompt;
import org.ruoyi.graph.constants.GraphConstants;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* 图谱实体关系抽取提示词
* 参考 Microsoft GraphRAG 项目
* https://github.com/microsoft/graphrag/blob/main/graphrag/index/graph/extractors/graph/prompts.py
*
* @author ruoyi
* @date 2025-09-30
*/
public class GraphExtractPrompt {
/**
* 中文实体关系抽取提示词
*/
public static final String GRAPH_EXTRACTION_PROMPT_CN = """
-目标-
给定一个可能与此活动相关的文本文档以及实体类型列表,从文本中识别出所有这些类型的实体以及识别出的实体之间的所有关系。
-步骤-
1. 识别所有实体。对于每个识别出的实体,提取以下信息:
- entity_name实体的名称首字母大写
- entity_type以下类型之一[{entity_types}]
- entity_description实体的属性和活动的全面描述
将每个实体格式化为 ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
2. 从步骤1中识别出的实体中识别出所有明确相关的 (source_entity, target_entity) 对。
对于每对相关的实体,提取以下信息:
- source_entity在步骤1中识别的源实体的名称
- target_entity在步骤1中识别的目标实体的名称
- relationship_description解释你认为源实体和目标实体之间相关的原因
- relationship_strength一个表示源实体和目标实体之间关系强度的数字分数0-10
将每个关系格式化为 ("relationship"{tuple_delimiter}<source_entity>{tuple_delimiter}<target_entity>{tuple_delimiter}<relationship_description>{tuple_delimiter}<relationship_strength>)
3. 以英文返回输出作为所有在步骤1和步骤2中识别的实体和关系的列表。使用 **{record_delimiter}** 作为列表分隔符。
4. 完成时,输出 {completion_delimiter}
######################
-示例-
######################
示例 1:
Entity_types: ORGANIZATION,PERSON
文本:
The Verdantis's Central Institution is scheduled to meet on Monday and Thursday, with the institution planning to release its latest policy decision on Thursday at 1:30 p.m. PDT, followed by a press conference where Central Institution Chair Martin Smith will take questions. Investors expect the Market Strategy Committee to hold its benchmark interest rate steady in a range of 3.5%-3.75%.
######################
输出:
("entity"{tuple_delimiter}CENTRAL INSTITUTION{tuple_delimiter}ORGANIZATION{tuple_delimiter}The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday)
{record_delimiter}
("entity"{tuple_delimiter}MARTIN SMITH{tuple_delimiter}PERSON{tuple_delimiter}Martin Smith is the chair of the Central Institution)
{record_delimiter}
("entity"{tuple_delimiter}MARKET STRATEGY COMMITTEE{tuple_delimiter}ORGANIZATION{tuple_delimiter}The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply)
{record_delimiter}
("relationship"{tuple_delimiter}MARTIN SMITH{tuple_delimiter}CENTRAL INSTITUTION{tuple_delimiter}Martin Smith is the Chair of the Central Institution and will answer questions at a press conference{tuple_delimiter}9)
{completion_delimiter}
######################
示例 2:
Entity_types: ORGANIZATION
文本:
TechGlobal's (TG) stock skyrocketed in its opening day on the Global Exchange Thursday. But IPO experts warn that the semiconductor corporation's debut on the public markets isn't indicative of how other newly listed companies may perform.
TechGlobal, a formerly public company, was taken private by Vision Holdings in 2014. The well-established chip designer says it powers 85% of premium smartphones.
######################
输出:
("entity"{tuple_delimiter}TECHGLOBAL{tuple_delimiter}ORGANIZATION{tuple_delimiter}TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones)
{record_delimiter}
("entity"{tuple_delimiter}VISION HOLDINGS{tuple_delimiter}ORGANIZATION{tuple_delimiter}Vision Holdings is a firm that previously owned TechGlobal)
{record_delimiter}
("relationship"{tuple_delimiter}TECHGLOBAL{tuple_delimiter}VISION HOLDINGS{tuple_delimiter}Vision Holdings formerly owned TechGlobal from 2014 until present{tuple_delimiter}5)
{completion_delimiter}
######################
示例 3:
Entity_types: ORGANIZATION,LOCATION,PERSON
文本:
Five Aurelians jailed for 8 years in Firuzabad and widely regarded as hostages are on their way home to Aurelia.
The swap orchestrated by Quintara was finalized when $8bn of Firuzi funds were transferred to financial institutions in Krohaara, the capital of Quintara.
The exchange initiated in Firuzabad's capital, Tiruzia, led to the four men and one woman, who are also Firuzi nationals, boarding a chartered flight to Krohaara.
They were welcomed by senior Aurelian officials and are now on their way to Aurelia's capital, Cashion.
The Aurelians include 39-year-old businessman Samuel Namara, who has been held in Tiruzia's Alhamia Prison, as well as journalist Durke Bataglani, 59, and environmentalist Meggie Tazbah, 53, who also holds Bratinas nationality.
######################
输出:
("entity"{tuple_delimiter}FIRUZABAD{tuple_delimiter}LOCATION{tuple_delimiter}Firuzabad held Aurelians as hostages)
{record_delimiter}
("entity"{tuple_delimiter}AURELIA{tuple_delimiter}LOCATION{tuple_delimiter}Country seeking to release hostages)
{record_delimiter}
("entity"{tuple_delimiter}QUINTARA{tuple_delimiter}LOCATION{tuple_delimiter}Country that negotiated a swap of money in exchange for hostages)
{record_delimiter}
("entity"{tuple_delimiter}TIRUZIA{tuple_delimiter}LOCATION{tuple_delimiter}Capital of Firuzabad where the Aurelians were being held)
{record_delimiter}
("entity"{tuple_delimiter}KROHAARA{tuple_delimiter}LOCATION{tuple_delimiter}Capital city in Quintara)
{record_delimiter}
("entity"{tuple_delimiter}CASHION{tuple_delimiter}LOCATION{tuple_delimiter}Capital city in Aurelia)
{record_delimiter}
("entity"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}PERSON{tuple_delimiter}Aurelian who spent time in Tiruzia's Alhamia Prison)
{record_delimiter}
("entity"{tuple_delimiter}ALHAMIA PRISON{tuple_delimiter}LOCATION{tuple_delimiter}Prison in Tiruzia)
{record_delimiter}
("entity"{tuple_delimiter}DURKE BATAGLANI{tuple_delimiter}PERSON{tuple_delimiter}Aurelian journalist who was held hostage)
{record_delimiter}
("entity"{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}PERSON{tuple_delimiter}Bratinas national and environmentalist who was held hostage)
{record_delimiter}
("relationship"{tuple_delimiter}FIRUZABAD{tuple_delimiter}AURELIA{tuple_delimiter}Firuzabad negotiated a hostage exchange with Aurelia{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}QUINTARA{tuple_delimiter}AURELIA{tuple_delimiter}Quintara brokered the hostage exchange between Firuzabad and Aurelia{tuple_delimiter}2)
{record_delimiter}
("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}ALHAMIA PRISON{tuple_delimiter}Samuel Namara was a prisoner at Alhamia prison{tuple_delimiter}8)
{record_delimiter}
("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}Samuel Namara and Meggie Tazbah were exchanged in the same hostage release{tuple_delimiter}2)
{completion_delimiter}
######################
-真实数据-
######################
Entity_types: {entity_types}
文本: {input_text}
######################
输出:
""".replace("{tuple_delimiter}", GraphConstants.GRAPH_TUPLE_DELIMITER)
.replace("{entity_types}", Arrays.stream(GraphConstants.DEFAULT_ENTITY_TYPES).collect(Collectors.joining(",")))
.replace("{completion_delimiter}", GraphConstants.GRAPH_COMPLETION_DELIMITER)
.replace("{record_delimiter}", GraphConstants.GRAPH_RECORD_DELIMITER);
/**
* 继续抽取提示词(当第一次抽取遗漏实体时使用)
*/
public static final String CONTINUE_PROMPT = """
在上一次抽取中遗漏了许多实体和关系。
请记住只提取与之前提取的类型匹配的实体。
使用相同的格式在下面添加它们:
""";
/**
* 循环检查提示词
*/
public static final String LOOP_PROMPT = """
似乎仍然可能遗漏了一些实体和关系。
如果还有需要添加的实体或关系,请回答 YES | NO。
""";
/**
* 生成提取提示词
*
* @param inputText 输入文本
* @return 完整的提示词
*/
public static String buildExtractionPrompt(String inputText) {
return GRAPH_EXTRACTION_PROMPT_CN.replace("{input_text}", inputText);
}
/**
* 生成提取提示词(自定义实体类型)
*
* @param inputText 输入文本
* @param entityTypes 实体类型列表
* @return 完整的提示词
*/
public static String buildExtractionPrompt(String inputText, String[] entityTypes) {
String entityTypesStr = Arrays.stream(entityTypes).collect(Collectors.joining(","));
return GRAPH_EXTRACTION_PROMPT_CN
.replace("{input_text}", inputText)
.replace(Arrays.stream(GraphConstants.DEFAULT_ENTITY_TYPES).collect(Collectors.joining(",")), entityTypesStr);
}
}

View File

@@ -0,0 +1,136 @@
package org.ruoyi.graph.service;
import org.ruoyi.graph.domain.GraphBuildTask;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 图谱构建任务服务接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface IGraphBuildTaskService {
/**
* 创建构建任务
*
* @param graphUuid 图谱UUID
* @param knowledgeId 知识库ID
* @param docId 文档ID可选
* @param taskType 任务类型
* @return 任务信息
*/
GraphBuildTask createTask(String graphUuid, String knowledgeId, String docId, Integer taskType);
/**
* 启动构建任务(异步)
*
* @param taskUuid 任务UUID
* @return 异步结果
*/
void startTask(String taskUuid);
/**
* 根据UUID获取任务
*
* @param taskUuid 任务UUID
* @return 任务信息
*/
GraphBuildTask getByUuid(String taskUuid);
/**
* 根据图谱UUID获取任务列表
*
* @param graphUuid 图谱UUID
* @return 任务列表
*/
List<GraphBuildTask> listByGraphUuid(String graphUuid);
/**
* 获取图谱的最新构建任务
*
* @param graphUuid 图谱UUID
* @return 最新任务
*/
GraphBuildTask getLatestTask(String graphUuid);
/**
* 根据知识库ID获取任务列表
*
* @param knowledgeId 知识库ID
* @return 任务列表
*/
List<GraphBuildTask> listByKnowledgeId(String knowledgeId);
/**
* 获取待执行和执行中的任务
*
* @return 任务列表
*/
List<GraphBuildTask> getPendingAndRunningTasks();
/**
* 更新任务进度
*
* @param taskUuid 任务UUID
* @param progress 进度百分比
* @param processedDocs 已处理文档数
* @return 是否成功
*/
boolean updateProgress(String taskUuid, Integer progress, Integer processedDocs);
/**
* 更新任务状态
*
* @param taskUuid 任务UUID
* @param status 状态
* @return 是否成功
*/
boolean updateStatus(String taskUuid, Integer status);
/**
* 更新提取统计信息
*
* @param taskUuid 任务UUID
* @param extractedEntities 提取的实体数
* @param extractedRelations 提取的关系数
* @return 是否成功
*/
boolean updateExtractionStats(String taskUuid, Integer extractedEntities, Integer extractedRelations);
/**
* 标记任务为成功
*
* @param taskUuid 任务UUID
* @param resultSummary 结果摘要
* @return 是否成功
*/
boolean markSuccess(String taskUuid, String resultSummary);
/**
* 标记任务为失败
*
* @param taskUuid 任务UUID
* @param errorMessage 错误信息
* @return 是否成功
*/
boolean markFailed(String taskUuid, String errorMessage);
/**
* 取消任务
*
* @param taskUuid 任务UUID
* @return 是否成功
*/
boolean cancelTask(String taskUuid);
/**
* 重试失败的任务
*
* @param taskUuid 任务UUID
* @return 新任务UUID
*/
String retryTask(String taskUuid);
}

View File

@@ -0,0 +1,46 @@
package org.ruoyi.graph.service;
import org.ruoyi.graph.dto.GraphExtractionResult;
/**
* 图谱实体关系抽取服务接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface IGraphExtractionService {
/**
* 从文本中抽取实体和关系
*
* @param text 输入文本
* @return 抽取结果
*/
GraphExtractionResult extractFromText(String text);
/**
* 从文本中抽取实体和关系(自定义实体类型)
*
* @param text 输入文本
* @param entityTypes 实体类型列表
* @return 抽取结果
*/
GraphExtractionResult extractFromText(String text, String[] entityTypes);
/**
* 从文本中抽取实体和关系使用指定的LLM模型
*
* @param text 输入文本
* @param modelName LLM模型名称
* @return 抽取结果
*/
GraphExtractionResult extractFromTextWithModel(String text, String modelName);
/**
* 解析LLM响应为实体和关系
*
* @param response LLM响应文本
* @return 抽取结果
*/
GraphExtractionResult parseGraphResponse(String response);
}

View File

@@ -0,0 +1,120 @@
package org.ruoyi.graph.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.ruoyi.graph.domain.GraphInstance;
import java.util.List;
/**
* 图谱实例服务接口
*
* @author ruoyi
* @date 2025-09-30
*/
public interface IGraphInstanceService {
/**
* 创建图谱实例
*
* @param knowledgeId 知识库ID
* @param graphName 图谱名称
* @param config 配置信息
* @return 图谱实例
*/
GraphInstance createInstance(String knowledgeId, String graphName, String config);
/**
* 根据主键ID获取图谱实例
*
* @param id 主键ID
* @return 图谱实例
*/
GraphInstance getById(Long id);
/**
* 根据UUID获取图谱实例
*
* @param graphUuid 图谱UUID
* @return 图谱实例
*/
GraphInstance getByUuid(String graphUuid);
/**
* 更新图谱实例
*
* @param instance 图谱实例
* @return 是否成功
*/
boolean updateInstance(GraphInstance instance);
/**
* 根据知识库ID获取图谱列表
*
* @param knowledgeId 知识库ID
* @return 图谱实例列表
*/
List<GraphInstance> listByKnowledgeId(String knowledgeId);
/**
* 条件查询图谱实例列表(分页)
*
* @param page 分页对象
* @param instanceName 图谱名称(模糊查询)
* @param knowledgeId 知识库ID
* @param graphStatus 图谱状态码
* @return 分页结果
*/
Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus);
/**
* 更新图谱状态
*
* @param graphUuid 图谱UUID
* @param status 状态
* @return 是否成功
*/
boolean updateStatus(String graphUuid, Integer status);
/**
* 更新图谱统计信息
*
* @param graphUuid 图谱UUID
* @param nodeCount 节点数量
* @param relationshipCount 关系数量
* @return 是否成功
*/
boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount);
/**
* 更新图谱配置
*
* @param graphUuid 图谱UUID
* @param config 配置信息
* @return 是否成功
*/
boolean updateConfig(String graphUuid, String config);
/**
* 删除图谱实例(软删除)
*
* @param graphUuid 图谱UUID
* @return 是否成功
*/
boolean deleteInstance(String graphUuid);
/**
* 物理删除图谱实例及其数据
*
* @param graphUuid 图谱UUID
* @return 是否成功
*/
boolean deleteInstanceAndData(String graphUuid);
/**
* 获取图谱统计信息
*
* @param graphUuid 图谱UUID
* @return 统计信息
*/
java.util.Map<String, Object> getStatistics(String graphUuid);
}

View File

@@ -0,0 +1,75 @@
package org.ruoyi.graph.service;
import org.ruoyi.graph.dto.GraphExtractionResult;
import java.util.Map;
/**
* GraphRAG服务接口
* 负责文档的图谱化处理和基于图谱的检索
*
* @author ruoyi
* @date 2025-09-30
*/
public interface IGraphRAGService {
/**
* 将文本入库到图谱
*
* @param text 文本内容
* @param knowledgeId 知识库ID
* @param metadata 元数据
* @return 抽取结果
*/
GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata);
/**
* 将文本入库到图谱(指定模型)
*
* @param text 文本内容
* @param knowledgeId 知识库ID
* @param metadata 元数据
* @param modelName LLM模型名称
* @return 抽取结果
*/
GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> metadata, String modelName);
/**
* 将文档入库到图谱(自动分片)
*
* @param documentText 文档内容
* @param knowledgeId 知识库ID
* @param metadata 元数据
* @return 总抽取结果(合并所有分片)
*/
GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map<String, Object> metadata);
/**
* 将文档入库到图谱(自动分片,指定模型)
*
* @param documentText 文档内容
* @param knowledgeId 知识库ID
* @param metadata 元数据
* @param modelName LLM模型名称
* @return 总抽取结果(合并所有分片)
*/
GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> metadata, String modelName);
/**
* 基于图谱检索相关内容
*
* @param query 查询文本
* @param knowledgeId 知识库ID
* @param maxResults 最大结果数
* @return 检索到的相关实体和关系
*/
String retrieveFromGraph(String query, String knowledgeId, int maxResults);
/**
* 删除知识库的图谱数据
*
* @param knowledgeId 知识库ID
* @return 是否成功
*/
boolean deleteGraphData(String knowledgeId);
}

View File

@@ -0,0 +1,267 @@
package org.ruoyi.graph.service;
import org.ruoyi.graph.domain.GraphEdge;
import org.ruoyi.graph.domain.GraphVertex;
import java.util.List;
/**
* 图存储服务接口
* 核心服务负责与Neo4j图数据库交互
*
* @author ruoyi
* @date 2025-09-30
*/
public interface IGraphStoreService {
// ==================== 节点操作 ====================
/**
* 添加单个节点
*
* @param vertex 节点信息
* @return 是否成功
*/
boolean addVertex(GraphVertex vertex);
/**
* 批量添加节点
*
* @param vertices 节点列表
* @return 成功添加的节点数
*/
int addVertices(List<GraphVertex> vertices);
/**
* 获取节点信息
*
* @param nodeId 节点ID
* @param graphUuid 图谱UUID
* @return 节点信息
*/
GraphVertex getVertex(String nodeId, String graphUuid);
/**
* 根据条件搜索节点
*
* @param graphUuid 图谱UUID
* @param label 节点标签(可选)
* @param limit 返回数量限制
* @return 节点列表
*/
List<GraphVertex> searchVertices(String graphUuid, String label, Integer limit);
/**
* 根据名称搜索节点
*
* @param graphUuid 图谱UUID
* @param name 节点名称
* @return 节点列表
*/
List<GraphVertex> searchVerticesByName(String graphUuid, String name);
/**
* 根据关键词和知识库ID搜索节点
*
* @param keyword 关键词
* @param knowledgeId 知识库ID可选
* @param limit 限制数量
* @return 节点列表
*/
List<GraphVertex> searchVerticesByName(String keyword, String knowledgeId, Integer limit);
/**
* 根据知识库ID查询节点
*
* @param knowledgeId 知识库ID
* @param limit 限制数量
* @return 节点列表
*/
List<GraphVertex> queryVerticesByKnowledgeId(String knowledgeId, Integer limit);
/**
* 更新节点信息
*
* @param vertex 节点信息
* @return 是否成功
*/
boolean updateVertex(GraphVertex vertex);
/**
* 删除节点
*
* @param nodeId 节点ID
* @param graphUuid 图谱UUID
* @param includeEdges 是否同时删除相关关系
* @return 是否成功
*/
boolean deleteVertex(String nodeId, String graphUuid, boolean includeEdges);
// ==================== 关系操作 ====================
/**
* 添加关系
*
* @param edge 关系信息
* @return 是否成功
*/
boolean addEdge(GraphEdge edge);
/**
* 批量添加关系
*
* @param edges 关系列表
* @return 成功添加的关系数
*/
int addEdges(List<GraphEdge> edges);
/**
* 获取关系信息
*
* @param edgeId 关系ID
* @param graphUuid 图谱UUID
* @return 关系信息
*/
GraphEdge getEdge(String edgeId, String graphUuid);
/**
* 搜索关系
*
* @param graphUuid 图谱UUID
* @param sourceNodeId 源节点ID可选
* @param targetNodeId 目标节点ID可选
* @param limit 返回数量限制
* @return 关系列表
*/
List<GraphEdge> searchEdges(String graphUuid, String sourceNodeId, String targetNodeId, Integer limit);
/**
* 根据知识库ID查询关系
*
* @param knowledgeId 知识库ID
* @param limit 限制数量
* @return 关系列表
*/
List<GraphEdge> queryEdgesByKnowledgeId(String knowledgeId, Integer limit);
/**
* 获取节点的所有关系
*
* @param nodeId 节点ID
* @param graphUuid 图谱UUID
* @param direction 方向: IN(入边), OUT(出边), BOTH(双向)
* @return 关系列表
*/
List<GraphEdge> getNodeEdges(String nodeId, String graphUuid, String direction);
/**
* 更新关系信息
*
* @param edge 关系信息
* @return 是否成功
*/
boolean updateEdge(GraphEdge edge);
/**
* 删除关系
*
* @param edgeId 关系ID
* @param graphUuid 图谱UUID
* @return 是否成功
*/
boolean deleteEdge(String edgeId, String graphUuid);
// ==================== 图谱管理 ====================
/**
* 创建图谱Schema
*
* @param graphUuid 图谱UUID
* @return 是否成功
*/
boolean createGraphSchema(String graphUuid);
/**
* 删除整个图谱数据
*
* @param graphUuid 图谱UUID
* @return 是否成功
*/
boolean deleteGraph(String graphUuid);
/**
* 根据知识库ID删除图谱数据
*
* @param knowledgeId 知识库ID
* @return 是否成功
*/
boolean deleteByKnowledgeId(String knowledgeId);
/**
* 获取图谱统计信息
*
* @param graphUuid 图谱UUID
* @return 统计信息 {nodeCount, relationshipCount}
*/
java.util.Map<String, Object> getGraphStatistics(String graphUuid);
/**
* 根据知识库ID获取统计信息
*
* @param knowledgeId 知识库ID
* @return 统计信息
*/
java.util.Map<String, Object> getStatistics(String knowledgeId);
// ==================== 高级查询 ====================
/**
* 查找两个节点之间的路径
*
* @param sourceNodeId 源节点ID
* @param targetNodeId 目标节点ID
* @param graphUuid 图谱UUID
* @param maxDepth 最大深度
* @return 路径列表
*/
List<List<GraphVertex>> findPaths(String sourceNodeId, String targetNodeId, String graphUuid, Integer maxDepth);
/**
* 查找路径(简化版)
*
* @param startNodeId 起始节点ID
* @param endNodeId 结束节点ID
* @param maxDepth 最大深度
* @return 路径列表
*/
List<List<GraphVertex>> findPaths(String startNodeId, String endNodeId, Integer maxDepth);
/**
* 查找节点的邻居节点
*
* @param nodeId 节点ID
* @param graphUuid 图谱UUID
* @param depth 深度(几度关系)
* @return 邻居节点列表
*/
List<GraphVertex> findNeighbors(String nodeId, String graphUuid, Integer depth);
/**
* 获取节点的邻居(简化版)
*
* @param nodeId 节点ID
* @param knowledgeId 知识库ID可选
* @param limit 限制数量
* @return 邻居节点列表
*/
List<GraphVertex> getNeighbors(String nodeId, String knowledgeId, Integer limit);
/**
* 执行自定义Cypher查询
*
* @param cypher Cypher查询语句
* @param params 参数
* @return 查询结果
*/
List<java.util.Map<String, Object>> executeCypher(String cypher, java.util.Map<String, Object> params);
}

View File

@@ -0,0 +1,734 @@
package org.ruoyi.graph.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.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.KnowledgeAttachBo;
import org.ruoyi.domain.vo.KnowledgeAttachVo;
import org.ruoyi.graph.domain.GraphBuildTask;
import org.ruoyi.graph.domain.GraphInstance;
import org.ruoyi.graph.dto.GraphExtractionResult;
import org.ruoyi.graph.mapper.GraphBuildTaskMapper;
import org.ruoyi.graph.service.IGraphBuildTaskService;
import org.ruoyi.graph.service.IGraphInstanceService;
import org.ruoyi.graph.service.IGraphRAGService;
import org.ruoyi.service.IKnowledgeAttachService;
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;
import java.util.concurrent.CompletableFuture;
/**
* 图谱构建任务服务实现
*
* @author ruoyi
* @date 2025-09-30
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GraphBuildTaskServiceImpl implements IGraphBuildTaskService {
private final GraphBuildTaskMapper taskMapper;
private final IGraphRAGService graphRAGService;
private final IKnowledgeAttachService knowledgeAttachService;
private final IGraphInstanceService graphInstanceService;
@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<GraphBuildTask> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GraphBuildTask::getTaskUuid, taskUuid);
return taskMapper.selectOne(wrapper);
}
@Override
public List<GraphBuildTask> listByGraphUuid(String graphUuid) {
LambdaQueryWrapper<GraphBuildTask> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GraphBuildTask::getGraphUuid, graphUuid);
wrapper.orderByDesc(GraphBuildTask::getCreateTime);
return taskMapper.selectList(wrapper);
}
@Override
public GraphBuildTask getLatestTask(String graphUuid) {
LambdaQueryWrapper<GraphBuildTask> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GraphBuildTask::getGraphUuid, graphUuid);
wrapper.orderByDesc(GraphBuildTask::getCreateTime);
wrapper.last("LIMIT 1");
return taskMapper.selectOne(wrapper);
}
@Override
public List<GraphBuildTask> listByKnowledgeId(String knowledgeId) {
LambdaQueryWrapper<GraphBuildTask> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GraphBuildTask::getKnowledgeId, knowledgeId);
wrapper.orderByDesc(GraphBuildTask::getCreateTime);
return taskMapper.selectList(wrapper);
}
@Override
public List<GraphBuildTask> getPendingAndRunningTasks() {
LambdaQueryWrapper<GraphBuildTask> 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<GraphBuildTask> 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<GraphBuildTask> 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<GraphBuildTask> 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<GraphBuildTask> 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<GraphBuildTask> 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<GraphBuildTask> 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<KnowledgeAttachVo> documents;
if (taskType == 1) {
// 类型1: 全量构建(知识库所有文档)
if (StrUtil.isBlank(knowledgeId)) {
throw new RuntimeException("知识库构建任务缺少知识库ID");
}
// 查询知识库下的所有文档
KnowledgeAttachBo bo = new KnowledgeAttachBo();
bo.setKid(knowledgeId);
log.info("🔍 准备查询文档: knowledgeId={}, bo.getKid()={}", knowledgeId, bo.getKid());
documents = knowledgeAttachService.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("⚠️ 旧图谱数据清空失败(可能是没有旧数据)");
}
// 查询知识库下的所有文档
KnowledgeAttachBo bo = new KnowledgeAttachBo();
bo.setKid(knowledgeId);
log.info("🔍 准备查询文档: knowledgeId={}, bo.getKid()={}", knowledgeId, bo.getKid());
documents = knowledgeAttachService.queryList(bo);
log.info("📋 查询返回文档数: {}", documents != null ? documents.size() : "null");
} else if (taskType == 3) {
// 类型3: 单文档增量构建
if (StrUtil.isBlank(docId)) {
throw new RuntimeException("单文档构建任务缺少文档ID");
}
// 根据docId查询单个文档
KnowledgeAttachBo bo = new KnowledgeAttachBo();
bo.setDocId(docId);
log.info("🔍 准备查询单个文档: docId={}, bo.getDocId()={}", docId, bo.getDocId());
documents = knowledgeAttachService.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<String, Object> 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<GraphBuildTask> 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<GraphBuildTask> 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++) {
KnowledgeAttachVo 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时被中断");
}
}
log.info("📄 处理文档 [{}/{}]: docId={}, docName={}",
i + 1, totalDocs, doc.getDocId(), doc.getDocName());
// 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);
}
// 2.2 准备元数据(不包含大字段)
Map<String, Object> metadata = new HashMap<>();
metadata.put("docId", doc.getDocId());
metadata.put("docName", doc.getDocName());
metadata.put("docType", doc.getDocType());
metadata.put("kid", doc.getKid());
// 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<String, Object> 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);
}
}
}

View File

@@ -0,0 +1,370 @@
package org.ruoyi.graph.service.impl;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.graph.constants.GraphConstants;
import org.ruoyi.graph.dto.ExtractedEntity;
import org.ruoyi.graph.dto.ExtractedRelation;
import org.ruoyi.graph.dto.GraphExtractionResult;
import org.ruoyi.graph.factory.GraphLLMServiceFactory;
import org.ruoyi.graph.prompt.GraphExtractPrompt;
import org.ruoyi.graph.service.IGraphExtractionService;
import org.ruoyi.graph.service.llm.IGraphLLMService;
import org.ruoyi.service.IChatModelService;
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
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<ExtractedEntity> entities = new ArrayList<>();
List<ExtractedRelation> 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) {
// 获取聊天分类的最高优先级模型作为默认模型
ChatModelVo defaultModel = chatModelService.selectModelByCategoryWithHighestPriority("chat");
if (defaultModel == null) {
// 如果没有chat分类的模型尝试查询任意可用模型
List<ChatModelVo> models = chatModelService.queryList(new org.ruoyi.domain.bo.ChatModelBo());
if (models != null && !models.isEmpty()) {
defaultModel = models.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;
}
}

View File

@@ -0,0 +1,284 @@
package org.ruoyi.graph.service.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.graph.domain.GraphInstance;
import org.ruoyi.graph.mapper.GraphInstanceMapper;
import org.ruoyi.graph.service.IGraphInstanceService;
import org.ruoyi.graph.service.IGraphStoreService;
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
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<GraphInstance> 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<GraphInstance> 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<GraphInstance> listByKnowledgeId(String knowledgeId) {
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
wrapper.orderByDesc(GraphInstance::getCreateTime);
return graphInstanceMapper.selectList(wrapper);
}
@Override
public Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus) {
LambdaQueryWrapper<GraphInstance> 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<GraphInstance> 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<GraphInstance> 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<GraphInstance> 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<GraphInstance> 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<String, Object> getStatistics(String graphUuid) {
try {
// 从 Neo4j 获取实时统计
Map<String, Object> 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<>();
}
}
}

View File

@@ -0,0 +1,460 @@
package org.ruoyi.graph.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.graph.constants.GraphConstants;
import org.ruoyi.graph.domain.GraphEdge;
import org.ruoyi.graph.domain.GraphVertex;
import org.ruoyi.graph.dto.ExtractedEntity;
import org.ruoyi.graph.dto.ExtractedRelation;
import org.ruoyi.graph.dto.GraphExtractionResult;
import org.ruoyi.graph.service.IGraphExtractionService;
import org.ruoyi.graph.service.IGraphRAGService;
import org.ruoyi.graph.service.IGraphStoreService;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* GraphRAG服务实现
*
* @author ruoyi
* @date 2025-09-30
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GraphRAGServiceImpl implements IGraphRAGService {
private final IGraphExtractionService graphExtractionService;
private final IGraphStoreService graphStoreService;
@Override
public GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata) {
return ingestTextWithModel(text, knowledgeId, metadata, null);
}
@Override
public GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> 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<GraphVertex> vertices = convertEntitiesToVertices(
extractionResult.getEntities(),
knowledgeId,
metadata
);
// 3. 批量添加节点到Neo4j并建立实体名称→nodeId的映射
Map<String, String> 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<GraphEdge> 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<String, Object> metadata) {
return ingestDocumentWithModel(documentText, knowledgeId, metadata, null);
}
@Override
public GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> 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<String> chunks = splitDocument(documentText);
log.info("文档已分割为 {} 个片段", chunks.size());
// 合并结果
List<ExtractedEntity> allEntities = new ArrayList<>();
List<ExtractedRelation> 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<String, Object> 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<ExtractedEntity> 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<String> keywords = extractKeywords(query);
log.debug("提取的关键词: {}", keywords);
if (keywords.isEmpty()) {
return "未能从查询中提取关键信息";
}
// 2. 在图谱中搜索相关实体节点
List<GraphVertex> matchedNodes = new ArrayList<>();
for (String keyword : keywords) {
List<GraphVertex> nodes = graphStoreService.searchVerticesByName(
keyword, knowledgeId, Math.min(5, maxResults)
);
matchedNodes.addAll(nodes);
}
if (matchedNodes.isEmpty()) {
return "图谱中未找到相关实体";
}
log.info("找到 {} 个匹配的实体节点", matchedNodes.size());
// 3. 去重按nodeId
Map<String, GraphVertex> 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<GraphVertex> neighbors = graphStoreService.getNeighbors(
node.getNodeId(), knowledgeId, 5
);
if (!neighbors.isEmpty()) {
result.append(" 关联实体: ");
List<String> 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<String> extractKeywords(String query) {
List<String> keywords = new ArrayList<>();
// 简单的中文分词策略
// 1. 去除标点符号
String cleaned = query.replaceAll("[\\p{Punct}\\s]+", " ");
// 2. 按空格分割
String[] words = cleaned.split("\\s+");
// 3. 过滤停用词和短词
Set<String> stopWords = new HashSet<>(java.util.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<GraphVertex> convertEntitiesToVertices(
List<ExtractedEntity> entities,
String knowledgeId,
Map<String, Object> metadata) {
List<GraphVertex> 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<GraphEdge> convertRelationsToEdges(
List<ExtractedRelation> relations,
String knowledgeId,
Map<String, Object> metadata,
Map<String, String> entityNameToNodeIdMap) {
List<GraphEdge> 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<String> splitDocument(String text) {
List<String> 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<ExtractedEntity> deduplicateEntities(List<ExtractedEntity> entities) {
Map<String, ExtractedEntity> 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());
}
}

View File

@@ -0,0 +1,927 @@
package org.ruoyi.graph.service.impl;
import com.alibaba.fastjson2.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.graph.config.GraphProperties;
import org.ruoyi.graph.domain.GraphEdge;
import org.ruoyi.graph.domain.GraphVertex;
import org.ruoyi.graph.service.IGraphStoreService;
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
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<GraphVertex> 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<GraphVertex> 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<GraphVertex> 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<String, Object> 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<GraphVertex> 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<GraphVertex> searchVerticesByName(String keyword, String knowledgeId, Integer limit) {
List<GraphVertex> vertices = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
String cypher;
Map<String, Object> 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<GraphEdge> 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<GraphEdge> 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<GraphEdge> 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<String, Object> 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<GraphEdge> 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<String, Object> getGraphStatistics(String graphUuid) {
Map<String, Object> 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<List<GraphVertex>> 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<List<GraphVertex>> paths = new ArrayList<>();
result.stream().forEach(record -> {
List<GraphVertex> 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<GraphVertex> 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<Map<String, Object>> executeCypher(String cypher, Map<String, Object> params) {
try (Session session = neo4jDriver.session()) {
Result result = session.run(cypher, params != null ? params : Collections.emptyMap());
return result.stream()
.map(Record::asMap)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("执行Cypher查询失败: {}", cypher, e);
return Collections.emptyList();
}
}
// ==================== 辅助方法 ====================
/**
* Neo4j Node 转换为 GraphVertex
*/
private GraphVertex nodeToVertex(Node node) {
GraphVertex vertex = new GraphVertex();
vertex.setNodeId(node.get("id").asString(null));
vertex.setLabel(node.labels().iterator().next());
vertex.setName(node.get("name").asString(null));
vertex.setDescription(node.get("description").asString(null));
vertex.setKnowledgeId(node.get("knowledgeId").asString(null));
vertex.setDocIds(node.get("docIds").asString(null));
// 处理 confidence可能为空
if (node.containsKey("confidence") && !node.get("confidence").isNull()) {
vertex.setConfidence(node.get("confidence").asDouble());
}
// 处理 properties转换为JSON字符串
if (node.containsKey("properties") && !node.get("properties").isNull()) {
Map<String, Object> propsMap = node.get("properties").asMap();
vertex.setProperties(JSON.toJSONString(propsMap));
}
return vertex;
}
/**
* Neo4j Relationship 转换为 GraphEdge
*/
private GraphEdge relationshipToEdge(Node source, Relationship rel, Node target) {
GraphEdge edge = new GraphEdge();
edge.setEdgeId(rel.get("id").asString(null));
edge.setLabel(rel.type());
edge.setSourceNodeId(source.get("id").asString(null));
edge.setTargetNodeId(target.get("id").asString(null));
edge.setDescription(rel.get("description").asString(null));
edge.setKnowledgeId(rel.get("knowledgeId").asString(null));
edge.setDocIds(rel.get("docIds").asString(null));
// 处理 weight可能为空
if (rel.containsKey("weight") && !rel.get("weight").isNull()) {
edge.setWeight(rel.get("weight").asDouble());
}
// 处理 confidence可能为空
if (rel.containsKey("confidence") && !rel.get("confidence").isNull()) {
edge.setConfidence(rel.get("confidence").asDouble());
}
// 处理 properties转换为JSON字符串
if (rel.containsKey("properties") && !rel.get("properties").isNull()) {
Map<String, Object> propsMap = rel.get("properties").asMap();
edge.setProperties(JSON.toJSONString(propsMap));
}
return edge;
}
// ==================== 新增的方法实现 ====================
@Override
public List<GraphVertex> queryVerticesByKnowledgeId(String knowledgeId, Integer limit) {
List<GraphVertex> vertices = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
String cypher = "MATCH (n {knowledgeId: $knowledgeId}) " +
"RETURN n LIMIT $limit";
Result result = session.run(cypher, parameters(
"knowledgeId", knowledgeId,
"limit", limit
));
result.stream().forEach(record -> {
Node node = record.get("n").asNode();
vertices.add(nodeToVertex(node));
});
log.info("查询到 {} 个节点知识库ID: {}", vertices.size(), knowledgeId);
return vertices;
} catch (Exception e) {
log.error("查询节点失败", e);
return vertices;
}
}
@Override
public List<GraphEdge> queryEdgesByKnowledgeId(String knowledgeId, Integer limit) {
List<GraphEdge> edges = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
// ⭐ 修复:通过节点的 knowledgeId 过滤关系,兼容旧数据
String cypher = "MATCH (s {knowledgeId: $knowledgeId})-[r]->(t {knowledgeId: $knowledgeId}) " +
"RETURN s, r, t LIMIT $limit";
log.info("🔍 开始查询关系 - knowledgeId: {}, limit: {}", knowledgeId, limit);
log.debug("执行Cypher: {}", cypher);
Result result = session.run(cypher, parameters(
"knowledgeId", knowledgeId,
"limit", limit
));
int count = 0;
while (result.hasNext()) {
Record record = result.next();
Node source = record.get("s").asNode();
Relationship rel = record.get("r").asRelationship();
Node target = record.get("t").asNode();
// 调试:打印关系详情
if (count < 3) { // 只打印前3个
log.debug("关系#{} - 类型: {}, 起点: {} ({}), 终点: {} ({})",
count + 1,
rel.type(),
source.get("name").asString(),
source.get("id").asString(),
target.get("name").asString(),
target.get("id").asString()
);
}
edges.add(relationshipToEdge(source, rel, target));
count++;
}
log.info("✅ 查询到 {} 个关系知识库ID: {}", edges.size(), knowledgeId);
return edges;
} catch (Exception e) {
log.error("❌ 查询关系失败 - knowledgeId: {}", knowledgeId, e);
return edges;
}
}
@Override
public boolean deleteByKnowledgeId(String knowledgeId) {
try (Session session = neo4jDriver.session()) {
log.info("🗑️ 开始删除知识库图谱数据knowledgeId: {}", knowledgeId);
// ⭐ 先删除关系通过节点的knowledgeId过滤兼容旧数据
String deleteRelsQuery = "MATCH (s {knowledgeId: $knowledgeId})-[r]->(t {knowledgeId: $knowledgeId}) DELETE r";
Result relResult = session.run(deleteRelsQuery, parameters("knowledgeId", knowledgeId));
int deletedRels = relResult.consume().counters().relationshipsDeleted();
log.info("✅ 删除了 {} 个关系", deletedRels);
// 再删除节点
String deleteNodesQuery = "MATCH (n {knowledgeId: $knowledgeId}) DELETE n";
Result nodeResult = session.run(deleteNodesQuery, parameters("knowledgeId", knowledgeId));
int deletedNodes = nodeResult.consume().counters().nodesDeleted();
log.info("✅ 删除了 {} 个节点", deletedNodes);
log.info("✅ 删除知识库图谱数据成功knowledgeId: {}, 节点: {}, 关系: {}",
knowledgeId, deletedNodes, deletedRels);
return true;
} catch (Exception e) {
log.error("❌ 删除知识库图谱数据失败knowledgeId: {}", knowledgeId, e);
return false;
}
}
@Override
public Map<String, Object> getStatistics(String knowledgeId) {
Map<String, Object> stats = new HashMap<>();
try (Session session = neo4jDriver.session()) {
// 统计节点数
String nodeCountQuery = "MATCH (n {knowledgeId: $knowledgeId}) RETURN count(n) as count";
Result nodeResult = session.run(nodeCountQuery, parameters("knowledgeId", knowledgeId));
int nodeCount = 0;
if (nodeResult.hasNext()) {
nodeCount = nodeResult.single().get("count").asInt();
stats.put("nodeCount", nodeCount);
stats.put("totalNodes", nodeCount); // ⭐ 前端需要的字段
}
// ⭐ 统计关系数(通过节点过滤,与查询/删除逻辑一致)
String relCountQuery = "MATCH (s {knowledgeId: $knowledgeId})-[r]->(t {knowledgeId: $knowledgeId}) RETURN count(r) as count";
Result relResult = session.run(relCountQuery, parameters("knowledgeId", knowledgeId));
int relCount = 0;
if (relResult.hasNext()) {
relCount = relResult.single().get("count").asInt();
stats.put("relationshipCount", relCount);
stats.put("totalEdges", relCount); // ⭐ 前端需要的字段
}
// 统计节点类型分布
String labelQuery = "MATCH (n {knowledgeId: $knowledgeId}) " +
"RETURN labels(n)[0] as label, count(*) as count " +
"ORDER BY count DESC LIMIT 10";
Result labelResult = session.run(labelQuery, parameters("knowledgeId", knowledgeId));
Map<String, Integer> labelDistribution = new HashMap<>();
labelResult.stream().forEach(record -> {
String label = record.get("label").asString();
int count = record.get("count").asInt();
labelDistribution.put(label, count);
});
stats.put("labelDistribution", labelDistribution);
stats.put("entityTypes", labelDistribution); // ⭐ 前端需要的字段
log.info("📊 获取图谱统计信息: knowledgeId={}, 节点={}, 关系={}, 类型={}",
knowledgeId, nodeCount, relCount, labelDistribution.size());
return stats;
} catch (Exception e) {
log.error("❌ 获取统计信息失败: knowledgeId={}", knowledgeId, e);
return stats;
}
}
@Override
public List<GraphVertex> getNeighbors(String nodeId, String knowledgeId, Integer limit) {
List<GraphVertex> neighbors = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
String cypher;
Map<String, Object> params = new HashMap<>();
params.put("nodeId", nodeId);
params.put("limit", limit);
if (knowledgeId != null && !knowledgeId.isEmpty()) {
cypher = "MATCH (n {id: $nodeId, knowledgeId: $knowledgeId})-[]-(neighbor {knowledgeId: $knowledgeId}) " +
"RETURN DISTINCT neighbor LIMIT $limit";
params.put("knowledgeId", knowledgeId);
} else {
cypher = "MATCH (n {id: $nodeId})-[]-(neighbor) " +
"RETURN DISTINCT neighbor LIMIT $limit";
}
Result result = session.run(cypher, params);
result.stream().forEach(record -> {
Node node = record.get("neighbor").asNode();
neighbors.add(nodeToVertex(node));
});
log.info("查询到 {} 个邻居节点", neighbors.size());
return neighbors;
} catch (Exception e) {
log.error("查询邻居节点失败", e);
return neighbors;
}
}
@Override
public List<List<GraphVertex>> findPaths(String startNodeId, String endNodeId, Integer maxDepth) {
List<List<GraphVertex>> paths = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
String cypher = "MATCH path = (start {id: $startNodeId})-[*1.." + maxDepth + "]-(end {id: $endNodeId}) " +
"RETURN nodes(path) as pathNodes " +
"LIMIT 10";
Result result = session.run(cypher, parameters(
"startNodeId", startNodeId,
"endNodeId", endNodeId
));
result.stream().forEach(record -> {
List<Object> pathNodes = record.get("pathNodes").asList();
List<GraphVertex> path = new ArrayList<>();
for (Object obj : pathNodes) {
if (obj instanceof Node) {
path.add(nodeToVertex((Node) obj));
}
}
if (!path.isEmpty()) {
paths.add(path);
}
});
log.info("查询到 {} 条路径", paths.size());
return paths;
} catch (Exception e) {
log.error("查询路径失败", e);
return paths;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.ruoyi.graph.service.llm;
import org.ruoyi.domain.vo.ChatModelVo;
/**
* 图谱LLM服务接口
* 参考 ruoyi-chat 的 IChatService 设计
* 支持多种LLM模型OpenAI、Qwen、Zhipu等
*
* @author ruoyi
* @date 2025-10-11
*/
public interface IGraphLLMService {
/**
* 调用LLM进行图谱实体关系抽取
*
* @param prompt 提示词(包含文本和抽取指令)
* @param chatModel 模型配置
* @return LLM响应文本
*/
String extractGraph(String prompt, ChatModelVo chatModel);
/**
* 获取此服务支持的模型类别
* 例如: "openai", "qwen", "zhipu", "ollama"
*
* @return 模型类别标识
*/
String getCategory();
}

View File

@@ -0,0 +1,85 @@
package org.ruoyi.graph.service.llm.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.domain.vo.ChatModelVo;
import org.ruoyi.graph.service.llm.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<String> 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 字段
}
}

View File

@@ -0,0 +1,107 @@
package org.ruoyi.graph.service.llm.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.domain.vo.ChatModelVo;
import org.ruoyi.graph.service.llm.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<String> 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 字段
}
}

View File

@@ -0,0 +1,78 @@
package org.ruoyi.graph.service.llm.impl;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.graph.service.llm.IGraphLLMService;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* OpenAI 图谱LLM服务实现
* 支持 OpenAI 兼容的模型GPT-3.5, GPT-4, 自定义OpenAI兼容接口等
*
* @author ruoyi
* @date 2025-10-11
*/
@Slf4j
@Service
public class OpenAIGraphLLMServiceImpl implements IGraphLLMService {
@Override
public String extractGraph(String prompt, ChatModelVo chatModel) {
log.info("OpenAI模型调用: model={}, apiHost={}, 提示词长度={}",
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
try {
// 创建 OpenAiStreamClient
OpenAiStreamClient client = ChatConfig.createOpenAiStreamClient(
chatModel.getApiHost(),
chatModel.getApiKey()
);
// 构建消息
List<Message> messages = Collections.singletonList(
Message.builder()
.role(Message.Role.USER)
.content(prompt)
.build()
);
// 构建请求(非流式,同步调用)
ChatCompletion completion = ChatCompletion.builder()
.messages(messages)
.model(chatModel.getModelName())
.stream(false) // 同步调用
.build();
// 同步调用 LLM
long startTime = System.currentTimeMillis();
ChatCompletionResponse response = client.chatCompletion(completion);
long duration = System.currentTimeMillis() - startTime;
// 提取响应文本
Object content = response.getChoices().get(0).getMessage().getContent();
String responseText = content != null ? content.toString() : "";
log.info("OpenAI模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
return responseText;
} catch (Exception e) {
log.error("OpenAI模型调用失败: {}", e.getMessage(), e);
throw new RuntimeException("OpenAI模型调用失败: " + e.getMessage(), e);
}
}
@Override
public String getCategory() {
return "openai"; // 对应 ChatModel 表中的 category 字段
}
}

View File

@@ -0,0 +1,80 @@
package org.ruoyi.graph.service.llm.impl;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.graph.service.llm.IGraphLLMService;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* 通义千问Qwen图谱LLM服务实现
* 支持阿里云通义千问系列模型
*
* 注意通义千问的API与OpenAI兼容因此可以复用 OpenAiStreamClient
*
* @author ruoyi
* @date 2025-10-11
*/
@Slf4j
@Service
public class QwenGraphLLMServiceImpl implements IGraphLLMService {
@Override
public String extractGraph(String prompt, ChatModelVo chatModel) {
log.info("Qwen模型调用: model={}, apiHost={}, 提示词长度={}",
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
try {
// 通义千问API与OpenAI兼容可以直接使用 OpenAiStreamClient
OpenAiStreamClient client = ChatConfig.createOpenAiStreamClient(
chatModel.getApiHost(),
chatModel.getApiKey()
);
// 构建消息
List<Message> messages = Collections.singletonList(
Message.builder()
.role(Message.Role.USER)
.content(prompt)
.build()
);
// 构建请求(非流式,同步调用)
ChatCompletion completion = ChatCompletion.builder()
.messages(messages)
.model(chatModel.getModelName())
.stream(false) // 同步调用
.build();
// 同步调用 LLM
long startTime = System.currentTimeMillis();
ChatCompletionResponse response = client.chatCompletion(completion);
long duration = System.currentTimeMillis() - startTime;
// 提取响应文本
Object content = response.getChoices().get(0).getMessage().getContent();
String responseText = content != null ? content.toString() : "";
log.info("Qwen模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
return responseText;
} catch (Exception e) {
log.error("Qwen模型调用失败: {}", e.getMessage(), e);
throw new RuntimeException("Qwen模型调用失败: " + e.getMessage(), e);
}
}
@Override
public String getCategory() {
return "qwen"; // 对应 ChatModel 表中的 category 字段
}
}

View File

@@ -0,0 +1,80 @@
package org.ruoyi.graph.service.llm.impl;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.graph.service.llm.IGraphLLMService;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* 智谱AIZhipu图谱LLM服务实现
* 支持智谱AI系列模型GLM-4等
*
* 注意智谱AI的API与OpenAI兼容因此可以复用 OpenAiStreamClient
*
* @author ruoyi
* @date 2025-10-11
*/
@Slf4j
@Service
public class ZhipuGraphLLMServiceImpl implements IGraphLLMService {
@Override
public String extractGraph(String prompt, ChatModelVo chatModel) {
log.info("Zhipu模型调用: model={}, apiHost={}, 提示词长度={}",
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
try {
// 智谱AI API与OpenAI兼容可以直接使用 OpenAiStreamClient
OpenAiStreamClient client = ChatConfig.createOpenAiStreamClient(
chatModel.getApiHost(),
chatModel.getApiKey()
);
// 构建消息
List<Message> messages = Collections.singletonList(
Message.builder()
.role(Message.Role.USER)
.content(prompt)
.build()
);
// 构建请求(非流式,同步调用)
ChatCompletion completion = ChatCompletion.builder()
.messages(messages)
.model(chatModel.getModelName())
.stream(false) // 同步调用
.build();
// 同步调用 LLM
long startTime = System.currentTimeMillis();
ChatCompletionResponse response = client.chatCompletion(completion);
long duration = System.currentTimeMillis() - startTime;
// 提取响应文本
Object content = response.getChoices().get(0).getMessage().getContent();
String responseText = content != null ? content.toString() : "";
log.info("Zhipu模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
return responseText;
} catch (Exception e) {
log.error("Zhipu模型调用失败: {}", e.getMessage(), e);
throw new RuntimeException("Zhipu模型调用失败: " + e.getMessage(), e);
}
}
@Override
public String getCategory() {
return "zhipu"; // 对应 ChatModel 表中的 category 字段
}
}

View File

@@ -0,0 +1,367 @@
package org.ruoyi.graph.util;
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.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import static org.neo4j.driver.Values.parameters;
/**
* Neo4j连接测试工具类
*
* @author ruoyi
* @date 2025-09-30
*/
@Slf4j
@Component
public class Neo4jTestUtil {
private final Driver driver;
public Neo4jTestUtil(Driver driver) {
this.driver = driver;
}
/**
* 测试Neo4j连接
*
* @return 测试结果
*/
public Map<String, Object> testConnection() {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
// 1. 测试基本连接
Result pingResult = session.run("RETURN 1 as num");
if (pingResult.hasNext()) {
Record record = pingResult.single();
result.put("connection", "SUCCESS");
result.put("pingResult", record.get("num").asInt());
}
// 2. 获取数据库信息
Result dbInfoResult = session.run("CALL dbms.components() YIELD name, versions, edition");
if (dbInfoResult.hasNext()) {
Record dbInfo = dbInfoResult.single();
result.put("neo4jVersion", dbInfo.get("versions").asList().get(0));
result.put("neo4jEdition", dbInfo.get("edition").asString());
}
// 3. 测试节点数量
Result countResult = session.run("MATCH (n) RETURN count(n) as count");
if (countResult.hasNext()) {
result.put("totalNodes", countResult.single().get("count").asInt());
}
log.info("Neo4j连接测试成功: {}", result);
return result;
} catch (Exception e) {
log.error("Neo4j连接测试失败", e);
result.put("connection", "FAILED");
result.put("error", e.getMessage());
return result;
}
}
/**
* 创建测试节点
*
* @param name 节点名称
* @return 创建的节点信息
*/
public Map<String, Object> createTestNode(String name) {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
Result queryResult = session.run(
"CREATE (p:TestNode {name: $name, createTime: datetime()}) RETURN p",
parameters("name", name)
);
if (queryResult.hasNext()) {
Record record = queryResult.single();
Node node = record.get("p").asNode();
result.put("success", true);
result.put("nodeId", node.elementId());
result.put("labels", node.labels());
result.put("properties", node.asMap());
log.info("测试节点创建成功: {}", result);
}
return result;
} catch (Exception e) {
log.error("创建测试节点失败", e);
result.put("success", false);
result.put("error", e.getMessage());
return result;
}
}
/**
* 查询测试节点
*
* @param name 节点名称
* @return 节点信息
*/
public Map<String, Object> queryTestNode(String name) {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
Result queryResult = session.run(
"MATCH (p:TestNode {name: $name}) RETURN p",
parameters("name", name)
);
if (queryResult.hasNext()) {
Record record = queryResult.single();
Node node = record.get("p").asNode();
result.put("found", true);
result.put("nodeId", node.elementId());
result.put("properties", node.asMap());
} else {
result.put("found", false);
}
return result;
} catch (Exception e) {
log.error("查询测试节点失败", e);
result.put("error", e.getMessage());
return result;
}
}
/**
* 删除所有测试节点
*
* @return 删除的节点数量
*/
public Map<String, Object> deleteAllTestNodes() {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
Result queryResult = session.run(
"MATCH (p:TestNode) DELETE p RETURN count(p) as deleted"
);
if (queryResult.hasNext()) {
int deleted = queryResult.single().get("deleted").asInt();
result.put("success", true);
result.put("deletedCount", deleted);
log.info("删除测试节点数量: {}", deleted);
}
return result;
} catch (Exception e) {
log.error("删除测试节点失败", e);
result.put("success", false);
result.put("error", e.getMessage());
return result;
}
}
/**
* 创建测试关系
*
* @param sourceName 源节点名称
* @param targetName 目标节点名称
* @return 创建结果
*/
public Map<String, Object> createTestRelationship(String sourceName, String targetName) {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
// 先创建两个节点
session.run(
"MERGE (s:TestNode {name: $sourceName}) " +
"MERGE (t:TestNode {name: $targetName})",
parameters("sourceName", sourceName, "targetName", targetName)
);
// 创建关系
Result queryResult = session.run(
"MATCH (s:TestNode {name: $sourceName}) " +
"MATCH (t:TestNode {name: $targetName}) " +
"CREATE (s)-[r:TEST_RELATION {createTime: datetime()}]->(t) " +
"RETURN r",
parameters("sourceName", sourceName, "targetName", targetName)
);
if (queryResult.hasNext()) {
result.put("success", true);
result.put("source", sourceName);
result.put("target", targetName);
log.info("测试关系创建成功: {} -> {}", sourceName, targetName);
}
return result;
} catch (Exception e) {
log.error("创建测试关系失败", e);
result.put("success", false);
result.put("error", e.getMessage());
return result;
}
}
/**
* 获取Neo4j统计信息
*
* @return 统计信息
*/
public Map<String, Object> getStatistics() {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
// 节点总数
Result nodeCountResult = session.run("MATCH (n) RETURN count(n) as count");
result.put("totalNodes", nodeCountResult.single().get("count").asInt());
// 关系总数
Result relCountResult = session.run("MATCH ()-[r]->() RETURN count(r) as count");
result.put("totalRelationships", relCountResult.single().get("count").asInt());
// 节点标签分布
Result labelResult = session.run(
"MATCH (n) RETURN labels(n) as label, count(*) as count ORDER BY count DESC LIMIT 10"
);
Map<String, Integer> labelDistribution = new HashMap<>();
labelResult.stream().forEach(record -> {
String label = record.get("label").asList().toString();
int count = record.get("count").asInt();
labelDistribution.put(label, count);
});
result.put("labelDistribution", labelDistribution);
log.info("Neo4j统计信息: {}", result);
return result;
} catch (Exception e) {
log.error("获取统计信息失败", e);
result.put("error", e.getMessage());
return result;
}
}
/**
* 调试关系查询 - 详细诊断指定知识库的关系数据
*
* @param knowledgeId 知识库ID
* @return 调试信息
*/
public Map<String, Object> debugRelationships(String knowledgeId) {
Map<String, Object> result = new HashMap<>();
try (Session session = driver.session()) {
log.info("🔍 开始调试关系查询 - knowledgeId: {}", knowledgeId);
// 1. 检查该知识库的节点数量
Result nodeCountResult = session.run(
"MATCH (n {knowledgeId: $knowledgeId}) RETURN count(n) as count",
parameters("knowledgeId", knowledgeId)
);
int nodeCount = nodeCountResult.single().get("count").asInt();
result.put("nodeCount", nodeCount);
log.info("✅ 该知识库节点数量: {}", nodeCount);
// 2. 检查总关系数量(不限制知识库)
Result totalRelResult = session.run("MATCH ()-[r]->() RETURN count(r) as count");
int totalRelCount = totalRelResult.single().get("count").asInt();
result.put("totalRelationships", totalRelCount);
log.info("✅ 数据库总关系数量: {}", totalRelCount);
// 3. 检查带 knowledgeId 的关系数量(旧查询方式)
Result relWithKnowledgeIdResult = session.run(
"MATCH ()-[r {knowledgeId: $knowledgeId}]->() RETURN count(r) as count",
parameters("knowledgeId", knowledgeId)
);
int relWithKnowledgeId = relWithKnowledgeIdResult.single().get("count").asInt();
result.put("relationshipsWithKnowledgeId", relWithKnowledgeId);
log.info("✅ 带 knowledgeId 的关系数量: {}", relWithKnowledgeId);
// 4. 通过节点过滤关系数量(新查询方式)
Result relByNodesResult = session.run(
"MATCH (s {knowledgeId: $knowledgeId})-[r]->(t {knowledgeId: $knowledgeId}) RETURN count(r) as count",
parameters("knowledgeId", knowledgeId)
);
int relByNodes = relByNodesResult.single().get("count").asInt();
result.put("relationshipsByNodes", relByNodes);
log.info("✅ 通过节点过滤的关系数量: {}", relByNodes);
// 5. 采样前5个关系详情如果有
if (relByNodes > 0) {
Result sampleResult = session.run(
"MATCH (s {knowledgeId: $knowledgeId})-[r]->(t {knowledgeId: $knowledgeId}) " +
"RETURN s.name as sourceName, s.id as sourceId, type(r) as relType, " +
"t.name as targetName, t.id as targetId, r.knowledgeId as relKnowledgeId " +
"LIMIT 5",
parameters("knowledgeId", knowledgeId)
);
java.util.List<Map<String, Object>> samples = new java.util.ArrayList<>();
sampleResult.stream().forEach(record -> {
Map<String, Object> sample = new HashMap<>();
sample.put("sourceName", record.get("sourceName").asString());
sample.put("sourceId", record.get("sourceId").asString());
sample.put("relationType", record.get("relType").asString());
sample.put("targetName", record.get("targetName").asString());
sample.put("targetId", record.get("targetId").asString());
sample.put("relationshipKnowledgeId",
record.get("relKnowledgeId").isNull() ? null : record.get("relKnowledgeId").asString());
samples.add(sample);
});
result.put("sampleRelationships", samples);
log.info("✅ 采样到 {} 个关系", samples.size());
}
// 6. 检查是否有孤立节点(没有任何关系的节点)
Result isolatedNodesResult = session.run(
"MATCH (n {knowledgeId: $knowledgeId}) " +
"WHERE NOT (n)-[]-() " +
"RETURN count(n) as count",
parameters("knowledgeId", knowledgeId)
);
int isolatedNodes = isolatedNodesResult.single().get("count").asInt();
result.put("isolatedNodes", isolatedNodes);
log.info("✅ 孤立节点数量: {}", isolatedNodes);
// 7. 诊断结论
Map<String, String> diagnosis = new HashMap<>();
if (nodeCount == 0) {
diagnosis.put("issue", "该知识库没有节点数据");
diagnosis.put("solution", "请先构建图谱或检查 knowledgeId 是否正确");
} else if (relByNodes == 0 && totalRelCount > 0) {
diagnosis.put("issue", "数据库中有关系,但该知识库查询不到");
diagnosis.put("solution", "可能是 knowledgeId 不匹配,检查关系的 knowledgeId 属性");
} else if (relByNodes == 0) {
diagnosis.put("issue", "该知识库没有关系数据");
diagnosis.put("solution", "检查 LLM 抽取是否识别到实体关系");
} else {
diagnosis.put("status", "正常");
diagnosis.put("message", "关系数据正常,可以正常查询");
}
result.put("diagnosis", diagnosis);
log.info("🎯 调试完成: {}", diagnosis);
return result;
} catch (Exception e) {
log.error("❌ 调试关系查询失败", e);
result.put("error", e.getMessage());
return result;
}
}
}