> 技术文档 > 【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎

【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎

52bc67966cad45eda96494d9b411954d.png

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 道阻且长,行则将至


目录

📚一、项目概述

📖1.项目背景

📖2.主要功能

📖3.界面展示

📚二、技术背景

📖1.技术栈

📖2.核心逻辑

📚三、后端实现

📖1.构建原生数据库

🔖网页爬取

🔖数据整理

🔖标签清洗

🔖保存信息

📖2.构建索引

🔖正排索引

🔖倒排索引

📖3. 编写查询程序

🔖对搜索关键字分词

🔖对检索结果进行去重

🔖对检索结果排序

🔖序列化与摘要生成

🔖构建JSON结果

📖4.编写服务器主程序

🔖初始化索引

🔖设置HTTP服务器

🔖处理搜索请求

📚四、前端实现

📖1.页面结构

📖2.搜索功能

📖3.结果展示

📖4.分页功能

📚五、完整演示

📖1.初始界面

📖2.无关键字输入

📖3.输入关键字(无返回)

​编辑

📖4.输入关键字(有返回) 

🔖默认排序 ​编辑

🔖按时间排序

📚六、总结

📖1.优化方向

📖2.源码及网址


📚一、项目概述

📖1.项目背景

杭州师范大学教务处官网是学校发布公告的重要平台,旨在为校内师生提供及时的信息服务。然而,目前官网存在以下问题:

① 更新滞后:主页展示的公告多为旧信息,用户难以快速获取最新动态,增加了时间成本。

② 搜索功能不足:官网搜索引擎缺乏按时间排序的功能,这显然满足不了用户的核心需求,因为公告具有时效性。

③ 界面设计欠佳:搜索界面不够美观,用户体验较差。

基于以上问题,我决定开发一个教务处官网公告的搜索引擎,旨在为校内师生提供一个更高效、更直观的信息检索工具,帮助用户快速获取最新公告信息,提升使用体验。 

📖2.主要功能

校园公告搜索引擎是一个专门服务于本校师生的信息检索平台,其核心功能是基于教务处官网的公告公文提供关键字搜索服务。用户可以通过在搜索框中输入关键字,快速浏览相关公告的摘要信息,并直接点击链接跳转至学校官网查看完整内容,实现高效便捷的信息获取。下面是项目的界面展示:

📖3.界面展示

界面设计简洁直观,包含以下内容:

① 搜索框:位于页面顶部显著位置,支持用户输入关键字进行公告检索;

② 按时间排序选项:位于搜索框侧边,提供将搜索结果按发布时间排序的功能。考虑到官网公告的时效性,这个功能是很必要的。

③ 翻页按钮:位于页面底部,方便用户在搜索结果较多时进行分页浏览。

学校官网也有自己的搜索引擎,但是不具备时间排序的功能,这就有一个问题:用户想通过关键词搜索到最新的公告,但是服务器返回的结果是默认按照关键词权重(关键词在文章0出现的频率)进行排序的,用户并不能立刻得到想要的结果:

这是学校官网的搜索结果: 

这是个人引擎的搜索结果:

由于引擎搜索数据来源全部来自学校官网,数据量其实并不大(从教务处官网爬下来的公告,总共也就两千多条),所以关键字的覆盖范围有限,如果用户输入了一个不存在的关键字,系统会贴心地给出提示,并给出以下选项:

跳转学校官网:可以直接去学校官网查看最新公告(目前项目还有瑕疵,尚未实现在线更新功能,有待后续开发);

② 访问博主个人博客:相当于打个广告吧hh;

查看项目源码:如果对这个项目感兴趣,也可以跳转查看源码。

📚二、技术背景

📖1.技术栈

本项目采用以下技术栈:

Boost准标准库:用于高效的文件操作和字符串处理。

cppjieba分词库:实现中文关键字的分词功能,提升搜索准确性。

jsoncpp序列化工具:将搜索结果序列化为JSON格式,便于前后端交互。

httplib服务器库:快速搭建轻量级HTTP服务器,处理搜索请求与响应。

接下来,详细介绍项目的具体实现过程。

📖2.核心逻辑

首先我们需要了解搜索引擎的核心逻辑:客户端发送搜索关键字,服务端根据关键字检索匹配对应的结果,并将结果返回给客户端。搜索结果通常由三部分组成:

文档标题:简明扼要地概括文档内容;

 文档摘要:包含关键字的部分内容,帮助用户快速了解文档相关性;

③ 文档URL:提供跳转链接,方便用户查看完整内容。

所以实现搜索返回结果,最关键的是:如何根据关键字匹配返回内容?返回内容从何而来? 我们不能简单地将客户端的关键字转发给其他搜索引擎,因为返回结果必须来自本地服务器。因此,我们需要在本地构建一个数据库,存储文档的基本数据单元,即:文档标题、文档摘要、文档URL。

文档内容又从何而来,市面上主流的搜索引擎(如Google、百度)通过网络爬虫不断从互联网上抓取网页内容, 将其转换为数据单元并存储在本地数据库中,从而实现全网搜索。

构建一个全网搜索引擎的成本和资源需求极高,尤其是对于个人开发者来说,无论是数据存储、计算能力还是网络带宽,都远远超出了博主现有云服务器的承载能力。然而,实现一个校园网站公告的搜索引擎则是一个更加实际和可行的选择。校园公告的数据量相对较小,存储和检索的开销也相对较低,完全可以在云服务器的能力范围内高效运行。这种小而精的项目不仅降低了技术门槛,还能为校园用户提供切实的便利,是一个理想的学习和实践目标。

📚三、后端实现

 在掌握了搜索引擎的基本原理和数据结构后,我们开始着手实现后端部分。以下是具体的设计与实现过程:

📖1.构建原生数据库

🔖网页爬取

首选,我们需要从教务处官网爬取内容,将结果保存为本地.html文件,这些文件是后续处理的基础数据源。这里我们使用wget工具进行网页爬取:

wget是一个强大的命令行工具,用于从网络上下载文件,具有递归下载、断点续传、限速等特性,非常适合用于批量下载文件或爬取网站内容。我们在云服务终端输入以下命令:

wget --recursive --no-clobber --no-parent --convert-links --domains jwc.hznu.edu.cn --directory-prefix=data/source_html -A .shtml https://jwc.hznu.edu.cn/

参数说明:

--recursive:递归下载整个网站的内容;

--no-clobber:如果文件已存在,则不会重复下载(避免覆盖);

--no-parent:不下载父目录中的内容,仅限当前目录及子目录;

--convert-links:将下载的文件中的链接转换为本地链接,方便本地查看;

--domains jwc.hznu.edu.cn:限制只下载指定域名下的内容

--directory-prefix=data/source_html:将下载的内容保存到 data/source_html 目录下;

-A .shtml:仅下载 .shtml 文件。

官网的文件就是shtml格式的,这样我们就能准确地将所有公告文件爬取到本地,忽略不需要的文件。由于官网中公告文件都保存在“/c”目录下,我们通过wget工具爬取到本地后转换成本地链接,也是保存在/c目录下面:

我们可以看到,公告文件是按照时间进行分目有序存储,我们可以通过find指令查看总文件数:

接下来的步骤,就是把下载下来的2064个shtml文件整理到一起:

🔖数据整理

爬取到的 .html 文件内容较为杂乱,需要进一步整理并提取关键信息。为了实现这一目标,首先需要递归地遍历 /c 目录,获取全部的 .shtml 文件。虽然可以通过 open 打开文件读取数据,但递归访问目录是一个需要解决的问题。这里,我们可以使用 Boost.Filesystem 库来实现这一功能。

Boost.Filesystem 是一个强大的文件系统操作库,提供了跨平台的目录遍历、文件操作等功能。然而,Boost 并不是 C++ 官方标准库,因此需要先下载并安装到本地才能使用。

下面是Boost::filesystem库的具体使用过程:

// 借助 Boost 库递归遍历目录,汇总 .html 文件bool Enumfile(const string &src_path, vector *files_list){ namespace fs = boost::filesystem; // 命名空间别名,简化代码 fs::path root_path(src_path); // 将字符串路径转换为 boost::filesystem::path 对象 // 判断路径是否存在 if (!fs::exists(root_path)) { cerr << src_path << \" not exists!\" <path().extension() != \".html\") continue; // 将文件名以字符串形式插入列表 files_list->push_back(it->path().string()); } return true;}

如此一来,我们所有的shtml文件内容就以一个个字符串的形式保存在了vector数组中。

🔖标签清洗

shtml文件中包含了关于整个网页的内容,但是我们只需要三部分内容:文档标题、文档摘要、文档URL。所以下一步,我们要从shtml源文件中提取出这三要素作为一个数据单元进行存储,这个步骤就是标签清洗的过程。

①文档标题

在html中,定义网页的标题,但是在官网公告中</p> <p><img alt="" height="339" src="https://i-blog.csdnimg.cn/direct/173a4fe6800d4469aa4494684cfc80ee.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1255" /></p> <p>将“杭州师范大学教务处”作为网页的标题,而并不是公告的标题,所以我们需要找到公告标题对应的标签是什么。在网页中,我们选中公告标题,选择网页检查,就可以看到源码中的位置:</p> <p><img alt="" height="365" src="https://i-blog.csdnimg.cn/direct/178ec1a8438d4a308c28c32ba078935c.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1290" /></p> <p>原来标题被定义为了</p> <h1>标签,也就是一级标题,所以我们在源文件中,只需要定位到</p> <h1>标签就可以找到文档标题了。</p> <p><strong><span>②文档正文</span></strong></p> <p>文档正文内容位于标签</p> <div>下,我们同样定位到该标签处,将后续标签清洗,保留正文部分。</p> <p><img alt="" height="486" src="https://i-blog.csdnimg.cn/direct/934fbf635db84cb59b7c935076c4f634.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1475" /></p> <p>标签清洗的核心逻辑是:在遍历 HTML 内容时,忽略掉所有被 <code><</code> 和 <code>></code> 包围的部分(即标签),而保留未被 <code><</code> 和 <code>></code> 包围的部分(即正文内容)。为了实现这一逻辑,我们可以通过定义一个状态机来实现。</p> <p>状态机的设计如下:</p> <p>① <strong>初始状态为 <code>TAG</code></strong>,因为遍历通常从 <code><</code> 开始;</p> <p>② 当遇到 <code>></code> 时,状态<strong>从 <code>TAG</code> 切换到 <code>CONTENT</code></strong>,表示接下来是正文部分;</p> <p>③ 当再次遇到 <code><</code> 时,状态<strong>从 <code>CONTENT</code> 切换回 <code>TAG</code></strong>,表示接下来的内容是标签;</p> <p>④ 重复上述过程,直到遍历完整个 HTML 内容。</p> <pre><code class="language-cpp">static bool ParseContent(const string &file, string *content){ size_t begin = file.find(\"<div>\"); if(begin == string::npos){ return false; } begin += string(\"<div>\").size(); size_t end = file.find(\"<div>\"); if(end == string::npos){ return false; } // 使用状态机,去除标签 enum status{ LABLE, CONTENT }; enum status s = LABLE; for(size_t i = begin; i < end; ++i){ // cout << \"curent status: \" << s <\'){ s = CONTENT; } break; case CONTENT: if(file[i] == \'push_back(tmp); } break; default: break; } } return true;}</code></pre> <p><span><strong>③文档URL</strong></span></p> <p>在 HTML 源码中,通常不会直接包含网页的完整 URL 信息,因此我们需要通过其他方式推断出 URL。<span>网页在网站中的存储通常遵循一定的路径规则</span>。以教务处官网为例,所有公告网页都存储在 <code>/c</code> 目录下。当我们使用 <code>wget</code> 工具将这些网页下载到本地时,文件的路径结构与官网保持一致,即在本地也保留了 <code>/c</code> 目录下的相对路径。基于这一特性,我们可以通过以下步骤获取网页的完整 URL:即<span>将官网的基础路径与本地文件的相对路径拼接,就得到了完整的URL</span>。</p> <p><img alt="" height="275" src="https://i-blog.csdnimg.cn/direct/5ee3efcbdb334c34ad0ad6688628e879.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="896" /></p> <h5 id="%F0%9F%94%96%E4%BF%9D%E5%AD%98%E4%BF%A1%E6%81%AF">🔖保存信息</h5> <p>我们将提取出的<strong>文档标题</strong>、<strong>文档摘要</strong>和<strong>文档URL</strong>这三个关键信息存储在一个 <code>DocInfo</code> 结构体中,作为基本的数据单元,然后将这些数据单元按行写入到一个文本文件( <code>raw.txt</code>)中,其中每个数据单元内部的字段之间用特殊分隔符(如 <code>\\3</code>)分隔,不同数据单元之间用换行符 <code>\\n</code> 分隔。这个 <code>raw.txt</code> 文件不仅实现了<span>数据的持久化存储</span>,还为后续索引构建和搜索功能提供<span>基础数据源</span>。</p> <p><strong>下面是构建原生数据库的核心代码片段:</strong></p> <pre><code class="language-cpp">const string src_path = \"data/source_html/test\";const string output = \"data/raw_data/test.txt\";typedef struct DocInfo{ string title; // 文档标题 string content; // 文档的内容 string url; // 文档的url}DocInfo_t;// const & 输入// * 输出// & 输入输出bool Enumfile (const string &src_path, vector *files_list);bool ParseHtml (const vector &files_list, vector *results);bool SaveHtml (const vector &results, const string &output);int main(){ // 1.遍历指定目录,将html文件汇总在列表里 vector files_list; if(!Enumfile(src_path, &files_list)) { cerr << \"Enum file name error!\" << endl; return 1; } // 2.将列表中的每个文件进行解析,提取关键数据 vector results; if(!ParseHtml(files_list, &results)) { cerr << \"Parse html error!\" << endl; return 2; } // 3.将解析后的数据保存到指定文件中 if(!SaveHtml(results, output)) { cerr << \"Save html error!\" << endl; return 3; } return 0;}</code></pre> <h4 id="%F0%9F%93%962.%E6%9E%84%E5%BB%BA%E7%B4%A2%E5%BC%95">📖2.构建索引</h4> <p>在数据库建立完成后,我们可以编写程序处理搜索关键字并返回相关内容:首先<span>接收用户输入的关键字,然后在数据库中检索 <code>title</code> 和 <code>content</code> 字段包含该关键字的文档</span>;由于一个关键字可能匹配多个文档,而数据库未对结果排序,我们需要将这些文档提取到 <code>vector</code> 容器中,按相关性或其他规则进行排序,最后将排序后的文档作为搜索结果返回给用户。</p> <p>所以第一步我们需要建立的是通过文档ID索引文档信息的<span>正排索引</span>。</p> <h5 id="%F0%9F%94%96%E6%AD%A3%E6%8E%92%E7%B4%A2%E5%BC%95">🔖正排索引</h5> <pre><code class="language-cpp"> // 正排索引数据节点 struct DocInfo{ string title; // 文档标题 string content; // 文档去标签后的内容 string url; // 官网的网址 string time; // 文档时间 uint64_t doc_id; // 文档ID }; // 正排索引通过数组实现,下标天然为文档ID vector forward_index;</code></pre> <p>构建正排索引的过程是<strong>通过文档 ID 索引文档信息</strong>。在 <code>vector</code> 容器中,下标天然可以作为文档 ID,而文档信息结构包括【title、content、URL、ID】。我们可以使用 <code>std::ifstream</code> 创建一个读取流,将文档内容写入流中,并通过 <code>std::getline</code> 方法循环读取,每次读取的恰好是一个文档(文档之间用 <code>\\n</code> 分隔)。</p> <p>读取到的文档是一个字符串,信息段之间用 <code>\\3</code> 分隔,因此需要对字符串进行分割。我们可以手动编写分割代码(遍历字符串,遇到 <code>\\3</code> 时分割),也可以使用<strong> <code>boost::split</code> </strong>方法,它能够根据指定字符分割字符串,并将结果存储到 <code>vector</code> 数组中。分割完成后,将数据段组合成 <code>DocInfo</code> 结构,并存储到正排索引的 <code>vector</code> 容器中。</p> <p><strong>下面是构建正排索引的代码实现:</strong></p> <pre><code class="language-cpp"> // 创建正排索引 DocInfo *BuildForwardInfo(const string &line) { // 1.对字符串进行切分:title、content、url vector results; const string sep = \"\\3\"; ns_util::StringUtil::CutString(line, &results, sep); if(results.size() < 3){ return nullptr; } // 2.字符串填充到DocInfo结构中 DocInfo doc; doc.title = results[0]; doc.content = results[1]; doc.url = results[2]; doc.doc_id = forward_index.size(); // 从URL中提取时间 doc.time = ExtractTimeFromUrl(doc.url); // 3.插入到正排索引的vector中 forward_index.push_back(move(doc)); return &forward_index.back(); }</code></pre> <h5 id="%F0%9F%94%96%E5%80%92%E6%8E%92%E7%B4%A2%E5%BC%95">🔖倒排索引</h5> <pre><code class="language-cpp"> // 倒排索引数据节点 struct InvertedElem{ uint64_t doc_id; // 文档ID string word; // 关键字 int weight; // 关键字的权重 }; // 倒排索引通过键值对实现,一个关键字映射一个/多个倒排拉链结构 unordered_map inverted_index;</code></pre> <p>为了通过关键字获取文档信息,我们需要构建倒排索引。倒排索引是一种映射关系,通过关键字映射到文档信息,文档信息结构包括【ID、word关键字、weight权重】。其中,<span>文档 ID 用于索引正排容器以获取更详细的文档信息,而 weight 权重则用于文档的排序</span>。由于关键字在每个文档中出现的频率不同,我们需要将检索到的文档按照关键字出现频率<strong>从高到低排列返回</strong>。</p> <p>因此,我们需要构建倒排索引结构,这就要求我们将所有关键字列举出来。关键字是从文档的 <code>title</code> 和 <code>content</code> 中提取的,因此<span>在构建每一个正排索引时,可以同时构建该文档的所有关键字的倒排索引</span>。那么,关键字的提取规则是什么呢?</p> <p>我们可以使用<strong> <code>cppjieba</code> 分词工具</strong>,其中的 <strong><code>jieba::for_search</code> </strong>方法专门用于搜索关键字的分词。由于关键字在 <code>title</code> 和 <code>content</code> 中出现的权重不同,我们需要定义两个 <code>vector</code> 容器,分别存储 <code>title</code> 和 <code>content</code> 的关键字分词结果,并分别统计关键字出现的次数。最后,按照特定算法计算关键字的权重,从而完成倒排索引的构建。</p> <p><strong>下面是构建倒排索引的代码实现:</strong></p> <pre><code class="language-cpp"> // 创建倒排索引 bool BuildInvertedIndex(const DocInfo &doc) { struct word_cnt{ int title_cnt; int content_cnt; word_cnt(): title_cnt(0), content_cnt(0) {}; }; unordered_map word_map; // 对标题进行分词 vector title_words; ns_util::JiebaUtil::CutString(doc.title, &title_words); // 统计标题中关键字的频次 for(auto &it : title_words){ word_map[it].title_cnt++; } // 对正文进行分词 vector content_words; ns_util::JiebaUtil::CutString(doc.content, &content_words); // 统计正文中关键字的频次 for(auto &it : content_words){ word_map[it].content_cnt++; } constexpr int X = 10; // 定义常量 X constexpr int Y = 1; // 定义常量 Y // 统计关键字及其权重,插入InvertedList倒排拉链中 for(auto &it : word_map) { InvertedElem word_elem; word_elem.doc_id = doc.doc_id; // 文档ID word_elem.word = it.first; // 关键字 word_elem.weight = it.second.title_cnt * X + it.second.content_cnt * Y; // 计算权重(简易版) InvertedList &inverted_list = inverted_index[it.first]; inverted_list.push_back(move(word_elem)); } return true; }</code></pre> <p>因此,构建索引的步骤如下:<span>循环读取数据库,提取每个文档并构建对应的正排索引,然后根据正排索引中的 <code>title</code> 和 <code>content</code> 提取全部关键字,构建倒排索引</span>。</p> <p>至此,我们已经可以正式编写执行查询流程的程序了。 </p> <h4 id="%F0%9F%93%963.%20%E7%BC%96%E5%86%99%E6%9F%A5%E8%AF%A2%E7%A8%8B%E5%BA%8F">📖3. 编写查询程序</h4> <h5 id="%F0%9F%94%96%E5%AF%B9%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E5%AD%97%E5%88%86%E8%AF%8D">🔖对搜索关键字分词</h5> <p><span>用户输入的关键字并不能直接用于索引搜索,而是需要先进行分词处理</span>。我们可以使用 <code>jieba</code> 分词工具对关键字进行切分,然后将分词结果放入倒排索引中进行检索。</p> <p>这里存在一个问题:同一个文档可能会被多次返回。例如,文档内容为“小明来了北京”,用户搜索的关键字也是“小明来了北京”,分词结果为“小明/来了/北京”,这三个词可能分别检索到同一个文档。如果不对这种情况进行去重处理,搜索结果中就会出现重复的文档。</p> <p>我们通过 <code>JiebaUtil::CutString</code> 方法对 <code>query</code> 进行分词,并将分词结果存储在 <code>words</code> 中:</p> <pre><code class="language-cpp"> class StringUtil{ public: static void CutString(const string &target, vector *out, const string &sep) { boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on); // 压缩重复字符 } };</code></pre> <h5 id="%F0%9F%94%96%E5%AF%B9%E6%A3%80%E7%B4%A2%E7%BB%93%E6%9E%9C%E8%BF%9B%E8%A1%8C%E5%8E%BB%E9%87%8D">🔖对检索结果进行去重</h5> <p>为了解决重复文档的问题,我们使用哈希表 <code>inverted_map</code>,通过文档 ID 映射倒排索引的方式完成去重操作。对于检索到的重复文档,将其权重累加起来。<span>由于这些文档是由不同关键字检索到的,还需要将这些关键字保存起来</span>。为此,我们定义了 <code>InvertedElemPrint</code> 结构,用于存储文档 ID、关键字列表和权重。</p> <p>我们遍历每个分词结果,从倒排索引中获取相关文档,并将其合并到 <code>inverted_map</code> 中:</p> <pre><code class="language-cpp">unordered_map inverted_map;for(string word : words) { boost::to_lower(word); ns_index::InvertedList *word_list = index->GetInvertedIndex(word); if(nullptr == word_list) continue; for(const auto &elem: *word_list) { auto &item = inverted_map[elem.doc_id]; item.doc_id = elem.doc_id; item.words.push_back(elem.word); item.weight += elem.weight; }}</code></pre> <h5 id="%F0%9F%94%96%E5%AF%B9%E6%A3%80%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%8E%92%E5%BA%8F">🔖对检索结果排序</h5> <p>在得到去重后的倒排索引集合后,需要按照权重 <code>weight</code> 对结果进行降序排列。我们使用 <code>std::sort</code> 函数实现这一排序操作,确保最相关的文档排在前面。</p> <p>我们将 <code>inverted_map</code> 中的数据移动到 <code>gather</code> 中,<strong>并按权重排序</strong>:</p> <pre><code class="language-cpp">vector gather;for(const auto &item : inverted_map) { gather.push_back(move(item.second));}sort(gather.begin(), gather.end(), [](const InvertedElemPrint &e1, const InvertedElemPrint &e2) { return e1.weight > e2.weight;});</code></pre> <h5 id="%F0%9F%94%96%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E6%91%98%E8%A6%81%E7%94%9F%E6%88%90">🔖序列化与摘要生成</h5> <p>整理后的索引结果<span>无法直接通过网络传输,需要以序列化和反序列化的方式进行处理</span>。我们使用 <code>Json</code> 作为通用的序列化工具,构建 <code>json</code> 字符串返回给用户。但还有一个问题:索引中提取的是文档的全部正文内容,如果直接将全部内容返回并显示在用户的搜索界面上,显然不够友好。因此,我们需要对正文部分<strong>生成摘要</strong>,便于用户快速了解文档内容并决定是否跳转查看详情。</p> <p>摘要内容最好包含用户搜索的关键字。我们通过 <code>GetDigest</code> 方法生成摘要:在 <code>content</code> 中查找第一个关键字的位置,然后取关键字前 50 字节和后 100 字节作为摘要内容。</p> <pre><code class="language-cpp">string GetDigest(const string &content, const string &key) { const int prev_chars = 50; const int next_chars = 100; auto itea = search(content.begin(), content.end(), key.begin(), key.end(), [](char a, char b) { return (tolower(a) == tolower(b)); }); if(content.end() == itea) return \"未找到关键词\"; // 计算字符位置并生成摘要 string utf8_content = content; vector char_positions; size_t byte_pos = 0; while (byte_pos < utf8_content.size()) { char_positions.push_back(byte_pos); unsigned char c = utf8_content[byte_pos]; if (c < 0x80) byte_pos += 1; else if (c < 0xE0) byte_pos += 2; else if (c < 0xF0) byte_pos += 3; else byte_pos += 4; } int char_pos = distance(content.begin(), itea); int char_index = 0; for (size_t i = 0; i = (size_t)char_pos) { char_index = i; break; } } int start_char = max(0, char_index - prev_chars); int end_char = min((int)char_positions.size() - 1, char_index + next_chars); size_t start_byte = char_positions[start_char]; size_t end_byte = (end_char + 1 0); bool has_more_at_end = (end_char < (int)char_positions.size() - 1); if (has_more_at_start) digest = \"...\" + digest; if (has_more_at_end) digest = digest + \"...\"; return digest;}</code></pre> <h5 id="%F0%9F%94%96%E6%9E%84%E5%BB%BAJSON%E7%BB%93%E6%9E%9C">🔖构建JSON结果</h5> <p>最后,我们将排序后的结果构建为 <code>json</code> 字符串返回给用户。</p> <pre><code class="language-cpp">void BuildJsonResult(const vector &gather, string *json_string) { Json::Value root; for(auto &item : gather) { ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id); if(nullptr == doc) continue; Json::Value elem; elem[\"title\"] = doc->title; elem[\"digest\"] = GetDigest(doc->content, item.words[0]); elem[\"url\"] = doc->url; elem[\"id\"] = doc->doc_id; elem[\"weight\"] = item.weight; elem[\"time\"] = doc->time; root.append(elem); } Json::StyledWriter writer; *json_string = writer.write(root);}</code></pre> <h4 id="%F0%9F%93%964.%E7%BC%96%E5%86%99%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%BB%E7%A8%8B%E5%BA%8F">📖4.编写服务器主程序</h4> <p>我们使用 <code>httplib</code> 库实现了一个简单的 HTTP 服务器,用于处理用户的搜索请求并返回结果。<code>httplib</code> 是一个轻量级的 C++ HTTP 库,易于集成和使用。 </p> <h5 id="%F0%9F%94%96%E5%88%9D%E5%A7%8B%E5%8C%96%E7%B4%A2%E5%BC%95">🔖初始化索引</h5> <p>在程序启动时,我们首先需要<span>初始化搜索引擎的索引</span>。通过调用 <code>ns_sercher::Sercher</code> 类的 <code>InitIndex</code> 方法,从指定的数据文件 <code>data/raw_data/raw.txt</code> 中加载数据并构建正排索引和倒排索引。</p> <pre><code class="language-cpp">ns_sercher::Sercher serch;serch.InitIndex(input);</code></pre> <h5 id="%F0%9F%94%96%E8%AE%BE%E7%BD%AEHTTP%E6%9C%8D%E5%8A%A1%E5%99%A8">🔖设置HTTP服务器</h5> <p>我们使用<strong> <code>httplib</code> 库</strong>创建一个 HTTP 服务器,并设置服务器的根目录为 <code>./wwwroot</code>。该目录用于存放静态资源文件(如 HTML、CSS、JavaScript 等),供客户端访问。</p> <pre><code class="language-cpp">httplib::Server svr;svr.set_base_dir(root_path.c_str());</code></pre> <h5 id="%F0%9F%94%96%E5%A4%84%E7%90%86%E6%90%9C%E7%B4%A2%E8%AF%B7%E6%B1%82">🔖处理搜索请求</h5> <p>我们为服务器定义了一个 <code>/search</code> 路由,用于处理用户的搜索请求。该路由通过 <code>GET</code> 方法接收用户输入的关键字,并根据请求参数执行不同的搜索逻辑。</p> <p>根据请求参数 <code>time_priority</code> 的值,决定是<span>按时间排序还是按权重排序</span>:</p> <pre><code class="language-cpp">if (time_priority){ cout << \"按时间排序\" << endl; serch.TimePrioritySerch(word, &json_string);} else { cout << \"按权重排序\" << endl; serch.CommonSerch(word, &json_string);}</code></pre> <p>如果搜索关键词为空,返回全部文档的时间排序结果(便于浏览最新公告):</p> <pre><code class="language-cpp">// 检查输入是否为空或仅包含空格if (word.empty() || word.find_first_not_of(\' \') == string::npos) { cout << \"返回所有文档信息\" << endl; serch.GetAllDocuments(&json_string); resp.set_content(json_string, \"application/json; charset=utf-8\"); return;}</code></pre> <p>如果搜索关键字不存在,则返回空结果和广告信息:</p> <pre><code class="language-cpp">if (json_string.empty()) { json_string = R\"({\"results\": [], \"ads\": [ {\"text\": \"进入校园官网:\", \"url\": \"https://jwc.hznu.edu.cn/\", \"linkText\": \"杭州师范大学教务处\"}, {\"text\": \"分享学习笔记,记录生活点滴,欢迎访问我的博客:\", \"url\": \"https://kanhai-night.blog.csdn.net\", \"linkText\": \"Kanhai\'s 技术博客\"}, {\"text\": \"本项目已开源:\", \"url\": \"https://gitee.com/HZNUYuwen/Linux_gitee/tree/master/HZNUSercher\", \"linkText\": \"查看项目源码\"} ]})\";}</code></pre> <h3 id="%F0%9F%93%9A%E5%9B%9B%E3%80%81%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0">📚四、前端实现</h3> <p>前端页面实现了搜索功能的核心交互逻辑,包括关键字输入、搜索请求、结果展示和分页浏览。以下是对主要功能的介绍:</p> <h4 id="%F0%9F%93%961.%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84">📖1.页面结构</h4> <p>页面分为以下几个部分:</p> <p><strong>① 搜索框</strong>:用户输入关键字,并选择是否按时间排序。</p> <p><strong>② 搜索结果区域</strong>:动态展示搜索结果的标题、摘要和链接。</p> <p><strong>③ 分页控件</strong>:支持上一页和下一页的翻页操作。</p> <pre><code class="language-html"><div class="container initial-state"> <div class="search"> <div class="search-options"> <label> 按时间先后 </label> </div> <button>搜索一下</button> </div> <div class="result hidden"> <!-- 动态生成网页内容 --> </div> <div class="pagination hidden"> <button>上一页</button> <span id="page-info"></span> <button>下一页</button> </div></div></code></pre> <h4 id="%F0%9F%93%962.%E6%90%9C%E7%B4%A2%E5%8A%9F%E8%83%BD">📖2.搜索功能</h4> <p>通过 <code>Search</code> 函数发起搜索请求,将用户输入的关键字发送到后端,并动态更新搜索结果:</p> <pre><code class="language-javascript">function Search() { currentQuery = $(\".container .search input\").val().trim(); // 获取关键字 let timePriority = $(\"#time-priority\").is(\":checked\"); // 是否按时间排序 $.ajax({ type: \"GET\", url: \"/search?word=\" + currentQuery + \"&time_priority=\" + timePriority, success: function(data) { searchResults = data; // 保存搜索结果 currentPage = 0; // 重置页码 BuildHtml(); // 渲染结果 setResultState(); // 切换到结果状态 } });}</code></pre> <h4 id="%F0%9F%93%963.%E7%BB%93%E6%9E%9C%E5%B1%95%E7%A4%BA">📖3.结果展示</h4> <p>通过 <code>BuildHtml</code> 函数动态生成搜索结果,并支持关键字高亮显示:</p> <pre><code class="language-javascript">function BuildHtml() { let result_lable = $(\".container .result\"); result_lable.empty(); // 清空之前的结果 let start = currentPage * resultsPerPage; let end = start + resultsPerPage; let pageResults = searchResults.slice(start, end); // 获取当前页结果 for (let elem of pageResults) { let highlightedTitle = highlightKeyword(elem.title, currentQuery); // 高亮标题 let highlightedDigest = highlightKeyword(elem.digest, currentQuery); // 高亮摘要 result_lable.append(` <div class="item"> <a href="${elem.url}" target="_blank">${highlightedTitle}</a> <p>${highlightedDigest}</p> <i>${elem.url}</i> <span style="color: #888;font-size: 12px;margin-top: 5px"> ${elem.time ? \"发布时间: \" + elem.time : \"\"} </span> </div> `); }}</code></pre> <h4 id="%F0%9F%93%964.%E5%88%86%E9%A1%B5%E5%8A%9F%E8%83%BD">📖4.分页功能</h4> <p>通过 <code>prevPage</code> 和 <code>nextPage</code> 函数实现分页浏览:</p> <pre><code class="language-javascript">function prevPage() { if (currentPage > 0) { currentPage--; BuildHtml(); // 更新结果 }}function nextPage() { if ((currentPage + 1) * resultsPerPage < searchResults.length) { currentPage++; BuildHtml(); // 更新结果 }}</code></pre> <h3 id="%F0%9F%93%9A%E4%BA%94%E3%80%81%E5%AE%8C%E6%95%B4%E6%BC%94%E7%A4%BA">📚五、完整演示</h3> <p>下面是<strong>《校园公告搜索引擎》</strong>项目功能的完整演示:</p> <h4 id="%F0%9F%93%961.%E5%88%9D%E5%A7%8B%E7%95%8C%E9%9D%A2">📖1.初始界面</h4> <p><img alt="" height="930" src="https://i-blog.csdnimg.cn/direct/455f1718fddd4d9abd9b4c0250475d60.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1919" /></p> <h4 id="%F0%9F%93%962.%E6%97%A0%E5%85%B3%E9%94%AE%E5%AD%97%E8%BE%93%E5%85%A5">📖2.无关键字输入</h4> <p>当无关键字输入时,返回用户的结果是经过时间排序的全部文档内容</p> <p><img alt="" height="934" src="https://i-blog.csdnimg.cn/direct/6257a71f4111442abec60358db4b57e6.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1919" /></p> <h4 id="%F0%9F%93%963.%E8%BE%93%E5%85%A5%E5%85%B3%E9%94%AE%E5%AD%97%EF%BC%88%E6%97%A0%E8%BF%94%E5%9B%9E%EF%BC%89">📖3.输入关键字(无返回)</h4> <h4 id="%E2%80%8B%E7%BC%96%E8%BE%91"><img alt="" height="934" src="https://i-blog.csdnimg.cn/direct/7372ab2e4bbd49c5a68ee33838b54de6.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1919" /></h4> <h4 id="%F0%9F%93%964.%E8%BE%93%E5%85%A5%E5%85%B3%E9%94%AE%E5%AD%97%EF%BC%88%E6%9C%89%E8%BF%94%E5%9B%9E%EF%BC%89%C2%A0">📖4.输入关键字(有返回) </h4> <h5 id="%F0%9F%94%96%E9%BB%98%E8%AE%A4%E6%8E%92%E5%BA%8F%C2%A0%E2%80%8B%E7%BC%96%E8%BE%91">🔖默认排序 <img alt="" height="935" src="https://i-blog.csdnimg.cn/direct/696a17bbcf35415e9f84623c1513bead.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1919" /></h5> <h5 id="%F0%9F%94%96%E6%8C%89%E6%97%B6%E9%97%B4%E6%8E%92%E5%BA%8F">🔖按时间排序</h5> <p><img alt="" height="934" src="https://i-blog.csdnimg.cn/direct/4c7978e22e154028b0b6b9e536dc2681.png" alt="【C++项目实战】校园公告搜索引擎:完整实现与优化指南_基于linux平台,利用c++开发一个搜索引擎" width="1919" /></p> <h3 id="%F0%9F%93%9A%E5%85%AD%E3%80%81%E6%80%BB%E7%BB%93">📚六、总结</h3> <p>在这里就不对项目本身过多赘述了,下面说一下项目的不足与优化方向:</p> <h4 id="%F0%9F%93%961.%E4%BC%98%E5%8C%96%E6%96%B9%E5%90%91">📖1.优化方向</h4> <p><strong>① 在线更新:</strong>目前项目尚未实现在线更新功能,获取的官网公告数据截至<strong> </strong>2025年3月14日,最新的官网公告未能同步到搜索引擎。未来可以引入定时任务或实时爬虫机制,确保数据及时更新。</p> <p><strong>② 热词统计:</strong>在搜索时,如果能智能显示热门搜索关键词,可以进一步提升用户体验。</p> <p><strong>③ 登录系统:</strong>由于该搜索引擎主要服务于本校师生,可以增加登录认证功能。</p> <p><strong>④ 响应速度:</strong>目前服务器的响应速度还有提升空间。可以通过优化索引结构、引入缓存机制等。</p> <p><strong>⑤ 扩大搜索范围:</strong>除了教务处官网,未来可以引入其他学校官网(如学院、图书馆、招生办等)的数据作为搜索对象。</p> <p><strong>⑥ 引入域名:</strong>目前项目通过 IP 地址和端口号访问服务器,这种方式不够直观且不利于记忆。</p> <h4 id="%F0%9F%93%962.%E6%BA%90%E7%A0%81%E5%8F%8A%E7%BD%91%E5%9D%80">📖2.源码及网址</h4> <p>这里给出项目源码以及访问网址:</p> <p>项目源码</p> <p>校园公告搜索引擎</p> <hr /> <p>以上就是【校园公告搜索引擎】的全部内容,欢迎指正~ </p> <p><strong>码文不易,还请多多关注支持,这是我持续创作的最大动力!</strong> </p> </div> <div class="clear"></div> <div class="article_tags"> <div class="tagcloud"> 网络标签:<a href="http://www.csdndoc.com/tag/gjz-2" rel="tag">关键字</a> <a href="http://www.csdndoc.com/tag/wd" rel="tag">文档</a> <a href="http://www.csdndoc.com/tag/sy" rel="tag">索引</a> </div> </div> </div> </div> <div> <ul class="post-navigation row"> <div class="post-previous twofifth"> 上一篇 <br> <a href="http://www.csdndoc.com/thread/8073.html" rel="prev">其实拼多多校招面试真的很水_拼多多秋招的猫腻</a> </div> <div class="post-next twofifth"> 下一篇 <br> <a href="http://www.csdndoc.com/thread/8075.html" rel="next">Git常用命令大全</a> </div> </ul> </div> <div class="article_container row box article_related"> <div class="related"> <div class="newrelated"> <h2>相关问题</h2> <ul> <li><a href="http://www.pcgg.com.cn/jw/55536.html">剑网3哪些职业吃香蕉</a></li> <li><a href="http://www.pcgg.com.cn/gl/4960.html">原神今昔剧画之恶尉怎么获得</a></li> <li><a href="http://www.pcgg.com.cn/gpqq/12280.html">和平精英电脑版如何下载破解(和平精英电脑版下载教程)</a></li> <li><a href="http://www.pcgg.com.cn/gpqq/11661.html">腾讯先锋和平精英如何隐藏鼠标</a></li> <li><a href="http://www.pcgg.com.cn/lol/26669.html">英雄联盟怎么调声音怎么回事</a></li> <li><a href="http://www.pcgg.com.cn/gpqq/12930.html">绝地求生刺激战场是不是改名叫和平精英了</a></li> <li><a href="http://www.pcgg.com.cn/lol/13611.html">英雄联盟为什么不能玩匹配</a></li> <li><a href="http://www.pcgg.com.cn/cj/39738.html">麻城快递分拣员过年放假吗</a></li> <li><a href="http://www.pcgg.com.cn/lol/30160.html">英雄联盟小丑皮肤哪个好看</a></li> <li><a href="http://www.pcgg.com.cn/xjzb/56404.html">星际争霸2策略程度怎么调</a></li> </ul> </div> </div> </div> <div class="clear"></div> <div id="comments_box"> </div> </div> <div id="sidebar"> <div id="sidebar-follow"> <div class="search box row"> <div class="search_site"> <form id="searchform" method="get" action="http://www.csdndoc.com/index.php"> <button type="submit" value="" id="searchsubmit" class="button"><i class="fasearch">☚</i></button> <label><input type="text" class="search-s" name="s" x-webkit-speech="" placeholder="请输入搜索内容"></label> </form></div></div> <div class="widget_text widget box row widget_custom_html"><h3>公告</h3><div class="textwidget custom-html-widget"><a target="_blank" href="http://www.5d.ink/deepseek/?d=DeepseekR1_local.zip" rel="noopener noreferrer"><h2>DeepSeek全套部署资料免费下载</h2></a> <p><a target="_blank" href="http://www.5d.ink/deepseek/?d=DeepseekR1_local.zip" rel="noopener noreferrer"><img src="http://css.5d.ink/img/deep.png" alt="DeepSeekR1本地部署部署资料免费下载"></a></p><br /><br /> <a target="_blank" href="http://www.5d.ink/freefonts/?d=FreeFontsdown.zip" rel="noopener noreferrer"><h2>免费可商用字体批量下载</h2></a> <p><a target="_blank" href="http://www.5d.ink/freefonts/?d=FreeFontsdown.zip" rel="noopener noreferrer"><img src="http://css.5d.ink/img/freefont.png" alt="免费可商用字体下载"></a></p></div></div> <div class="widget box row"> <div id="tab-title"> <div class="tab"> <ul id="tabnav"> <li class="selected">猜你想看的文章</li> </ul> </div> <div class="clear"></div> </div> <div id="tab-content"> <ul> <li><a href="http://www.pcgg.com.cn/lol/22456.html">lol零胜点会不会掉段</a></li> <li><a href="http://www.pcgg.com.cn/lol/33159.html">英雄联盟fps是什么意思</a></li> <li><a href="http://www.pcgg.com.cn/gpqq/8070.html">如何获得和平精英体验服资格(如何获得和平精英体验服资格申请)</a></li> <li><a href="http://www.pcgg.com.cn/lol/28949.html">lol剑魔赛季皮肤怎么得</a></li> <li><a href="http://www.pcgg.com.cn/gpqq/8284.html">Ios和平精英老是闪退</a></li> <li><a href="http://www.pcgg.com.cn/lol/23899.html">lol有什么好的接单软件</a></li> <li><a href="http://www.pcgg.com.cn/gl/5166.html">原神保底是所有池子加起来吗</a></li> <li><a href="http://www.pcgg.com.cn/lol/13475.html">英雄联盟动物英雄有哪些</a></li> <li><a href="http://www.pcgg.com.cn/lol/21468.html">lol怎么设置视野随英雄走</a></li> <li><a href="http://www.pcgg.com.cn/lol/24755.html">lol怎么经常崩溃</a></li> </ul> </div> </div> </div> </div> </div> </div> <div class="clear"></div> <div id="footer"> <div class="container"> <div class="twothird"> </div> </div> <div class="container"> <div class="twothird"> <div class="copyright"> <p> Copyright © 2012 - 2025 <a href="http://www.csdndoc.com/"><strong>程序员档案馆</strong></a> Powered by <a href="/lists">网站分类目录</a> | <a href="/top100.php" target="_blank">精选推荐文章</a> | <a href="/sitemap.xml" target="_blank">网站地图</a> | <a href="/post/" target="_blank">疑难解答</a> <a href="https://beian.miit.gov.cn/" rel="external">京ICP备05034492号</a> </p> <p>声明:本站内容来自互联网,如信息有错误可发邮件到f_fb#foxmail.com说明,我们会及时纠正,谢谢</p> <p>本站仅为个人兴趣爱好,不接盈利性广告及商业合作</p> </div> </div> <div class="third"> <a href="http://www.xiaoboy.cn" target="_blank">小男孩</a> </div> </div> </div> <!--gototop--> <div id="tbox"> <a id="home" href="http://www.csdndoc.com" title="返回首页"><i class="fa fa-gohome"></i></a> <a id="pinglun" href="#comments_box" title="前往评论"><i class="fa fa-commenting"></i></a> <a id="gotop" href="javascript:void(0)" title="返回顶部"><i class="fa fa-chevron-up"></i></a> </div> <script src="//css.5d.ink/body5.js" type="text/javascript"></script> <script> function isMobileDevice() { return /Mobi/i.test(navigator.userAgent) || /Android/i.test(navigator.userAgent) || /iPhone|iPad|iPod/i.test(navigator.userAgent) || /Windows Phone/i.test(navigator.userAgent); } // 加载对应的 JavaScript 文件 if (isMobileDevice()) { var script = document.createElement('script'); script.src = '//css.5d.ink/js/menu.js'; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); } </script> <script> $(document).ready(function() { $("#sidebar-follow").pin({ containerSelector: ".main-container", padding: {top:64}, minWidth: 768 }); $(".mainmenu").pin({ containerSelector: ".container", padding: {top:0} }); $(".swipebox").swipebox(); }); </script> </body></html>