|
@@ -0,0 +1,441 @@
|
|
|
|
|
+package com.lingyue.graph.neo4j;
|
|
|
|
|
+
|
|
|
|
|
+import com.lingyue.graph.dto.KnowledgeGraphDTO;
|
|
|
|
|
+import com.lingyue.graph.dto.KnowledgeGraphDTO.*;
|
|
|
|
|
+import com.lingyue.graph.entity.GraphNode;
|
|
|
|
|
+import com.lingyue.graph.entity.GraphRelation;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.neo4j.driver.Driver;
|
|
|
|
|
+import org.neo4j.driver.Result;
|
|
|
|
|
+import org.neo4j.driver.Session;
|
|
|
|
|
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Neo4j 图服务
|
|
|
|
|
+ *
|
|
|
|
|
+ * 提供高级图操作功能:
|
|
|
|
|
+ * - 同步 PostgreSQL 数据到 Neo4j
|
|
|
|
|
+ * - 图遍历和路径查询
|
|
|
|
|
+ * - 图分析(中心性、社区检测等)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author lingyue
|
|
|
|
|
+ * @since 2026-01-21
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+@ConditionalOnBean(Driver.class)
|
|
|
|
|
+public class Neo4jGraphService {
|
|
|
|
|
+
|
|
|
|
|
+ private final Driver driver;
|
|
|
|
|
+ private final Neo4jNodeRepository nodeRepository;
|
|
|
|
|
+ private final Neo4jRelationRepository relationRepository;
|
|
|
|
|
+
|
|
|
|
|
+ // 类型映射
|
|
|
|
|
+ private static final Map<String, String> TYPE_LABELS = Map.ofEntries(
|
|
|
|
|
+ Map.entry("person", "Person"),
|
|
|
|
|
+ Map.entry("org", "Organization"),
|
|
|
|
|
+ Map.entry("loc", "Location"),
|
|
|
|
|
+ Map.entry("location", "Location"),
|
|
|
|
|
+ Map.entry("date", "Date"),
|
|
|
|
|
+ Map.entry("number", "Number"),
|
|
|
|
|
+ Map.entry("money", "Money"),
|
|
|
|
|
+ Map.entry("data", "Data"),
|
|
|
|
|
+ Map.entry("concept", "Concept"),
|
|
|
|
|
+ Map.entry("device", "Device"),
|
|
|
|
|
+ Map.entry("term", "Term"),
|
|
|
|
|
+ Map.entry("entity", "Entity")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ private static final Map<String, String> TYPE_ICONS = Map.ofEntries(
|
|
|
|
|
+ Map.entry("person", "👤"),
|
|
|
|
|
+ Map.entry("org", "🏢"),
|
|
|
|
|
+ Map.entry("loc", "📍"),
|
|
|
|
|
+ Map.entry("location", "📍"),
|
|
|
|
|
+ Map.entry("date", "📅"),
|
|
|
|
|
+ Map.entry("number", "🔢"),
|
|
|
|
|
+ Map.entry("money", "💰"),
|
|
|
|
|
+ Map.entry("data", "📊"),
|
|
|
|
|
+ Map.entry("concept", "💡"),
|
|
|
|
|
+ Map.entry("device", "🔧"),
|
|
|
|
|
+ Map.entry("term", "📝"),
|
|
|
|
|
+ Map.entry("entity", "🏷️")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ private static final Map<String, String> TYPE_COLORS = Map.ofEntries(
|
|
|
|
|
+ Map.entry("person", "#1890ff"),
|
|
|
|
|
+ Map.entry("org", "#52c41a"),
|
|
|
|
|
+ Map.entry("loc", "#fa8c16"),
|
|
|
|
|
+ Map.entry("location", "#fa8c16"),
|
|
|
|
|
+ Map.entry("date", "#722ed1"),
|
|
|
|
|
+ Map.entry("number", "#13c2c2"),
|
|
|
|
|
+ Map.entry("money", "#52c41a"),
|
|
|
|
|
+ Map.entry("data", "#13c2c2"),
|
|
|
|
|
+ Map.entry("concept", "#722ed1"),
|
|
|
|
|
+ Map.entry("device", "#eb2f96"),
|
|
|
|
|
+ Map.entry("term", "#2f54eb"),
|
|
|
|
|
+ Map.entry("entity", "#1890ff")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 同步方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 同步单个节点到 Neo4j
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncNode(GraphNode node) {
|
|
|
|
|
+ String type = node.getType() != null ? node.getType().toLowerCase() : "entity";
|
|
|
|
|
+ String label = TYPE_LABELS.getOrDefault(type, "Entity");
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> properties = new HashMap<>();
|
|
|
|
|
+ properties.put("id", node.getId());
|
|
|
|
|
+ properties.put("name", node.getName());
|
|
|
|
|
+ properties.put("type", type);
|
|
|
|
|
+ properties.put("value", node.getValue());
|
|
|
|
|
+ properties.put("documentId", node.getDocumentId());
|
|
|
|
|
+ properties.put("userId", node.getUserId());
|
|
|
|
|
+ properties.put("createdAt", node.getCreateTime() != null ? node.getCreateTime().toString() : null);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加位置信息
|
|
|
|
|
+ if (node.getPosition() instanceof Map) {
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> pos = (Map<String, Object>) node.getPosition();
|
|
|
|
|
+ properties.put("charStart", pos.get("charStart"));
|
|
|
|
|
+ properties.put("charEnd", pos.get("charEnd"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nodeRepository.createOrUpdateNode(node.getId(), List.of("Entity", label), properties);
|
|
|
|
|
+ log.debug("同步节点到 Neo4j: id={}, name={}", node.getId(), node.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量同步节点
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncNodes(List<GraphNode> nodes) {
|
|
|
|
|
+ List<Map<String, Object>> nodeDataList = nodes.stream()
|
|
|
|
|
+ .map(node -> {
|
|
|
|
|
+ String type = node.getType() != null ? node.getType().toLowerCase() : "entity";
|
|
|
|
|
+ String label = TYPE_LABELS.getOrDefault(type, "Entity");
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ data.put("id", node.getId());
|
|
|
|
|
+ data.put("labels", List.of("Entity", label));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> props = new HashMap<>();
|
|
|
|
|
+ props.put("name", node.getName());
|
|
|
|
|
+ props.put("type", type);
|
|
|
|
|
+ props.put("value", node.getValue());
|
|
|
|
|
+ props.put("documentId", node.getDocumentId());
|
|
|
|
|
+ props.put("userId", node.getUserId());
|
|
|
|
|
+ data.put("properties", props);
|
|
|
|
|
+
|
|
|
|
|
+ return data;
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ nodeRepository.batchCreateNodes(nodeDataList);
|
|
|
|
|
+ log.info("批量同步节点到 Neo4j: count={}", nodes.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 同步单个关系到 Neo4j
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncRelation(GraphRelation relation) {
|
|
|
|
|
+ Map<String, Object> properties = new HashMap<>();
|
|
|
|
|
+ properties.put("id", relation.getId());
|
|
|
|
|
+ properties.put("relationType", relation.getRelationType());
|
|
|
|
|
+ properties.put("actionType", relation.getActionType());
|
|
|
|
|
+ properties.put("orderIndex", relation.getOrderIndex());
|
|
|
|
|
+
|
|
|
|
|
+ String relType = relation.getRelationType() != null ? relation.getRelationType() : "RELATED";
|
|
|
|
|
+
|
|
|
|
|
+ relationRepository.createRelation(
|
|
|
|
|
+ relation.getFromNodeId(),
|
|
|
|
|
+ relation.getToNodeId(),
|
|
|
|
|
+ relType,
|
|
|
|
|
+ properties
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ log.debug("同步关系到 Neo4j: from={}, to={}, type={}",
|
|
|
|
|
+ relation.getFromNodeId(), relation.getToNodeId(), relType);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量同步关系
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncRelations(List<GraphRelation> relations) {
|
|
|
|
|
+ List<Map<String, Object>> relDataList = relations.stream()
|
|
|
|
|
+ .map(rel -> {
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ data.put("fromId", rel.getFromNodeId());
|
|
|
|
|
+ data.put("toId", rel.getToNodeId());
|
|
|
|
|
+ data.put("type", rel.getRelationType() != null ? rel.getRelationType() : "RELATED");
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> props = new HashMap<>();
|
|
|
|
|
+ props.put("id", rel.getId());
|
|
|
|
|
+ props.put("actionType", rel.getActionType());
|
|
|
|
|
+ props.put("orderIndex", rel.getOrderIndex());
|
|
|
|
|
+ data.put("properties", props);
|
|
|
|
|
+
|
|
|
|
|
+ return data;
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ relationRepository.batchCreateRelations(relDataList);
|
|
|
|
|
+ log.info("批量同步关系到 Neo4j: count={}", relations.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 图查询方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取文档的完整知识图谱(从 Neo4j)
|
|
|
|
|
+ */
|
|
|
|
|
+ public KnowledgeGraphDTO getDocumentGraph(String documentId) {
|
|
|
|
|
+ List<Map<String, Object>> nodes = nodeRepository.findByDocumentId(documentId);
|
|
|
|
|
+ if (nodes.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> relations = relationRepository.findByDocumentId(documentId);
|
|
|
|
|
+
|
|
|
|
|
+ // 转换节点
|
|
|
|
|
+ List<NodeDTO> nodeDTOs = nodes.stream()
|
|
|
|
|
+ .map(this::convertToNodeDTO)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 计算关联数量
|
|
|
|
|
+ Map<String, Integer> relationCounts = new HashMap<>();
|
|
|
|
|
+ for (Map<String, Object> rel : relations) {
|
|
|
|
|
+ String fromId = (String) rel.get("fromId");
|
|
|
|
|
+ String toId = (String) rel.get("toId");
|
|
|
|
|
+ relationCounts.merge(fromId, 1, Integer::sum);
|
|
|
|
|
+ relationCounts.merge(toId, 1, Integer::sum);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新节点的关联数量和大小
|
|
|
|
|
+ for (NodeDTO node : nodeDTOs) {
|
|
|
|
|
+ int count = relationCounts.getOrDefault(node.getId(), 0);
|
|
|
|
|
+ node.setRelationCount(count);
|
|
|
|
|
+ node.setSize(40 + Math.min(count * 5, 30));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 转换边
|
|
|
|
|
+ List<EdgeDTO> edgeDTOs = relations.stream()
|
|
|
|
|
+ .map(rel -> EdgeDTO.builder()
|
|
|
|
|
+ .id((String) rel.get("id"))
|
|
|
|
|
+ .source((String) rel.get("fromId"))
|
|
|
|
|
+ .sourceName((String) rel.get("fromName"))
|
|
|
|
|
+ .target((String) rel.get("toId"))
|
|
|
|
|
+ .targetName((String) rel.get("toName"))
|
|
|
|
|
+ .relationType((String) rel.get("type"))
|
|
|
|
|
+ .label((String) rel.get("type"))
|
|
|
|
|
+ .weight(1.0)
|
|
|
|
|
+ .build())
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 统计
|
|
|
|
|
+ GraphStats stats = GraphStats.builder()
|
|
|
|
|
+ .totalNodes(nodeDTOs.size())
|
|
|
|
|
+ .totalEdges(edgeDTOs.size())
|
|
|
|
|
+ .nodesByType(nodes.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ n -> (String) n.getOrDefault("type", "other"),
|
|
|
|
|
+ Collectors.collectingAndThen(Collectors.counting(), Long::intValue))))
|
|
|
|
|
+ .edgesByType(relations.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ r -> (String) r.getOrDefault("type", "RELATED"),
|
|
|
|
|
+ Collectors.collectingAndThen(Collectors.counting(), Long::intValue))))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ return KnowledgeGraphDTO.builder()
|
|
|
|
|
+ .documentId(documentId)
|
|
|
|
|
+ .title("标记要素关系图谱")
|
|
|
|
|
+ .nodes(nodeDTOs)
|
|
|
|
|
+ .edges(edgeDTOs)
|
|
|
|
|
+ .stats(stats)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查找两节点之间的最短路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<Map<String, Object>> findShortestPath(String fromNodeId, String toNodeId, int maxDepth) {
|
|
|
|
|
+ String cypher =
|
|
|
|
|
+ "MATCH path = shortestPath((a {id: $fromId})-[*1.." + maxDepth + "]-(b {id: $toId})) " +
|
|
|
|
|
+ "RETURN [n IN nodes(path) | {id: n.id, name: n.name, type: n.type}] AS nodes, " +
|
|
|
|
|
+ " [r IN relationships(path) | {type: type(r)}] AS relations";
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (Session session = driver.session()) {
|
|
|
|
|
+ Result queryResult = session.run(cypher, Map.of("fromId", fromNodeId, "toId", toNodeId));
|
|
|
|
|
+ if (queryResult.hasNext()) {
|
|
|
|
|
+ var record = queryResult.next();
|
|
|
|
|
+ result = record.get("nodes").asList(v -> v.asMap());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取节点的 N 跳邻居
|
|
|
|
|
+ */
|
|
|
|
|
+ public KnowledgeGraphDTO getNeighborhood(String nodeId, int depth) {
|
|
|
|
|
+ String cypher =
|
|
|
|
|
+ "MATCH path = (center {id: $nodeId})-[*1.." + depth + "]-(neighbor) " +
|
|
|
|
|
+ "WITH center, neighbor, relationships(path) AS rels " +
|
|
|
|
|
+ "RETURN DISTINCT neighbor, labels(neighbor) AS labels, " +
|
|
|
|
|
+ " [r IN rels | {type: type(r), from: startNode(r).id, to: endNode(r).id}] AS relInfo";
|
|
|
|
|
+
|
|
|
|
|
+ Set<String> nodeIds = new HashSet<>();
|
|
|
|
|
+ nodeIds.add(nodeId);
|
|
|
|
|
+
|
|
|
|
|
+ List<NodeDTO> nodes = new ArrayList<>();
|
|
|
|
|
+ List<EdgeDTO> edges = new ArrayList<>();
|
|
|
|
|
+ Set<String> edgeKeys = new HashSet<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 添加中心节点
|
|
|
|
|
+ Map<String, Object> centerNode = nodeRepository.findById(nodeId);
|
|
|
|
|
+ if (centerNode != null) {
|
|
|
|
|
+ nodes.add(convertToNodeDTO(centerNode));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try (Session session = driver.session()) {
|
|
|
|
|
+ Result result = session.run(cypher, Map.of("nodeId", nodeId));
|
|
|
|
|
+
|
|
|
|
|
+ while (result.hasNext()) {
|
|
|
|
|
+ var record = result.next();
|
|
|
|
|
+ var neighbor = record.get("neighbor").asNode();
|
|
|
|
|
+ var labels = record.get("labels").asList(v -> v.asString());
|
|
|
|
|
+ var relInfo = record.get("relInfo").asList(v -> v.asMap());
|
|
|
|
|
+
|
|
|
|
|
+ String neighborId = neighbor.get("id").asString();
|
|
|
|
|
+
|
|
|
|
|
+ if (!nodeIds.contains(neighborId)) {
|
|
|
|
|
+ nodeIds.add(neighborId);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> nodeMap = new HashMap<>(neighbor.asMap());
|
|
|
|
|
+ nodeMap.put("_labels", labels);
|
|
|
|
|
+ nodes.add(convertToNodeDTO(nodeMap));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理关系
|
|
|
|
|
+ for (var rel : relInfo) {
|
|
|
|
|
+ String from = (String) rel.get("from");
|
|
|
|
|
+ String to = (String) rel.get("to");
|
|
|
|
|
+ String type = (String) rel.get("type");
|
|
|
|
|
+ String edgeKey = from + "-" + to + "-" + type;
|
|
|
|
|
+
|
|
|
|
|
+ if (!edgeKeys.contains(edgeKey)) {
|
|
|
|
|
+ edgeKeys.add(edgeKey);
|
|
|
|
|
+ edges.add(EdgeDTO.builder()
|
|
|
|
|
+ .id(edgeKey)
|
|
|
|
|
+ .source(from)
|
|
|
|
|
+ .target(to)
|
|
|
|
|
+ .relationType(type)
|
|
|
|
|
+ .label(type)
|
|
|
|
|
+ .build());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return KnowledgeGraphDTO.builder()
|
|
|
|
|
+ .title("邻域图谱")
|
|
|
|
|
+ .nodes(nodes)
|
|
|
|
|
+ .edges(edges)
|
|
|
|
|
+ .stats(GraphStats.builder()
|
|
|
|
|
+ .totalNodes(nodes.size())
|
|
|
|
|
+ .totalEdges(edges.size())
|
|
|
|
|
+ .build())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取高度中心性节点(PageRank)
|
|
|
|
|
+ * 需要 Graph Data Science 插件
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<Map<String, Object>> getTopNodesByPageRank(String documentId, int limit) {
|
|
|
|
|
+ // 简化版:按关系数量排序
|
|
|
|
|
+ String cypher =
|
|
|
|
|
+ "MATCH (n {documentId: $documentId})-[r]-() " +
|
|
|
|
|
+ "WITH n, count(r) AS degree " +
|
|
|
|
|
+ "ORDER BY degree DESC " +
|
|
|
|
|
+ "LIMIT $limit " +
|
|
|
|
|
+ "RETURN n.id AS id, n.name AS name, n.type AS type, degree";
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (Session session = driver.session()) {
|
|
|
|
|
+ Result queryResult = session.run(cypher, Map.of("documentId", documentId, "limit", limit));
|
|
|
|
|
+ while (queryResult.hasNext()) {
|
|
|
|
|
+ var record = queryResult.next();
|
|
|
|
|
+ result.add(Map.of(
|
|
|
|
|
+ "id", record.get("id").asString(),
|
|
|
|
|
+ "name", record.get("name").asString(),
|
|
|
|
|
+ "type", record.get("type").asString(),
|
|
|
|
|
+ "degree", record.get("degree").asInt()
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 辅助方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private NodeDTO convertToNodeDTO(Map<String, Object> nodeMap) {
|
|
|
|
|
+ String type = (String) nodeMap.getOrDefault("type", "entity");
|
|
|
|
|
+ if (type == null) type = "entity";
|
|
|
|
|
+ type = type.toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ return NodeDTO.builder()
|
|
|
|
|
+ .id((String) nodeMap.get("id"))
|
|
|
|
|
+ .name((String) nodeMap.get("name"))
|
|
|
|
|
+ .type(type)
|
|
|
|
|
+ .icon(TYPE_ICONS.getOrDefault(type, "📌"))
|
|
|
|
|
+ .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
|
|
|
|
|
+ .size(40)
|
|
|
|
|
+ .value((String) nodeMap.get("value"))
|
|
|
|
|
+ .documentIds(nodeMap.get("documentId") != null
|
|
|
|
|
+ ? List.of((String) nodeMap.get("documentId"))
|
|
|
|
|
+ : Collections.emptyList())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除文档的所有图数据
|
|
|
|
|
+ */
|
|
|
|
|
+ public void deleteDocumentGraph(String documentId) {
|
|
|
|
|
+ nodeRepository.deleteByDocumentId(documentId);
|
|
|
|
|
+ log.info("删除文档图数据: documentId={}", documentId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 初始化图数据库索引
|
|
|
|
|
+ */
|
|
|
|
|
+ public void initializeIndexes() {
|
|
|
|
|
+ String[] indexes = {
|
|
|
|
|
+ "CREATE INDEX node_id IF NOT EXISTS FOR (n:Entity) ON (n.id)",
|
|
|
|
|
+ "CREATE INDEX node_document IF NOT EXISTS FOR (n:Entity) ON (n.documentId)",
|
|
|
|
|
+ "CREATE INDEX node_user IF NOT EXISTS FOR (n:Entity) ON (n.userId)",
|
|
|
|
|
+ "CREATE INDEX node_name IF NOT EXISTS FOR (n:Entity) ON (n.name)"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try (Session session = driver.session()) {
|
|
|
|
|
+ for (String index : indexes) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ session.run(index);
|
|
|
|
|
+ log.info("创建索引成功: {}", index);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("创建索引失败(可能已存在): {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|