ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django百科问答系统源码解析:知识图谱+MySQL+Neo4j实战

Django百科问答系统源码解析:知识图谱+MySQL+Neo4j实战 简介一套基于知识图谱的百科知识问答平台毕业设计源码后端采用Python/Django前端为HTML/CSS/JS数据库使用MySQL与Neo4j附说明文档、LW笔记和汇报PPT适合毕业设计、课程设计或知识图谱问答入门实践。压缩包共444个文件总大小约171MB主要包含py源码、html/css/js前端页面、jar依赖库、Neo4j图数据库存储文件、sql脚本、docx/pptx文档及bat/ps1环境脚本目录结构清晰便于按模块学习。已有63人浏览学习。资料从知识建模、数据存储到问答解析提供了完整工程内置数据库和依赖可直接运行能帮助理解Django项目结构、知识图谱构建方法及前端交互逻辑也可作为答辩展示和二次开发的基础。1. 拿到这套百科问答源码先弄清楚它到底解决了什么很多刚接触 Django 的人会误以为“知识图谱问答平台”是一个需要大量机器学习参与的 NLP 项目实际上拿到这套源码你会看到核心并不是训练模型而是把百科知识结构化之后用一套确定性匹配规则把用户的自然语言问题映射成图数据库查询。这套项目的前后端是完整的Django 负责路由、鉴权和业务逻辑MySQL 存结构化元数据和用户行为知识图谱部分则负责承载实体和关系的三元组前端用 Django 模板语言即标题里说的 html 部分直接渲染页面不需要单独搭 Vue 或 React 前端。对谁有参考价值首先是正在做课程设计或者毕业设计的学生这套源码里带了说明文档、LW论文和 PPT结构上就是按答辩材料组织的其次是刚入手 DjangoMySQL 这套组合、想看看“一个完整项目如何分层”的后端初学者。我自己拿到这种项目包的第一反应不是急着跑起来而是先看它的 ER 图和查询模板是怎么设计的因为知识图谱问答的效果上限在数据建模时就定死了后面写的视图函数只是在消费这个模型。这套平台能做的事情用一句话概括用户输入“李白是哪个朝代的诗人”系统解析出实体“李白”和属性“朝代”去图数据库里查出对应关系再把答案以自然语言句子返回到页面上。整套流程不涉及训练模型也不依赖外部 API属于典型的“规则在前、数据为王”的落地实现。你不需要一台带 GPU 的服务器普通笔记本把 MySQL 和 Neo4j或项目里用的图数据库装好就能跑。2. Django 项目的骨架拆解从目录结构到 MySQL 数据建模2.1 拿到项目压缩包后我通常先看这四个文件一套完整的 Django 百科问答项目解压之后你应该先按这个顺序去读代码而不是急着python manage.py runserver。requirements.txt确认 Django 版本、MySQL 驱动mysqlclient 还是 pymysql、图数据库驱动neo4j 的neo4jPython 驱动或py2neosettings.py里的DATABASES和INSTALLED_APPS看它连的是本地 MySQL 还是远程App 拆了几个urls.py主路由看接口是按index、search、qa这样分的还是按 App 分的图数据库初始化脚本通常是kg_data/或onto_data/下的 .cypher 或 .csv 文件这是整个项目的数据源头。my_qa_platform/ ├── manage.py ├── knowledge_app/ # 核心应用 │ ├── models.py # ORM 模型Question/History 等 │ ├── views.py # 视图首页渲染、问答接口、搜索 │ ├── utils/ │ │ ├── query_parser.py # 问句解析模板 │ │ └── kg_client.py # 图数据库连接与查询 │ ├── templates/ │ │ ├── index.html │ │ └── result.html ├── static/ # CSS/JS/图片 ├── manage.py └── requirements.txt这个结构里最值得关注的是utils/query_parser.py和kg_client.py—— 前者是把“用户问的一句中文”转成“图查询模板”的地方后者封装了所有对知识图谱的读写操作。项目里大部分的业务逻辑都会汇聚在这两个文件里。2.2 MySQL 表结构设计为什么问句历史要单独建表百科问答平台里知识本身不放在 MySQL 里但有两类数据必须放一类是平台运行数据比如用户提问历史、错误日志另一类是辅助语料比如同义词映射表和意图分类规则。我见过很多初学者把实体属性直接塞进 MySQL结果图数据库反而成了装饰品这是典型的建模失误。CREATE TABLE question_history ( id INT AUTO_INCREMENT PRIMARY KEY, user_query VARCHAR(500) NOT NULL, answer_text TEXT, is_hit BOOLEAN DEFAULT FALSE, parse_template VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE synonym_map ( id INT AUTO_INCREMENT PRIMARY KEY, original_term VARCHAR(100) NOT NULL, mapped_entity VARCHAR(100) NOT NULL, category VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;上面这段建表语句是给 Django 的models.py做参考的。question_history用来记录每一次问答is_hit字段很关键——它标记这条问题是否在图谱中匹配到了答案后续优化查询模板时直接按这个字段筛数据即可。synonym_map表是同义词归一化的基础“李白”和“诗仙”指向同一个实体靠的就是这张表中mapped_entity字段的约束。utf8mb4字符集在这里是必选项因为百科数据里有生僻字和特殊符号utf8mb4才能完整覆盖。在 Django 的models.py中对应的 ORM 写法是from django.db import models class QuestionHistory(models.Model): user_query models.CharField(max_length500) answer_text models.TextField(nullTrue, blankTrue) is_hit models.BooleanField(defaultFalse) parse_template models.CharField(max_length100, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table question_historydb_table显式指定表名避免 Django 自动加前缀导致和手动创建的 SQL 表对不上。另外Django 默认不建synonym_map这张表的 ORM 映射——你是可以把它当成原始表用objects.raw()去查的但更规范的做法是为它也建一个模型类。手动建表后让 Djangoinspectdb反向生辰模型代码是一条省事的路径。2.3 创建 App 并注册Django 项目里最常见的卡点热词里有一条是“django创建app”这正是这套项目里最常见的实操起点。进入虚拟环境后在项目根目录执行python manage.py startapp knowledge_app这条命令会在根目录下生成knowledge_app/文件夹包含views.py、models.py等骨架文件。注意startapp 之后必须去settings.py里的INSTALLED_APPS手动注册否则 Django 不认这个 App迁移和模板加载都会出问题。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, knowledge_app, # 新注册的 App ]对 Python 环境还不熟悉的同学建议先查pip list | grep -i djangoLinux/macOS或pip freeze | findstr djangoWindows确认当前环境里已安装 Django。另外热词里提到“python django搭建web项目”对应的初始化命令是django-admin startproject my_qa_platform .注意结尾的点表示在当前目录生成避免嵌套一层同名文件夹。3. 知识图谱的构建与查询从三元组到 Cypher 语句3.1 百科知识图谱的数据模型怎么定知识图谱的核心是三元组(实体, 关系, 属性)对应到图数据库里就是两个节点实体和一条边关系属性的值可以挂在节点上作为节点的 property。百科类问答平台最常见的模型是实体节点人物、地点、作品、朝代、事件关系边出生于、属于哪个朝代、代表作、作者是、发生于节点属性出生日期、逝世日期、简介、别称。CREATE (lb:Person {name: 李白, dynasty: 唐, birth_year: 701}) CREATE (tc:Poem {title: 静夜思, genre: 五言绝句}) CREATE (lb)-[:WROTE {year: 726}]-(tc)这个 Cypher 片段演示了节点和关系的创建。lb和tc是变量的别名Person和Poem是标签相当于类型花括号里是属性键值对。-[:WROTE]-表示从lb指向tc的一条有向边。year是边的属性可以记录这段关系发生的时间。在生产项目里数据量不可能靠手写 Cypher 完成常见做法是准备 CSV 文件再用LOAD CSV批量导入。我通常把实体文件和关系文件分开LOAD CSV WITH HEADERS FROM file:///entities.csv AS row CREATE (:Entity {name: row.name, type: row.type, intro: row.intro})file:///指向 Neo4j 安装目录下的import文件夹。WITH HEADERS表示首行是列名AS row把每行数据映射成一个 map 对象后续直接取row.name、row.type这种字段。这块儿的坑在于 CSV 文件路径和字符编码utf-8无 BOM 是底线Windows 上用 Excel 另存的 CSV 容易带 BOM 头导致第一个字段名解析错乱处理方式是用 VS Code 重新保存成 UTF-8。在kg_client.py里封装一个通用查询函数是整个后端最值得复用的部分from neo4j import GraphDatabase class KGClient: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def query(self, cypher, paramsNone): with self.driver.session() as session: result session.run(cypher, params or {}) return [record.data() for record in result] def close(self): self.driver.close()GraphDatabase.driver()的作用是建立一个连接池with self.driver.session()是上下文管理器会在执行完自动归还连接。record.data()能把 Cypher 返回的记录直接转成 Python 字典前端模板遍历时非常方便。这里的uri通常是bolt://localhost:7687user/password是 Neo4j 初始化时设置的。3.2 问句如何映射成查询一套不依赖 NLP 的解析方案百科问答平台里最核心的技术点是“自然语言问题到 Cypher 的转换”。不引入复杂模型的前提下常见做法是用模板匹配 词槽填充。先把常见问题分成几类意图属性查询、关系查询、列表查询、计数查询。patterns [ { intent: attribute_query, template: r(.?)是(.?)(的|是)(什么|谁|哪里), cypher: MATCH (n {name: $entity}) RETURN n.$attr AS answer }, { intent: relation_query, template: r(.?)和(.?)是什么关系, cypher: MATCH (a {name: $e1})-[r]-(b {name: $e2}) RETURN type(r) AS answer } ]用re模块去匹配用户输入命中哪个正则就往对应的 Cypher 模板里填空。比如“李白的代表作是什么”正则会把$entity填成“李白”$attr填成“代表作”然后交给图数据库执行。这里的正则表达式需要严谨处理中文标点(的|是)这种多分支匹配要放在最前面否则贪婪模式会把整句话吃掉。对于“李白”“诗仙”这种同义实体在查询前先查synonym_map表做归一化def normalize_entity(raw_entity): cursor connection.cursor() cursor.execute(SELECT mapped_entity FROM synonym_map WHERE original_term %s, (raw_entity,)) row cursor.fetchone() return row[0] if row else raw_entitynormalize_entity()里先查 MySQL 再走图数据库这种“先关系库后图库”的两段式查询是这类问答平台的标准姿势。如果原词能映射到标准实体就替换映射不到就当扩展实体查保证查询不漏数据。3.3 属性名的映射矛盾图谱里叫 dynasty用户问“朝代”一个非常隐蔽但高频出现的问题是图数据库里的 property 名称是英文比如dynasty而用户问句里是中文“朝代”。解决方案是维护一张属性名映射表attr_alias { 朝代: dynasty, 出生年份: birth_year, 代表作: famous_work, 别称: alias }先normalize_attr()把中文属性名转成图里的真实属性名再拼进 Cypher。这套思路比在 Neo4j 里做全文检索要稳因为属性数量是可穷举的一张字典就能覆盖。每次问答系统答不上来优先查是不是属性名映射漏了——这是排错的第一优先事项。4. 前后端链路打通Django 视图、模板渲染和问答接口实战4.1 视图函数如何把图谱数据填进 html 模板在 Django 里views.py中的函数接收request返回render渲染后的页面。对问答平台来说最关键的是qa视图——它接收 GET 参数、调解析器、查图库、把结果放进模板上下文。from django.shortcuts import render from .utils.query_parser import parse_query from .utils.kg_client import KGClient def qa_view(request): question request.GET.get(q, ).strip() if not question: return render(request, qa_page.html, {error: 请输入问题}) kg KGClient(bolt://localhost:7687, neo4j, your_password) try: plan parse_query(question) result kg.query(plan[cypher], plan[params]) if result: answer result[0].get(answer, 未找到答案) else: answer 数据库中没有对应答案换个问法试试 except Exception as exc: answer f查询异常{exc} kg.close() return render(request, qa_result.html, {question: question, answer: answer})request.GET.get(q, )是从 URL 里取查询参数比如访问/qa?q李白是哪个朝代的这里就拿到了“李白是哪个朝代的”。parse_query返回一个字典里面包含拼好的cypher和params这样视图层不用关心解析细节。try...except是必须的——图库连接超时或者 Cypher 语法错误都属于运行期异常不捕获的话用户会看到 500 页面。在模板qa_result.html里展示答案的方式是!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title问答结果/title /head body h2你的问题{{ question }}/h2 div classanswer-box {% if error %} p stylecolor: red;{{ error }}/p {% else %} p{{ answer }}/p {% endif %} /div a href/返回继续提问/a /body /html{{ question }}和{{ answer }}是 Django 模板变量由视图函数里render的第三个参数字典填充。{% if error %}是模板标签用来做条件渲染。这里的关键点是视图函数里的 key比如question必须和模板里的变量名完全一致否则渲染出来是空白。4.2 首页与搜索一个平台不只一个页面百科问答平台的完整体验应该是首页展示热门实体和入口用户点实体名可以跳转到明细页。Django 里给这套设计两个路由在urls.py里配置from django.urls import path from . import views urlpatterns [ path(, views.index_view, nameindex), path(qa, views.qa_view, nameqa), path(entity/str:name, views.entity_detail, nameentity_detail), ]str:name是 Django 路径转换器它会截取 URL 中对应位置的一段字符串作为name参数传给entity_detail视图。entity_detail里做的事是拿 name 去图谱里查实体属性找到就渲染详情页找不到跳转到 404 页。首页视图则可以做实体推荐从图库里随机取几个节点展示到页面上让用户一眼就知道这个平台能问什么。def index_view(request): kg KGClient(bolt://localhost:7687, neo4j, your_password) hot_entities kg.query(MATCH (n:Entity) RETURN n.name AS name LIMIT 10) kg.close() return render(request, index.html, {entities: hot_entities})LIMIT 10限制返回条数避免首页渲染卡顿kg.close()放在return之前是因为图数据库连接是资源占用型对象不关闭会占满连接池。热词里有“html一键返回顶部算法”这种纯前端脚本放在static/js/里由模板引用与后端视图无关但可以加在首页右侧做长页面锚点定位。4.3 Django ORM 的历史记录落库与查询问答结果返回的同时把用户问题和是否命中写入question_history表是后续优化问句模板的数据来源。from .models import QuestionHistory def qa_view(request): # ... 前面拼接 answer 的逻辑 ... QuestionHistory.objects.create( user_queryquestion, answer_textanswer, is_hit(answer ! 数据库中没有对应答案换个问法试试) ) # ... 渲染模板 ...objects.create()是 Django ORM 里最直接的插入方式内部会自动执行 INSERT 语句并返回新对象。存储历史后后续可以写一个管理命令“统计没有命中的问题按高频词找出未覆盖的问法”这是知识图谱问答平台持续迭代的常规手段。5. 知识图谱只显示 25 个标签的坑与处理热词里有一句“知识图谱只显示25个标签”这是 Neo4j Browser 的默认显示策略不是数据问题。浏览器端的图谱可视化默认只渲染前 25 个标签节点防止页面卡死。解决思路有两个一是在 Cypher 里LIMIT自己控制返回数量二是到 Neo4j Browser 的设置里调大Max nodes阈值。但要注意前端页面如果接了ECharts 或 D3.js做图谱可视化这个 25 的限制就不是 Neo4j 的事了而是你的 Python 后端传给前端的数据量。推荐的做法是在视图层做分页或按实体类型过滤def graph_data(request): kg KGClient(bolt://localhost:7687, neo4j, your_password) entity_type request.GET.get(type, Person) cypher fMATCH (n:{entity_type})-[r]-(m) RETURN n.name AS source, type(r) AS relation, m.name AS target LIMIT 100 data kg.query(cypher) kg.close() return JsonResponse({nodes: data})LIMIT 100是硬上限避免接口被前端拖死。type(r)返回关系类型字符串前端拿到source、relation、target三元组后直接喂给 ECharts 的 graph 系列即可。需要人工遍历大量节点时不用一次全量拉取按实体类型拆分请求配合前端懒加载体验更好。6. 优化问句匹配的三板斧属性归一化、关系扩展和数据回灌问答平台上线后你会发现模型跑通只是第一步真正决定好不好用的是匹配率和答案质量。我通常会做三轮优化。第一板斧是属性归一化。用户不会按图谱设计者的命名来提问“生卒年”“寿命”“活了多少岁”都可能指同一组属性。把这类表达收集起来扩充attr_alias字典即可。每次遇到答不上来的问题把问题文本记进question_history表定期统计is_hitFalse的记录识别高频未命中模式。第二板斧是关系扩展。初始图谱可能只有一级关系比如“李白写过的诗”。但用户可能问“杜甫写过哪些诗”这个图谱里若没有WROTE边的反转查询索引就需要在 Cypher 里处理反向遍历MATCH (p:Person {name: $entity})-[:WROTE]-(poem) RETURN poem.title AS answer如果想让平台回答“为李白写过诗的人有哪些”那就是反方向遍历MATCH (p:Person {name: $entity})-[:WROTE]-(other)。这两种方向都建索引可以显著提高查询效率——Neo4j 里的索引要手动建CREATE INDEX entity_name IF NOT EXISTS FOR (n:Entity) ON (n.name)IF NOT EXISTS保证可重复执行不会因索引已存在而报错。CREATE INDEX ... FOR语法是 Neo4j 4.x 之后的写法老版本用CREATE INDEX ON :Entity(name)换版本过了记得核对语法。第三板斧是数据回灌。把用户问过但没有答案的问题作为新知识补充入口设计一个简易审核后台运营人员可以把新三元组通过页面直接写进图库。回灌的代码路径是def add_triple(request): head request.POST.get(head) relation request.POST.get(relation) tail request.POST.get(tail) kg KGClient(bolt://localhost:7687, neo4j, your_password) cypher fMERGE (a {{name: $head}}) MERGE (b {{name: $tail}}) MERGE (a)-[r:{relation}]-(b) kg.query(cypher, {head: head, tail: tail}) kg.close() return JsonResponse({status: ok})MERGE的语义是“存在就匹配不存在就创建”比CREATE更适合数据回灌不会重复造节点。这个接口放在 Django 的管理员页面或自定义 action 里均可关键点是relation需要通过白名单校验不能直接拼接用户输入否则图库会被注入恶意关系名。最后别忘了使用python manage.py runserver时修改模板和静态文件是不需要重启服务的但修改views.py和新建文件后需要按 CtrlC 重启开发过程中注意这个差别可以少浪费很多等待时间。本文还有配套的精品资源点击获取
返回列表