CPP项目:Boost搜索引擎

1.项目背景

对于Boost库来说,它是没有搜索功能的,所以我们可以实现一个Boost搜索引擎来实现一个简单的搜索功能,可以更快速的实现Boost库的查找,在这里,我们实现的是站内搜索,而不是全网搜索。

2.对于搜索引擎的相关宏观理解

3.搜索引擎技术栈及项目环境

技术栈:c/c++,c++11,STL,Boost准标准库,Jsoncpp,cppjieba,cpp-httplib,html5,css,js,

Ajax,jQuery,不使用爬虫。

项目环境:Centos 7云服务器,vim/gcc(g++)/Makefile , vs code

4.正排索引、倒排索引

正排索引:通过文档ID寻找文档内容:

比如:文档1:西游记有一只猴子

           文档2:西游记有一只六耳猕猴

文档1西游记有一只猴子
文档2西游记有一只六耳猕猴

目标文档进行分词(目的:方便建立倒排索引和查找):

文档1:西游记/有/一只/猴子/

文档2:西游记/有/一只/六耳/猕猴/

注:停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑

倒排索引:根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案。

关键字:文档ID, weight(权重)
西游记文档1.文档2
文档1,文档2
一只文档1.文档2
猴子文档1
六耳文档2
猕猴文档2

模拟一次查找的过程:

用户输入:西游记->倒排索引中查找->提取出文档id->根据正排索引->找到文档内容,通过文档标题,内容,URL,对文档内容进行摘要->构建响应结果。

5.编写数据去标签与数据清洗的模块 Parser

5.1 什么是标签:

我们既然要去标签,那么就要知道什么是标签,比如:

//原始数据 -> 去标签之后的数据
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html> <!--这是一个标签-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Chapter 30. Boost.Process</title>
<link rel="stylesheet" href="../../doc/src/boostbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
<link rel="home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation
Subset">
<link rel="up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook
Subset)">
<link rel="prev" href="poly_collection/acknowledgments.html" title="Acknowledgments">
<link rel="next" href="boost_process/concepts.html" title="Concepts">

上述代码中,<!--这是一个标签-->,这就是一个标签,<>这些标签对于我们的搜索来说是没有意义的,所以我们需要去掉这些标签。标签分为两类,一类是只有一个的<>,一类是有两个的<>,</>,这些都是我们需要去掉的。

5.2 搜索内容

由于我们使用的是本地搜索,即将Boost库下载下来,通过本地搜索,然后再通过建立网站,实现站内搜索。所以第一步,下载Boost库:

Boost库官网:Boost C++ Libraries

Boost库下载:Index of main/release/1.84.0/source

目前只需要boost库/doc/html目录下的html文件,用它来进行建立索引,不需要使用其他的

下载完成之后,我们把他上传到云服务器上。之后建立一个保存*.html文件的文件夹,我们之后需要使用它,然后再创建一个保存去标签之后的*.html文件的文档,方便我们搜索。

5.3 具体逻辑实现

const std::string src_path = "/home/SSS/data/input";//搜索路径
const std::string output = "/home/SSS/data/raw_html/raw.txt";//保存文档typedef struct DocInfo
{std::string title;   // 文档标题std::string content; // 文档标题std::string url;     // 文档url
} DocInfo_t;// &: 输入
//*: 输出
//&:输入输出
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results,const std::string& output);int main()
{std::vector<std::string> files_list; //保存文件路径//递归式的把每个html文件名带路径,保存到files_list中,方便后期进行一个一个的文件进行读取if (!EnumFile(src_path, &files_list)){std::cerr << "enum file name error1" << std::endl;return 1;}// 按照files_list读取每个文件的内容,并进行解析std::vector<DocInfo_t> results;if (!ParseHtml(files_list, &results)){//std::cout<<"1"<<std::endl;std::cerr << "enum file name error2" << std::endl;return 2;}std::cout<<results.size()<<std::endl;//把解析完毕的各个文件内容,写入到output,按照\3作为每个文档的分割符 if (!SaveHtml(results,output)){std::cerr << "enum file name error3" << std::endl;return 3;}return 0;
}

第一个函数通过对文件路径的处理,将我们所需要的html文件保存到vector中,方便我们后续查找。第二个函数进行对html文件的处理,将html文件的标题,内容,url进行提取,保存到数组中。第三个函数将处理好的内容放入到文件中,方便后续进行建立索引。

三个函数的具体实现过程:

第一步:

bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{namespace fs = boost::filesystem;fs::path root_path(src_path);if (!fs::exists(root_path)) // 判断文件路径是否存在,在这里我们使用Boost库中的函数,判断文件路径是否存在{std::cerr << "root_path not exits" << std::endl;return false;}fs::recursive_directory_iterator end; // 递归的判断文件for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++){if (!fs::is_regular_file(*iter)) // 文件是否为普通文件,Boost库中判断文件是否为普通文件。{continue;}if (iter->path().extension() != ".html") // 文件是否为html文件,Boost库函数{continue;}files_list->push_back(iter->path().string());//将所有带路径的html保存在files_list,方便后续进行文本分析}return true;
}

第二步:

namespace ns_util
{class FileUtil{   public:static bool ReadFile(const std::string &file_path, std::string *out){std::ifstream in(file_path,std::ios::in);if(!in.is_open())//c++中文件处理函数{std::cerr <<"open in error"<<std::endl;return false;}std::string line;while(std::getline(in,line))//将文件内容写入到(string)out中{*out += line;//std::cout<<line<<std::endl;}in.close();//关闭文件!!!return true;}};
}// 文档标题
static bool ParseTitle(const std::string &file, std::string *title)
{std::size_t begin = file.find("<title>");//对文档标题进行处理,由于网页的标题保存在<title>标//签中,以</title>结尾,所以中间的内容就是我们所要的标题//std::cout<<begin<<std::endl;if (begin == std::string::npos){//std::cout<<"1:"<<begin<<std::endl;return false;}std::size_t end = file.find("</title>");if (end == std::string::npos){return false;}begin += std::string("<title>").size();if (begin > end){return false;}*title = file.substr(begin, end - begin);//标题//std::cout<<*title<<std::endl;return true;
}
// 对内容进行解析
static bool ParseContent(const std::string &file, std::string *content)
{//去标签,基于一个简易的状态机enum status{LABLE,CONTENT};enum status s = LABLE;for (char c : file){switch (s){case LABLE:if (c == '>')s = CONTENT;break;case CONTENT :if (c == '<')s = LABLE;else {// 不想保留原始文件中的\n,用\n作为html解析之后文本的分隔符if (c == '\n') c = ' ';content->push_back(c);} break;default:break;}}return true;
}
static bool ParseUrl(const std::string &file_path, std::string *url)
{std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html";std::string url_tail = file_path.substr(src_path.size());*url = url_head + url_tail;//文档的urlreturn true;
}
// 对内容进行解析
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t>* results)
{for (const std::string &file : files_list){std::string result; // 读取文件if (!ns_util::FileUtil::ReadFile(file, &result)){continue;}DocInfo_t doc;// 解析文件,提取titleif (!ParseTitle(result, &doc.title)){//std::cout<<"1"<<std::endl;continue;}// 解析文件内容,去标签if (!ParseContent(result, &doc.content)){continue;}// 解析内容,获得urlif (!ParseUrl(file, &doc.url)){continue;}results->push_back(std::move(doc)); // 减少拷贝次数,提高效率//std::cout<<results->back()->title<<std::endl;}return true;
}

ParseHtml()这个函数内部包含四个函数,其中包括读取文件,将文件标题,内容,url分别提取出来,放入到数组中。

网站处理:

官网URL样例: https /www.boost.org/doc/libs/1_79_0/doc/html/accumulators.html
我们下载下来的url样例:boost_1_79_0 / doc / html / accumulators.html
我们拷贝到我们项目中的样例:data / input / accumulators.html
url_head = “https://www.boost.org/doc/libs/1_79_0/doc/html”;
url_tail = (data / input)(删除) / accumulators.html->url_tail = / accumulators.html
url = url_head + url_tail; 相当于形成了一个官网链接。

5.4建立索引

实现原理:

建立索引我们需要建立正排索引和倒排索引,这需要我们建立一个struct,用来保存文档的title,content,url,id。因为正排索引是通过文档id来寻找文档内容的,所以我们可以通过一个数组来实现id的编号,倒排索引是通过关键字来锁定在哪一个文档id里面的,所以我们可以通过建立一个哈希映射来完成,通过关键字来寻找文档id。由于我们只需要建立一次索引,所以我们可以通过创建一个单例模式来实现所以得初始化。

由于我们通过关键字可以映射到多个文档内容中,所以我们可以通过建立相关性的方式来排序我们所查找得的文档内容。

相关性的建立:由于相关性的问题涉及到的范围十分的大,可能有相近词,同义词,不连续的关键词,字符拆分的不确定性等等,在这里我们使用关键字在文档中出现的频次来决定。由于关键字可以出现在标题中,也可以出现在文档内容中,所以我们通过简单粗暴的方式来解决,即在标题中出现的关键在是10倍与在内容中出现的关键字。当然,相关性的处理方式可以自己设定。

建立索引对象:

 //文档属性struct DocInfo{std::string _title;//文档标题std::string _contant;//文档内容std::string _url;//文档urluint64_t  _id;//文档id};//倒排索引struct InvertedElem{std::string _word;//关键字uint64_t _id;//idint _weight;//权重InvertedElem():_weight(0){}};//倒排拉链typedef std::vector<InvertedElem> InvertedList;

建立索引类:

 class index{private:index(){};index(const index&)=delete;index operator=(const index&)=delete;public:static index* GetIndex()//初始化单例模式,在这里我们使用懒汉模式来建立索引{if(nullptr==instance){mtx.lock();//多线程问题if(nullptr==instance){instance=new index();}mtx.nulock();}return instance;}public://正排索引DocInfo *GetForwardIndex(uint64_t _id)//根据文档id找文档内容{if(_id>=_forward_list.size())//数组下标从0开始{std::cerr << "doc_id out range, error!" << std::endl;return nullptr;}return &_forward_list[_id];}//根据关键字,获得文档倒排拉链,倒排索引InvertedList *GetInvertedList(const std::string &word){auto iter = _inverted_index.find(word);//寻找关键字if(iter==_inverted_index.end())//没找到{std::cerr << "_inverted_index out range, error!" << std::endl;return nullptr;}return &(iter->second);}//根据去标签,格式化之后的文档,构建正排和倒排索引bool BuildIndex(const std::string &input){std::ifstream in(input,std::ios::in|std::ios::binary);if(!in.is_open()){std::cerr<<" open file error"<<std::endl;}std::string line;int count=0;while (std::getline(in, line))//读取内容{DocInfo *doc = BuildForwardIndex(line);if (nullptr == doc){std::cerr << "build " << line << " error" << std::endl; // for deubgcontinue;}BuildInvertedIndex(*doc);count++;}return true;}private:DocInfo* BuildForwardIndex(const std::string &line){//进行字符串切分std::vector<std::string> results;const std::string sep = "\3";   //行内分隔符ns_util::StringUtil::Split(line, &results, sep);//分词if(results.size()!=3){std::cout<<"split error"std::endl;return nullptr;}//2. 字符串进行填充到DocIinfoDocInfo doc;doc._title = results[0];//titledoc._content = results[1];// contentdoc._url = results[2];/// urldoc._id = forward_index.size(); // 先进行保存id,在插入,对应的id就是当前doc在vector中的下标!// 3. 插入到正排索引的vector_forward_index.push_back(std::move(doc)); // doc,html文件内容return &forward_index.back();}bool BuildInvertedIndex(const DocInfo &doc){// DocInfo{title, content, url, doc_id}// word -> 倒排拉链struct word_cnt{int _title_cnt;int _content_cnt;word_cnt() : _title_cnt(0), _content_cnt(0) {}};std::unordered_map<std::string, word_cnt> word_map; // 用来暂存词频的映射表// 对标题进行分词std::vector<std::string> title_words;ns_util::JiebaUtil::CutString(doc._title, &title_words);//分词//计算权重for(auto e:title_words){boost::to_lower(s);      // 需要统一转化成为小写word_map[s].title_cnt++; // 如果存在就获取,如果不存在就新建}//内容分词std::vector<std::string> contant_words;ns_util::JiebaUtil::CutString(doc._contant, &contant_words);//分词for (std::string s : content_words){boost::to_lower(s);word_map[s].content_cnt++;}//权重计算#define X 10#define Y 1for(auto e:word_map){InvertedElem item;item._id = doc._id;item.word = e.first;item.weight = X * e.second.title_cnt + Y * e.second.content_cnt; // 相关性InvertedList &inverted_list = inverted_index[e.first];_inverted_list.push_back(std::move(item));}}private:std::vector<DocInfo> _forward_list;//正排//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_map<std::string, InvertedList> _inverted_index;static Index* instance;static std::mutex mtx;};index* index::instance = nullptr;std::mutex index::mtx;

 在创建索引的过程中,我们需要对我们搜索的关键字进行分词,而分词需要我们使用cppjieba分词工具,通过使用分词工具来进行分词(当然,有能力的同学可以自己实现一个分词程序)。

由于我们需要使用cppjieba分词工具,我们就需要下载

cppjiaba下载地址:git clone https://gitcode.com/yanyiwu/cppjieba-server.git

下载完成之后,我们只需要使用 这个目录下的文件即可:cppjieba/include/cppjieba

使用细节:使用cppjieba需要注意,我们需要自己执行:cd cppjieba; cp -rf deps/limonp include/cppjieba/, 不然会编译报错。

我们可以建立软链接来使用cppjieba库。

对于cppjieba的使用来说,我们可以到这个路径下查看使用情况:cppjieba/test.

对于使用cppjieba来说,在这个项目中这需要掌握CutString()这个函数的使用,其他的不用我们掌握太多,当然如果你想要学习的更多,可以学习其他函数的使用。 

切分函数的实现

#pragma once 
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <mutex>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include "cppjieba/Jieba.hpp"namespace ns_util
{class FileUtil{   public:static bool ReadFile(const std::string &file_path, std::string *out){std::ifstream in(file_path,std::ios::in);if(!in.is_open())//c++中文件处理函数{std::cerr <<"open in error2"<<std::endl;return false;}std::string line;while(std::getline(in,line))//将文件内容写入到(string)out中{*out += line;//std::cout<<line<<std::endl;}in.close();//关闭文件!!!return true;}};class StringUtil//切割字符串{public:static void Split(const std::string &target, std::vector<std::string> *out, const std::string &sep){//boost splitboost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);}};const char* const DICT_PATH = "/home/SSS/cppjieba/dict/jieba.dict.utf8";const char* const HMM_PATH = "/home/SSS/cppjieba/dict/hmm_model.utf8";const char* const USER_DICT_PATH = "/home/SSS/cppjieba/dict/user.dict.utf8";const char* const IDF_PATH = "/home/SSS/cppjieba/dict/idf.utf8";const char* const STOP_WORD_PATH = "/home/SSS/cppjieba/dict/stop_words.utf8";class JiebaUtil{private:static cppjieba::Jieba _jieba;//不去暂停词private://去暂停词//cppjieba::Jieba _jieba;//创建对象//std::unordered_map<std::string, bool> _stop_words;//哈希映射//static JiebaUtil* _instance;private://JiebaUtil():_jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) {}//JiebaUtil(const JiebaUtil &) = delete;//ns_util::JiebaUtil operaror=(const JiebaUtil& x)=delete;public://去暂停词// static JiebaUtil* GetInstance()//初始化// {//     static std::mutex _mtx;//     if(nullptr==_instance)//     {//         _mtx.lock();//         if(nullptr==_instance)//         {//             _instance=new JiebaUtil();//             _instance->InitJiebaUtil();//加载文件//         }//         _mtx.unlock();//     }//     return _instance;//     //return nullptr;// }// void InitJiebaUtil()// {//     std::ifstream in(STOP_WORD_PATH);//     if(!in.is_open()) //     {//         std::cerr<<"open file error"<<std::endl;//         return;//     }//     std::string line;//     while(std::getline(in,line))//     {//         _stop_words.insert({line, true});//     }//     in.close();// }// void CutStringHelper(const std::string &src, std::vector<std::string> *out)//去暂停词// {//     _jieba.CutForSearch(src, *out);//切分//     for(auto iter = out->begin(); iter != out->end();)//     {//         auto it=_stop_words.find(*iter);//         if(it!=_stop_words.end())//         {//             iter = out->erase(iter);//去暂停词//         }//         else iter++;//     }// }public:static void CutString(const std::string &src, std::vector<std::string> *out){//去暂停词,如果云服务器配置高的情况下可以使用,性能不高可能导致出现不必要的错误//ns_util::JiebaUtil::GetInstance()->CutStringHelper(src, out);//低配版,不去暂停词_jieba.CutForSearch(src, *out);}};//不去暂停词cppjieba::Jieba JiebaUtil::_jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);//去暂停词//JiebaUtil* _instance=nullptr;
}

 由于我们使用的是cppjieba分词工具,所以我们需要在使用的时建立分词路径,防止出现错误,在建立cppjieba类的时候,我们也是创建的单例模式,只需要建立一个对象即可,不需要创建多个对象,这和建立索引时候的功能是一样的。

对于切分字符来说,也就是CutStringHelper()函数来说,我们有两种方法实现它,可以直接调用jieba分词工具直接使用,生成关键字分词,这样做的情况下可能会生成较多的暂停词,搜索结果可能又不太准确的情况,所以我们可以去掉暂停词,这样可以减小关键词的数量,减小查找次数,二区掉暂停词依然需要调用Jieba分词库当中的函数。

对于去暂停词来说,如果服务器配置不高的情况下,不要去暂停词,可能程序运行直接报错!!

5.5建立搜索

索引建议好之后,我们进行搜索模块的建立,对于搜索模块来说,我们需要使用到的工具为Jsoncpp和cpp-httplib这两个工具,使用这两个工具我们先下载下来。

Jsoncpp下载方式:sudo yum install -y jsoncpp-devel

json的功能是实现序列化和反序列化,当然如果你可以使用其他序列化和反序列化的工具,你也可是使用和其他的,比如ProtoBuf,XML等。

cpp-httplib下载方式:GitHub - yhirose/cpp-httplib: A C++ header-only HTTP/HTTPS server and client library

注意:如果使用 centOS 环境,yum源带的 g++ 最新版本是4.8.5,发布于2015年,年代久远。编译该项目会出现异常。将 gcc/g++ 升级为更高版本可解决问题。

# 升级参考:https://juejin.cn/post/6844903873111392263
# 安装gcc 8版本
yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++
# 启用版本
source /opt/rh/devtoolset-8/enable
# 查看版本已经变成gcc 8.3.1
gcc -v

# 启动: 细节,命令行启动只能在本会话有效

source /opt/rh/devtoolset-8/enable

#可选:如果想每次登陆的时候,都是较新的gcc

cat ~/.bash_profile

# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
#每次启动的时候,都会执行这个命令

source /opt/rh/devtoolset-8/enable

httplib使用·:

测试代码:

#include "cpp-httplib/httplib.h"
int main()
{httplib::Server svr;svr.Get("/hi", [](const httplib::Request &req, httplib::Response &rsp){rsp.set_content("你好,世界!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8081);return 0;
}

搜索代码实现:

建立搜索代码之前,我们需要先建立索引,这样才可以快速查找目标。所以我们先实现一个类,包含我们所需要的内容,因为我们是通过关键字找文档id,再通过文档id寻找文档内容,在这期间我们需要通过权重,来找到不同文档的先后顺序,这样我们的类里面应该包含文档id,权重,关键字等。

 struct InvertedElemPrint{uint64_t _id;int _weight;std::vector<std::string> _words;InvertedElemPrint() : _id(0), _weight(0) {}};

对于建立搜索得类:

    class Searcher{private:ns_index::index *_index=nullptr; // 建立索引,进行查找public:Searcher() {}~Searcher() {}public:void InitSearch(const std::string &input){// 获取index对象index = ns_index::index::GetInstance();//根据index对象建立索引index->BulidIndex();}// query: 搜索关键字// json_string: 返回给用户浏览器的搜索结果void search(const std::string &query, std::string *json_string){std::vector<std::string> words;ns_util::JiebaUtil::CutString(query, &words);//2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索,关键字也需要//ns_index::InvertedList inverted_list_all; //内部InvertedElemstd::vector<InvertedElemPrint> inverted_list_all;std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;for(auto word:words){boost::to_lower(word);ns_index::InvertedList *inverted_list = index->GetInvertedList(word);if(inverted==nullptr) continue;for(const auto &elem:*inverted_list){auto &item = tokens_map[elem._id]; //[]:如果存在直接获取,如果不存在新建// item一定是doc_id相同的print节点item._id = elem._id;item._weight += elem._weight;item.words.push_back(elem._words);}}for (const auto &item : tokens_map){inverted_list_all.push_back(std::move(item.second));}//3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序std::sort(inverted_list_all.begin(), inverted_list_all.end(),[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){return e1._weight > e2._weight;});//序列化和反序列化Json::Value root;for(auto &item : inverted_list_all){ns_index::DocInfo * doc = index->GetForwardIndex(item._id);if(doc==nullptr) continue;Json::Value elem;elem["title"]=doc->_title;elem["desc"] = GetDesc(doc->content, item.words[0]); //content是文档的去标签的结果,但是不是我们想要的,我们要的是一部分 TODOelem["url"]=doc->url;elem["id"] = (int)item._id;elem["weight"] = item._weight; // int->stringroot.append(elem);}Json::FastWriter writer;*json_string = writer.write(root);}//得到contantstd::string GetDesc(const std::string &html_content, const std::string &word){//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)//截取出这部分内容const int prev_step=50;const int next_step=150;//找到关键字auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y){ return (std::tolower(x) == std::tolower(y)); });if(iter==html.content.end()) return "NONE1";int pos = std::distance(html_content.begin(), iter);int start=0;int end=html_content.size()-1;if(pos > start + prev_step) start = pos - prev_step;if(pos < end - next_step) end = pos + next_step;if(start>end) return "NONE2";std::string desc = html_content.substr(start, end - start);desc += "...";return desc;}};
}

对于搜索类来讲。我们通过实现三个函数来解决,第一个函数为InitSearch(),实现初始化函数

完成获取对象和建立对象索引。

search()函数需要我们通过搜索关键字来返回给用户浏览器的搜索结果。这就需要通过序列化和反序列化来实现,通过查找到的内容,合并文档信息,按照权重来进行排序。

GetDesc()函数,通过对得到的信息进行打印,具体实现看代码细节。

至此,后端代码全部实现完成,开始实现前端代码,前端代码的实现需要html,css,js三种编程语言。

5.6 前端代码实现

html: 是网页的骨骼 -- 负责网页结构
css:网页的皮肉 -- 负责网页美观的
js(javascript):网页的灵魂---负责动态效果,和前后端交互

前端教程:w3school 在线教程

前端代码的编写

<!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();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){// 获取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>

5.7 添加日志

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cstdarg>
#include <ctime>#define DEBUG 0
#define NORMAL 1
#define WARING 2
#define ERROR 3
#define FATAL 4
#define LOGFILE "./calculator.log"const char* gLevelMap[]={"DEBUG","NORMAL","WARING","ERROR","FATAL"};//完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名),可以将其写到文件中
void logMessage(int level,const char* format,...)
{//if(level==0) printf("正确");char stdBuff[10024];time_t timestamp = time(nullptr);//时间snprintf(stdBuff,sizeof(stdBuff),"[%s] [%ld] ", gLevelMap[level], timestamp);char logBuff[1024]; //自定义部分// va_list args;// va_start(args, format);// // vprintf(format, args);// vsnprintf(logBuffer, sizeof logBuffer, format, args);// va_end(args);snprintf(logBuff,sizeof(logBuff),"[%s] [%ld] ", gLevelMap[level], timestamp);FILE *fp = fopen(LOGFILE, "a");// printf("%s%s\n", stdBuffer, logBuffer);fprintf(fp, "%s %s\n", stdBuff, logBuff);fclose(fp);
}

日志分为5个等级,每个等级对应不同的内容,不同的等级需要不同的数字来对应。可以将其写到文件中将其保存。当然,对于不同的级别,日志可能会有不同的表现,具体情况视情况而定。

5.8 结尾

最后需要将写好的程序部署到linux服务器上,完成最后网站的建立。

nohup ./http_server > log/log.txt 2>&1 &[1] 26890

项目扩展方向

1. 建立整站搜索
2. 设计一个在线更新的方案,信号,爬虫,完成整个服务器的设计
3. 不使用组件,而是自己设计一下对应的各种方案(有时间,有精力)
4. 在我们的搜索引擎中,添加竞价排名(强烈推荐)
5. 热次统计,智能显示搜索关键词(字典树,优先级队列)(比较推荐)
6. 设置登陆注册,引入对mysql的使用(比较推荐的)

有兴趣的可以尝试做下。

项目完整代码网址:

C-C++项目: C/C++项目 - Gitee.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/673605.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

qt/c++实现表情选择框

&#x1f482; 个人主页:pp不会算法^ v ^ &#x1f91f; 版权: 本文由【pp不会算法v】原创、在CSDN首发、需要转载请联系博主 &#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 实现功能 。编解码的设计 。映射关系设计 。匹配机制设计 演示效…

Lustre文件系统fid介绍

fid介绍 fid是lustre文件系统中文件的唯一标识&#xff0c;总共128位&#xff0c;fid序列、fid序列内编号、fid版本号&#xff08;目前未使用默认为0&#xff09; /*** File IDentifier.** FID is a cluster-wide unique identifier of a file or an object (stripe).* FIDs …

HarmonyOS SDK 助力新浪新闻打造精致易用的新闻应用

原生智能是HarmonyOS NEXT的核心亮点之一&#xff0c;依托HarmonyOS SDK丰富全面的开放能力&#xff0c;开发者只需通过几行代码&#xff0c;即可快速实现AI功能。新浪新闻作为鸿蒙原生应用开发的先行者之一&#xff0c;从有声资讯入手&#xff0c;基于Speech Kit朗读控件上线听…

【C#】.net core 6.0 设置根目录下某个文件夹可访问,访问创建的图片等资源

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌握。…

记录 | python list extend()

extend() 函数用于在列表末尾一次性追加另一个序列中的多个值&#xff08;用新列表扩展原来的列表&#xff09;。 以下实例展示了 extend()函数的使用方法&#xff1a; #!/usr/bin/pythonaList [123, xyz, zara, abc, 123]; bList [2009, manni]; aList.extend(bList)print …

应用层DoS

应用层&#xff08;application layer&#xff09;是七层OSI模型的第七层。应用层直接和应用程序 对接并提供常见的网络应用服务&#xff0c;能够在实现多个系统应用进程相互通信的同 时&#xff0c;完成一系列业务处理所需的服务。位于应用层的协议有很多&#xff0c;常见的包…

SpringBoot实现统一异常处理

文章目录 前言实现步骤定义统一响应对象类定义业务异常枚举接口和实现定义业务异常基类定义全局异常处理切面测试和验证 总结 前言 近日心血来潮想做一个开源项目&#xff0c;目标是做一款可以适配多端、功能完备的模板工程&#xff0c;包含后台管理系统和前台系统&#xff0c…

Android CMakeLists.txt语法详解

一.CMake简介 你或许听过好几种 Make 工具&#xff0c;例如 GNU Make &#xff0c;QT 的 qmake &#xff0c;微软的 MSnmake&#xff0c;BSD Make&#xff08;pmake&#xff09;&#xff0c;Makepp&#xff0c;等等。这些 Make 工具遵循着不同的规范和标准&#xff0c;所执行的…

设计模式2-对象池模式

对象池模式&#xff0c;Object Pool Pattern&#xff0c;当你的应用程序需要频繁创建和销毁某种资源&#xff08;比如数据库连接、线程、socket连接等&#xff09;时&#xff0c;Object Pool 设计模式就变得很有用。它通过预先创建一组对象并将它们保存在池中&#xff0c;以便在…

Python datetime 模块的高级应用

Python datetime 模块的高级应用 介绍方法时区处理日期格式化日期计算常见问题及解决方案代码日历应用时间序列分析 介绍 datetime 模块是 Python 中用于处理日期和时间的标准库模块。它提供了日期和时间类型&#xff08;date、time、datetime&#xff09;以及与日期和时间相关…

机器人运动学林沛群——变换矩阵

对于仅有移动&#xff0c;由上图可知&#xff1a; A P B P A P B o r g ^AP^BP^AP_{B org} APBPAPBorg​ 对于仅有转动&#xff0c;可得&#xff1a; A P B A R B P ^AP^A_BR^BP APBA​RBP 将转动与移动混合后&#xff0c;可得&#xff1a; 一个例子 在向量中&#xff…

「递归算法」:二叉树剪枝

一、题目 给你二叉树的根结点 root &#xff0c;此外树的每个结点的值要么是 0 &#xff0c;要么是 1 。 返回移除了所有不包含 1 的子树的原二叉树。 节点 node 的子树为 node 本身加上所有 node 的后代。 示例 1&#xff1a; 输入&#xff1a;root [1,null,0,0,1] 输出&…

grafana+prometheus+hiveserver2(jmx_exporter+metrics)

一、hiveserver2开启metrics&#xff0c;并启动jmx_exporter 1、修改hive-site.xml文件开启metrics <property><name>hive.server2.metrics.enabled</name><value>true</value> </property> <property><name>hive.service.m…

ChatGPT高效提问—prompt常见用法(续篇三)

ChatGPT高效提问—prompt常见用法&#xff08;续篇三&#xff09; 1.1 多选项 ​ 多选项技术为模型提供了一个清晰的问题或任务&#xff0c;并附带一组预先定义的潜在答案。这种方法在生成仅限于特定选项集的文本方面表现出色&#xff0c;适用于问答、文本补全和其他任务。利…

MySQL 日志管理

4.6&#xff09;日志管理 MySQL 支持丰富的日志类型&#xff0c;如下&#xff1a; 事务日志&#xff1a;transaction log 事务日志的写入类型为 "追加"&#xff0c;因此其操作为 "顺序IO"&#xff1b; 通常也被称为&#xff1a;预写式日志 write ahead…

《MySQL 简易速速上手小册》第1章:MySQL 基础和安装(2024 最新版)

文章目录 1.1 MySQL 概览&#xff1a;版本、特性和生态系统1.1.1 基础知识1.1.2 重点案例1.1.3 拓展案例 1.2 安装和配置 MySQL1.2.1 基础知识1.2.2 安装步骤1.2.3 重点案例1.2.4 拓展案例 1.3 基础命令和操作1.3.1 基础知识1.3.2 重点案例1.3.3 拓展案例 1.1 MySQL 概览&#…

【Web】vulhub Fastjson反序列化漏洞复现学习笔记

目录 1.2.24 RCE CVE-2017-18349 复现流程 原理分析 1.2.47 RCE CNVD-2019-22238 复现流程 原理分析 漏洞探测 1.2.24 RCE CVE-2017-18349 复现流程 vulhub启动靶场 用marshalsec启动LDAP/RMI服务 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRef…

Redis篇之redis是单线程

一、redis是单线程 Redis是单线程的&#xff0c;但是为什么还那么快&#xff1f;主要原因有下面3点原因&#xff1a; 1. Redis是纯内存操作&#xff0c;执行速度非常快。 2. 采用单线程&#xff0c;避免不必要的上下文切换可竞争条件&#xff0c;多线程还要考虑线程安全问题。 …

联合体的深入了解

1.联合体类型的声明 像结构体一样&#xff0c;联合体也是由一个或者多个成员构成&#xff0c;这些成员可以不同的类型。 但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用同一块内存空间。所以联合体也叫&#xff1a;共用体。 给联合体其中一个成员赋值…

SpringBoot + Tess4J 实现本地与远程图片的文字识别

1 前言 1.1 概要 在本文中&#xff0c;我们将探讨如何在Spring Boot应用程序里集成Tess4J来实现OCR&#xff08;光学字符识别&#xff09;&#xff0c;以识别出本地和远程图片中的文字。 我们将从添加依赖说起&#xff0c;然后创建服务类以实现OCR&#xff0c;最后展示如何处…