建立文档标识索引

C++Beginner
立即练习

介绍

你记得某篇笔记中的一个词,却想不起它所在的目录。实用的搜索工具应该找出所有匹配笔记,区分同名文件,并显示可在原文件中核对的真实行。

用 C++ 的 map 和 set 建立这种关系。加载、词语解析、查询处理和行显示已经准备好;你将实现索引,并用不断变化的练习笔记测试它。你需要掌握基本终端导航、C++ 字符串、vector、函数、文件流和异常。这里会教授 map 和 set。50 分钟是作者估计的时长。

把一个词关联到所有笔记

本步骤先使用可运行的样例,再为自己的可执行程序建立词到文档的索引。整门课程都使用 Terminal 标签页。它打开支持 UTF-8 的 shell;请使用这个具名标签页,而非 WebIDE 内部的终端。工作目录会在当前连接中保留。如果打开另一连接,请重新执行正文中的 cd。

cd 改变工作目录。从准备好的项目目录开始:

cd /home/labex/project/note-search

notes/reference 包含受保护的原始笔记;notes/practice 包含可写的副本。find 递归列出路径;-type f 将列表限制为普通文件。列出的顺序不重要。

find notes -type f

notes/reference/archive/notes.txt 和 notes/reference/field/notes.txt 都存在。它们的最后一个文件名分量,也就是基本文件名,相同,但完整路径标识不同的文档。./ 运行当前目录中的可执行程序。完整的 sample 接收笔记目录和查询作为参数:

./sample notes cat

它返回三个文档组,包括以下两组:

File: reference/archive/notes.txt
  1: CAT, cat!
  3: cat blue together

File: reference/field/notes.txt
  1: cat; red

第三组是 practice/旅行 notes.txt。每个标识都相对于传入的 notes 根目录,因此省略了 notes/。即使词出现多次,每个文档也只显示一次。数字是真实的原文行号,从 1 开始。这里只显示相关行;某个行号未出现并不表示原文行被删除。cat 打印文本文件,便于对照:

cat notes/reference/archive/notes.txt

第 2 行 Blue ledger — 東京 确实存在,但不含 cat。原有的 UTF-8 路径和行文本均被保留。

本课程的小型词法规则很明确:一个词元是由 ASCII 字母 A-Z 或 a-z 构成的最长连续片段,ASCII 大写字母折叠为小写。数字、下划线、标点、空格以及每个非 ASCII 字节都分隔词元。CAT-blue_42 变成 cat 和 blue;cat 不同于 catalog;猫cat狗 包含 cat;café 产生 caf,而不是 cafe。保留 Unicode 文本并不等于理解所有语言的词语。本课程不实现多语言分词。

阅读准备好的接口和起始代码:

cat search.hpp
cat engine.cpp

Document 保存完整相对路径 id,以及由原始行字符串组成的 vector lines。准备好的加载器递归读取普通 .txt 文件,并按文档标识排序。笔记采用 UTF-8 文本和 LF 换行。路径允许包含空格和 Unicode;路径中的制表符或换行符、符号链接、NUL 数据和任意二进制文件不属于此格式。无法读取的输入会产生错误,而不会给出部分结果。

Index 是 std::map<std::string, std::set<std::string>> 的别名。map 将每个不同的词键关联到一个值,这里是文档标识的集合。set 只保留每个标识一次。因此,cat 在同一文档中出现三次,也只产生一条关联。这种从词反向查找文档的结构称为倒排索引。

起始代码返回空索引。provided.inc 包含本单元中已经可用的查询函数。#include 会在编译时插入一个文件。保留 search.hpp、provided.inc、support.cpp、main.cpp、sample,以及 notes/reference/ 下的每个路径和内容。编辑 engine.cpp;允许增加辅助函数或使用等价算法。只有 notes/practice/ 中的数据可以编辑、新增或删除。评分不要求保留它原来的文件成员集合。

bash 运行准备好的自检脚本。index 选择本步骤教授的行为。第一次运行会有意失败,输出 FAIL exact whole-word postings retain complete document identities 并以非零状态退出。这意味着索引尚未完成,而不是环境损坏:

bash /opt/labex-note-search/check.sh index

遍历每个文档、每一行和每个解析出的词。基于范围的 for 访问各元素;const auto& 提供只读引用,避免复制元素。index[word] 访问该词的集合;如果键是新的,就创建一个空集合。.insert(doc.id) 加入完整标识,并忽略重复关联。把 map 放在所有循环外,才能累积所有文档的关系。

下一条命令只替换 engine.cpp。here-document 将多行文字发送给 cat;> 把它们写入指定文件,替换原内容。带引号的分隔符 'CPP' 防止 shell 展开 C++ 文本。最后必须让 CPP 单独占一行。保留两条 include 指令,使声明和准备好的查询实现仍然可用。

cat > engine.cpp <<'CPP'
#include "search.hpp"
#include "provided.inc"
Index buildIndex(const std::vector<Document>& documents) {
    Index index;
    for (const auto& doc : documents) {
        for (const auto& line : doc.lines) {
            for (const auto& word : words(line)) {
                index[word].insert(doc.id);
            }
        }
    }
    return index;
}
CPP

words(line) 是准备好的 ASCII 解析器。不要只提取基本文件名、在首个匹配处停止,或用出现次数代替文档集合。

编译要交付的程序。g++ 是编译器;-std=c++17 选择语言版本,-Wall -Wextra 启用实用警告,-o findnotes 指定输出可执行文件的名称。三个 .cpp 输入分别是你的引擎、准备好的加载器与解析器,以及命令接口。被 include 的文件会自动读取。

g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o findnotes

构建成功通常没有输出。如果出错,阅读第一条诊断中的文件名和行号,修正 engine.cpp,或重新完整替换该文件,然后再次编译。构建失败可能留下旧的可执行文件。只编辑源码并不会更新它。自检既会在隔离测试数据上运行你的原样函数,也会测试现有的 findnotes;它不会替你重新构建该可执行文件。

./findnotes notes cat

结果应包含全部三个文档标识,以及与样例相同的原始行。允许采用其他可重复的文档顺序;每个文档内的行号必须递增,且每行只出现一次。本步骤评分索引及单词查询行为。组合不同查询词的功能目前已准备好,它将是下一个实验中你的任务。

根据当前练习笔记重建索引

本步骤在保留原始笔记的前提下,编辑、新增和删除练习数据。留在 Terminal 中的 /home/labex/project/note-search。每次调用都会加载当前文件,并建立全新的内存索引。没有保存到磁盘的索引文件、刷新守护进程或额外重建命令。

先比较完整词的边界:

./findnotes notes catalog

应出现 reference/catalog.txt 和 practice/旅行 notes.txt,并显示包含 catalog 的行。这些不是额外的 cat 匹配。查询未知词也是一次正常搜索:

./findnotes notes absent

程序打印 No matching documents. 并成功退出。这与格式不正确的输入不同,后者会被准备好的接口拒绝。下一个实验将讨论这种区别。

练习副本已经可写;参考原件是只读的。单引号让含空格的路径保持为一个参数,并保留 Unicode 字符。printf 写出文本;\n 产生换行。> 替换指定练习文件的全部内容。通过替换内容,让这个副本不再包含 cat:

printf 'owl green\n東京 — preserved\n' > 'notes/practice/旅行 notes.txt'
./findnotes notes cat

现在只有两篇参考笔记匹配。练习文件仍然存在,只是当前内容不再匹配。新增另一篇允许修改的练习笔记:

printf 'CAT beside the window\nA new observation\n' > 'notes/practice/new notes.txt'
./findnotes notes cat

又有三个文档匹配,其中包括第 1 行命中的 practice/new notes.txt。这里没有重新编译:改变数据只需重新运行程序;改变 C++ 源码才需要编译。

rm 删除文件。这里唯一的带引号操作数是刚新增的练习笔记,而不是原件或整个目录:

rm 'notes/practice/new notes.txt'
./findnotes notes cat

该标识消失了,两篇参考笔记的结果仍然存在。不要删除原件或修改解析器来强行获得某个结果。如果路径拼错,阅读 No such file or directory,用 find notes -type f 列出路径,再使用准确路径。如果误将操作指向只读原件,请保持其不变,改用练习文件。普通路径错误无需重置环境。

再次运行自检。它读取当前现场中的文件,然后为其他输入建立一次性测试数据。测试数据中的编辑、新增和删除绝不会修改你的练习数据。隔离的非法输入探测可能在 PASS 行旁输出预期错误消息;运行成功时以 CLI CHECKS PASSED 结束。

bash /opt/labex-note-search/check.sh index

检查按钮也应通过。重复检查不会重写代码、替换可执行文件或重置练习笔记。如果编辑源码后失败,请重建实际的 findnotes 再试。临时函数测试本身不足以证明可执行文件已更新。

总结

你建立了倒排索引,其值是完整文档标识的集合。重复词不会造成重复文档,同名文件保持独立,真实行号可以带你回到原文。重新运行会根据允许的练习编辑、新增和删除透明地重建索引。接下来,实现要求多个词都存在于同一文档中的查询交集。