Boost搜索引擎测试报告

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_lowersplit
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 统计 descNone/空 的比例 不应出现失败占位 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 抽查结果链接 hreftarget 完整 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.hppSearch() 方法把倒排拉链命中的全部文档直接写入 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.hppJson::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.htmlBuildHtml() 未对 data 做空值判断:

javascript 复制代码
function BuildHtml(data){
    let result_lable = $(".container .result");
    result_lable.empty();               // ← 先清空历史结果
    for( let elem of data){             // ← data 为 null 时抛 TypeError
        ...
    }
}

故障链 :后端返回 nullresult_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");
});

IndexInitSearcher() 阶段已完成构建,检索过程本身是只读 的(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: 3115347Content-Type: application/jsonKeep-Alive: timeout=5, max=5

JSON 文本的压缩率极高,仅开启 gzip 就能把最大响应从 2.97 MB 降到 0.32 MB,是改造成本最低、收益最直接的优化项。


八. 测试结论与改进建议

8.1 测试结论

功能层面 :核心检索链路是可用 的------8689 篇文档索引构建正确,检索召回准确(抽查关键词全部命中)、结果字段完整、权重排序正确、大小写不敏感、多次查询结果稳定一致;安全性方面表现良好,SQL 注入无攻击面、XSS 未生效、目录穿越被拦截、超长 URI 被正确拒绝。43 条功能用例通过 32 条,通过率 74.4%。

质量层面:问题集中在**"结果规模控制"与"空结果处理"**这两个点上,且这 2 个点会连锁放大:

  1. 结果无上限(D1) 既是功能缺陷,也是性能缺陷的首要根因;
  2. 无结果返回 null(D2) 直接导致前端崩溃,用户搜索一个不存在的词就会遇到一个"空白页面";
  3. 全局互斥锁(D3) 使系统在并发下完全没有扩展能力;
  4. 性能测试进一步暴露:搜索接口的容量问题已经外溢影响首页可用性(失败率 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)阈值,过滤 boostlicense 这类出现在全库中的词 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.hppGetindexlist() 每次未命中都输出 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 分事务性能指标表
相关推荐
小王的创意工坊10 小时前
C++ 在线判题平台(OJ)测试报告
c++·可用性测试
devpotato4 天前
高可用系统如何定义可用性
服务器·网络·可用性测试
深圳安车昇辉检测机构4 天前
DO-160G 工作冲击与坠撞安全试验有什么区别
可用性测试
深圳安车昇辉检测机构5 天前
GJB150环境试验是什么?军工装备环境试验项目与测试要点
可用性测试
Elastic 中国社区官方博客18 天前
从告警到根因仅需 3 分钟:使用 Elastic Agent Builder 实现自动化根因分析
运维·数据库·人工智能·elasticsearch·自动化·可用性测试
jjjava2.018 天前
初步认识接口测试
java·可用性测试
星核0penstarry20 天前
文本之外:API 如何接入图像生成能力
图像处理·ai作画·可用性测试·人工智能作画·风景
汽车仪器仪表相关领域21 天前
ZDT‑I伺服电机测试系统:四象限动态加载
大数据·数据库·分布式·功能测试·汽车·压力测试·可用性测试
qdwyj0011 个月前
Windows窗口跑到屏幕外拉
可用性测试·使用技巧