ARTICLE DETAIL

资讯详情

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

Django + MySQL 构建知识图谱百科问答平台实战指南

Django + MySQL 构建知识图谱百科问答平台实战指南 简介基于Python与Django框架的百科知识问答平台完整源码面向毕业设计、Web开发学习者解决传统百科检索效率低的问题融合知识图谱、自然语言处理与智能问答。压缩包共444个文件包含31个py后端源码、44个js前端脚本、30个css样式、30个pyc编译文件以及79个jar依赖、75个gif动图等整体大小约171.29MB另附Neo4j图数据库存储文件与MySQL数据库脚本可还原完整运行环境。项目前后端齐全覆盖用户交互界面、知识图谱构建、问答逻辑与数据存储并配有说明文档、源码解析、学习笔记LW和演示PPT便于理解架构与二次开发。文档对系统架构、功能模块及数据库表结构均有说明LW记录了关键实现思路可帮助读者快速上手。目前已有63人浏览学习对希望掌握Django全栈、知识图谱应用实战的读者来说是一份系统的毕业设计参考资料。1. 为什么知识图谱百科问答平台选 Django MySQL 这套组合输入“爱因斯坦的出生地”一个基于 Python 的 Django-html 知识图谱百科问答平台会直接返回“乌尔姆”而不是给出一堆网页让你自己翻。实现这种问答不需要训练大模型也不需要额外部署图数据库用 Django 写接口、MySQL 存三元组、HTML 做展示就能覆盖百科知识问答的大多数场景。做这个项目的人通常已经有 Django 的 Web 基础但对知识图谱怎么落库、问答意图怎么解析、前端怎么把实体关系画出来没有完整链路。这篇文章就从这三条线展开核心目标是让你拿到一份数据后能快速搭出一个可运行、可演示、还能继续扩展的知识问答平台。2. 知识图谱的 MySQL 存储设计实体表、三元组表与 Python 导入脚本2.1 为什么用 MySQL 存知识图谱而不是 Neo4j知识图谱的标准表达是三元组(主语, 谓语, 宾语)例如(爱因斯坦, 出生地, 乌尔姆)。原生图数据库 Neo4j 存这类数据确实顺手但百科问答平台的查询量不大通常只做“实体 - 某类关系 - 宾语”的一跳查询偶尔需要反查“某实体都关联了什么”。这种规模用 MySQL 完全足够而且题目要求里明确带了 mysql。把知识图谱放进关系型数据库的额外好处是能直接复用 Django ORM 的事务、迁移和备份机制部署环境里不用再维护一个 Java 服务。需要注意的点是不要照搬“属性表 关系表”的宽表设计而是要把实体、关系、三元组分清楚否则查询语句会越写越别扭。我一般会建三张核心表entity存实体relation存关系名triple存三元组。triple里同时保留object_id和object_value是因为百科问答中有大量属性值不需要作为实体继续展开比如日期、获奖名称、地理位置坐标。如果宾语是另一条知识的主语才把它放进object_id走外键。字段类型用途subject_idBIGINT主语实体 ID指向 entity.idrelation_idINT关系 ID指向 relation.idobject_idBIGINT宾语为实体时的实体 ID可为空object_valueVARCHAR(512)宾语为字符串时的值可为空object_typeENUM区分当前宾语是实体还是普通值下面的建表语句可以直接在 MySQL 中执行字符集使用 utf8mb4避免中文生僻字写入报错。CREATE DATABASE IF NOT EXISTS wiki_qa CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE wiki_qa; CREATE TABLE entity ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, type VARCHAR(64) NOT NULL DEFAULT item, intro TEXT, INDEX idx_name (name), INDEX idx_type (type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE relation ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL UNIQUE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE triple ( id BIGINT PRIMARY KEY AUTO_INCREMENT, subject_id BIGINT NOT NULL, relation_id INT NOT NULL, object_id BIGINT, object_value VARCHAR(512), object_type ENUM(entity,value) NOT NULL DEFAULT entity, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (subject_id) REFERENCES entity(id), FOREIGN KEY (relation_id) REFERENCES relation(id), FOREIGN KEY (object_id) REFERENCES entity(id), INDEX idx_subject_relation (subject_id, relation_id), INDEX idx_object_id (object_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_subject_relation联合索引覆盖“查询某个实体的某个关系”这一高频场景。例如“爱因斯坦的出生地”最终会被翻译成WHERE subject_id? AND relation_id?联合索引能直接命中。idx_object_id用于反查“哪些实体指向了当前实体”在实现“人物关系”这类反问时会用到。如果考虑后续扩展实体别名可以再单独维护一张entity_alias表不要在entity.name上塞太多别名。2.2 Django 模型映射与 MySQL 连接配置Django 侧配置数据库时需要在settings.py中显式指定 MySQL 引擎。下面是常见的配置块DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: wiki_qa, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }OPTIONS里的charset一定要写成utf8mb4否则 Django 写入 emoji 表情或冷僻字时可能变成乱码。init_command设置STRICT_TRANS_TABLES让字段超长、非法日期在写入阶段直接报错而不是被 MySQL 静默截断。如果你在 Windows 上使用 PyMySQL可以在应用目录的__init__.py里写import pymysql pymysql.install_as_MySQLdb()这是 python django 搭建 web 项目时最常用的 MySQL 客户端兼容方式。Linux 系统安装 python 环境后我更推荐直接安装mysqlclient但需要先装系统依赖libmysqlclient-dev和python3-dev否则编译阶段会报mysql_config not found。模型的写法要和上面的表结构对应注意给外键指定db_column否则 Django 会默认生成subject_id_id这样的列名和建表语句对不上。from django.db import models class Entity(models.Model): name models.CharField(max_length255, db_indexTrue) type models.CharField(max_length64, defaultitem) intro models.TextField(blankTrue, default) class Meta: db_table entity class Relation(models.Model): name models.CharField(max_length64, uniqueTrue) class Meta: db_table relation class Triple(models.Model): subject models.ForeignKey( Entity, db_columnsubject_id, on_deletemodels.CASCADE, related_nametriples_as_subject, ) relation models.ForeignKey( Relation, db_columnrelation_id, on_deletemodels.CASCADE, ) object_entity models.ForeignKey( Entity, db_columnobject_id, nullTrue, blankTrue, on_deletemodels.CASCADE, related_nametriples_as_object, ) object_value models.CharField(max_length512, blankTrue, nullTrue) object_type models.CharField( max_length16, choices[(entity, 实体), (value, 值)], defaultentity, ) class Meta: db_table triple indexes [ models.Index(fields[subject, relation]), models.Index(fields[object_entity]), ]使用on_deletemodels.CASCADE可以保证删除实体时相关三元组同步删除避免出现悬挂引用。related_name要区分开否则反向查询会冲突。如果你的数据中存在大量同名实体建议在Entity.name上再建一个普通索引不要加唯一约束后续可以通过type或额外的消歧字段来处理同名问题。2.3 Python 导入脚本把干净数据批量写入 MySQL知识图谱构建完成后数据通常以 CSV 形式存在。百科数据可能来自爬虫、开源开放图谱或者手工整理导入前需要统一列名subject, relation, object, subject_type, object_type。下面是一个可重复执行的 Django 脚本放在项目根目录scripts/kb_import.py。import os import csv import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, wiki_qa.settings) django.setup() from kb.models import Entity, Relation, Triple def clear_all(): Triple.objects.all().delete() Relation.objects.all().delete() Entity.objects.all().delete() def get_or_create_entity(name, type_item): name name.strip() if not name: return None obj Entity.objects.filter(namename).first() if obj is None: obj Entity.objects.create(namename, typetype_) return obj def import_csv(path): clear_all() batch [] total 0 with open(path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: subject get_or_create_entity(row[subject], row.get(subject_type, item)) object_entity None object_value None object_type value if row[object].startswith(ENTITY:): object_entity get_or_create_entity( row[object][7:], row.get(object_type, item)) object_type entity else: object_value row[object].strip() relation, _ Relation.objects.get_or_create(namerow[relation].strip()) batch.append(Triple( subjectsubject, relationrelation, object_entityobject_entity, object_valueobject_value, object_typeobject_type, )) if len(batch) 2000: Triple.objects.bulk_create(batch) total len(batch) batch.clear() if batch: Triple.objects.bulk_create(batch) total len(batch) print(fimported {total} triples)代码逻辑分三步走先清空旧数据保证可重复导入然后逐行解析 CSV最后用bulk_create批量插入。批量大小取 2000 是一个折中值太小会频繁提交事务太大会让单次 INSERT 语句过长。ENTITY:前缀是我常用的约定用来表示这一行的宾语确实是一个实体需要写入object_id普通字符串则写入object_value。导入完成后可以用一条几表关联的 SQL 检查知识图谱是否构建正确SELECT e.name AS subject, r.name AS relation, COALESCE(ee.name, t.object_value) AS object FROM triple t JOIN entity e ON t.subject_id e.id JOIN relation r ON t.relation_id r.id LEFT JOIN entity ee ON t.object_id ee.id WHERE e.name 爱因斯坦 LIMIT 10;这里使用COALESCE把实体宾语和字符串宾语统一成可读文本方便人工核对。如果返回结果为空优先检查 CSV 中的实体名有没有前后空格或者subject字段是否被 Excel 转成了奇怪的编码。3. Django 问答接口实体识别、意图解析与 MySQL 查询3.1 问答的最小流水线实体识别、谓词匹配、结果抽取百科知识问答在不引入大模型的情况下最可靠的方案是规则加词典。用户问题进来后按顺序做三件事第一从问题里切出实体名第二识别用户想问的关系类型第三把这两者拼成三元组查询。整个过程不涉及自然语言理解模型但已经能覆盖大部分“某实体的某属性”句式。实体识别这里不建议直接上 jieba 分词因为百科实体大量是专有名词例如“小王子”“相对论”“李白”分词器很可能把它们切成更短的词。常见做法是把所有实体名加载进内存按长度从大到小排序然后逐个判断是否在问题字符串中出现这样能保证“爱因斯坦”优先于“爱因”被命中。实体量达到十万级以上时可以换成字典树或 AC 自动机但中小型百科平台用列表加排序就能跑得很快。意图识别则需要维护一张关系别名表把用户口语映射到relation.name用户写法的正则结果关系出生地、生于、哪里出生、出生在出生地别称、又叫、又称、也被叫做别称简介、介绍、是谁、是什么人__intro__作品、著作、写过、写了作品毕业院校、毕业学校毕业院校__intro__是一个特殊谓词它不查询triple表而是直接返回entity.intro字段。原因是简介通常是一大段文字不适合拆成三元组。这样设计以后新增问答类型只需要往alias_map里加正则再在查询逻辑中补充对应的分支。3.2 核心问答引擎实现基于正则的子串匹配下面是一个独立的qa.py把它放在 Django 应用目录下后续视图层直接调用。代码保持轻量方便你改成独立服务。import re from kb.models import Entity, Relation, Triple class KnowledgeQA: def __init__(self): self.entity_names list(Entity.objects.values_list(name, flatTrue)) self.entity_names.sort(keylen, reverseTrue) self.patterns [ (re.compile(r出生地|生于|哪里出生|出生在), 出生地), (re.compile(r别称|又叫|又称|也被叫做), 别称), (re.compile(r简介|介绍|是谁|是什么人), __intro__), (re.compile(r作品|著作|写过|写了|创作), 作品), (re.compile(r毕业院校|毕业学校), 毕业院校), ] def extract_entity(self, question): for name in self.entity_names: if name in question: return name return None def parse(self, question): entity self.extract_entity(question) if not entity: return {error: entity_not_found, message: 知识库中没有找到相关实体} for pattern, predicate in self.patterns: if pattern.search(question): return {entity: entity, predicate: predicate} return {entity: entity, predicate: default} def answer(self, parsed): entity_name parsed[entity] subject Entity.objects.filter(nameentity_name).first() if subject is None: return {answer: 知识库还没有收录该实体, type: text} predicate parsed[predicate] if predicate __intro__: return {answer: subject.intro, type: text} if predicate default: triples Triple.objects.filter(subjectsubject).select_related( relation, object_entity)[:20] return self._format_triples(triples) relation Relation.objects.filter(namepredicate).first() if relation is None: return {answer: 暂不支持这个关系查询, type: text} triples Triple.objects.filter( subjectsubject, relationrelation ).select_related(object_entity) return self._format_triples(triples) def _format_triples(self, triples): items [] for t in triples: if t.object_type entity and t.object_entity: items.append({ type: entity, entity: t.object_entity.name, }) else: items.append({ type: value, value: t.object_value, }) return {answer: items, type: triples}extract_entity返回的是第一个命中的实体名。如果用户问“爱因斯坦的母校是哪所大学”entity是“爱因斯坦”predicate会命中“毕业院校”。在answer中我用Relation.objects.filter(namepredicate).first()把谓词字符串转成关系 ID再用filter(subjectsubject, relationrelation)查询三元组。select_related(object_entity)会在一次 SQL JOIN 中把宾语实体查出来避免逐条循环触发数据库查询。default分支处理用户只输入实体名、没有明确关系关键词的情况比如直接搜“李白”。此时返回该实体的前 20 条三元组前端可以把这些关系渲染成卡片列表也算是一种“实体百科页”。3.3 视图、路由与接口测试接口层不需要引入 Django REST Framework用 Django 自带的JsonResponse足够。注意 POST 接口默认需要 CSRF token纯 JSON API 建议用csrf_exempt标记并在前端统一走fetch。import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from kb.qa import KnowledgeQA qa_engine KnowledgeQA() csrf_exempt require_POST def qa_api(request): try: body json.loads(request.body.decode(utf-8)) question body.get(question, ).strip() except (json.JSONDecodeError, AttributeError): return JsonResponse({error: invalid json}, status400) if not question: return JsonResponse({error: question is empty}, status400) parsed qa_engine.parse(question) if error in parsed: return JsonResponse(parsed, status404) result qa_engine.answer(parsed) return JsonResponse({ question: question, parsed: parsed, result: result, })require_POST确保接口只接受 POST 方法csrf_exempt只加在 API 视图上页面表单相关的视图不要照搬。路由配置如下from django.urls import path from kb import views urlpatterns [ path(api/qa, views.qa_api, nameqa_api), ]本地启动后用 curl 验证是最快的curl -X POST http://127.0.0.1:8000/api/qa \ -H Content-Type: application/json \ -d {question: 爱因斯坦的出生地}返回的 JSON 中result.answer是一个数组每个元素要么是{type: entity, entity: 乌尔姆}要么是{type: value, value: 1879年3月14日}。前端拿到这个结构后可以统一渲染不需要再关心数据来自哪张表。4. Django 模板里的 HTML 问答页与知识图谱可视化4.1 页面结构与静态资源组织前后端在同域下运行时最简单的方式是让 Django 渲染初始 HTML再通过fetch调用第 3 章实现的/api/qa接口。模板目录建议放在项目根目录的templates下静态文件放在static下。资源路径作用templates/index.html问答页面主结构static/css/style.css页面样式static/js/qa.js请求后端接口并渲染结果static/vendor/echarts.min.js知识图谱关系图渲染index.html的核心结构如下!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title知识图谱百科问答/title {% load static %} link relstylesheet href{% static css/style.css %} /head body div classcontainer h1知识图谱百科问答/h1 div classsearch-box input typetext idquestionInput placeholder输入问题例如爱因斯坦的出生地 button idaskBtn提问/button /div div idanswerBox/div div idgraphBox styleheight:480px; width:100%;/div /div script src{% static vendor/echarts.min.js %}/script script src{% static js/qa.js %}/script /body /html{% load static %}必须出现在模板顶部否则{% static %}标签无法使用。graphBox需要显式设置高度ECharts 初始化时才能拿到有效容器尺寸否则图表只会渲染成一片空白。如果你不想引入完整的 ECharts也可以先用表格把result.answer列出来后续再补关系图。4.2 JavaScript 调用问答接口并渲染答案先实现提问主流程。fetch发送 JSON POST接收到结果后分别调用文本渲染和关系图渲染函数。async function askQuestion() { const input document.getElementById(questionInput); const question input.value.trim(); if (!question) return; const answerBox document.getElementById(answerBox); const graphBox document.getElementById(graphBox); answerBox.innerHTML 正在查询…; const resp await fetch(/api/qa, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question: question }) }); const data await resp.json(); if (resp.ok data.result) { renderAnswer(data.result); renderGraph(data.result); } else { answerBox.textContent data.message || data.error || 查询失败; } } document.getElementById(askBtn).addEventListener(click, askQuestion);fetch的method、headers、body三个参数必须和后端视图对齐。后端用csrf_exempt所以这里不需要携带 CSRF token如果将来去掉csrf_exempt就需要先请求一个带 cookie 的页面再从 cookie 中取csrftoken放到请求头。答案文本区的渲染要区分两种返回结构function renderAnswer(result) { const box document.getElementById(answerBox); if (result.type text) { box.textContent result.answer; return; } if (result.type triples) { box.innerHTML result.answer.map(item { if (item.type entity) { return a classentity-link>let graphChart null; function renderGraph(result) { const graphBox document.getElementById(graphBox); if (result.type ! triples || !result.answer.length) { graphBox.style.display none; return; } graphBox.style.display block; if (graphChart null) { graphChart echarts.init(graphBox); } const nodes [ { id: root, name: 当前实体, category: 0 } ]; const links []; result.answer.forEach((item, index) { const nodeId node_ index; const name item.entity || item.value; const category item.type entity ? 1 : 2; nodes.push({ id: nodeId, name: name, category: category }); links.push({ source: root, target: nodeId }); }); graphChart.setOption({ tooltip: {}, series: [{ type: graph, layout: force, roam: true, data: nodes, links: links, categories: [ { name: 查询主体 }, { name: 实体 }, { name: 属性值 } ], force: { repulsion: 300 }, label: { show: true, position: right } }] }); }layout: force使用力引导布局适合展示 10 到 100 个节点的图。repulsion控制节点之间的斥力值太小节点会挤成一团太大则关系图过于分散。roam: true允许用户拖拽和缩放演示时体验更好。这里节点 ID 用index保证唯一如果同一个实体出现多次仍会被渲染成多个节点实际落地时可以按实体名去重合并让图更接近“知识图谱”而不是“关系列表”。4.4 Django 模板变量和前端变量的常见冲突如果要在页面初始化时把后端实体名传给 JavaScript不要直接拼接字符串。假设代码写成var name {{ question }}一旦问题里包含双引号页面就会脚本报错。推荐使用 Django 的json_script过滤器{{ question|json_script:question-data }}然后在 JavaScript 中读取const question JSON.parse(document.getElementById(question-data).textContent);这样既能保证 HTML 安全转义又能避免引号和换行破坏脚本。这个技巧在写 Django-html 混合页面时需要特别留意尤其是把用户输入回显到页面上的时候。5. 上线前必做问答缓存、准确率评测与部署参数5.1 给问答引擎套一层 Django 缓存当前KnowledgeQA每次查询都直接访问 MySQL 和关系表同一个问题反复提交时会产生大量重复查询。可以使用 Django 缓存框架在视图层做一个轻量缓存先查缓存再走数据库。下面是一个适合放在views.py中的包装函数from django.core.cache import cache def get_answer_with_cache(parsed): cache_key fqa:{parsed[entity]}:{parsed[predicate]} result cache.get(cache_key) if result is None: result qa_engine.answer(parsed) cache.set(cache_key, result, 60 * 30) return result缓存 key 由实体名和谓词组成过期为 30 分钟。settings.py中如果只配置了LocMemCache本地开发和演示够用多进程部署时建议换成 Redis否则每个进程各自维护一份缓存命中率会下降。新增实体数据后可以手动调用cache.delete_pattern(qa:*)清理相关缓存避免用户看到旧数据。5.2 用回归测试集卡死问答准确率扩展实体和关系之前先跑一遍回归测试能避免“修好了 A 问题弄坏了 B 问题”。测试集可以是一个简单的 Python 列表也可以从 CSV 读取。下面是精简版评测脚本from kb.qa import KnowledgeQA TEST_CASES [ (爱因斯坦的出生地, 乌尔姆), (李白是谁, 唐代), (中国的别称, 中华人民共和国), ] qa KnowledgeQA() hit 0 for question, expect in TEST_CASES: parsed qa.parse(question) result qa.answer(parsed) if expect in str(result): hit 1 else: print(FAIL:, question, result) print(faccuracy: {hit}/{len(TEST_CASES)})评判逻辑采用“包含式比较”只要答案结果字符串包含期望值就算命中。这个标准比较宽松但用来发现实体识别或谓词匹配的整体性断裂已经足够。测试集建议覆盖每个意图类型至少两条并包含一个实体不在知识库中的反向用例。每次调整正则的先后顺序或新增实体别名后都执行这个脚本验证。5.3 部署到 Linux 时需要检查的三个参数部署环节最常见的坑都集中在 MySQL 驱动、静态文件、域名白名单三块。Linux 系统中如果使用 mysqlclient先执行系统包安装再回到虚拟环境执行 pip 安装顺序反了就会出现mariadb_config not found。Django 设置DEBUGFalse之后collectstatic必须执行并让 nginx 或宝塔面板把静态请求指到STATIC_ROOT。另外记得把服务器 IP 或域名加进ALLOWED_HOSTS否则所有请求都会返回 400。这三个参数通常占用部署排错时间的八成sudo apt install libmysqlclient-dev python3-dev pip install mysqlclient python manage.py collectstatic --noinput如果你的运行环境是 CentOS把libmysqlclient-dev换成mysql-devel其余步骤一致。这些配置完成后再用第 5.2 节的评测脚本跑一遍接口没问题就可以准备演示了。本文还有配套的精品资源点击获取
返回列表