Boost搜索引擎项目(详细思路版)

目录

项目相关背景

搜索引擎原理技术栈和项目环境

导入数据到自己的本地

数据去标签与数据清洗模块

[Enumfile(src_path, &file_list)递归式写入](#Enumfile(src_path, &file_list)递归式写入)

[Parsehtml(file_list, &results)去标签](#Parsehtml(file_list, &results)去标签)

[bool Parsetitle(const string& file, string* title)拆分标题](#bool Parsetitle(const string& file, string* title)拆分标题)

[bool Parsecontent(const string& file, string* content)拆分内容](#bool Parsecontent(const string& file, string* content)拆分内容)

[bool Parseurl(const string& file, string* url)](#bool Parseurl(const string& file, string* url))

[bool Savehtml(const vector& results, const string& output)](#bool Savehtml(const vector& results, const string& output))

main函数编写

建立正排/倒排索引模块

正排/倒排索引是什么以及编写思路

储存索引的结构

[Build_index(const string &input)建立索引](#Build_index(const string &input)建立索引)

[Docinfo *ret = Build_forward_index(line)](#Docinfo *ret = Build_forward_index(line))

[bool Build_inverted_index(const Docinfo &doc)](#bool Build_inverted_index(const Docinfo &doc))

设计单例模式

[Docinfo *getforwardindex(uint64_t doc_id)](#Docinfo *getforwardindex(uint64_t doc_id))

[invertedlist_t *getinvertedlist(const string &word)](#invertedlist_t *getinvertedlist(const string &word))

使用搜索引擎返回搜索内容模块

编写思路

[Initsearcher(const string& input)调用建立索引](#Initsearcher(const string& input)调用建立索引)

[void Search(const string& query, string* out)](#void Search(const string& query, string* out))

相同id的去重逻辑

方法1:

方法2:

构建输出json串序列化

[string Getdesc(const string& content, const string& word)截取摘要](#string Getdesc(const string& content, const string& word)截取摘要)

search.cc本地测试模块

http_server模块

编写前端模块

引入日志

项目总结与扩展

分享我遇到的困难


项目相关背景

Boost搜索引擎并非指某个商业化的通用搜索引擎,而是一个专为Boost C++库设计的站内搜索项目。

搜索引擎原理技术栈和项目环境

我们可以先将整个boost库导入到我们当前目录下的某个文件里面,从全网导入可以使用爬虫,也可以去官网下载然后re -E,再tar解包导入。形成我们本次boost的数据源,数据源很混乱而且要查找是需要构建引索的,也就是下标,且所有的数据都在.html文件里面展示也就是我们接着需要将得来的数据源进行去标签,数据清洗,然后建立索引。建立索引的过程分为正排索引和倒排索引。将这些索引链存在内存。等客户端通过浏览器什么的,通过get/post方法访问时拼接多个网页的title+desc(索引链中的内容)构建一个新的网页返还给用户。这样就完成了我们这次小项目的框架和逻辑。

导入数据到自己的本地

首先我们下载最新的1_88_0的库,我推荐你们使用google进行下载,然后将其rz到我们的项目目录里面,这里我的是boost_searcher,然后由于所有的库函数等的数据信息都在boost_searcher/boost_1_88_0/doc/html这个路径下,所以我们需要将里面的所有的文件(.html)都拷贝到我们专门放数据的地方当前目录下的data/input,但是其实里面不是全部都是.html结尾的,可能还有图片啥的,这就是我们需要进行数据清洗的原因,所以在data/里面再新建一个.txt文件用来保存清洗过后的信息,每行代表一条.html结尾的文件。这样数据的准备就完成了!

数据去标签与数据清洗模块

在parser.cc里面进行数据的清洗与去标签,我们需要先清洗再去html里面的标签,在执行前先将数据路径保存体现。

//是一个目录,下面放的是所有的html网页

const string src_path = "data/input";

const string output = "data/raw_html/raw.txt";

我们分三步完成我们的模块,第一步:我们先递归式的把每一个带有.html文件名带路径,保持在一个数组files_list中,方便后期一个一个的文件进行读取,对应函数Enumfile(src_path, &file_list),第二步:按照files_list读取每个文件的内容,并进行解析,对应函数Parsehtml(file_list, &results),第三步:把解析完毕的各个文件内容,写入output,按照\3作为每个文档的分隔符对应函数Savehtml(results, output),我们提取的格式为每个结构体要由标题+正文+url组成如下:

typedef struct Docinfo{

string title; //文档的标题

string content; //文档的正文

string url; //文档在官网的url

}Docinfo_t;

Enumfile(src_path, &file_list)递归式写入

std库里面的ifstream是很难做到只写入.html的且很难做到递归,所以我们使用boost库里面的文件操作,包含头文件#include<boost/filesystem.hpp>。引入命名空间boost::filesystem,我们先使用之前导入的src_path创建一个由src_path初始化完成的path对象,此时这个对象就是src_path,然后定义一个空的迭代器,用来判断递归结束fs::recursive_directory_iterator end; 他能自动帮你深入所有子目录的迭代器,然后初始就是src_path,不断for循环遍历的时候,进行两个判断,is_regular_file判断指向的文件是否为普通文件,以及iter->path().extension()判断后缀是否为.html,然后判断都通过的路径就push_back到file_list里面,当然我推荐使用move进行右值转换因为可以提高效率。提醒一下:需判断文件是否打开。

Parsehtml(file_list, &results)去标签

将file_list里面的数据再存入results数组里面,此时results里面的元素就是之前定义好的去标签的Docinfo_t。首先需要对file_list进行按行遍历,之后使用std的ifstream进行读取html里面的内容,常规的读取操作getline全部读取完存入string result里面,这时候应该html里面的信息(含标签)都在里面了,之后我分别做的三个函数来切分标题,内容,url。得到的值构造Docinfo_t,push_back入results,之后全部html读取完毕,result就是所有的拆分后的boost库内源文件的内容了。

bool Parsetitle(const string& file, string* title)拆分标题

首先我很明确的告诉你,标题只有一个,也就是只有一个<title>......</title>标签,这就好办了,在string file里面使用两次find方法找到两个左<括号的下标位置f1,f2,然后<title>的左<位置向右平移<title>长度为f3,再substr截取f3到f2的距离返回就是标题了。注意判断不成立情况,find返回值需判断,f3的值是否会大于f2也需要判断。这种常规的判断我后面就不一一写出来了,应该成为一种自觉。

bool Parsecontent(const string& file, string* content)拆分内容

要提取内容其实就是在去标签,首先内容不可避免的会把标题也算进去,我们这里认为标题也属于内容,这样简单点,我们先构建一个状态机(enum)就两个状态,LABLE和CONTENT,因为处在string里面的要么是标签要么就是标签以外的内容,所以status s默认初始值为标签状态,因为最开始遍历的时候就是先遇到标签,当遇到>时说明单标签结束或者双标签的左标签结束,剩下的接着就是正文s=CONTENT,当s=CONTENT遇到<时说明这段内容结束了,接着的就是新的标签或者右标签了。什么时候开始push呢,在没有遇到<且s等于正文状态时push仅content。这里注意,当读取到'\n'时,我们将其变成读取空格,以防止之后getline从中再读取分词的影响,getline是不会读取\n的,遇到\n就停下了。

enum status{

LABLE, //标签状态

CONTENT //正文状态

};

bool Parseurl(const string& file, string* url)

提取url就需要结合官网了,官网的所有的html总的文件夹的地址是"https://www.boost.org/doc/libs/1_88_0/doc/html",而我们查找到的对应的.html文件的地址是这个file比如是data/input/xxx.html,而data/input是src_path,所以我们只需要从file中截取从src_path长度下标到最后位置再拼接到官网url就完成了。依次判断以上三个部分的函数之后,将Docinfo_t插入我们实现准备好的容器results里面就完成了构建。

bool Savehtml(const vector<Docinfo_t>& results, const string& output)

将所构建好的results按行写入output,但是写入格式得是string,所以我们整个提取格式作为string写入时也应有办法分辨哪里是title,哪里是url,所以string写入的格式我们用\3作为分隔符,两个string之间使用\n方便getline分开。为什么\3进行分割整合呢,因为再ascll码里面\3属于不可转义字符不会被打印显示处理干扰文本。格式如下:

results[i].title \3 results[i].content \3 results[i].url \n

就这样逐行使用ofstream写入output就完成了,注意ios必须如下编写ios::binary | ios::app。最后记得关上ofstream养成好习惯。

main函数编写

依次对上面三个部分的函数进行if判断即可。

建立正排/倒排索引模块

正排/倒排索引是什么以及编写思路

首先这两个索引都是哈希的结构,正排索引是由一个从0开始的不重复的序号分别映射不同的搜索文档,而倒排索引是由一个唯一关键字作为k值映射所有可以包含其关键字的文档id(内部含有权值),权值就是这个关键字被包含了几次。所以对于倒排索引一个关键字可以映射多个文档id,一个文档id可以被多个关键字映射。对于倒排索引,每个关建字的每个文档是按权重排成倒序的,大的在前面。

也就是说我们对于一个文档需要先建立其的正排索引然后再进行分词,对所有分好的词建立倒排索引,为了保证k值的唯一性,我们使用unordered_map进行完成。并且需要先进行正排索引地道某个文档id,然后再进行倒排索引。我们的index.hpp完成建立索引

储存索引的结构

struct Docinfo {

string title; //文档标题

string content; //文档内容

string url; //文档url

uint64_t doc_id; //文档id //存入方便倒排索引找到id

};

struct Invertedelem {

uint64_t doc_id;

string word; //关键字

int weight; //权重

};

typedef vector<Invertedelem> invertedlist_t;

//正派索引用的是数组,索引值就是下标

vector<Docinfo> forward_index;

//倒排索引使用哈希表映射多个值

unordered_map<string, invertedlist_t> inverted_index;

为什么Invertedelem要体现关键词呢,这个是为了方便调试以及后续的查找工作。

Build_index(const string &input)建立索引

从input就是我们存放全部html解析完后的由\3分割完后的文件 data/raw_html/raw.txt中使用ifstream配合getline读取每一行,然后一行一行的先建立正排索引以及倒排索引。

Docinfo *ret = Build_forward_index(line)

我们根据/3分段line,将其分成3断,分别为title,content,url,然后将其存入Docinfo,然后push_back入forward_index就完事了,要得到最新的插入id可以相当于得到最新插入的整个元素,直接调用.back方法就可以了。这里的难点就是分段,可以使用C语言的strtok,也可以string::find直接写,但是都比较麻烦,我们直接使用boost库的split。

static void Cutstring(const string &target, vector<string> *out, string sep) {

boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);

}

bool Build_inverted_index(const Docinfo &doc)

因为建立倒排索引需要得到权值,所以我们还需要一个结构体统计分出的词在doc.content和doc.title里面的出现次数,这个可能有误差,但不大的。然后整合起来成为一个哈希表。

struct word_cnt //统计在标题和内容里面的词频

{

word_cnt() : title_cnt(0), content_cnt(0) {}

int title_cnt;

int content_cnt;

};

unordered_map<string, word_cnt> word_map; //词频统计

第一步使用cppjieba分词工具,有机会去gitcode平台下载一下,然后调用jieba.Cutstring进行对title和content的分词然后分完的词会自动存入一个数组中,这里注意一下在const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";这个路径有很多需要分词屏蔽的暂停词,暂停词就是is这种连接词以及&这种符号等等,然后在分词时需要去除不然搜索时会有影响。然后进行词频统计存入哈希表word_map,然后算权值,这个权值我们可以自己规定算法,我们权值的计算是使用权值= title_cnt * 10 + content_cnt * 1,因为我们的content是包括了title里面的内容了,所以content_cnt会大一点点。因为全部的分词都存在了 word_map里面,所以我们只需遍历 word_map就可以了,然后逐个存入 invertedlist_t,进而纯如哈希表inverted_index就可以了。这里边遍历其实Invertedelem里面的信息都是清晰的。

因为我们的搜索引擎是不区分大小写的,所以inverted_index的k值需要先进行小写转化再存入,确保大写关键字和小写的关键字匹配插入到同一个k值,这样查找时都用小写的就可以连同大写的一起查到了。对于小写转化我们可以使用boost::tolower就地转换,也可以使用std::transform 。

std::transform(ret.begin(), ret.end(), ret.begin(),

\](unsigned char c) { return std::tolower(c); });

设计单例模式

这里主要是我们不希望正排/倒排拉链,以及切词组合的vector是每人一份的,所以都将这两个类设计成单例模式。注意我们设计的单例模式是需要加锁的,并且我们使用if双循环判断更保险。

class Index {

private:

Index() {}

Index(const Index&) = delete;

Index& operator=(const Index&) = delete;

static Index* instance;

static std::mutex mtx;

public:

~Index() {}

//构建单例

static Index* Getinstance()

{

//双层判断进行二次分流

if (nullptr == instance)

{

mtx.lock();

if (nullptr == instance)

{

instance = new Index();

}

mtx.unlock();

}

return instance;

}
class Jiebautil {

private:

cppjieba::Jieba jieba;

unordered_map<string, bool> stop_words;

Jiebautil():jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH,

STOP_WORD_PATH)

{}

Jiebautil(const Jiebautil& ) = delete;

Jiebautil& operator=(const Jiebautil& ) = delete;

static Jiebautil* instance;

public:

static Jiebautil* get_instance()

{

static std::mutex mtx;

if (instance == nullptr)

{

mtx.lock();

if (instance == nullptr)

{

instance = new Jiebautil();

instance->initjiebautil();

}

mtx.unlock();

}

return instance;

}

接着为了查找,我们还需要编写根据id查找正排和根据关键字查找倒排的函数。

Docinfo *getforwardindex(uint64_t doc_id)

正常的返回unordered_map的v值很简单。

invertedlist_t *getinvertedlist(const string &word)

先调用find方法,看是否查找得到,然后查找并返回返回值的second就可以了,注意这个second值是个vector,别忘了。

使用搜索引擎返回搜索内容模块

编写思路

搜索模块我们就分为初始化或者说是在通过初始化调用建立索引,以及获取用户输入的string转成小写后的search函数。最后search得到的结果写成json形式序列化返回就可以了,3个部分。

Initsearcher(const string& input)调用建立索引

将Index* index;设置入searcher类中,建立索引时只需要index = Index::Getinstance();创建单例对象,然后调用index的Build_index(input)方法,此时倒排索引就在searcher类中了。

void Search(const string& query, string* out)

我们传入的用户搜索词是query对吧,这个可能是短语可能是单词,短语在匹配时就等价于匹配多个词,然后将多个词的结果整合,排序,再去重,因为多个词可能存在映射一个文档的情况。

也就是说我们需要先对query进行分词,统计分词结果,使用jieba::Cutstring,存在vector::words里面,然后遍历words,接着运用我们前面getinvertedlist方法得到每个词的倒排拉链,这里要判断是否存在(nullptr),如果存在再另起一个vector装入全部的结果,注意这里另起的装倒排拉链的vector不设计成二维的,我们仅仅尾插倒排拉链,所以只是拉长的整个一维数组。使用insert尾插。

inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
std::vector::insert 是 C++ 标准库 <vector> 提供的成员函数,用来在任意位置 插入一个或多个元素。由于 vector 的底层是一段连续内存 ,插入点之后的所有元素都必须向后搬移,因此平均复杂度为 O(n),在尾部插入才是 O(1)。

在inverted_list_all.end()位置插入inverted_list->begin()到inverted_list->end()区间长度的数据。但是相同的id不能留下超过一个呀,而且相同id的文档属于重匹配,权重得相加呀,怎么办。

相同id的去重逻辑

方法1:

我们可以再创建一个struct Invertedelem,以及对应的新的 unordered_map<string, invertedlist_t> inverted_index,只是这时对应一个id是多个关键字(数组什么的)了,然后遍历整个之前插入好的全部的vector,把id相同的word存入新的 Invertedelem,权值在这个时候相加,多少个加入就加对应的值就可以了。就看成一个id映射多个关键字的问题,哈希表前面的关键字的映射就被打破了,此时不是很重要了,多余的映射被剪掉了。

方法2:

这种方法不需要改动原本的结构,仅仅只是在后面编写json串,以json串的形式发送时进行去重和加权值。我们如下采用两个哈希表进行去重, Id负责去掉重复的元素,让相同的id所对应的内容不插入,Id_weight负责统计相同id累加的权值,先运行Id_weight,这样最后编写json串时权值到Id_weight里面去取就可以了。

///读取倒排拉链去重逻辑///

unordered_map<int, vector<int>> Id_weight;

unordered_map<int, int> Id;

不知道这样讲你们能不能听懂。

如果是采用了第一种方法进行先去重再构建json格式返回,就还需要对整个倒排拉链进行sort自定义weight排序。

sort(inverted_list_all.begin(), inverted_list_all.end(), [](const Invertedelem& s1, const Invertedelem& s2){

return s1.weight > s2.weight; //不要加上=

});

接着构建json串

构建输出json串序列化

在Linux上下载完json的库后就可以使用了,包含一下头文件

#include <json/json.h>

#include <json/value.h>

#include <json/writer.h>

构建Json::Value root的哈希结构(可以自己指定k值),作为总的json容器,内部装的也是Json::Value,我们这里叫做elem,然后遍历总的倒排拉链,通过id调用index->getforwardindex(e.doc_id)获得正排拉链,然后元素都存在且找得到的做如下插入即可:

elem["title"] = doc->title;

elem["desc"] = Getdesc(doc->content, e.word);//我们只要content的一部分作为摘要

elem["url"] = doc->url;

elem["weight"] = e.weight(或者weight的总数ret)

root.append(elem);

插入时content不能插入全部内容,作为网页展示应为content的部分内容,也就是要获取摘要。在这个部分获取摘要不是主要问题,如果去重做法采用的是如上方法1:那此时root里面就是有序的,直接如下write写入即可。

cpp 复制代码
Json::FastWriter write;  //提供打印出来的类对象
*out = write.write(root);  //调用write方法进行打印, 打印格式是有个string

这里为了调试好看可以使用Json::StyledWriter,使用Json::FastWriter打印出来比较干脆,没有那么杂。

如果你使用的是如上方法2进行的去重,那这时root里面就是乱序的,也就是你还需要多做一步对root里面的Value进行排序再调用Json::StyledWriter。由于json不支持sort那种通过迭代器进行的指定排序,所以比较麻烦,需要先将root里面的内容那出来存入vector中,然后在对vector使用sort指定weight排序后,再写回root,你如果不这么做,代码本身没错报错但是将来编译的时候会报错一堆,你看不懂的,所以写法如下:

// 1. 先把 root 里的元素搬到 vector

vector<Json::Value> vec;

for (const auto &v : root)

vec.push_back(v);

// 2. 排序

std::sort(vec.begin(), vec.end(),

\](const Json::Value \&a, const Json::Value \&b) { return a\["weight"\] \> b\["weight"\]; }); // 3. 清空原数组并重新填充 root.clear(); for (auto \&v : vec) { root.append(std::move(v)); }

string Getdesc(const string& content, const string& word)截取摘要

我们首先要截取需要知道一下关键词在指定content的哪个位置,找到word在content首次出现的位置,然后往前读取50个字节,然后往后读取100个字节,无法读取这么多就默认从开头或者结尾开始。但是由于我们的word如果原本是有大写字符的,已经被之前boost::toslower变全小写了,但是content里面可能匹配这个词的原本是大写的,这样就可能会匹配不到从而无法找到在哪里。

我们就需要在查找content的同时,边对content进行小写转化,边匹配。直接先将content转成全小写的代价比较大,所以更喜欢引入匹配规则,规则:逐个字符先转小写再匹配,刚好std::search库函数可以满足我们的需求。

std::search 是 C++ <algorithm> 中的序列匹配算法 ,用来在一段区间中查找第一次出现子序列(subsequence),返回指向该子序列首元素的迭代器。

复制代码
// 带自定义比较器的版本
template<class ForwardIt1, class ForwardIt2, class BinaryPredicate>
ForwardIt1 search(ForwardIt1 first1, ForwardIt1 last1,
                  ForwardIt2 first2, ForwardIt2 last2,
                  BinaryPredicate pred);

#include <boost/algorithm/string/case_conv.hpp> //头文件

cpp 复制代码
auto seq = search(content.begin(), content.end(), word.begin(), word.end(), [](char a, char b){
            return std::tolower(a) == std::tolower(b);
        });

返回值就是word在content刚开始位置的迭代器位置,也就是指向该位置的指针。如果不存在word则会返回查找序列的尾端指针,也就是xxx.end(),作为判断条件可以如下书写。

cpp 复制代码
if (seq == content.end())
        {
            return "None1 未找到word";
        } 

然后如下几步就是利用指针-指针获得下标位置,利用我们之前定义的截取长度进行向前向后substr截取就可以了,没有什么好说的了,很简单,最后在截取的摘要后面添加......就完成了。

search.cc本地测试模块

cpp 复制代码
#include"searcher.hpp"
using namespace ns_searcher;
const string input = "data/raw_html/raw.txt";
int main()
{
    shared_ptr<Searcher> search = make_shared<Searcher>();
    search->Initsearcher(input);
    string query;
    string json_string;
    while (true)
    {
        cout << "Please Enter Your Search Query! ";
        getline(cin, query);
        search->Search(query, &json_string);
        cout << json_string << endl;
    }
    return 0;
}

使用智能指针更安全,按照逻辑完成各种准备即可,比较简单,直接给了。如果本地测试那些内容索引什么的,每个权值都正确(要去官网找计算比对)就可以进入下一个模块编写http_server了。

http_server模块

自行在gitee中输入cpp_httplib,然后下载对应的库。然后导入在文件目录里面,成功后我们就可以用了。如果你的g++版本太低了这个库就运行不起来,我没有这个问题,如果你的centos有这个问题另行在CSDN或者别的地方找解决办法,有解决办法的,我没有学,真的很抱歉不能帮到你。

然后我们包含头文件#include"cpp-httplib-master/httplib.h",注意cpp-httplib-master是我的库导入时的名字,你们不一定跟我一样。我们用 C++ 的 HTTP 库 httplib 在本地启动一个"搜索服务器",客户端通过浏览器或 curl 访问 http://ip:8082/s?word=xxx 就能拿到搜索结果(JSON),先创建 HTTP 服务器对象Server server,接着如本地测试那些得先建立索引,然后调用server.Get方法,注册 GET 路由 /s,收到请求后执行后面的 Lambda 。Lambda 捕获列表 [&search] 把前面创建的 Searcher 智能指针以引用方式捕获进来,供后续使用。这里不是服务器调用get/post方法,是处理客户端的get/post请求的函数。

server.Get("/s", [&search](const httplib::Request& req, httplib::Response& rep)

捕获search用以后面调用,"/s"指定搜索路径的前缀必须带/s,再接?。const httplib::Request& req, httplib::Response& rep 这两个形参出现在 httplib 的 路由处理函数 (handler)里,它们是 httplib 库与业务代码之间的"接口对象"req 用来"读"请求,rep 用来"写回"响应。接着在Get函数里面的Lambda操作中先检查 URL 是否带 word 参数,例如 /s?word=C++。

rep.set_content("必须要有搜索关键字!", "text/plain: charset=utf-8");

set_content有rep调用用于返回应答(写入指定内容可以被浏览器渲染在浏览器上),后面的是content_Type,是解读类型,文件解读类型如上,如果你不知道某个你希望的被解读的类型是什么,可以在浏览器上查找content_Type表,有对应映射关系。

接着在lambda中,调用req.get_param_value("word"),从 HTTP 请求行(GET 时即 URL,POST 时即表单)里取出名为 "word" 的参数值(获取V值),把这个值以 std::string 的形式赋给局部变量 words,后续业务逻辑就能直接使用它,事先写成json串后,调用rep.set_content,把 JSON 作为 HTTP 响应体发回给客户端,并声明 Content-Type: application/json,浏览器/前端就能直接解析。当然我们还需要指定访问的跟目录,所以在当前路径下建立wwwhu(自定义的),然后内部编写根目录体现的.html,将来直接如果没有指定搜索内容裸给一个ip就会自动访问跟目录./wwwhu,浏览器自动运行里面的html体现一个界面给我们,这个就是搜索界面。使用set_base_dir可以指定访问的根目录。

const string root_path = "./wwwhu";

server.set_base_dir(root_path);

最后server.listen("0.0.0.0", 8082);开始监听本机所有网卡的 8082 端口。现在任何人访问http://<服务器IP>:8082/s?word=C++都会得到一段 JSON 搜索结果。"0.0.0.0"的意思是可以监听所有ip发来的请求。

这边肯定有人有一个小疑问,为什么是先Get处理再Listen监听呢,这里是因为,要先注册处理方法,有了处理方法才能监听信息,跟socket编程不一样的点就是socket是在写之前已经有方法了才,listen自然在前面了。server.Get不是处理结果用的而是向底层注册处理的方法还是要等到listen成功不阻塞之后底层才调用处理的。

编写前端模块

前端代码如下:

javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

    <title>boost 搜索引擎</title>
    <style>
        /* 去掉网页中的所有的默认内外边距,html的盒子模型 */
        * {
            /* 设置外边距 */
            margin: 0;
            /* 设置内边距 */
            padding: 0;
        }
        /* 将我们的body内的内容100%和html的呈现吻合 */
        html,
        body {
            height: 100%;
        }
        /* 类选择器.container */
        .container {
            /* 设置div的宽度 */
            width: 800px;
            /* 通过设置外边距达到居中对齐的目的 */
            margin: 0px auto;
            /* 设置外边距的上边距,保持元素和网页的上部距离 */
            margin-top: 15px;
        }
        /* 复合选择器,选中container 下的 search */
        .container .search {
            /* 宽度与父标签保持一致 */
            width: 100%;
            /* 高度设置为52px */
            height: 52px;
        }
        /* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*/
        /* input在进行高度设置的时候,没有考虑边框的问题 */
        .container .search input {
            /* 设置left浮动 */
            float: left;
            width: 600px;
            height: 50px;
            /* 设置边框属性:边框的宽度,样式,颜色 */
            border: 1px solid black;
            /* 去掉input输入框的有边框 */
            border-right: none;
            /* 设置内边距,默认文字不要和左侧边框紧挨着 */
            padding-left: 10px;
            /* 设置input内部的字体的颜色和样式 */
            color: #CCC;
            font-size: 14px;
        }
        /* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/
        .container .search button {
            /* 设置left浮动 */
            float: left;
            width: 150px;
            height: 52px;
            /* 设置button的背景颜色,#4e6ef2 */
            background-color: #4e6ef2;
            /* 设置button中的字体颜色 */
            color: #FFF;
            /* 设置字体的大小 */
            font-size: 19px;
            font-family:Georgia, 'Times New Roman', Times, serif;
        }
        .container .result {
            width: 100%;
        }
        .container .result .item {
            margin-top: 15px;
        }

        .container .result .item a {
            /* 设置为块级元素,单独站一行 */
            display: block;
            /* a标签的下划线去掉 */
            text-decoration: none;
            /* 设置a标签中的文字的字体大小 */
            font-size: 20px;
            /* 设置字体的颜色 */
            color: #4e6ef2;
        }
        .container .result .item a:hover {
            text-decoration: underline;
        }
        .container .result .item p {
            margin-top: 5px;
            font-size: 16px;
            font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
        }

        .container .result .item i{
            /* 设置为块级元素,单独站一行 */
            display: block;
            /* 取消斜体风格 */
            font-style: normal;
            color: green;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="search">
            <input type="text" value="请输入搜索关键字">
            <button onclick="Search()">搜索一下</button>
        </div>
        <div class="result">
            <!-- 动态生成网页内容 -->
            <!-- <div class="item">
                <a href="#">这是标题</a>
                <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
                <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
            </div>
            <div class="item">
                <a href="#">这是标题</a>
                <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
                <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
            </div>
            <div class="item">
                <a href="#">这是标题</a>
                <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
                <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
            </div>
            <div class="item">
                <a href="#">这是标题</a>
                <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
                <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
            </div>
            <div class="item">
                <a href="#">这是标题</a>
                <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
                <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
            </div> -->
        </div>
    </div>
    <script>
        function Search(){
            // 是浏览器的一个弹出框
            // alert("hello js!");
            // 1. 提取数据, $可以理解成就是JQuery的别称
            let query = $(".container .search input").val();
            if (query == '' || query == null)
            {
                return;
            }
            console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据

            //2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
            $.ajax({
                type: "GET",
                url: "/s?word=" + query,
                success: function(data){
                    console.log(data);
                    BuildHtml(data);
                }
            });
        }

        function BuildHtml(data){
            if (data == '' || data == null){
                document.write("搜索内容为空");
                return;
            }
            // 获取html中的result标签
            let result_lable = $(".container .result");
            // 清空历史搜索结果
            result_lable.empty();

            for( let elem of data){
                // console.log(elem.title);
                // console.log(elem.url);
                let a_lable = $("<a>", {
                    text: elem.title,
                    href: elem.url,
                    // 跳转到新的页面
                    target: "_blank"
                });
                let p_lable = $("<p>", {
                    text: elem.desc
                });
                let i_lable = $("<i>", {
                    text: elem.url
                });
                let div_lable = $("<div>", {
                    class: "item"
                });
                a_lable.appendTo(div_lable);
                p_lable.appendTo(div_lable);
                i_lable.appendTo(div_lable);
                div_lable.appendTo(result_lable);
            }
        }
    </script>
</body>
</html>

就是相当于前后端建立页面的链接,使得点击搜索按钮可以通过js联系到后端从而得到搜索内容。

引入日志

可以使用我之前使用观察者模式写好的Log.hpp,这个日志很不错直接拿来用就完了,效果如下,感兴趣的可以私信我!!!

logmodule::LOG(loglevel::INFO)<< "当前已经建立的索引文档:" << count << '\n';

项目总结与扩展

我们这个项目的基础版本就做完了,之前我们说了获取数据源可以使用爬虫工具,所以数据源可以使用爬虫进行摘取,然后用信号进行实时更新,我们会发现在数据源中不只有doc/html里面存在.html文件其他目录里面也有,但是由于boost库太大了,所以未来有机会可以做全站搜索的。另外,在浏览器搜索中,搜索条目一般都是有竞价排名的,我们可以添加竞价排名来有意抬高某条内容的优先级(可以将权值搞得很大)。我们还可以添加搜索框的热词统计,只能在前端页面显示搜索关键词(可以使用字典树,优先级队列这种结构完成)。最后可以考虑添加登入注册界面,通过http提交/login信息完成读取登入。反正学有余力者自行扩展,扩展的这部分我回来之后再好好考虑一下!!!

分享我遇到的困难

1。boost库下载那个tar.gz,我用的是最新版的1.88.0的,用edge下不了,以我现成的浏览器只能使用google,然后导入linux时rz指令啥也不带的导入会乱码,必须rz -E才行,如果说你命令行输入rz -E导入还是会很多乱码,先重新下载rz,然后不用命令行了,手动使用xshell8的上排按钮导入。

2。关于cppjieba这个库,GitHub和gitee上面的limonp配置文件都不全不要下载,gitee的里面还没有demo.cc(教你如何使用的),gitcode里面的已经没有了,如果有需要cppjieba的私信我,我免费发完整的。

3。关于jieba分词逻辑,由于如果不做任何的干预,jieba分词会将一些连接词,符号也算进去,filesystem_filesystem就会被分成filesystem/_/filesystem,这个_会影响这个给短句的匹配,会多匹配很多文档的下标(从0-8千多)当你发现某个短语匹配时中间匹配了所有下标,就是jieba没有去连接词的原因。如果加上连接词的判断那就建立引索就会很慢,这时如果你的虚拟机或者云服务器配置很低的话很容易崩溃。

4。对于http与浏览器链接的问题,如果你使用的是虚拟机,那要确保你的虚拟机要有处在公网中的ip,这样才可以通过浏览器访问,如果浏览器报错标号是502或者超时的话,如果多次更换端口还是显示502或者超时,那我也没有找到很好的方法解决了,使用云服务器就没有这个问题。

5。如果在搜索时摘要返回的是None1的话,就是word是原本有大写的,转成小写之后去匹配content匹配不上的问题。

6。如果导入boost库进行tar解压时,报错了但是内容大写大概4096字节就没有问题不用管了,反正所有的html总共2000多万字节吧。

相关推荐
tt55555555555511 分钟前
C/C++嵌入式笔试核心考点精解
c语言·开发语言·c++
lg_cool_14 分钟前
Qt 中最经典、最常用的多线程通信场景
c++·qt6.3
吱吱企业安全通讯软件35 分钟前
吱吱企业通讯软件保证内部通讯安全,搭建数字安全体系
大数据·网络·人工智能·安全·信息与通信·吱吱办公通讯
科大饭桶41 分钟前
C++入门自学Day14-- Stack和Queue的自实现(适配器)
c语言·开发语言·数据结构·c++·容器
tt5555555555551 小时前
字符串与算法题详解:最长回文子串、IP 地址转换、字符串排序、蛇形矩阵与字符串加密
c++·算法·矩阵
云边云科技2 小时前
零售行业新店网络零接触部署场景下,如何选择SDWAN
运维·服务器·网络·人工智能·安全·边缘计算·零售
AOwhisky2 小时前
Linux 文本处理三剑客:awk、grep、sed 完全指南
linux·运维·服务器·网络·云计算·运维开发
long_run3 小时前
C++之模板函数
c++
NuyoahC3 小时前
笔试——Day43
c++·算法·笔试