SQL Server RAG 笔记2:图数据库服务层与前端可视化构建

SQL Server RAG 笔记2:图数据库服务层与前端可视化构建

摘要

此篇是 SQL Server 图数据库实战的第二篇,继续构建知识图谱前端,将介绍基于 FastAPI 的服务层架构设计,以及 Vue3 + D3.js 前端图可视化实现。内容涵盖 API 路由设计、数据传输模型、前端组件化架构、D3.js 力导向图布局等核心知识点。


服务层架构设计

整体架构

┌─────────────────────────────────────────────────────┐
│                    前端 (Vue3)                       │
│         GraphVisualization / NodeList / EdgeList      │
├─────────────────────────────────────────────────────┤
│                   API 服务层                          │
│         REST API (FastAPI) + 路由分发                  │
├─────────────────────────────────────────────────────┤
│                   DAO 数据访问层                      │
│         NodeDAO / EdgeDAO / GraphDAO                  │
├─────────────────────────────────────────────────────┤
│                数据库 (SQL Server)                    │
│              原生图表 (AS NODE/EDGE)                  │
└─────────────────────────────────────────────────────┘

FastAPI 应用入口

因为接口会比较多,所以用路由的方式分开管理。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api import nodes_router, edges_router, graph_router

app = FastAPI(
    title="SQLServer GraphDB API",
    description="基于SQLServer的图数据库API系统",
    version="1.0.0"
)

# CORS 中间件配置,允许前端跨域访问
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境应限制具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 路由模块注册
app.include_router(nodes_router)
app.include_router(edges_router)
app.include_router(graph_router)

API 路由设计

节点路由 (nodes.py)

from fastapi import APIRouter, HTTPException, Query
from typing import List, Optional
from models import NodeCreate, NodeUpdate, NodeResponse
from dao import NodeDAO

router = APIRouter(prefix="/api/nodes", tags=["nodes"])

@router.post("/", response_model=NodeResponse, status_code=201)
def create_node(node: NodeCreate):
    """创建新节点"""
    node_id = NodeDAO.create_node(node)
    if node_id:
        result = NodeDAO.get_node_by_id(node_id)
        if result:
            return result
    raise HTTPException(status_code=400, detail="Failed to create node")

@router.get("/{node_id}", response_model=NodeResponse)
def get_node(node_id: int):
    """根据ID获取节点"""
    node = NodeDAO.get_node_by_id(node_id)
    if not node:
        raise HTTPException(status_code=404, detail="Node not found")
    return node

@router.get("/", response_model=List[NodeResponse])
def get_nodes(
    node_type: Optional[str] = Query(None, description="按节点类型筛选"),
    limit: int = Query(100, ge=1, le=1000),
    offset: int = Query(0, ge=0)
):
    """分页获取节点列表"""
    return NodeDAO.get_all_nodes(node_type=node_type, limit=limit, offset=offset)

@router.put("/{node_id}", response_model=NodeResponse)
def update_node(node_id: int, node_update: NodeUpdate):
    """更新节点信息"""
    result = NodeDAO.update_node(node_id, node_update)
    if not result:
        raise HTTPException(status_code=404, detail="Node not found or update failed")
    return result

@router.delete("/{node_id}", status_code=204)
def delete_node(node_id: int):
    """软删除节点"""
    success = NodeDAO.delete_node(node_id)
    if not success:
        raise HTTPException(status_code=404, detail="Node not found or already deleted")
    return None

@router.get("/search/", response_model=List[NodeResponse])
def search_nodes(keyword: str = Query(..., min_length=1), limit: int = Query(50, ge=1, le=200)):
    """关键词搜索节点"""
    return NodeDAO.search_nodes(keyword, limit)

知识点笔记:

  • response_model:自动进行数据序列化和验证
  • Query 参数:支持参数校验和文档生成
  • HTTP 状态码:201 (创建成功)、204 (删除成功)、404 (未找到)
  • 软删除:通过 IsDeleted 标志位实现,支持数据恢复

边路由和图路由可以在项目里对应目录查看,这里不一一列出,实现的套路跟上面节点路由基本一致。


前端架构设计

Vue3 组件化架构

src/
├── App.vue                    # 根组件,布局管理
├── main.js                    # 应用入口
├── services/
│   └── api.js                 # API 调用封装
└── components/
    ├── GraphVisualization.vue # 图可视化核心组件
    ├── NodeList.vue           # 节点列表管理
    └── EdgeList.vue           # 边列表管理

知识点笔记:

  • Composition API (<script setup>):更灵活的逻辑复用
  • 组件单向数据流:父组件通过 Props 传递数据
  • 事件向上传递:子组件通过 Emit 向父组件通信

API 服务封装

import axios from 'axios'

const api = axios.create({
  baseURL: '/api',  // 代理到后端服务
  timeout: 10000
})

// 响应拦截器,统一处理错误
api.interceptors.response.use(
  response => response.data,
  error => {
    console.error('API Error:', error)
    return Promise.reject(error)
  }
)

export default {
  // 节点操作
  getNodes(nodeType = null, limit = 100, offset = 0) {
    const params = new URLSearchParams({ limit, offset })
    if (nodeType) params.append('node_type', nodeType)
    return api.get(`/nodes/?${params}`)
  },

  createNode(nodeData) {
    return api.post('/nodes/', nodeData)
  },

  // 图查询操作
  getFullGraph(limit = 1000) {
    return api.get(`/graph/full?limit=${limit}`)
  },

  getNeighbors(nodeId) {
    return api.get(`/graph/neighbors/${nodeId}`)
  },

  traverseGraph(startNodeId, maxDepth = 3) {
    return api.post('/graph/traverse', {
      start_node_id: startNodeId,
      max_depth: maxDepth
    })
  },

  findPath(startNodeId, endNodeId) {
    return api.get(`/graph/path/${startNodeId}/${endNodeId}`)
  },

  getStatistics() {
    return api.get('/graph/statistics')
  }
}

知识点笔记:

  • axios.create:创建自定义配置的实例
  • 响应拦截器:在统一位置处理错误和数据转换
  • URLSearchParams:构建查询参数,避免手动拼接字符串
  • Promise 返回:支持 async/await 异步调用

图可视化核心实现

D3.js 力导向图布局

d3.js 是整个前端可视化的核心库,负责将 SQL Server 图数据库中的节点和关系数据转换为直观、可交互的力导向图,让用户能够:

  • 可视化查看图结构
  • 与节点进行交互
  • 缩放和平移探索图数据

再往下的代码可能有点枯燥了。建议保存此篇文章的URL,后续可以提供给AI,让AI以此篇URL的方式来画前端的知识图谱。

// GraphVisualization.vue
import * as d3 from 'd3'

const NODE_RADIUS = 20

const initGraph = () => {
  // 清除旧画布
  d3.select('#graph-container').selectAll('*').remove()

  const width = container.value.clientWidth
  const height = container.value.clientHeight

  const svg = d3.select('#graph-container')
    .append('svg')
    .attr('width', '100%')
    .attr('height', '100%')
    .attr('viewBox', `0 0 ${width} ${height}`)

  // 缩放行为
  const zoom = d3.zoom()
    .scaleExtent([0.1, 4])
    .on('zoom', (event) => {
      g.attr('transform', event.transform)
    })
  svg.call(zoom)

  // 节点数据处理
  const nodes = props.nodes.map(d => ({
    ...d,
    node_id: d.node_id,
    x: Math.random() * width,
    y: Math.random() * height
  }))

  const nodeIds = new Set(nodes.map(n => n.node_id))

  // 边数据处理,过滤无效引用
  const edges = props.edges
    .map(d => ({
      ...d,
      source: d.source_node_id || d.source,
      target: d.target_node_id || d.target
    }))
    .filter(d => d.source !== undefined &&
                 d.target !== undefined &&
                 nodeIds.has(d.source) &&
                 nodeIds.has(d.target))

  // 力导向模拟
  simulation = d3.forceSimulation(nodes)
    .force('link', d3.forceLink(edges)
      .id(d => d.node_id)
      .distance(120))  // 连线距离
    .force('charge', d3.forceManyBody().strength(-150))  // 节点排斥力
    .force('center', d3.forceCenter(width / 2, height / 2))  // 中心引力
    .force('collide', d3.forceCollide().radius(NODE_RADIUS + 30))  // 碰撞检测
}

知识点笔记:

  • forceSimulation:D3 力导向图核心,启动物理模拟
  • forceLink:节点间的弹簧力,拉近相连节点
  • forceManyBody:节点间排斥力,防止重叠
  • forceCenter:中心引力,将节点拉向画布中心
  • forceCollide:碰撞检测,防止节点重叠

节点与边的渲染

// 渲染连线
const link = g.append('g')
  .selectAll('line')
  .data(edges)
  .join('line')
  .attr('stroke', '#999')
  .attr('stroke-width', 1.5)
  .attr('opacity', 0.6)
  .attr('marker-end', 'url(#arrowhead)')  // 箭头标记

// 渲染节点
const node = g.append('g')
  .selectAll('g')
  .data(nodes)
  .join('g')
  .attr('cursor', 'pointer')
  .call(d3.drag()  // 拖拽交互
    .on('start', dragStarted)
    .on('drag', dragged)
    .on('end', dragEnded))

// 节点圆形
node.append('circle')
  .attr('r', NODE_RADIUS)
  .attr('fill', d => getNodeColor(d.node_type))
  .attr('stroke', '#fff')
  .attr('stroke-width', 2)

// 节点图标
node.append('text')
  .attr('text-anchor', 'middle')
  .attr('dy', '0.35em')
  .attr('font-size', '16px')
  .text(d => getNodeIcon(d.node_type))

// 节点标签
node.append('text')
  .attr('dy', NODE_RADIUS + 15)
  .attr('text-anchor', 'middle')
  .attr('font-size', '12px')
  .text(d => d.name)

// 连线标签
const linkLabels = g.append('g')
  .selectAll('text')
  .data(edges)
  .join('text')
  .attr('dy', -5)
  .attr('text-anchor', 'middle')
  .attr('font-size', '10px')
  .attr('fill', '#666')
  .text(d => d.edge_type)

知识点笔记:

  • join():D3 数据绑定语法,新旧数据对比后智能更新
  • marker-end:SVG 箭头标记,标识边的方向
  • getNodeColor:根据节点类型返回不同颜色
  • 拖拽交互:通过 drag() 行为启用节点拖拽

力模拟动态更新

// 模拟 tick 事件,更新图形位置
simulation.on('tick', () => {
  link
    .attr('x1', d => d.source.x)
    .attr('y1', d => d.source.y)
    .attr('x2', d => d.target.x)
    .attr('y2', d => d.target.y)

  node.attr('transform', d => `translate(${d.x},${d.y})`)

  linkLabels
    .attr('x', d => (d.source.x + d.target.x) / 2)
    .attr('y', d => (d.source.y + d.target.y) / 2)
})

知识点笔记:

  • simulation.on('tick'):每帧更新时调用,驱动动画
  • 位置插值:D3 自动计算节点间的平滑过渡
  • 箭头位置:实时更新连线的起点和终点坐标

交互功能实现

节点拖拽

const dragStarted = (event, d) => {
  if (!event.active) simulation.alphaTarget(0.3).restart()
  d.fx = d.x
  d.fy = d.y
}

const dragged = (event, d) => {
  d.fx = event.x
  d.fy = event.y
}

const dragEnded = (event, d) => {
  if (!event.active) simulation.alphaTarget(0)
  d.fx = null
  d.fy = null
}

知识点笔记:

  • alphaTarget:调整模拟热力,重启或停止模拟
  • fx / fy:固定节点位置,脱离模拟影响
  • 拖拽时提高 alphaTarget,让其他节点响应调整

总结

前后两篇,我们使用的技术框架总体如下:

层级 技术 作用
前端框架 Vue3 + Composition API 组件化开发
UI 组件 Element Plus 快速构建管理界面
图可视化 D3.js 力导向图布局渲染
HTTP 客户端 axios API 调用封装
后端框架 FastAPI 高性能 REST API
数据验证 Pydantic 请求/响应模型校验
数据库 SQL Server 原生图 图数据存储

通过SQLServer也是可以搭建图数据库的,但也需留意在Neo4J里很多东西跟SQLServer里的还是有区别的,这个在实际项目中是需要考虑的因素。但如果项目中不打算额外采购Neo4J,并且在已经有SQLServer2027+版本的部署的话,是可以考虑的。

本篇代码内容比较长,但也只展示了一部分,完整的项目代码可以在以下网址下载:

https://github.com/microsoftbi/SQLServerRAG

最终运行起来之后,可以看到读取出的知识图谱如下:

img


posted on 2026-05-05 00:19  哥本哈士奇(aspnetx)  阅读(0)  评论(0)    收藏  举报

导航