boost 文档搜索引擎 测试报告(持续更新中)
Author:<小王的创意工坊>
被测系统:boost 文档搜索引擎(C++ 自研全文检索)
被测地址:http://139.196.124.168:8080/
测试相关文件地址:https://gitee.com/xiao-wangs-private-workshop_0/boost
测试日期:2026-09-14
测试类型:功能测试 + 接口测试 + Web UI 自动化测试 + 性能测试
目录
- 一. 项目简介
- 二. 开发技术与测试环境
- 三. 测试策略与用例设计
- 3.1 测试范围与分层
- 3.2 首页与静态资源
- 3.3 搜索接口功能与边界
- 3.4 检索质量与数据一致性
- 3.5 并发
- 3.6 Web UI
- 四. 自动化测试代码
- 五. 测试执行结果
- 六. 缺陷清单与根因分析
- 七. 性能测试报告
- 八. 测试结论与改进建议
一. 项目简介
本项目实现了一个面向 Boost 官方文档的全文搜索引擎,整体分为「离线建索引」与「在线检索服务」两条链路:
离线链路(parser)
- 递归遍历
input/html下的全部 Boost 文档,共 8689 个 HTML 文件; - 逐个文件完成去标签 清洗,抽取
<title>标题、正文内容、并由文件路径反向拼出 Boost 官网原文 URL; - 将结构化结果以
\3作为字段分隔符、逐行写入raw_html/raw.txt,产出的语料文件约 21.2 MB。
在线链路(http_server)
- 启动时加载
raw.txt,一次性构建正排索引 (文档 ID → 标题/正文/URL)与倒排索引(词 → 倒排拉链,记录文档 ID 与权重); - 权重采用
权重 = 10 × 标题词频 + 1 × 正文词频的简单相关性打分; - 对外暴露两个入口:
GET /返回前端页面,GET /s?word=关键字返回 JSON 格式的搜索结果; - 前端为单页 AJAX 检索页,用户输入关键字后异步请求
/s,并将结果动态渲染为「标题 + 摘要 + 原文链接」列表。
当前规模实测数据
| 指标 | 数值 |
|---|---|
| 语料文档总数 | 8689 篇 |
| 清洗后语料体积 | 21.2 MB |
| 单次搜索最大返回条数 | 8689 条(= 全库) |
| 单次搜索最大响应体 | 2.97 MB |
| 首页响应体 | 7.7 KB |
二. 开发技术与测试环境
2.1 被测系统技术栈
| 分类 | 技术选型 |
|---|---|
| 开发语言 | C++ |
| 中文分词 | cppjieba |
| HTTP 服务 | cpp-httplib v0.7.15 |
| 文件系统遍历 | Boost.Filesystem |
| 字符串处理 | Boost.Algorithm(to_lower、split) |
| JSON 序列化 | jsoncpp |
| 索引结构 | 正排数组 + unordered_map 倒排拉链 |
| 并发模型 | httplib 线程池 + 全局互斥锁 |
| 前端 | 原生 HTML/CSS/JS + jQuery(AJAX) |
| 部署 | Linux 服务器 + 8080 端口对外提供服务 |
2.2 测试环境
| 项目 | 说明 |
|---|---|
| 被测地址 | http://139.196.124.168:8080/ |
| 接口测试 | Python 3.10 + requests |
| UI 自动化 | Selenium 4 + Chrome 153(headless)+ ChromeDriver 153 |
| 性能测试 | Apache JMeter 5.6.3(非 GUI 模式) |
| 接口用例数 | 31 条 |
| UI 用例数 | 12 条 |
| 性能场景数 | 2 个(静态首页 / 搜索接口),共 250 个有效采样 |
三. 测试策略与用例设计
3.1 测试范围与分层
被测系统是「前端单页 + 单一 JSON 接口」的轻量架构,功能面很窄,因此测试不能只停留在"能不能搜出结果",而要往三个方向加深:
| 层次 | 关注点 | 用例数 |
|---|---|---|
| 功能正确性 | 搜索主流程、字段完整性、排序正确性、大小写、召回一致性 | 31 条接口 + 12 条 UI |
| 边界与异常 | 缺参、空参、超长、特殊字符、中文、注入、方法不匹配、路径穿越 | 同上 |
| 非功能 | 并发一致性、响应体规模、响应时间、错误率、资源争用 | 2 条并发用例 + JMeter 2 场景 |
用例优先级判定标准:
- P0(冒烟):该用例失败则核心搜索链路不可用(如首页打不开、搜索无结果、脚本注入生效);
- P1(常规):正常功能验证与常见异常处理;
- P2(边界):极端输入、低频场景、体验类问题。
说明:本节各表同时给出设计预期 与实测结果 ,便于对照查看;标红(❌ 失败)的用例即为本章发现的问题,其根因分析见第六章。
3.2 首页与静态资源
验证静态页面可访问、DOM 结构完整、外部依赖可用,并确认静态资源服务未越权。
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|---|---|
| TC-HOME-01 | 首页可正常访问 | P0 | 请求 GET / |
HTTP 200,返回 HTML 页面 | HTTP 200,7732 字节,Content-Type=text/html |
✅ 通过 |
| TC-HOME-02 | 首页标题与搜索框渲染正确 | P0 | 请求 GET /,检查标题、输入框、按钮 |
标题含"boost 搜索引擎",输入框与搜索按钮均存在 | 标题=True 输入框=True 按钮=True | ✅ 通过 |
| TC-HOME-03 | 首页外部 CDN 依赖可用 | P2 | 请求 code.jquery.com/jquery-2.1.1.min.js |
CDN 可访问,否则页面脚本全失效 | HTTP 200,84245 字节 | ✅ 通过 |
| TC-HOME-04 | 不存在的路径返回 404 | P1 | 请求 GET /not-exist-page-xyz |
HTTP 404,不返回首页内容 | HTTP 404,0 字节 | ✅ 通过 |
| TC-HOME-05 | 静态资源目录穿越被拦截 | P1 | 请求 GET /../makefile、/%2e%2e/makefile |
不泄露 wwwroot 之外的服务器文件 |
HTTP 404,未泄露任何文件内容 | ✅ 通过 |
观察 :静态资源服务安全性良好,目录穿越被 cpp-httplib 正常拦截;但首页
Content-Type仅声明text/html而未带charset=utf-8,页面编码完全依赖 HTML 内的<meta charset="UTF-8">,对非浏览器客户端不够友好(低危,见 D12)。
3.3 搜索接口功能与边界
这是本项目最核心的接口,覆盖正常检索、字段契约、参数校验、注入防护与超限保护。
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|---|---|
| TC-SEARCH-01 | 英文关键字搜索返回结果 | P0 | GET /s?word=boost |
HTTP 200,application/json,返回非空数组 |
HTTP 200,结果 8689 条,3115347 字节 | ✅ 通过 |
| TC-SEARCH-02 | 多词查询可合并返回 | P0 | GET /s?word=boost asio |
返回结果非空,覆盖两个词的文档 | HTTP 200,结果 8689 条,7400ms | ✅ 通过 |
| TC-SEARCH-03 | 结果 JSON 字段完整 | P0 | GET /s?word=boost,检查每条记录字段 |
每条含 title/desc/url/id/weight |
8689 条字段缺失 0 条 | ✅ 通过 |
| TC-SEARCH-04 | 结果按权重降序排列 | P0 | 校验 weight 序列单调性 |
weight 单调不增 | weight 范围 1~1866,降序=True | ✅ 通过 |
| TC-SEARCH-05 | 结果 url 指向 boost 官方文档 | P1 | 校验全部 url 前缀 | 均以 https://www.boost.org/doc/libs/latest/doc/html 开头 |
8689 条前缀异常 0 条 | ✅ 通过 |
| TC-SEARCH-06 | 无匹配关键字返回空数组 | P1 | GET /s?word=zzqqxxwwvv |
HTTP 200,返回 [] |
HTTP 200,响应为 null |
❌ 失败 |
| TC-SEARCH-07 | 搜索结果无重复文档 | P1 | 对结果 id 去重计数 | 同一 id 不重复出现 | 8689 条去重后仍 8689 条,重复 0 条 | ✅ 通过 |
| TC-SEARCH-08 | 缺少 word 参数给出友好提示 | P0 | GET /s(不带参数) |
返回"必须要有搜索关键字!",不返回 500 | HTTP 200,响应=必须要有搜索关键字! | ✅ 通过 |
| TC-SEARCH-09 | word 参数为空串 | P1 | GET /s?word= |
不应返回全量文档,应返回空数组或提示 | HTTP 200,响应 null,与缺参处理不一致 |
❌ 失败 |
| TC-SEARCH-10 | 大小写不敏感 | P1 | 分别请求 boost / Boost / BOOST |
三者结果完全一致 | 三者均 8689 条,id 序列一致 | ✅ 通过 |
| TC-SEARCH-11 | 中文关键字查询不导致服务异常 | P1 | GET /s?word=搜索引擎 |
HTTP 200,不返回 5xx、不崩溃 | HTTP 200,响应类型 null |
✅ 通过 |
| TC-SEARCH-12 | 中文常见词(语料中不存在) | P2 | GET /s?word=你好世界 |
返回 [],不报错 |
HTTP 200,响应 null |
❌ 失败 |
| TC-SEARCH-13 | 纯符号 / 特殊字符关键字 | P1 | GET /s?word=!@#$%^&*() |
不崩溃、不返回 500 | HTTP 200,却返回 3102564 字节结果 | ✅ 通过 |
| TC-SEARCH-14 | SQL 注入式关键字不生效 | P0 | GET /s?word=' OR '1'='1 |
不返回全量文档、不暴露错误 | HTTP 200,返回 8689 条(分词命中 1/or,非注入) |
✅ 通过 |
| TC-SEARCH-15 | XSS 脚本关键字不被原样回显 | P0 | GET /s?word=<script>alert(1)</script> |
响应中不出现未转义的 <script> |
原始脚本回显=False | ✅ 通过 |
| TC-SEARCH-16 | 超长关键字(10000 字符) | P1 | 发送 10000 个 a 作为关键字 |
明确 4xx 拒绝或正常处理,不得 5xx | HTTP 414,耗时 0.12s | ✅ 通过 |
| TC-SEARCH-17 | URL 编码与空格关键字 | P2 | GET /s?word=boost%20%25 |
含空格/百分号的关键字可正常解析 | HTTP 200,结果 8689 条 | ✅ 通过 |
| TC-SEARCH-18 | 不支持的请求方法被拒绝 | P2 | POST /s?word=boost |
应返回 4xx/405,而非静默成功 | HTTP 404 | ✅ 通过 |
观察 1(安全性) :注入类风险整体可控。系统没有数据库,SQL 注入无攻击面;XSS 方面,接口返回的是 JSON 且前端统一使用 jQuery
.text()写入 DOM,未使用innerHTML,脚本不会被执行;超长 URI 由 HTTP 层以 414 正确拒绝;目录穿越被拦截。观察 2(契约一致性) :
/s接口的响应类型不统一------有结果时是JSON 数组,无结果时是null,缺少参数时又变成text/plain字符串。其中null会直接引发前端崩溃(详见 3.6 节)。
3.4 检索质量与数据一致性
功能"能跑通"不等于检索"有意义",本组用例从摘要质量、召回规模、结果稳定性三个角度验证检索质量。
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|---|---|
| TC-DATA-01 | 摘要字段不为空占位值 | P1 | 统计 desc 为 None/空 的比例 |
不应出现失败占位 | 8689 条中异常 0 条(0.0%) | ✅ 通过 |
| TC-DATA-02 | 摘要命中关键字(相关性) | P1 | 统计 desc 中含关键字的比例 |
摘要应包含用户搜索的关键字 | 8689 条中命中 8689 条(100.0%) | ✅ 通过 |
| TC-DATA-03 | 摘要长度受控 | P2 | 统计 desc 长度分布 |
接近 50+100 的截取窗口,不应超长 | min=94 max=153 均值=151.3 | ✅ 通过 |
| TC-DATA-04 | 检索召回一致性 | P1 | 连续两次 GET /s?word=boost |
两次返回完全相同的 id 序列 | 两次各 8689 条,顺序一致=True | ✅ 通过 |
| TC-DATA-05 | 结果数量应受上限约束 | P1 | GET /s?word=boost,统计结果数与全库占比 |
结果条数应有合理上限(如 Top-N) | 8689 条 / 全库 8689 篇,召回率 100%,响应 2.97 MB | ❌ 失败 |
| TC-DATA-06 | 索引覆盖率抽查 | P2 | 抽查 6 个语料关键词的召回条数 | 各关键词均能召回对应文档 | accumulators:592, xpressive:188, lockfree:19, signals2:8, tribool:21, yap:86 | ✅ 通过 |
关键发现 :搜索
boost会返回全部 8689 篇文档 。原因是 Boost 文档的每篇页脚都带有Distributed under the Boost Software License版权声明,导致boost这个词在几乎每一篇文档中都出现------系统没有做停用词/高频词过滤,也没有对结果条数做任何截断。索引本身的覆盖率是正常的(抽查的 6 个关键词均按预期召回),问题出在结果规模控制上。
3.5 并发
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|---|---|
| TC-CONC-01 | 并发查询可用性 | P1 | 8 线程并发发起 24 次 GET /s?word=boost |
全部成功,无 5xx、无连接被拒 | 耗时 178.43s ,平均 7435ms/请求 ,23 次 200 + 1 次 ConnectionError | ❌ 失败 |
| TC-CONC-02 | 并发混合查询 | P1 | 4 种关键词并发 24 次,校验各词结果稳定 | 各词结果正确、互不串扰 | 耗时 54.11s,错误 0;同词结果数完全稳定(boost:8689 / asio:1867 / xpressive:188 / lockfree:19) | ✅ 通过 |
关键发现 :并发场景下响应时间没有随并发而恶化,而是等于串行累加 。24 次请求总耗时 178.43s,恰好约等于
24 × 单次 7.4s,说明所有搜索请求被全局串行化 执行,并发没有带来任何吞吐收益,反而出现了连接被重置的情况。根因是http_server.cc中用一把全局互斥锁包住了整个检索过程。
3.6 Web UI
UI 用例除断言页面表现外,还通过 Chrome 的 performance 日志 采集真实发出的网络请求、通过 browser 日志采集 JS 运行时异常,以保证结论有可追溯证据。
| 用例编号 | 用例名称 | 优先级 | 操作步骤 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|---|---|---|
| TC-UI-01 | 首页打开并渲染搜索框 | P0 | 打开首页,检查标题与控件可见性 | 标题正确,输入框与按钮可见 | title=boost 搜索引擎;输入框可见=True;按钮文案=搜索一下 | ✅ 通过 |
| TC-UI-02 | 输入框默认提示不占用真实输入 | P1 | 读取输入框 value 属性 |
提示文案应用 placeholder,不进入真实取值 |
默认 value='请输入搜索关键字' |
❌ 失败 |
| TC-UI-03 | 前端依赖 jQuery 加载成功 | P0 | 检查 window.jQuery |
存在,搜索按钮可用 | jQuery 已加载=True | ✅ 通过 |
| TC-UI-04 | 关键字 boost 搜索出结果 | P0 | 输入 boost 点击搜索,等待结果渲染 |
结果区渲染出结果列表,标题非空 | 结果 8689 条 ,首条标题=Getting started with Boost.Metaparse |
✅ 通过 |
| TC-UI-05 | 单页结果条数受控 | P1 | 统计结果区 .item 节点数 |
单页应有合理上限(分页或截断) | 单页渲染 8689 条 DOM 结果 | ❌ 失败 |
| TC-UI-06 | 结果链接格式正确且新窗口打开 | P1 | 抽查结果链接 href 与 target |
完整 http(s) 地址且 target=_blank |
抽查 5 条全部合法,target 均为 _blank |
✅ 通过 |
| TC-UI-07 | 未输入内容直接点击搜索 | P1 | 不输入任何内容,直接点"搜索一下" | 应提示用户输入关键字 | 实际发出请求 word=请输入搜索关键字,页面无任何提示 |
❌ 失败 |
| TC-UI-08 | 无匹配结果时给出空状态提示 | P1 | 搜索 zzqqxxwwvvnotexist |
应显示"未找到相关结果"类提示 | 结果条目=0,页面提示=False,控制台 1 条 JS 异常 | ❌ 失败 |
| TC-UI-09 | 无结果响应不触发前端脚本异常 | P0 | 搜索无匹配词后读取控制台 SEVERE 日志 | 前端应正常处理空结果,控制台无异常 | Uncaught TypeError: data is not iterable |
❌ 失败 |
| TC-UI-10 | 搜索框脚本注入不触发弹窗 | P0 | 输入 <script>alert('xss')</script> 并搜索 |
不弹出 alert | 出现 alert 弹窗=False | ✅ 通过 |
| TC-UI-11 | 刷新页面回到干净首页 | P2 | 搜索结果页按 F5 刷新 | 结果清空,输入框恢复默认 | 残留结果=0 条,输入框恢复默认 | ✅ 通过 |
| TC-UI-12 | 窄屏下页面无横向溢出 | P2 | 窗口宽度调至 375px | 容器自适应,无横向滚动 | 出现横向滚动=True | ❌ 失败 |
关键发现 :TC-UI-09 捕获到的
Uncaught TypeError: data is not iterable是本次测试最有价值的缺陷证据 ------它把接口层"无结果返回null"(TC-SEARCH-06)与前端for (let elem of data)未做空值判断这两个问题串成了一条完整的故障链:用户搜索一个不存在的词 → 后端返回null→ 前端遍历null抛异常 → 结果区被清空且没有任何提示 → 用户完全不知道发生了什么。





四. 自动化测试代码
4.1 接口自动化(Python + requests)
采用「用例登记 + 顺序执行」的结构:装饰器只登记用例元信息,由 main() 统一按顺序执行,避免导入时即发起网络请求,也便于通过 --base 参数切换被测环境。
python
CASE_REGISTRY = [] # 按定义顺序登记的用例函数
def run_case(cid, name, priority, expect, request_desc):
"""用例装饰器:只登记用例元信息,实际执行由 main() 统一按顺序触发。"""
def wrapper(fn):
def execute():
c = Case(cid, name, priority, expect, request_desc)
start = time.time()
try:
ok, actual, evidence = fn()
c.status = "通过" if ok else "失败"
c.actual, c.evidence = actual, evidence or ""
except Exception as exc:
c.status = "失败"
c.actual = "执行异常:%s: %s" % (type(exc).__name__, exc)
c.elapsed = time.time() - start
RESULTS.append(c)
print("[%s] %-13s %-36s %7.0fms %s" % (
"PASS" if c.status == "通过" else "FAIL",
c.cid, c.name, c.elapsed * 1000, c.actual[:80]))
return c
CASE_REGISTRY.append((cid, execute))
return execute
return wrapper
首页安全检查与关键字搜索用例示例:
python
@run_case("TC-HOME-05", "静态资源目录穿越被拦截", "P1",
"不泄露 wwwroot 之外的服务器文件(如 /../../etc/passwd)", "GET /../makefile")
def tc_home_05():
r = requests.get(BASE + "/../makefile", timeout=TIMEOUT)
leaked = ("PARSER" in r.text and "g++" in r.text) or "root:" in r.text
return not leaked, "HTTP %d,%d 字节,泄露=%s" % (
r.status_code, len(r.content), leaked), r.text[:150]
@run_case("TC-DATA-05", "结果数量无上限(返回超大响应)", "P1",
"结果条数应受合理上限约束", "GET /s?word=boost")
def tc_data_05():
r, data = search("boost")
n = len(data) if isinstance(data, list) else 0
return n <= 100, "结果 %d 条 / 全库 8689 篇(召回率 %.1f%%),响应 %.2f MB" % (
n, n / 8689.0 * 100, len(r.content) / 1024.0 / 1024.0), ""
并发用例(含结果一致性校验):
python
@run_case("TC-CONC-02", "并发混合查询(不同关键字)", "P1",
"不同关键字并发查询结果各自正确、互不串扰", "GET /s?word=<4 种词> ×24 并发")
def tc_conc_02():
import concurrent.futures
words = ["boost", "asio", "xpressive", "lockfree"]
def one(w):
try:
r = requests.get(BASE + "/s", params={"word": w}, timeout=TIMEOUT)
return w, len(r.json()), None
except Exception as exc:
return w, 0, type(exc).__name__
t0 = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
res = list(ex.map(one, words * 6))
el = time.time() - t0
errs = [x for x in res if x[2]]
byword = {}
for w, n, e in res:
byword.setdefault(w, set()).add(n)
mixed = [w for w, s in byword.items() if len(s) > 1]
return (not errs and not mixed), "耗时 %.2fs,错误 %d,同词结果数波动=%s" % (
el, len(errs), mixed), ""
4.2 UI 自动化(Selenium 4 + Chrome)
UI 自动化的关键不只是"点按钮看页面",而是采集可追溯的执行证据。本脚本额外抓取两类日志:
python
def js_errors(driver):
"""读取浏览器控制台中的 JS 异常"""
out = []
try:
for entry in driver.get_log("browser"):
if entry.get("level") in ("SEVERE", "ERROR"):
out.append(entry.get("message", "").strip())
except Exception:
pass
return out
def search_requests(driver):
"""从 performance 日志中提取实际发出的 /s?word= 请求 URL"""
urls = []
for entry in driver.get_log("performance"):
msg = json.loads(entry["message"])["message"]
if msg.get("method") != "Network.requestWillBeSent":
continue
url = msg.get("params", {}).get("request", {}).get("url", "")
if "/s?word=" in url:
urls.append(url)
return urls
驱动初始化需开启日志能力:
python
def build_driver(headless=True):
opt = Options()
if headless:
opt.add_argument("--headless=new")
opt.add_argument("--window-size=1440,900")
opt.add_argument("--disable-gpu")
opt.add_argument("--no-sandbox")
# 开启浏览器与网络日志,用于采集 JS 异常和真实请求
opt.set_capability("goog:loggingPrefs", {"browser": "ALL", "performance": "ALL"})
driver_path = os.environ.get("CHROMEDRIVER_PATH") or DEFAULT_DRIVER
if os.path.exists(driver_path):
return webdriver.Chrome(service=Service(driver_path), options=opt)
return webdriver.Chrome(options=opt)
核心用例------无结果时捕获前端崩溃:
python
# ------------------------------------------------ TC-UI-08 无结果空状态
drain(driver)
do_search(driver, "zzqqxxwwvvnotexist")
empty_items = result_items(driver)
body_text = driver.find_element(By.TAG_NAME, "body").text
has_tip = any(k in body_text for k in ("没有找到", "无结果", "未找到", "暂无", "无匹配"))
errs = js_errors(driver)
record("TC-UI-08", "无匹配结果时给出空状态提示", "P1",
"页面应显示"未找到相关结果"类提示,且不报 JS 异常",
"结果条目=%d,页面提示=%s,JS异常=%d 条" % (len(empty_items), has_tip, len(errs)),
len(empty_items) == 0 and has_tip and not errs)
# ------------------------------------------------ TC-UI-09 前端脚本异常
record("TC-UI-09", "无结果响应不触发前端脚本异常", "P0",
"后端返回空结果时前端应正常处理,控制台无 SEVERE 异常",
("无 JS 异常" if not errs else "捕获 %d 条:%s" % (len(errs), errs[0][:100])),
not errs)
未输入直接点击搜索(用网络日志证明提示文案被当成了关键字):
python
drain(driver)
open_home(driver, wait)
driver.find_element(By.CSS_SELECTOR, ".container .search button").click()
time.sleep(4)
reqs = search_requests(driver)
submitted = [unquote(u.split("word=", 1)[1]) for u in reqs if "word=" in u]
has_hint = any(k in driver.find_element(By.TAG_NAME, "body").text
for k in ("请输入关键字", "不能为空", "请输入搜索"))
record("TC-UI-07", "未输入内容直接点击搜索", "P1",
"应提示用户输入关键字,不应把提示文案当作关键词提交",
"实际发出请求=%s,页面提示=%s" % (submitted or "无", has_hint),
(not submitted) or has_hint)
实际输出:
text
[PASS] TC-UI-01 首页打开并渲染搜索框
[FAIL] TC-UI-02 输入框默认提示不占用真实输入
[PASS] TC-UI-03 前端依赖 jQuery 加载成功
[PASS] TC-UI-04 关键字 boost 搜索出结果 结果 8689 条
[FAIL] TC-UI-05 单页结果条数受控 单页渲染 8689 条结果
[PASS] TC-UI-06 结果链接格式正确且新窗口打开
[FAIL] TC-UI-07 未输入内容直接点击搜索 实际发出请求=['请输入搜索关键字']
[FAIL] TC-UI-08 无匹配结果时给出空状态提示 结果条目=0,页面提示=False,JS异常=1 条
[FAIL] TC-UI-09 无结果响应不触发前端脚本异常 捕获 1 条:Uncaught TypeError: data is not iterable
[PASS] TC-UI-10 搜索框脚本注入不触发弹窗
[PASS] TC-UI-11 刷新页面回到干净首页
[FAIL] TC-UI-12 窄屏下页面无横向溢出 出现横向滚动=True
4.3 性能测试(JMeter)
测试计划结构:TestPlan → 公共「HTTP 请求默认值」→ 两个线程组 → 汇总报告(输出 JTL)。
| 线程组 | 线程数 | Ramp-up | 循环 | 采样数 | 采样器 |
|---|---|---|---|---|---|
| 场景1_静态首页 | 20 | 5s | 10 | 200 | GET / |
| 场景2_搜索接口 | 10 | 5s | 5 | 50 | GET /s?word=${word}(CSV 参数化 10 个关键词) |
搜索接口的断言配置:
xml
<!-- 断言 1:响应码必须为 200 -->
<ResponseAssertion testname="断言_响应码200">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<!-- 断言 2:响应体必须是 JSON 数组 -->
<ResponseAssertion testname="断言_返回JSON数组">
<collectionProp name="Asserion.test_strings">
<stringProp name="1">^\[.*\]$</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<intProp name="Assertion.test_type">2</intProp>
</ResponseAssertion>
CSV 数据驱动(words.csv):
csv
word
boost
asio
xpressive
lockfree
signals2
tribool
yap
any
align
chrono
执行命令:
bash
jmeter -n -t boost_search_perf.jmx \
-l results/perf_result.jtl \
-j results/jmeter.log
五. 测试执行结果
5.1 总体统计
| 测试类型 | 用例/采样数 | 通过 | 失败 | 通过率 |
|---|---|---|---|---|
| 接口功能测试 | 31 | 26 | 5 | 83.9% |
| Web UI 自动化测试 | 12 | 6 | 6 | 50.0% |
| 功能测试合计 | 43 | 32 | 11 | 74.4% |
| 性能测试(JMeter 采样) | 250 | 226 | 24 | 90.4% |
按优先级统计(功能用例):
| 优先级 | 用例数 | 通过 | 失败 | 通过率 | 失败用例分布 |
|---|---|---|---|---|---|
| P0(冒烟) | 14 | 13 | 1 | 92.9% | TC-UI-09(空结果触发前端崩溃) |
| P1(常规) | 21 | 13 | 8 | 61.9% | 接口 4 条(含并发 1 条)+ UI 4 条 |
| P2(边界) | 8 | 6 | 2 | 75.0% | TC-SEARCH-12、TC-UI-12 |
| 合计 | 43 | 32 | 11 | 74.4% | --- |
5.2 接口功能测试结果
被测地址:http://139.196.124.168:8080
执行时间:2026-09-14 19:14:25
用例总数:31 通过:26 失败:5 通过率:83.9%
一、首页与静态资源
| 用例编号 | 用例名称 | 优先级 | 操作 | 实际结果 | 耗时(ms) | 结论 |
|---|---|---|---|---|---|---|
| TC-HOME-01 | 首页可正常访问 | P0 | GET / | HTTP 200,7732 字节 | 164 | ✅ 通过 |
| TC-HOME-02 | 首页标题与搜索框渲染正确 | P0 | GET / | 标题=True 输入框=True 按钮=True | 160 | ✅ 通过 |
| TC-HOME-03 | 首页外部 CDN 依赖可用(jQuery) | P2 | GET jquery CDN | HTTP 200,84245 字节 | 189 | ✅ 通过 |
| TC-HOME-04 | 不存在的路径返回 404 | P1 | GET /not-exist-page-xyz | HTTP 404,0 字节 | 110 | ✅ 通过 |
| TC-HOME-05 | 静态资源目录穿越被拦截 | P1 | GET /.../makefile | HTTP 404,未泄露 | 120 | ✅ 通过 |
二、搜索接口功能与边界
| 用例编号 | 用例名称 | 优先级 | 操作 | 实际结果 | 耗时(ms) | 结论 |
|---|---|---|---|---|---|---|
| TC-SEARCH-01 | 英文关键字搜索返回结果 | P0 | word=boost |
结果 8689 条,3115347 字节 | 6739 | ✅ 通过 |
| TC-SEARCH-02 | 多词查询可合并返回 | P0 | word=boost asio |
结果 8689 条 | 7400 | ✅ 通过 |
| TC-SEARCH-03 | 结果 JSON 字段完整 | P0 | word=boost |
字段缺失 0 条 | 2 | ✅ 通过 |
| TC-SEARCH-04 | 结果按权重降序排列 | P0 | word=boost |
weight 1~1866,降序=True | 1 | ✅ 通过 |
| TC-SEARCH-05 | 结果 url 指向 boost 官方文档 | P1 | word=boost |
前缀异常 0 条 | 1 | ✅ 通过 |
| TC-SEARCH-06 | 无匹配关键字返回空数组 | P1 | word=zzqqxxwwvv |
响应为 null |
213 | ❌ 失败 |
| TC-SEARCH-07 | 搜索结果无重复文档 | P1 | word=boost |
重复 0 条 | 1 | ✅ 通过 |
| TC-SEARCH-08 | 缺少 word 参数给出友好提示 | P0 | GET /s |
必须要有搜索关键字! | 223 | ✅ 通过 |
| TC-SEARCH-09 | word 参数为空串 | P1 | word= |
响应 null,与缺参不一致 |
195 | ❌ 失败 |
| TC-SEARCH-10 | 大小写不敏感 | P1 | boost/Boost/BOOST | 三者一致 | 14052 | ✅ 通过 |
| TC-SEARCH-11 | 中文关键字查询不导致服务异常 | P1 | word=搜索引擎 |
HTTP 200,响应 null |
208 | ✅ 通过 |
| TC-SEARCH-12 | 中文常见词(语料中不存在) | P2 | word=你好世界 |
响应 null |
229 | ❌ 失败 |
| TC-SEARCH-13 | 纯符号 / 特殊字符关键字 | P1 | word=!@#$%^&*() |
3102564 字节 | 6983 | ✅ 通过 |
| TC-SEARCH-14 | SQL 注入式关键字不生效 | P0 | word=' OR '1'='1 |
8689 条,无注入 | 6944 | ✅ 通过 |
| TC-SEARCH-15 | XSS 脚本关键字不被原样回显 | P0 | word=<script>... |
脚本回显=False | 7316 | ✅ 通过 |
| TC-SEARCH-16 | 超长关键字(10000 字符) | P1 | 10000 个 a | HTTP 414 | 122 | ✅ 通过 |
| TC-SEARCH-17 | URL 编码与空格关键字 | P2 | word=boost%20%25 |
8689 条 | 7272 | ✅ 通过 |
| TC-SEARCH-18 | 不支持的请求方法被拒绝 | P2 | POST /s |
HTTP 404 | 116 | ✅ 通过 |
三、检索质量与数据一致性
| 用例编号 | 用例名称 | 优先级 | 操作 | 实际结果 | 耗时(ms) | 结论 |
|---|---|---|---|---|---|---|
| TC-DATA-01 | 摘要字段不为空占位值 | P1 | word=boost |
异常 0 条(0.0%) | 1 | ✅ 通过 |
| TC-DATA-02 | 摘要命中关键字(相关性) | P1 | word=boost |
命中 8689 条(100.0%) | 3 | ✅ 通过 |
| TC-DATA-03 | 摘要长度受控 | P2 | word=boost |
min=94 max=153 均值=151.3 | 2 | ✅ 通过 |
| TC-DATA-04 | 检索召回一致性 | P1 | 连续两次 word=boost |
两次一致=True | 14615 | ✅ 通过 |
| TC-DATA-05 | 结果数量应受上限约束 | P1 | word=boost |
8689/8689,召回率 100%,2.97 MB | 0 | ❌ 失败 |
| TC-DATA-06 | 索引覆盖率抽查 | P2 | 6 个关键词 | 全部正常召回 | 1488 | ✅ 通过 |
四、并发
| 用例编号 | 用例名称 | 优先级 | 操作 | 实际结果 | 耗时(ms) | 结论 |
|---|---|---|---|---|---|---|
| TC-CONC-01 | 并发查询可用性(8 线程 × 3 轮) | P1 | word=boost ×24 并发 |
耗时 178.43s,平均 7435ms,1 次 ConnectionError | 178440 | ❌ 失败 |
| TC-CONC-02 | 并发混合查询 | P1 | 4 种词 ×24 并发 | 耗时 54.11s,错误 0,结果稳定 | 54113 | ✅ 通过 |
5.3 UI 自动化测试结果
被测地址:http://139.196.124.168:8080
执行时间:2026-09-14 19:15:58
用例总数:12 通过:6 失败:6
| 用例编号 | 用例名称 | 优先级 | 实际结果 | 结论 |
|---|---|---|---|---|
| TC-UI-01 | 首页打开并渲染搜索框 | P0 | title=boost 搜索引擎;输入框可见=True | ✅ 通过 |
| TC-UI-02 | 输入框默认提示不占用真实输入 | P1 | 默认 value='请输入搜索关键字' |
❌ 失败 |
| TC-UI-03 | 前端依赖 jQuery 加载成功 | P0 | jQuery 已加载=True | ✅ 通过 |
| TC-UI-04 | 关键字 boost 搜索出结果 | P0 | 结果 8689 条,首条标题正常 | ✅ 通过 |
| TC-UI-05 | 单页结果条数受控 | P1 | 单页渲染 8689 条结果 | ❌ 失败 |
| TC-UI-06 | 结果链接格式正确且新窗口打开 | P1 | 抽查 5 条全部合法,target=_blank | ✅ 通过 |
| TC-UI-07 | 未输入内容直接点击搜索 | P1 | 实际发出请求 word=请输入搜索关键字 |
❌ 失败 |
| TC-UI-08 | 无匹配结果时给出空状态提示 | P1 | 结果条目=0,页面提示=False,JS异常=1 条 | ❌ 失败 |
| TC-UI-09 | 无结果响应不触发前端脚本异常 | P0 | Uncaught TypeError: data is not iterable |
❌ 失败 |
| TC-UI-10 | 搜索框脚本注入不触发弹窗 | P0 | 出现 alert 弹窗=False | ✅ 通过 |
| TC-UI-11 | 刷新页面回到干净首页 | P2 | 残留结果=0 条 | ✅ 通过 |
| TC-UI-12 | 窄屏下页面无横向溢出 | P2 | 出现横向滚动=True | ❌ 失败 |
六. 缺陷清单与根因分析
本次共确认 11 个缺陷,其中严重 3 个、高 3 个、中 2 个、低 3 个。
缺陷汇总
| 编号 | 缺陷描述 | 严重程度 | 关联用例 | 根因位置 |
|---|---|---|---|---|
| D1 | 搜索结果为全量返回,无 Top-N 截断与分页,最大响应体 2.97 MB、单次最慢 7.4s | 🔴 严重 | TC-DATA-05 | searcher.hpp Search() |
| D2 | 无匹配时接口返回 null 而非 [],导致前端 for...of 抛异常、搜索彻底失效 |
🔴 严重 | TC-SEARCH-06 / TC-UI-09 | searcher.hpp + index.html |
| D3 | 全局互斥锁串行化全部搜索请求,并发零吞吐收益,并发下出现连接重置 | 🔴 严重 | TC-CONC-01 | http_server.cc |
| D4 | 输入框提示文案用 value 实现,未输入直接点击会把提示文案当关键字提交 |
🟠 高 | TC-UI-02 / TC-UI-07 | index.html |
| D5 | 首页静态资源在搜索压力下响应时间劣化 5 倍,11.5% 请求超过 2s | 🟠 高 | JMeter 场景1 | 线程池资源争用 |
| D6 | 无结果时页面无任何空状态提示,用户无法区分"无结果"与"请求失败" | 🟠 高 | TC-UI-08 | index.html BuildHtml() |
| D7 | 前端依赖第三方 HTTP CDN 加载 jQuery,无本地兜底副本 | 🟡 中 | TC-HOME-03 / TC-UI-03 | index.html |
| D8 | 8689 条结果一次性渲染进 DOM,无分页与虚拟滚动 | 🟡 中 | TC-UI-05 | index.html BuildHtml() |
| D9 | word= 空串与"缺少 word 参数"处理不一致(null vs 友好提示) |
🟢 低 | TC-SEARCH-09 | http_server.cc |
| D10 | 页面未做响应式适配,375px 窄屏出现横向滚动(容器固定 width:800px) |
🟢 低 | TC-UI-12 | index.html |
| D11 | 日志级别标签拼写错误 NOMARL,运行日志输出 [NOMARL] |
🟢 低 | 代码审查 | searcher.hpp:34,38 |
重点缺陷根因分析
D1:搜索结果无上限,全量返回(严重)
现象 :搜索 boost 返回 8689 条 结果(等于全库总量),响应体 2.97 MB,单次耗时 6.7 秒;搜索纯符号 !@#$%^&*() 同样返回 3.1 MB。
根因 :searcher.hpp 的 Search() 方法把倒排拉链命中的全部文档直接写入 JSON,没有任何截断:
cpp
std::sort(index_all.begin(), index_all.end(),
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2) {
return e1.weight > e2.weight;
});
Json::Value root;
for (auto &item : index_all) { // ← 遍历全部命中,无 Top-N 限制
ns_index::ConInfo *con = index->Getconinfo(item.con_id);
...
root.append(elem);
}
而 boost 这个词会命中全库,是因为 Boost 官网每篇文档页脚都带有版权声明 Distributed under the Boost Software License, Version 1.0,导致该词在几乎每篇文档的正文中出现。系统没有做停用词/高频词过滤 ,weight = 10 × 标题词频 + 1 × 正文词频 的打分方式也无法把这类"全库词"与真正的主题词区分开。
实测佐证 :对 11 个关键词做单线程 3 次重复测量,结果条数与平均响应时间呈近乎完美的线性关系(Pearson r = 0.9997):
| 关键词 | 结果条数 | 平均响应时间(ms) |
|---|---|---|
| boost | 8689 | 7162 |
| asio | 1867 | 1520 |
| accumulators | 592 | 566 |
| xpressive | 188 | 233 |
| yap | 86 | 174 |
| chrono | 69 | 177 |
| tribool | 21 | 165 |
| align | 20 | 174 |
| lockfree | 19 | 156 |
| signals2 | 8 | 165 |
| zzqqxx(无匹配) | 0 | 205 |
可见基础开销约 150~200ms,之后响应时间几乎完全由结果条数决定。
D2:无结果返回 null 导致前端崩溃(严重)
现象 :搜索不存在的词(如 zzqqxxwwvv),接口返回 null(5 字节);前端控制台抛出 Uncaught TypeError: data is not iterable,结果区被清空且无任何提示,用户看到的是一个"什么都没有发生"的页面。
根因(后端) :searcher.hpp 中 Json::Value root; 默认构造为 nullValue,当没有任何命中时循环体不执行,Json::FastWriter::write() 对 nullValue 输出字符串 null:
cpp
Json::Value root; // 默认 nullValue
for (auto &item : index_all) { ... root.append(elem); }
Json::FastWriter writer;
*json_string = writer.write(root); // 无命中时输出 "null"
根因(前端) :index.html 的 BuildHtml() 未对 data 做空值判断:
javascript
function BuildHtml(data){
let result_lable = $(".container .result");
result_lable.empty(); // ← 先清空历史结果
for( let elem of data){ // ← data 为 null 时抛 TypeError
...
}
}
故障链 :后端返回 null → result_lable.empty() 先执行清空了页面 → for...of null 抛异常中断渲染 → 用户既看不到结果也看不到错误提示。这是本次测试发现的影响最严重、用户可直接感知的缺陷。
D3:全局互斥锁导致搜索完全串行(严重)
现象:24 次并发请求总耗时 178.43s,平均 7435ms/请求,恰好等于 24 × 单次耗时,并发吞吐没有任何提升,并出现 1 次连接被重置。
根因 :http_server.cc 中把整个检索过程(分词、倒排查找、排序、JSON 序列化)都放在一把全局互斥锁内:
cpp
std::mutex mtx;
svr.Get("/s", [&search, &mtx](const httplib::Request &req, httplib::Response &rsp){
...
std::lock_guard<std::mutex> lock(mtx); // ← 锁覆盖整个搜索流程
std::string json_string;
search.Search(word, &json_string);
rsp.set_content(json_string, "application/json");
});
而 Index 在 InitSearcher() 阶段已完成构建,检索过程本身是只读 的(Getindexlist / Getconinfo 均不修改索引),理论上完全不需要互斥保护。锁的存在使得 httplib 的多线程能力被彻底抵消。
叠加 D1 的超大响应体(3 MB 写回客户端耗时更长),锁被持有的时间被进一步拉长,最终表现为"并发越多、排队越久"。
七. 性能测试报告
7.1 测试场景
使用 Apache JMeter 5.6.3 非 GUI 模式对两个核心入口施压:
| 线程组 | 并发用户 | Ramp-up | 循环次数 | 采样数 | 目标接口 |
|---|---|---|---|---|---|
| 场景1_静态首页 | 20 | 5s | 10 | 200 | GET / |
| 场景2_搜索接口 | 10 | 5s | 5 | 50 | GET /s?word=${word}(10 个关键词参数化) |
断言:响应码须为 200;搜索接口响应体须匹配 ^\[.*\]$;首页响应时间须小于 2000ms。
7.2 总体指标
| 指标 | 数值 |
|---|---|
| 有效采样总数 | 250 |
| 平均响应时间 | 1935 ms |
| P90 响应时间 | 5457 ms |
| P99 响应时间 | 40240 ms |
| 最大响应时间 | 46078 ms |
| 最小响应时间 | 0 ms |
| 失败样本 | 24 |
| 失败率 | 9.6% |
| 整体吞吐量 | ≈ 4.6 个/秒 |
7.3 分事务指标
| 事务 | 样本数 | 失败数 | 失败率 | 平均(ms) | 中位数(ms) | P90(ms) | P99(ms) | 最大(ms) |
|---|---|---|---|---|---|---|---|---|
打开首页 GET / |
200 | 23 | 11.5% | 869 | 179 | 2417 | 8250 | 8389 |
| 搜索 boost | 5 | 0 | 0.0% | 39164 | 40240 | 46078 | 46078 | 46078 |
| 搜索 asio | 5 | 1 | 20.0% | 8702 | 8102 | 14214 | 14214 | 14214 |
| 搜索 any | 5 | 0 | 0.0% | 8313 | 8969 | 11130 | 11130 | 11130 |
| 搜索 yap | 5 | 0 | 0.0% | 2040 | 791 | 7287 | 7287 | 7287 |
| 搜索 xpressive | 5 | 0 | 0.0% | 849 | 676 | 1782 | 1782 | 1782 |
| 搜索 signals2 | 5 | 0 | 0.0% | 786 | 110 | 3546 | 3546 | 3546 |
| 搜索 lockfree | 5 | 0 | 0.0% | 754 | 115 | 3177 | 3177 | 3177 |
| 搜索 chrono | 5 | 0 | 0.0% | 650 | 311 | 2175 | 2175 | 2175 |
| 搜索 tribool | 5 | 0 | 0.0% | 371 | 330 | 684 | 684 | 684 |
| 搜索 align | 5 | 0 | 0.0% | 364 | 116 | 1416 | 1416 | 1416 |
注:JMX 中同时存在「汇总报告」监听器与命令行
-l参数指向同一文件,JMeter 会写入两次,分析前需按timeStamp + label + threadName + elapsed + responseCode去重(原始 500 条 → 有效 250 条)。
7.4 失败样本归类
| 事务 | 失败数 | 失败原因 |
|---|---|---|
打开首页 GET / |
23 | 响应时间超过 2000ms 断言阈值(DurationAssertion) |
| 搜索 asio | 1 | 服务端未响应、连接被中断(NoHttpResponseException) |
7.5 指标分析
(1)搜索接口响应时间由结果条数决定,而非由并发决定
boost(8689 条)平均 39164ms、最大 46078ms;tribool(21 条)平均仅 371ms、align 平均 364ms。两者相差 100 倍以上 。这与 3.4 节单线程测量得到的 Pearson r = 0.9997 完全吻合,进一步确认 D1(结果无上限)是性能问题的首要根因:不是"算得慢",而是"返回得太多"。
(2)静态首页出现严重的资源争用劣化
首页只有一个 7.7 KB 的静态文件,单请求基准耗时约 160ms。但在搜索压力下:
- 平均响应时间劣化到 869ms(约 5.4 倍)
- P99 达到 8250ms(约 51 倍)
- 200 次请求中 23 次超过 2000ms,失败率 11.5%
这说明搜索请求长时间占用服务端线程资源(单次最长 46 秒),静态资源请求被迫排队,搜索接口的容量问题已经外溢影响到最基础的首页可用性。
(3)出现连接被重置,服务稳定性存在隐患
JMeter 侧捕获到 NoHttpResponseException: 139.196.124.168:8080 failed to respond,Python 并发用例侧也捕获到 1 次 ConnectionError。结合响应头 Keep-Alive: timeout=5, max=5 判断,在长耗时搜索请求堆积时,连接复用与超时策略会导致部分连接被服务端关闭,客户端表现为请求失败。
(4)响应体未启用压缩,传输开销被放大
| 项目 | 数值 |
|---|---|
| 原始响应体 | 3115347 字节(2.97 MB) |
| gzip 压缩后 | 338737 字节(0.32 MB) |
| 压缩比 | 10.9%(可节省 89.1% 传输量) |
服务端 Content-Encoding |
未设置 |
| 响应头 | Content-Length: 3115347、Content-Type: application/json、Keep-Alive: timeout=5, max=5 |
JSON 文本的压缩率极高,仅开启 gzip 就能把最大响应从 2.97 MB 降到 0.32 MB,是改造成本最低、收益最直接的优化项。
八. 测试结论与改进建议
8.1 测试结论
功能层面 :核心检索链路是可用 的------8689 篇文档索引构建正确,检索召回准确(抽查关键词全部命中)、结果字段完整、权重排序正确、大小写不敏感、多次查询结果稳定一致;安全性方面表现良好,SQL 注入无攻击面、XSS 未生效、目录穿越被拦截、超长 URI 被正确拒绝。43 条功能用例通过 32 条,通过率 74.4%。
质量层面:问题集中在**"结果规模控制"与"空结果处理"**这两个点上,且这 2 个点会连锁放大:
- 结果无上限(D1) 既是功能缺陷,也是性能缺陷的首要根因;
- 无结果返回
null(D2) 直接导致前端崩溃,用户搜索一个不存在的词就会遇到一个"空白页面"; - 全局互斥锁(D3) 使系统在并发下完全没有扩展能力;
- 性能测试进一步暴露:搜索接口的容量问题已经外溢影响首页可用性(失败率 11.5%),整体失败率 9.6%。
结论 :项目在功能正确性与安全性上达到了可演示、可交付的水平 ,但在健壮性与性能上尚不具备生产可用性,建议按下方优先级完成优化。
8.2 改进建议
按「收益 / 成本」排序:
| 优先级 | 建议 | 对应缺陷 | 预期收益 |
|---|---|---|---|
| P0 | 后端统一空结果响应为 [](root 无命中时显式 root = Json::Value(Json::arrayValue);),前端 BuildHtml() 增加 `data = data |
[];` | |
| P0 | 检索结果截断为 Top-N(建议 100~200 条)并加 offset/limit 分页参数 |
D1、D8 | 响应体从 2.97 MB 降至 KB 级,boost 查询从 7.4s 降到毫秒级 |
| P0 | 移除 http_server.cc 中的全局互斥锁(索引为只读,检索无共享写) |
D3 | 并发吞吐线性提升,消除连接重置 |
| P1 | 开启 gzip 压缩(svr.set_compress(true)) |
D1 | 最大响应体 2.97 MB → 0.32 MB,节省 89.1% 传输量 |
| P1 | 引入停用词表 / 文档频率(DF)阈值,过滤 boost、license 这类出现在全库中的词 |
D1 | 提升检索相关性,避免"搜什么都是全库" |
| P1 | 输入框改用 placeholder="请输入搜索关键字",并在 JS 中判空后再发起请求 |
D4 | 消除"未输入却搜了提示文案"的误导行为 |
| P1 | 增加空状态提示("未找到相关结果,请更换关键字")与请求失败提示 | D6 | 用户能明确区分"无结果"与"系统故障" |
| P2 | 将 jQuery 下载到 wwwroot/js/ 本地引用,或改用原生 fetch/XMLHttpRequest |
D7 | 摆脱第三方 HTTP CDN 依赖(国内访问不稳定、明文传输) |
| P2 | 前端结果列表加分页或虚拟滚动 | D8 | 避免一次性渲染 8689 个 DOM 节点导致页面卡顿 |
| P2 | 修复 searcher.hpp 中的 LOG(NOMARL, ...) 拼写,恢复为 NORMAL |
D11 | 日志级别标签输出正确,便于问题排查 |
| P3 | 删除 index.hpp 中 Getindexlist() 每次未命中都输出 std::cerr 的调试日志 |
--- | 消除高频日志噪音与潜在的 stderr 写放大 |
| P3 | 首页 Content-Type 补充 ; charset=utf-8;/s 对非 GET 方法返回 405 而非 404;word= 空串与缺参保持一致的提示文案 |
D9、D12 | 提升接口语义规范性与客户端兼容性 |
| P3 | 容器宽度改为 max-width + 媒体查询,做基础响应式适配 |
D10 | 移动端可正常浏览 |
8.3 后续测试计划
- 按 D1 修复后回归性能场景,验证 boost 查询响应时间与并发吞吐改善幅度;
- 补充索引正确性专项:随机抽取若干文档,验证"标题/正文/URL"与官网原文的一致性(当前仅验证了召回数量);
- 补充分词效果专项:验证中文语料场景下的分词命中情况(当前语料为英文,中文查询恒为空);
- 补充长稳测试:连续运行 2 小时以上,观察内存占用与索引常驻内存的稳定性;
- 补充多浏览器兼容性:Chrome / Edge / Firefox 下前端表现一致性。
附录:测试资产清单
| 资产 | 路径 | 说明 |
|---|---|---|
| 接口测试脚本 | api-test/test_boost_search_api.py |
31 条接口用例,一键执行 |
| 接口测试结果 | api-test/results/api_result.json / api_report.md |
原始结果与结果表 |
| UI 测试脚本 | ui-test/test_boost_search_ui.py |
12 条 UI 用例,含 JS 异常与网络日志采集 |
| UI 测试结果 | ui-test/results/ui_result.json / ui_report.md |
原始结果与结果表 |
| 执行截图 | screenshots/*.png |
7 张执行过程截图 |
| JMeter 测试计划 | perf-test/boost_search_perf.jmx |
2 个线程组 + 断言 + CSV 参数化 |
| JMeter 原始数据 | perf-test/results/perf_result.jtl |
500 条原始采样(去重后 250 条) |
| 性能分析脚本 | perf-test/analyze_jtl.py |
JTL 去重与指标汇总 |
| 性能指标汇总 | perf-test/results/perf_summary.md |
分事务性能指标表 |
