Flask实现Neo4j知识图谱Web应用

创建一个完整的Flask Web应用,用于管理和可视化Neo4j知识图谱。

1. 项目结构

text

复制代码
flask_kg_app/
│
├── app.py              # 主应用文件
├── requirements.txt    # 依赖包
├── config.py          # 配置文件
├── .env              # 环境变量
│
├── static/           # 静态文件
│   ├── css/
│   ├── js/
│   └── images/
│
├── templates/        # HTML模板
│   ├── base.html
│   ├── index.html
│   ├── query.html
│   ├── visualize.html
│   ├── manage.html
│   └── dashboard.html
│
├── utils/           # 工具模块
│   ├── neo4j_connector.py
│   ├── kg_builder.py
│   └── visualizer.py
│
└── data/           # 数据文件
    ├── sample_data.csv
    └── imports/

2. 配置文件

config.py

python

复制代码
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    """应用配置"""
    
    # 基础配置
    SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
    
    # Neo4j配置
    NEO4J_URI = os.getenv('NEO4J_URI', 'bolt://localhost:7687')
    NEO4J_USER = os.getenv('NEO4J_USER', 'neo4j')
    NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD', 'password')
    
    # 应用配置
    UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'data/uploads')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB
    
    # 允许的文件扩展名
    ALLOWED_EXTENSIONS = {'csv', 'json', 'txt'}
    
    @staticmethod
    def init_app(app):
        """初始化应用配置"""
        # 确保上传目录存在
        if not os.path.exists(Config.UPLOAD_FOLDER):
            os.makedirs(Config.UPLOAD_FOLDER)

.env

env

复制代码
SECRET_KEY=your-secret-key-change-this
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-password
DEBUG=True

3. Neo4j连接工具

utils/neo4j_connector.py

python

复制代码
from neo4j import GraphDatabase, BoltDriver
from typing import List, Dict, Any, Optional
import pandas as pd
import json

class Neo4jConnector:
    """Neo4j数据库连接管理器"""
    
    def __init__(self, uri: str, user: str, password: str):
        """
        初始化Neo4j连接
        :param uri: Neo4j连接URI
        :param user: 用户名
        :param password: 密码
        """
        self.uri = uri
        self.user = user
        self.password = password
        self.driver = None
        self._connect()
    
    def _connect(self):
        """建立数据库连接"""
        try:
            self.driver = GraphDatabase.driver(
                self.uri, 
                auth=(self.user, self.password),
                max_connection_lifetime=30 * 60,
                connection_timeout=15,
                connection_acquisition_timeout=2 * 60
            )
            # 测试连接
            with self.driver.session() as session:
                session.run("RETURN 1")
            print("Neo4j连接成功")
        except Exception as e:
            print(f"Neo4j连接失败: {e}")
            self.driver = None
    
    def close(self):
        """关闭数据库连接"""
        if self.driver:
            self.driver.close()
    
    def is_connected(self) -> bool:
        """检查连接状态"""
        try:
            with self.driver.session() as session:
                session.run("RETURN 1")
            return True
        except:
            return False
    
    def execute_query(self, query: str, params: dict = None) -> List[Dict]:
        """
        执行Cypher查询
        :param query: Cypher查询语句
        :param params: 查询参数
        :return: 查询结果列表
        """
        if not self.is_connected():
            self._connect()
        
        try:
            with self.driver.session() as session:
                result = session.run(query, parameters=params or {})
                records = []
                for record in result:
                    # 将Record对象转换为字典
                    record_dict = {}
                    for key in record.keys():
                        value = record[key]
                        # 处理Neo4j的特殊类型
                        if hasattr(value, '__dict__'):
                            value = dict(value)
                        record_dict[key] = value
                    records.append(record_dict)
                return records
        except Exception as e:
            print(f"查询执行失败: {e}")
            return []
    
    def get_database_info(self) -> Dict[str, Any]:
        """获取数据库信息"""
        queries = {
            "节点统计": """
            CALL db.labels() YIELD label
            CALL apoc.cypher.run('MATCH (n:`' + label + '`) RETURN count(*) as count', {})
            YIELD value
            RETURN label, value.count as count
            ORDER BY label
            """,
            "关系统计": """
            CALL db.relationshipTypes() YIELD relationshipType
            CALL apoc.cypher.run('MATCH ()-[r:`' + relationshipType + '`]->() RETURN count(*) as count', {})
            YIELD value
            RETURN relationshipType, value.count as count
            ORDER BY relationshipType
            """,
            "属性统计": """
            MATCH (n) 
            UNWIND keys(n) as key
            RETURN key, count(*) as count, collect(distinct apoc.convert.toString(n[key]))[0..5] as sample_values
            ORDER BY count DESC
            LIMIT 10
            """
        }
        
        info = {"状态": "已连接" if self.is_connected() else "未连接"}
        
        if self.is_connected():
            for name, query in queries.items():
                try:
                    info[name] = self.execute_query(query)
                except:
                    info[name] = []
        
        return info
    
    def import_csv(self, filepath: str, label: str = None) -> Dict[str, Any]:
        """
        从CSV文件导入数据
        :param filepath: CSV文件路径
        :param label: 节点标签
        :return: 导入结果
        """
        try:
            df = pd.read_csv(filepath)
            
            if 'label' in df.columns and not label:
                # 使用CSV中的label列
                labels = df['label'].unique()
                results = {}
                
                for lbl in labels:
                    nodes_df = df[df['label'] == lbl]
                    properties = {}
                    
                    for _, row in nodes_df.iterrows():
                        node_props = {}
                        for col in nodes_df.columns:
                            if col != 'label' and pd.notna(row[col]):
                                node_props[col] = row[col]
                        
                        query = f"""
                        CREATE (n:{lbl} $props)
                        RETURN id(n) as id
                        """
                        result = self.execute_query(query, {"props": node_props})
                        
                        if result:
                            node_id = result[0]['id']
                            properties[str(node_id)] = node_props
                    
                    results[lbl] = {
                        "count": len(nodes_df),
                        "properties": properties
                    }
                
                return {
                    "success": True,
                    "message": f"成功导入 {len(df)} 个节点",
                    "results": results
                }
            
            else:
                # 使用指定的label
                nodes_created = 0
                properties = {}
                
                for _, row in df.iterrows():
                    props = {}
                    for col in df.columns:
                        if pd.notna(row[col]):
                            props[col] = row[col]
                    
                    query = f"""
                    CREATE (n:{label} $props)
                    RETURN id(n) as id
                    """
                    result = self.execute_query(query, {"props": props})
                    
                    if result:
                        node_id = result[0]['id']
                        properties[str(node_id)] = props
                        nodes_created += 1
                
                return {
                    "success": True,
                    "message": f"成功导入 {nodes_created} 个节点",
                    "results": {label: {"count": nodes_created, "properties": properties}}
                }
        
        except Exception as e:
            return {
                "success": False,
                "message": f"导入失败: {str(e)}"
            }
    
    def export_to_json(self, filename: str = "knowledge_graph.json") -> Dict[str, Any]:
        """导出知识图谱为JSON格式"""
        try:
            # 获取所有节点
            nodes_query = """
            MATCH (n)
            RETURN 
                id(n) as id,
                labels(n) as labels,
                properties(n) as properties
            """
            
            # 获取所有关系
            relationships_query = """
            MATCH (a)-[r]->(b)
            RETURN 
                id(a) as start_id,
                type(r) as type,
                properties(r) as properties,
                id(b) as end_id,
                labels(a) as start_labels,
                labels(b) as end_labels
            """
            
            nodes = self.execute_query(nodes_query)
            relationships = self.execute_query(relationships_query)
            
            # 构建图结构
            graph_data = {
                "nodes": nodes,
                "relationships": relationships,
                "metadata": {
                    "node_count": len(nodes),
                    "relationship_count": len(relationships),
                    "export_time": pd.Timestamp.now().isoformat()
                }
            }
            
            # 保存到文件
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(graph_data, f, ensure_ascii=False, indent=2)
            
            return {
                "success": True,
                "message": f"数据已导出到 {filename}",
                "filepath": filename,
                "stats": {
                    "nodes": len(nodes),
                    "relationships": len(relationships)
                }
            }
        
        except Exception as e:
            return {
                "success": False,
                "message": f"导出失败: {str(e)}"
            }
    
    def clear_database(self) -> Dict[str, Any]:
        """清空数据库"""
        try:
            query = "MATCH (n) DETACH DELETE n"
            self.execute_query(query)
            
            # 重建索引
            self.execute_query("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.id IS UNIQUE")
            self.execute_query("CREATE CONSTRAINT IF NOT EXISTS FOR (n:Company) REQUIRE n.id IS UNIQUE")
            self.execute_query("CREATE INDEX IF NOT EXISTS FOR (n:Person) ON (n.name)")
            self.execute_query("CREATE INDEX IF NOT EXISTS FOR (n:Company) ON (n.name)")
            
            return {
                "success": True,
                "message": "数据库已清空并重建索引"
            }
        except Exception as e:
            return {
                "success": False,
                "message": f"清空失败: {str(e)}"
            }

4. Flask主应用

app.py

python

复制代码
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_file
from flask_cors import CORS
import os
import json
from datetime import datetime

from config import Config
from utils.neo4j_connector import Neo4jConnector
from utils.kg_builder import KnowledgeGraphBuilder
from utils.visualizer import KGVisualizer

# 初始化应用
app = Flask(__name__)
app.config.from_object(Config)
CORS(app)

# 初始化Neo4j连接
neo4j_conn = Neo4jConnector(
    uri=app.config['NEO4J_URI'],
    user=app.config['NEO4J_USER'],
    password=app.config['NEO4J_PASSWORD']
)

# 初始化知识图谱构建器
kg_builder = KnowledgeGraphBuilder(neo4j_conn)

@app.route('/')
def index():
    """首页"""
    db_info = neo4j_conn.get_database_info()
    return render_template('index.html', db_info=db_info)

@app.route('/dashboard')
def dashboard():
    """仪表盘"""
    # 获取统计信息
    stats_query = """
    MATCH (n)
    WITH labels(n) as labels
    UNWIND labels as label
    RETURN label, count(*) as count
    ORDER BY count DESC
    """
    
    stats = neo4j_conn.execute_query(stats_query)
    
    # 获取最近添加的节点
    recent_query = """
    MATCH (n)
    WHERE n.created_at IS NOT NULL
    RETURN n.name as name, labels(n)[0] as label, n.created_at as created_at
    ORDER BY n.created_at DESC
    LIMIT 10
    """
    
    recent_nodes = neo4j_conn.execute_query(recent_query)
    
    # 获取数据库信息
    db_info = neo4j_conn.get_database_info()
    
    return render_template('dashboard.html', 
                          stats=stats, 
                          recent_nodes=recent_nodes,
                          db_info=db_info)

@app.route('/query', methods=['GET', 'POST'])
def query():
    """查询页面"""
    results = []
    query_text = ""
    
    if request.method == 'POST':
        query_text = request.form.get('query', '')
        if query_text:
            results = neo4j_conn.execute_query(query_text)
    
    # 获取预定义查询模板
    query_templates = [
        {
            "name": "查找所有人物",
            "query": "MATCH (p:Person) RETURN p.name as name, p.age as age, p.profession as profession LIMIT 50"
        },
        {
            "name": "查找所有关系",
            "query": "MATCH (a)-[r]->(b) RETURN a.name as from, type(r) as relation, b.name as to LIMIT 50"
        },
        {
            "name": "查找朋友的朋友",
            "query": "MATCH (p:Person {name: 'Alice'})-[:FRIEND*2]->(fof) RETURN DISTINCT fof.name as name"
        },
        {
            "name": "查找所有公司及其员工",
            "query": "MATCH (c:Company)<-[:WORKS_AT]-(p:Person) RETURN c.name as company, collect(p.name) as employees"
        }
    ]
    
    return render_template('query.html', 
                          results=results, 
                          query_text=query_text,
                          query_templates=query_templates)

@app.route('/visualize')
def visualize():
    """可视化页面"""
    # 获取所有节点类型用于筛选
    node_labels_query = "CALL db.labels() YIELD label RETURN label"
    node_labels = [item['label'] for item in neo4j_conn.execute_query(node_labels_query)]
    
    # 获取所有关系类型
    rel_types_query = "CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType"
    rel_types = [item['relationshipType'] for item in neo4j_conn.execute_query(rel_types_query)]
    
    # 获取示例数据用于预览
    sample_query = """
    MATCH (n)
    RETURN labels(n)[0] as label, properties(n) as properties
    LIMIT 5
    """
    sample_data = neo4j_conn.execute_query(sample_query)
    
    return render_template('visualize.html',
                          node_labels=node_labels,
                          rel_types=rel_types,
                          sample_data=sample_data)

@app.route('/api/graph_data', methods=['GET'])
def get_graph_data():
    """获取图谱数据API"""
    try:
        # 获取查询参数
        label_filter = request.args.get('label', '')
        limit = int(request.args.get('limit', 100))
        
        # 构建查询
        if label_filter:
            query = f"""
            MATCH (n:{label_filter})-[r]-(m)
            RETURN 
                id(n) as source_id,
                labels(n)[0] as source_label,
                properties(n) as source_props,
                id(m) as target_id,
                labels(m)[0] as target_label,
                properties(m) as target_props,
                type(r) as relationship_type,
                properties(r) as relationship_props
            LIMIT {limit}
            """
        else:
            query = f"""
            MATCH (n)-[r]-(m)
            RETURN 
                id(n) as source_id,
                labels(n)[0] as source_label,
                properties(n) as source_props,
                id(m) as target_id,
                labels(m)[0] as target_label,
                properties(m) as target_props,
                type(r) as relationship_type,
                properties(r) as relationship_props
            LIMIT {limit}
            """
        
        results = neo4j_conn.execute_query(query)
        
        # 构建节点和边数据
        nodes = {}
        edges = []
        node_counter = 0
        
        for record in results:
            # 处理源节点
            source_id = record['source_id']
            if source_id not in nodes:
                nodes[source_id] = {
                    "id": node_counter,
                    "neo4j_id": source_id,
                    "label": record['source_label'],
                    "properties": record['source_props'],
                    "name": record['source_props'].get('name', f"Node_{source_id}")
                }
                node_counter += 1
            
            # 处理目标节点
            target_id = record['target_id']
            if target_id not in nodes:
                nodes[target_id] = {
                    "id": node_counter,
                    "neo4j_id": target_id,
                    "label": record['target_label'],
                    "properties": record['target_props'],
                    "name": record['target_props'].get('name', f"Node_{target_id}")
                }
                node_counter += 1
            
            # 添加边
            edges.append({
                "source": nodes[source_id]["id"],
                "target": nodes[target_id]["id"],
                "type": record['relationship_type'],
                "properties": record['relationship_props']
            })
        
        return jsonify({
            "success": True,
            "nodes": list(nodes.values()),
            "edges": edges,
            "count": {
                "nodes": len(nodes),
                "edges": len(edges)
            }
        })
    
    except Exception as e:
        return jsonify({
            "success": False,
            "error": str(e)
        }), 500

@app.route('/manage', methods=['GET', 'POST'])
def manage():
    """管理页面"""
    message = None
    message_type = None
    
    if request.method == 'POST':
        action = request.form.get('action')
        
        if action == 'clear':
            result = neo4j_conn.clear_database()
            message = result['message']
            message_type = 'success' if result['success'] else 'error'
        
        elif action == 'export':
            filename = f"kg_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
            result = neo4j_conn.export_to_json(filename)
            message = result['message']
            message_type = 'success' if result['success'] else 'error'
            
            if result['success']:
                return send_file(filename, as_attachment=True)
        
        elif action == 'import_csv':
            if 'csv_file' in request.files:
                file = request.files['csv_file']
                if file.filename != '':
                    filename = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
                    file.save(filename)
                    
                    label = request.form.get('label', 'Node')
                    result = neo4j_conn.import_csv(filename, label)
                    
                    message = result['message']
                    message_type = 'success' if result['success'] else 'error'
        
        elif action == 'create_sample':
            result = kg_builder.create_sample_data()
            message = result['message']
            message_type = 'success' if result['success'] else 'error'
    
    return render_template('manage.html', message=message, message_type=message_type)

@app.route('/api/create_node', methods=['POST'])
def create_node():
    """创建节点API"""
    try:
        data = request.json
        label = data.get('label')
        properties = data.get('properties', {})
        
        # 添加创建时间戳
        properties['created_at'] = datetime.now().isoformat()
        
        query = f"""
        CREATE (n:{label} $props)
        RETURN id(n) as id, labels(n) as labels, properties(n) as properties
        """
        
        result = neo4j_conn.execute_query(query, {"props": properties})
        
        if result:
            return jsonify({
                "success": True,
                "message": "节点创建成功",
                "node": result[0]
            })
        else:
            return jsonify({
                "success": False,
                "message": "节点创建失败"
            }), 400
    
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e)
        }), 500

@app.route('/api/create_relationship', methods=['POST'])
def create_relationship():
    """创建关系API"""
    try:
        data = request.json
        source_id = data.get('source_id')
        target_id = data.get('target_id')
        rel_type = data.get('type')
        properties = data.get('properties', {})
        
        query = f"""
        MATCH (a) WHERE id(a) = $source_id
        MATCH (b) WHERE id(b) = $target_id
        CREATE (a)-[r:{rel_type} $props]->(b)
        RETURN id(r) as id, type(r) as type, properties(r) as properties
        """
        
        params = {
            "source_id": source_id,
            "target_id": target_id,
            "props": properties
        }
        
        result = neo4j_conn.execute_query(query, params)
        
        if result:
            return jsonify({
                "success": True,
                "message": "关系创建成功",
                "relationship": result[0]
            })
        else:
            return jsonify({
                "success": False,
                "message": "关系创建失败"
            }), 400
    
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e)
        }), 500

@app.route('/api/search', methods=['GET'])
def search():
    """搜索API"""
    try:
        keyword = request.args.get('q', '')
        search_type = request.args.get('type', 'both')  # node, relation, both
        
        results = []
        
        if search_type in ['node', 'both']:
            # 搜索节点
            node_query = """
            MATCH (n)
            WHERE n.name CONTAINS $keyword OR n.id CONTAINS $keyword
            RETURN 
                id(n) as id,
                labels(n) as labels,
                properties(n) as properties,
                'node' as type
            LIMIT 20
            """
            nodes = neo4j_conn.execute_query(node_query, {"keyword": keyword})
            results.extend(nodes)
        
        if search_type in ['relation', 'both']:
            # 搜索关系
            rel_query = """
            MATCH (a)-[r]-(b)
            WHERE type(r) CONTAINS $keyword
            RETURN 
                id(r) as id,
                type(r) as type,
                properties(r) as properties,
                id(a) as source_id,
                id(b) as target_id,
                'relationship' as result_type
            LIMIT 20
            """
            relationships = neo4j_conn.execute_query(rel_query, {"keyword": keyword})
            results.extend(relationships)
        
        return jsonify({
            "success": True,
            "count": len(results),
            "results": results
        })
    
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e)
        }), 500

@app.route('/api/statistics')
def get_statistics():
    """获取统计数据API"""
    try:
        # 节点统计
        node_stats = neo4j_conn.execute_query("""
        CALL db.labels() YIELD label
        CALL apoc.cypher.run('MATCH (n:`' + label + '`) RETURN count(*) as count', {})
        YIELD value
        RETURN label, value.count as count
        ORDER BY count DESC
        """)
        
        # 关系统计
        rel_stats = neo4j_conn.execute_query("""
        CALL db.relationshipTypes() YIELD relationshipType
        CALL apoc.cypher.run('MATCH ()-[r:`' + relationshipType + '`]->() RETURN count(*) as count', {})
        YIELD value
        RETURN relationshipType, value.count as count
        ORDER BY count DESC
        """)
        
        # 属性统计
        prop_stats = neo4j_conn.execute_query("""
        MATCH (n) 
        UNWIND keys(n) as key
        RETURN key, count(*) as count
        ORDER BY count DESC
        LIMIT 10
        """)
        
        return jsonify({
            "success": True,
            "statistics": {
                "nodes_by_label": node_stats,
                "relationships_by_type": rel_stats,
                "top_properties": prop_stats
            }
        })
    
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e)
        }), 500

@app.route('/health')
def health_check():
    """健康检查"""
    db_status = neo4j_conn.is_connected()
    
    return jsonify({
        "status": "healthy" if db_status else "unhealthy",
        "database": "connected" if db_status else "disconnected",
        "timestamp": datetime.now().isoformat()
    })

@app.teardown_appcontext
def teardown_db(exception):
    """关闭数据库连接"""
    neo4j_conn.close()

if __name__ == '__main__':
    app.run(debug=True, port=5000, host='0.0.0.0')

5. 知识图谱构建器

utils/kg_builder.py

python

复制代码
class KnowledgeGraphBuilder:
    """知识图谱构建器"""
    
    def __init__(self, neo4j_connector):
        self.conn = neo4j_connector
    
    def create_sample_data(self):
        """创建示例数据"""
        try:
            # 创建电影知识图谱示例
            sample_data = [
                # 创建电影
                ("CREATE (m:Movie {title: 'The Matrix', year: 1999, genre: 'Sci-Fi', rating: 8.7})", {}),
                ("CREATE (m:Movie {title: 'Inception', year: 2010, genre: 'Sci-Fi', rating: 8.8})", {}),
                ("CREATE (m:Movie {title: 'The Godfather', year: 1972, genre: 'Crime', rating: 9.2})", {}),
                
                # 创建演员
                ("CREATE (p:Person {name: 'Keanu Reeves', born: 1964, nationality: 'Canadian'})", {}),
                ("CREATE (p:Person {name: 'Laurence Fishburne', born: 1961, nationality: 'American'})", {}),
                ("CREATE (p:Person {name: 'Leonardo DiCaprio', born: 1974, nationality: 'American'})", {}),
                ("CREATE (p:Person {name: 'Marlon Brando', born: 1924, nationality: 'American'})", {}),
                
                # 创建导演
                ("CREATE (p:Person {name: 'The Wachowskis', profession: 'Directors'})", {}),
                ("CREATE (p:Person {name: 'Christopher Nolan', born: 1970, nationality: 'British'})", {}),
                ("CREATE (p:Person {name: 'Francis Ford Coppola', born: 1939, nationality: 'American'})", {}),
                
                # 创建关系
                ("""
                MATCH (a:Person {name: 'Keanu Reeves'})
                MATCH (b:Movie {title: 'The Matrix'})
                CREATE (a)-[r:ACTED_IN {role: 'Neo'}]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'Laurence Fishburne'})
                MATCH (b:Movie {title: 'The Matrix'})
                CREATE (a)-[r:ACTED_IN {role: 'Morpheus'}]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'Leonardo DiCaprio'})
                MATCH (b:Movie {title: 'Inception'})
                CREATE (a)-[r:ACTED_IN {role: 'Cobb'}]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'Marlon Brando'})
                MATCH (b:Movie {title: 'The Godfather'})
                CREATE (a)-[r:ACTED_IN {role: 'Vito Corleone'}]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'The Wachowskis'})
                MATCH (b:Movie {title: 'The Matrix'})
                CREATE (a)-[r:DIRECTED]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'Christopher Nolan'})
                MATCH (b:Movie {title: 'Inception'})
                CREATE (a)-[r:DIRECTED]->(b)
                """, {}),
                
                ("""
                MATCH (a:Person {name: 'Francis Ford Coppola'})
                MATCH (b:Movie {title: 'The Godfather'})
                CREATE (a)-[r:DIRECTED]->(b)
                """, {})
            ]
            
            for query, params in sample_data:
                self.conn.execute_query(query, params)
            
            return {
                "success": True,
                "message": "示例数据创建成功",
                "data": {
                    "movies": 3,
                    "people": 7,
                    "relationships": 7
                }
            }
        
        except Exception as e:
            return {
                "success": False,
                "message": f"创建示例数据失败: {str(e)}"
            }
    
    def build_company_kg(self):
        """构建公司知识图谱"""
        try:
            # 创建公司节点
            companies = [
                {"name": "Apple", "industry": "Technology", "founded": 1976},
                {"name": "Google", "industry": "Technology", "founded": 1998},
                {"name": "Microsoft", "industry": "Technology", "founded": 1975}
            ]
            
            # 创建人物节点
            people = [
                {"name": "Steve Jobs", "role": "Co-founder", "company": "Apple"},
                {"name": "Tim Cook", "role": "CEO", "company": "Apple"},
                {"name": "Larry Page", "role": "Co-founder", "company": "Google"},
                {"name": "Sundar Pichai", "role": "CEO", "company": "Google"},
                {"name": "Bill Gates", "role": "Co-founder", "company": "Microsoft"},
                {"name": "Satya Nadella", "role": "CEO", "company": "Microsoft"}
            ]
            
            # 创建产品节点
            products = [
                {"name": "iPhone", "type": "Smartphone", "company": "Apple"},
                {"name": "MacBook", "type": "Laptop", "company": "Apple"},
                {"name": "Google Search", "type": "Search Engine", "company": "Google"},
                {"name": "Android", "type": "Operating System", "company": "Google"},
                {"name": "Windows", "type": "Operating System", "company": "Microsoft"},
                {"name": "Office", "type": "Productivity Suite", "company": "Microsoft"}
            ]
            
            # 批量创建节点
            for company in companies:
                self.conn.execute_query(
                    "CREATE (c:Company $props)",
                    {"props": company}
                )
            
            for person in people:
                self.conn.execute_query(
                    "CREATE (p:Person $props)",
                    {"props": person}
                )
            
            for product in products:
                self.conn.execute_query(
                    "CREATE (p:Product $props)",
                    {"props": product}
                )
            
            # 创建关系
            relationships = [
                # 人物-公司关系
                ("MATCH (p:Person {name: 'Steve Jobs'}) MATCH (c:Company {name: 'Apple'}) CREATE (p)-[:FOUNDED]->(c)", {}),
                ("MATCH (p:Person {name: 'Tim Cook'}) MATCH (c:Company {name: 'Apple'}) CREATE (p)-[:WORKS_AS {position: 'CEO'}]->(c)", {}),
                ("MATCH (p:Person {name: 'Larry Page'}) MATCH (c:Company {name: 'Google'}) CREATE (p)-[:FOUNDED]->(c)", {}),
                ("MATCH (p:Person {name: 'Sundar Pichai'}) MATCH (c:Company {name: 'Google'}) CREATE (p)-[:WORKS_AS {position: 'CEO'}]->(c)", {}),
                ("MATCH (p:Person {name: 'Bill Gates'}) MATCH (c:Company {name: 'Microsoft'}) CREATE (p)-[:FOUNDED]->(c)", {}),
                ("MATCH (p:Person {name: 'Satya Nadella'}) MATCH (c:Company {name: 'Microsoft'}) CREATE (p)-[:WORKS_AS {position: 'CEO'}]->(c)", {}),
                
                # 产品-公司关系
                ("MATCH (p:Product {name: 'iPhone'}) MATCH (c:Company {name: 'Apple'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                ("MATCH (p:Product {name: 'MacBook'}) MATCH (c:Company {name: 'Apple'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                ("MATCH (p:Product {name: 'Google Search'}) MATCH (c:Company {name: 'Google'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                ("MATCH (p:Product {name: 'Android'}) MATCH (c:Company {name: 'Google'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                ("MATCH (p:Product {name: 'Windows'}) MATCH (c:Company {name: 'Microsoft'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                ("MATCH (p:Product {name: 'Office'}) MATCH (c:Company {name: 'Microsoft'}) CREATE (p)-[:PRODUCED_BY]->(c)", {}),
                
                # 竞争关系
                ("MATCH (c1:Company {name: 'Apple'}) MATCH (c2:Company {name: 'Google'}) CREATE (c1)-[:COMPETES_WITH]->(c2)", {}),
                ("MATCH (c1:Company {name: 'Apple'}) MATCH (c2:Company {name: 'Microsoft'}) CREATE (c1)-[:COMPETES_WITH]->(c2)", {}),
                ("MATCH (c1:Company {name: 'Google'}) MATCH (c2:Company {name: 'Microsoft'}) CREATE (c1)-[:COMPETES_WITH]->(c2)", {})
            ]
            
            for query, params in relationships:
                self.conn.execute_query(query, params)
            
            return {
                "success": True,
                "message": "公司知识图谱创建成功",
                "data": {
                    "companies": len(companies),
                    "people": len(people),
                    "products": len(products),
                    "relationships": len(relationships)
                }
            }
        
        except Exception as e:
            return {
                "success": False,
                "message": f"构建公司知识图谱失败: {str(e)}"
            }

6. HTML模板

templates/base.html

html

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}知识图谱管理系统{% endblock %}</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    
    <!-- 自定义样式 -->
    <style>
        :root {
            --primary-color: #3498db;
            --secondary-color: #2c3e50;
            --accent-color: #e74c3c;
            --light-bg: #f8f9fa;
            --dark-bg: #343a40;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: var(--light-bg);
        }
        
        .navbar-brand {
            font-weight: bold;
            color: var(--primary-color) !important;
        }
        
        .sidebar {
            min-height: calc(100vh - 56px);
            background-color: var(--secondary-color);
            color: white;
        }
        
        .sidebar a {
            color: rgba(255, 255, 255, 0.8);
            text-decoration: none;
            padding: 10px 15px;
            display: block;
            border-radius: 5px;
            margin: 5px 0;
            transition: all 0.3s;
        }
        
        .sidebar a:hover {
            background-color: rgba(255, 255, 255, 0.1);
            color: white;
        }
        
        .sidebar a.active {
            background-color: var(--primary-color);
            color: white;
        }
        
        .sidebar i {
            margin-right: 10px;
            width: 20px;
            text-align: center;
        }
        
        .card {
            border: none;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 20px;
            transition: transform 0.3s;
        }
        
        .card:hover {
            transform: translateY(-2px);
        }
        
        .card-header {
            background-color: var(--primary-color);
            color: white;
            font-weight: bold;
        }
        
        .stat-card {
            text-align: center;
            padding: 20px;
            background: linear-gradient(135deg, var(--primary-color), #2980b9);
            color: white;
            border-radius: 10px;
        }
        
        .stat-card i {
            font-size: 3rem;
            margin-bottom: 10px;
        }
        
        .stat-card .number {
            font-size: 2.5rem;
            font-weight: bold;
        }
        
        .stat-card .label {
            font-size: 1rem;
            opacity: 0.9;
        }
        
        .btn-primary {
            background-color: var(--primary-color);
            border-color: var(--primary-color);
        }
        
        .btn-primary:hover {
            background-color: #2980b9;
            border-color: #2980b9;
        }
        
        .table-hover tbody tr:hover {
            background-color: rgba(52, 152, 219, 0.1);
        }
        
        .alert {
            border: none;
            border-radius: 5px;
        }
        
        .alert-success {
            background-color: #d4edda;
            color: #155724;
        }
        
        .alert-error {
            background-color: #f8d7da;
            color: #721c24;
        }
        
        .cypher-editor {
            font-family: 'Courier New', monospace;
            min-height: 200px;
        }
        
        #graph-container {
            width: 100%;
            height: 600px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: white;
        }
        
        .node-tooltip {
            position: absolute;
            background: white;
            border: 1px solid #ddd;
            border-radius: 3px;
            padding: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            display: none;
            z-index: 1000;
            max-width: 300px;
        }
        
        @media (max-width: 768px) {
            .sidebar {
                min-height: auto;
                margin-bottom: 20px;
            }
        }
    </style>
    
    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('index') }}">
                <i class="fas fa-project-diagram"></i> 知识图谱管理系统
            </a>
            
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('health') }}">
                            <i class="fas fa-heartbeat"></i> 健康检查
                        </a>
                    </li>
                    <li class="nav-item">
                        <span class="nav-link">
                            <i class="fas fa-database"></i> 
                            {% if neo4j_conn.is_connected() %}
                                <span class="text-success">数据库已连接</span>
                            {% else %}
                                <span class="text-danger">数据库未连接</span>
                            {% endif %}
                        </span>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="container-fluid">
        <div class="row">
            <!-- 侧边栏 -->
            <div class="col-md-3 col-lg-2 sidebar d-md-block">
                <div class="position-sticky pt-3">
                    <ul class="nav flex-column">
                        <li>
                            <a href="{{ url_for('dashboard') }}" 
                               class="{% if request.endpoint == 'dashboard' %}active{% endif %}">
                                <i class="fas fa-tachometer-alt"></i> 仪表盘
                            </a>
                        </li>
                        <li>
                            <a href="{{ url_for('query') }}"
                               class="{% if request.endpoint == 'query' %}active{% endif %}">
                                <i class="fas fa-search"></i> 查询
                            </a>
                        </li>
                        <li>
                            <a href="{{ url_for('visualize') }}"
                               class="{% if request.endpoint == 'visualize' %}active{% endif %}">
                                <i class="fas fa-eye"></i> 可视化
                            </a>
                        </li>
                        <li>
                            <a href="{{ url_for('manage') }}"
                               class="{% if request.endpoint == 'manage' %}active{% endif %}">
                                <i class="fas fa-cog"></i> 管理
                            </a>
                        </li>
                    </ul>
                    
                    <hr class="bg-light">
                    
                    <h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
                        <span>数据库信息</span>
                    </h6>
                    
                    <ul class="nav flex-column">
                        <li>
                            <a href="#" class="text-info">
                                <i class="fas fa-server"></i> {{ neo4j_conn.uri }}
                            </a>
                        </li>
                        <li>
                            <a href="#" class="text-info">
                                <i class="fas fa-user"></i> {{ neo4j_conn.user }}
                            </a>
                        </li>
                    </ul>
                </div>
            </div>

            <!-- 主内容区域 -->
            <div class="col-md-9 col-lg-10 ms-sm-auto px-md-4">
                {% with messages = get_flashed_messages(with_categories=true) %}
                    {% if messages %}
                        {% for category, message in messages %}
                            <div class="alert alert-{{ category }} mt-3">
                                {{ message }}
                            </div>
                        {% endfor %}
                    {% endif %}
                {% endwith %}
                
                {% block content %}{% endblock %}
            </div>
        </div>
    </div>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    
    <!-- 全局JS -->
    <script>
        // 显示加载指示器
        function showLoading() {
            const loadingEl = document.createElement('div');
            loadingEl.id = 'loading';
            loadingEl.innerHTML = `
                <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; 
                            background: rgba(0,0,0,0.5); z-index: 9999; display: flex; 
                            align-items: center; justify-content: center;">
                    <div class="spinner-border text-light" role="status">
                        <span class="visually-hidden">加载中...</span>
                    </div>
                </div>
            `;
            document.body.appendChild(loadingEl);
        }
        
        // 隐藏加载指示器
        function hideLoading() {
            const loadingEl = document.getElementById('loading');
            if (loadingEl) {
                loadingEl.remove();
            }
        }
        
        // 显示消息
        function showMessage(message, type = 'info') {
            const alertDiv = document.createElement('div');
            alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
            alertDiv.innerHTML = `
                ${message}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            `;
            
            const container = document.querySelector('.container-fluid .col-md-9');
            container.insertBefore(alertDiv, container.firstChild);
            
            setTimeout(() => {
                alertDiv.remove();
            }, 5000);
        }
        
        // 处理表单提交
        function handleFormSubmit(formId, successCallback) {
            const form = document.getElementById(formId);
            if (form) {
                form.addEventListener('submit', function(e) {
                    e.preventDefault();
                    showLoading();
                    
                    const formData = new FormData(this);
                    const action = this.getAttribute('action');
                    const method = this.getAttribute('method') || 'POST';
                    
                    fetch(action, {
                        method: method,
                        body: formData
                    })
                    .then(response => response.json())
                    .then(data => {
                        hideLoading();
                        if (data.success) {
                            showMessage(data.message, 'success');
                            if (successCallback) successCallback(data);
                        } else {
                            showMessage(data.message, 'danger');
                        }
                    })
                    .catch(error => {
                        hideLoading();
                        showMessage('请求失败: ' + error.message, 'danger');
                    });
                });
            }
        }
        
        // 页面加载完成
        document.addEventListener('DOMContentLoaded', function() {
            // 为所有带有 data-confirm 属性的链接添加确认对话框
            document.querySelectorAll('a[data-confirm]').forEach(link => {
                link.addEventListener('click', function(e) {
                    if (!confirm(this.getAttribute('data-confirm'))) {
                        e.preventDefault();
                    }
                });
            });
            
            // 为所有表格行添加点击效果
            document.querySelectorAll('table tbody tr').forEach(row => {
                row.addEventListener('click', function() {
                    this.classList.toggle('table-active');
                });
            });
        });
    </script>
    
    {% block extra_js %}{% endblock %}
</body>
</html>

templates/dashboard.html

html

复制代码
{% extends "base.html" %}

{% block title %}仪表盘 - 知识图谱管理系统{% endblock %}

{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    <h1 class="h2">
        <i class="fas fa-tachometer-alt"></i> 仪表盘
    </h1>
    <div class="btn-toolbar mb-2 mb-md-0">
        <div class="btn-group me-2">
            <button type="button" class="btn btn-sm btn-outline-secondary" οnclick="refreshDashboard()">
                <i class="fas fa-sync-alt"></i> 刷新
            </button>
        </div>
    </div>
</div>

<!-- 统计卡片 -->
<div class="row mb-4">
    <div class="col-md-3">
        <div class="stat-card">
            <i class="fas fa-cube"></i>
            <div class="number" id="node-count">0</div>
            <div class="label">节点总数</div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="stat-card">
            <i class="fas fa-link"></i>
            <div class="number" id="rel-count">0</div>
            <div class="label">关系总数</div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="stat-card">
            <i class="fas fa-tag"></i>
            <div class="number" id="label-count">0</div>
            <div class="label">标签类型</div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="stat-card">
            <i class="fas fa-code-branch"></i>
            <div class="number" id="rel-type-count">0</div>
            <div class="label">关系类型</div>
        </div>
    </div>
</div>

<div class="row">
    <!-- 节点统计 -->
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <i class="fas fa-chart-pie"></i> 节点统计(按标签)
            </div>
            <div class="card-body">
                <table class="table table-hover">
                    <thead>
                        <tr>
                            <th>标签</th>
                            <th>数量</th>
                            <th>百分比</th>
                        </tr>
                    </thead>
                    <tbody id="node-stats">
                        {% for stat in stats %}
                        <tr>
                            <td>{{ stat.label }}</td>
                            <td>{{ stat.count }}</td>
                            <td>
                                <div class="progress">
                                    <div class="progress-bar" 
                                         role="progressbar" 
                                         style="width: {{ stat.count * 100 / (stats|sum(attribute='count') or 1) }}%">
                                    </div>
                                </div>
                            </td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- 最近添加的节点 -->
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <i class="fas fa-history"></i> 最近添加的节点
            </div>
            <div class="card-body">
                <div class="list-group">
                    {% for node in recent_nodes %}
                    <a href="#" class="list-group-item list-group-item-action">
                        <div class="d-flex w-100 justify-content-between">
                            <h6 class="mb-1">
                                <span class="badge bg-primary">{{ node.label }}</span>
                                {{ node.name }}
                            </h6>
                            <small>{{ node.created_at }}</small>
                        </div>
                    </a>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 数据库信息 -->
<div class="card mt-4">
    <div class="card-header">
        <i class="fas fa-info-circle"></i> 数据库信息
    </div>
    <div class="card-body">
        <div class="accordion" id="dbInfoAccordion">
            {% for section, data in db_info.items() %}
            <div class="accordion-item">
                <h2 class="accordion-header" id="heading{{ loop.index }}">
                    <button class="accordion-button collapsed" type="button" 
                            data-bs-toggle="collapse" data-bs-target="#collapse{{ loop.index }}">
                        {{ section }} ({{ data|length }})
                    </button>
                </h2>
                <div id="collapse{{ loop.index }}" class="accordion-collapse collapse" 
                     data-bs-parent="#dbInfoAccordion">
                    <div class="accordion-body">
                        {% if data is string %}
                            <p>{{ data }}</p>
                        {% elif data is mapping %}
                            <pre>{{ data|tojson(indent=2) }}</pre>
                        {% else %}
                            <table class="table table-sm">
                                {% for item in data %}
                                <tr>
                                    {% for key, value in item.items() %}
                                    <td><strong>{{ key }}:</strong></td>
                                    <td>{{ value }}</td>
                                    {% endfor %}
                                </tr>
                                {% endfor %}
                            </table>
                        {% endif %}
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
    // 刷新统计数据
    function refreshDashboard() {
        showLoading();
        
        fetch('/api/statistics')
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    updateStatistics(data.statistics);
                }
                hideLoading();
            })
            .catch(error => {
                console.error('Error:', error);
                hideLoading();
            });
    }
    
    // 更新统计数据显示
    function updateStatistics(stats) {
        // 更新节点统计表
        const nodeStatsBody = document.getElementById('node-stats');
        if (nodeStatsBody && stats.nodes_by_label) {
            let totalNodes = 0;
            stats.nodes_by_label.forEach(item => {
                totalNodes += item.count;
            });
            
            nodeStatsBody.innerHTML = stats.nodes_by_label.map(item => `
                <tr>
                    <td>${item.label}</td>
                    <td>${item.count}</td>
                    <td>
                        <div class="progress">
                            <div class="progress-bar" 
                                 role="progressbar" 
                                 style="width: ${totalNodes ? (item.count * 100 / totalNodes) : 0}%">
                            </div>
                        </div>
                    </td>
                </tr>
            `).join('');
            
            // 更新统计卡片
            document.getElementById('node-count').textContent = totalNodes;
            document.getElementById('label-count').textContent = stats.nodes_by_label.length;
        }
        
        // 更新关系统计
        if (stats.relationships_by_type) {
            let totalRels = 0;
            stats.relationships_by_type.forEach(item => {
                totalRels += item.count;
            });
            
            document.getElementById('rel-count').textContent = totalRels;
            document.getElementById('rel-type-count').textContent = stats.relationships_by_type.length;
        }
    }
    
    // 页面加载时获取统计数据
    document.addEventListener('DOMContentLoaded', function() {
        refreshDashboard();
        
        // 每30秒自动刷新
        setInterval(refreshDashboard, 30000);
    });
</script>
{% endblock %}

templates/query.html

html

复制代码
{% extends "base.html" %}

{% block title %}查询 - 知识图谱管理系统{% endblock %}

{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    <h1 class="h2">
        <i class="fas fa-search"></i> Cypher 查询
    </h1>
    <div class="btn-toolbar mb-2 mb-md-0">
        <div class="btn-group me-2">
            <button type="button" class="btn btn-sm btn-outline-primary" οnclick="clearQuery()">
                <i class="fas fa-eraser"></i> 清空
            </button>
            <button type="button" class="btn btn-sm btn-primary" οnclick="executeQuery()">
                <i class="fas fa-play"></i> 执行
            </button>
        </div>
    </div>
</div>

<div class="row">
    <!-- 查询编辑器 -->
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">
                <i class="fas fa-code"></i> 查询编辑器
            </div>
            <div class="card-body">
                <form id="queryForm" method="POST">
                    <div class="mb-3">
                        <textarea class="form-control cypher-editor" id="query" name="query" 
                                  rows="8" placeholder="输入Cypher查询语句...">{{ query_text }}</textarea>
                    </div>
                    
                    <div class="mb-3">
                        <label class="form-label">查询参数 (JSON格式):</label>
                        <textarea class="form-control" id="params" name="params" 
                                  rows="3" placeholder='{"name": "Alice"}'></textarea>
                    </div>
                    
                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-play"></i> 执行查询
                    </button>
                    
                    <button type="button" class="btn btn-outline-secondary ms-2" οnclick="explainQuery()">
                        <i class="fas fa-info-circle"></i> 解释执行计划
                    </button>
                </form>
            </div>
        </div>
    </div>

    <!-- 查询模板 -->
    <div class="col-md-4">
        <div class="card">
            <div class="card-header">
                <i class="fas fa-clipboard-list"></i> 查询模板
            </div>
            <div class="card-body">
                <div class="list-group">
                    {% for template in query_templates %}
                    <a href="#" class="list-group-item list-group-item-action" 
                       οnclick="loadTemplate('{{ template.query }}')">
                        <div class="d-flex w-100 justify-content-between">
                            <h6 class="mb-1">{{ template.name }}</h6>
                        </div>
                        <small class="text-muted">{{ template.query|truncate(50) }}</small>
                    </a>
                    {% endfor %}
                </div>
                
                <div class="mt-3">
                    <h6>常用操作:</h6>
                    <div class="btn-group-vertical w-100">
                        <button class="btn btn-outline-info text-start" 
                                οnclick="loadTemplate('MATCH (n) RETURN n LIMIT 10')">
                            <i class="fas fa-cube"></i> 查看所有节点
                        </button>
                        <button class="btn btn-outline-info text-start" 
                                οnclick="loadTemplate('MATCH ()-[r]->() RETURN r LIMIT 10')">
                            <i class="fas fa-link"></i> 查看所有关系
                        </button>
                        <button class="btn btn-outline-info text-start" 
                                οnclick="loadTemplate('CALL db.labels()')">
                            <i class="fas fa-tag"></i> 查看所有标签
                        </button>
                        <button class="btn btn-outline-info text-start" 
                                οnclick="loadTemplate('CALL db.relationshipTypes()')">
                            <i class="fas fa-code-branch"></i> 查看所有关系类型
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 查询结果 -->
{% if results %}
<div class="card mt-4">
    <div class="card-header d-flex justify-content-between align-items-center">
        <div>
            <i class="fas fa-table"></i> 查询结果
            <span class="badge bg-primary ms-2">{{ results|length }} 条记录</span>
        </div>
        <div>
            <button class="btn btn-sm btn-outline-success" οnclick="exportResults()">
                <i class="fas fa-download"></i> 导出为JSON
            </button>
        </div>
    </div>
    
    <div class="card-body">
        <div class="table-responsive">
            {% if results %}
                {% set headers = results[0].keys() %}
                <table class="table table-striped table-hover">
                    <thead>
                        <tr>
                            {% for header in headers %}
                            <th>{{ header }}</th>
                            {% endfor %}
                        </tr>
                    </thead>
                    <tbody>
                        {% for row in results %}
                        <tr>
                            {% for header in headers %}
                            <td>
                                {% if row[header] is mapping or row[header] is iterable and row[header] is not string %}
                                    <button class="btn btn-sm btn-outline-info" 
                                            οnclick="showDetail('{{ row[header]|tojson|escape }}')">
                                        查看详情
                                    </button>
                                {% else %}
                                    {{ row[header] }}
                                {% endif %}
                            </td>
                            {% endfor %}
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            {% else %}
                <div class="alert alert-info">
                    没有查询到数据
                </div>
            {% endif %}
        </div>
    </div>
</div>
{% endif %}

<!-- 执行计划结果模态框 -->
<div class="modal fade" id="explainModal" tabindex="-1">
    <div class="modal-dialog modal-xl">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">执行计划</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <pre id="explainResult" class="bg-light p-3"></pre>
            </div>
        </div>
    </div>
</div>

<!-- 详情模态框 -->
<div class="modal fade" id="detailModal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">详情</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <pre id="detailContent" class="bg-light p-3"></pre>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
    // 加载查询模板
    function loadTemplate(query) {
        document.getElementById('query').value = query;
    }
    
    // 清空查询
    function clearQuery() {
        document.getElementById('query').value = '';
        document.getElementById('params').value = '';
    }
    
    // 执行查询
    function executeQuery() {
        document.getElementById('queryForm').submit();
    }
    
    // 解释执行计划
    function explainQuery() {
        const query = document.getElementById('query').value;
        if (!query.trim()) {
            alert('请输入查询语句');
            return;
        }
        
        showLoading();
        
        const explainQuery = 'EXPLAIN ' + query;
        const params = document.getElementById('params').value;
        
        let paramObj = {};
        try {
            if (params.trim()) {
                paramObj = JSON.parse(params);
            }
        } catch (e) {
            alert('参数格式错误: ' + e.message);
            hideLoading();
            return;
        }
        
        fetch('/query', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                query: explainQuery,
                params: paramObj,
                explain: true
            })
        })
        .then(response => response.json())
        .then(data => {
            hideLoading();
            
            if (data.success) {
                const explainResult = document.getElementById('explainResult');
                explainResult.textContent = JSON.stringify(data.result, null, 2);
                const modal = new bootstrap.Modal(document.getElementById('explainModal'));
                modal.show();
            } else {
                alert('错误: ' + data.message);
            }
        })
        .catch(error => {
            hideLoading();
            alert('请求失败: ' + error.message);
        });
    }
    
    // 显示详情
    function showDetail(content) {
        const detailContent = document.getElementById('detailContent');
        
        try {
            const parsed = JSON.parse(content);
            detailContent.textContent = JSON.stringify(parsed, null, 2);
        } catch (e) {
            detailContent.textContent = content;
        }
        
        const modal = new bootstrap.Modal(document.getElementById('detailModal'));
        modal.show();
    }
    
    // 导出结果为JSON
    function exportResults() {
        const results = {{ results|tojson|safe }};
        const dataStr = JSON.stringify(results, null, 2);
        const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
        
        const exportFileDefaultName = `query_results_${new Date().toISOString().slice(0,10)}.json`;
        
        const linkElement = document.createElement('a');
        linkElement.setAttribute('href', dataUri);
        linkElement.setAttribute('download', exportFileDefaultName);
        linkElement.click();
    }
    
    // 添加语法高亮(简化版)
    document.addEventListener('DOMContentLoaded', function() {
        const textarea = document.getElementById('query');
        if (textarea) {
            textarea.addEventListener('keydown', function(e) {
                if (e.key === 'Tab') {
                    e.preventDefault();
                    const start = this.selectionStart;
                    const end = this.selectionEnd;
                    
                    // 插入制表符
                    this.value = this.value.substring(0, start) + '  ' + this.value.substring(end);
                    
                    // 移动光标位置
                    this.selectionStart = this.selectionEnd = start + 2;
                }
            });
        }
        
        // 自动调整textarea高度
        function autoResize(textarea) {
            textarea.style.height = 'auto';
            textarea.style.height = (textarea.scrollHeight) + 'px';
        }
        
        if (textarea) {
            textarea.addEventListener('input', function() {
                autoResize(this);
            });
            autoResize(textarea); // 初始调整
        }
    });
</script>
{% endblock %}

7. 可视化模块

utils/visualizer.py

python

复制代码
import networkx as nx
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from pyvis.network import Network
import json
import os

class KGVisualizer:
    """知识图谱可视化器"""
    
    def __init__(self, neo4j_connector):
        self.conn = neo4j_connector
    
    def create_networkx_graph(self, limit=100):
        """创建NetworkX图"""
        G = nx.Graph()
        
        query = f"""
        MATCH (n)-[r]->(m)
        RETURN 
            id(n) as source_id,
            labels(n)[0] as source_label,
            n.name as source_name,
            type(r) as relation_type,
            id(m) as target_id,
            labels(m)[0] as target_label,
            m.name as target_name
        LIMIT {limit}
        """
        
        results = self.conn.execute_query(query)
        
        for record in results:
            source_id = record['source_id']
            target_id = record['target_id']
            
            # 添加节点
            if source_id not in G:
                G.add_node(
                    source_id,
                    label=record['source_label'],
                    name=record['source_name'],
                    size=10,
                    color=self._get_color(record['source_label'])
                )
            
            if target_id not in G:
                G.add_node(
                    target_id,
                    label=record['target_label'],
                    name=record['target_name'],
                    size=10,
                    color=self._get_color(record['target_label'])
                )
            
            # 添加边
            G.add_edge(
                source_id, 
                target_id,
                type=record['relation_type'],
                weight=1
            )
        
        return G
    
    def _get_color(self, label):
        """根据标签获取颜色"""
        color_map = {
            "Person": "#FF6B6B",
            "Company": "#4ECDC4",
            "Movie": "#45B7D1",
            "Product": "#96CEB4",
            "Disease": "#FFEAA7",
            "Symptom": "#DDA0DD",
            "Drug": "#98D8C8",
            "Location": "#F7DC6F"
        }
        return color_map.get(label, "#AAAAAA")
    
    def create_pyvis_network(self, output_file="graph.html", limit=200):
        """创建Pyvis交互式网络图"""
        net = Network(
            height="750px",
            width="100%",
            bgcolor="#222222",
            font_color="white",
            directed=True
        )
        
        query = f"""
        MATCH (n)-[r]->(m)
        RETURN 
            id(n) as source_id,
            labels(n)[0] as source_label,
            n.name as source_name,
            type(r) as relation_type,
            properties(r) as rel_props,
            id(m) as target_id,
            labels(m)[0] as target_label,
            m.name as target_name
        LIMIT {limit}
        """
        
        results = self.conn.execute_query(query)
        
        added_nodes = set()
        
        for record in results:
            source_id = record['source_id']
            target_id = record['target_id']
            
            # 添加源节点
            if source_id not in added_nodes:
                net.add_node(
                    source_id,
                    label=record['source_name'] or f"{record['source_label']}_{source_id}",
                    title=f"""
                    标签: {record['source_label']}<br>
                    ID: {source_id}<br>
                    {self._format_properties(record.get('source_props', {}))}
                    """,
                    color=self._get_color(record['source_label']),
                    size=15
                )
                added_nodes.add(source_id)
            
            # 添加目标节点
            if target_id not in added_nodes:
                net.add_node(
                    target_id,
                    label=record['target_name'] or f"{record['target_label']}_{target_id}",
                    title=f"""
                    标签: {record['target_label']}<br>
                    ID: {target_id}<br>
                    {self._format_properties(record.get('target_props', {}))}
                    """,
                    color=self._get_color(record['target_label']),
                    size=15
                )
                added_nodes.add(target_id)
            
            # 添加边
            rel_title = f"类型: {record['relation_type']}<br>"
            if record.get('rel_props'):
                rel_title += self._format_properties(record['rel_props'])
            
            net.add_edge(
                source_id,
                target_id,
                title=rel_title,
                label=record['relation_type'],
                color="#888888",
                width=2
            )
        
        # 设置物理布局
        net.set_options("""
        var options = {
          "physics": {
            "barnesHut": {
              "gravitationalConstant": -80000,
              "centralGravity": 0.3,
              "springLength": 95,
              "springConstant": 0.04,
              "damping": 0.09,
              "avoidOverlap": 0
            },
            "minVelocity": 0.75,
            "solver": "barnesHut"
          }
        }
        """)
        
        # 保存为HTML文件
        net.save_graph(output_file)
        
        return output_file
    
    def _format_properties(self, props):
        """格式化属性显示"""
        if not props:
            return ""
        
        lines = []
        for key, value in props.items():
            if isinstance(value, str):
                value = value[:50] + "..." if len(value) > 50 else value
            lines.append(f"{key}: {value}")
        
        return "<br>".join(lines)
    
    def create_plotly_visualization(self, limit=100):
        """创建Plotly可视化"""
        G = self.create_networkx_graph(limit)
        
        if not G.nodes():
            return None
        
        # 获取节点位置(使用spring布局)
        pos = nx.spring_layout(G, k=1, iterations=50)
        
        # 创建边迹
        edge_x = []
        edge_y = []
        edge_text = []
        
        for edge in G.edges(data=True):
            x0, y0 = pos[edge[0]]
            x1, y1 = pos[edge[1]]
            
            edge_x.append(x0)
            edge_x.append(x1)
            edge_x.append(None)
            
            edge_y.append(y0)
            edge_y.append(y1)
            edge_y.append(None)
            
            edge_text.append(edge[2].get('type', ''))
        
        edge_trace = go.Scatter(
            x=edge_x, y=edge_y,
            line=dict(width=0.5, color='#888'),
            hoverinfo='none',
            mode='lines'
        )
        
        # 创建节点迹
        node_x = []
        node_y = []
        node_text = []
        node_color = []
        node_size = []
        
        for node in G.nodes(data=True):
            x, y = pos[node[0]]
            node_x.append(x)
            node_y.append(y)
            
            node_data = node[1]
            label = node_data.get('label', 'Node')
            name = node_data.get('name', f'Node_{node[0]}')
            
            node_text.append(f"{label}: {name}")
            node_color.append(node_data.get('color', '#AAAAAA'))
            node_size.append(node_data.get('size', 10))
        
        node_trace = go.Scatter(
            x=node_x, y=node_y,
            mode='markers',
            hoverinfo='text',
            marker=dict(
                size=node_size,
                color=node_color,
                line_width=2
            ),
            text=node_text
        )
        
        # 创建图
        fig = go.Figure(data=[edge_trace, node_trace],
                       layout=go.Layout(
                           title='知识图谱可视化',
                           titlefont_size=16,
                           showlegend=False,
                           hovermode='closest',
                           margin=dict(b=20, l=5, r=5, t=40),
                           xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
                           yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
                       ))
        
        return fig.to_html(full_html=False)
    
    def export_graph_json(self, filename="graph_data.json"):
        """导出图为JSON格式"""
        query = """
        MATCH (n)-[r]->(m)
        RETURN 
            id(n) as source_id,
            labels(n) as source_labels,
            properties(n) as source_properties,
            type(r) as relationship_type,
            properties(r) as relationship_properties,
            id(m) as target_id,
            labels(m) as target_labels,
            properties(m) as target_properties
        LIMIT 500
        """
        
        results = self.conn.execute_query(query)
        
        # 构建图数据
        nodes = {}
        edges = []
        
        for record in results:
            # 处理源节点
            source_id = record['source_id']
            if source_id not in nodes:
                nodes[source_id] = {
                    "id": source_id,
                    "labels": record['source_labels'],
                    "properties": record['source_properties']
                }
            
            # 处理目标节点
            target_id = record['target_id']
            if target_id not in nodes:
                nodes[target_id] = {
                    "id": target_id,
                    "labels": record['target_labels'],
                    "properties": record['target_properties']
                }
            
            # 添加边
            edges.append({
                "source": source_id,
                "target": target_id,
                "type": record['relationship_type'],
                "properties": record['relationship_properties']
            })
        
        graph_data = {
            "nodes": list(nodes.values()),
            "edges": edges,
            "metadata": {
                "node_count": len(nodes),
                "edge_count": len(edges)
            }
        }
        
        # 保存到文件
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(graph_data, f, ensure_ascii=False, indent=2)
        
        return filename

8. 运行应用

requirements.txt

text

复制代码
flask==2.3.2
neo4j==5.14.0
py2neo==2021.2.3
pandas==2.0.3
python-dotenv==1.0.0
flask-cors==4.0.0
networkx==3.1
matplotlib==3.7.2
plotly==5.15.0
pyvis==0.3.2

运行步骤:

  1. 安装依赖:

bash

复制代码
pip install -r requirements.txt
  1. 配置Neo4j:

    • 安装并启动Neo4j Desktop

    • 修改默认密码

    • 更新.env文件中的配置

  2. 启动应用:

bash

复制代码
python app.py
  1. 访问应用:

    • 打开浏览器访问 http://localhost:5000

    • 查看仪表盘、执行查询、可视化图谱

9. 功能特点

  1. 完整的CRUD操作:支持节点和关系的创建、读取、更新、删除

  2. 强大的查询功能:支持Cypher查询、查询模板、执行计划

  3. 交互式可视化:使用Pyvis和Plotly实现图谱可视化

  4. 数据导入导出:支持CSV/JSON格式的数据导入导出

  5. 用户友好的界面:Bootstrap响应式设计,支持移动设备

  6. 实时统计:实时显示数据库统计信息

  7. 健康检查:监控数据库连接状态

  8. 示例数据:内置电影和公司知识图谱示例

相关推荐
KG_LLM图谱增强大模型2 小时前
[290页电子书]打造企业级知识图谱的实战手册,Neo4j 首席科学家力作!从图数据库基础到图原生机器学习
人工智能·知识图谱·neo4j
Leinwin2 小时前
Azure 存储重磅发布系列创新 以 AI 与云原生能力解锁数据未来
后端·python·flask
世界尽头与你2 小时前
Flask开启Debug模式
后端·网络安全·渗透测试·flask
飞Link2 小时前
后端架构选型:Django、Flask 与 Spring Boot 的三剑客之争
spring boot·python·django·flask
Psycho_MrZhang11 小时前
Neo4j Python SDK手册
开发语言·python·neo4j
B站计算机毕业设计超人13 小时前
计算机毕业设计Python知识图谱中华古诗词可视化 古诗词情感分析 古诗词智能问答系统 AI大模型自动写诗 大数据毕业设计(源码+LW文档+PPT+讲解)
大数据·人工智能·hadoop·python·机器学习·知识图谱·课程设计
坠金15 小时前
图数据库neo4j自建及使用
数据库·neo4j
愈努力俞幸运15 小时前
flask 入门 token, headers,cookie
后端·python·flask
aspxiy15 小时前
知识求解器:教会大型语言模型从知识图谱中搜索领域知识
人工智能·语言模型·自然语言处理·知识图谱