【C++|Linux|计网】构建Boost站内搜索引擎的技术实践与探索

news2025/5/21 5:00:58

目录

1、项目的相关背景

2.搜索引擎的相关宏观原理

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

4.正排索引vs倒排索引-搜索引擎具体原理

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

5.1.去标签

目标:

5.2.代码的整体框架:

EnumFile函数的实现:

EnumFile测试结果

如何提取网页的url呢?

测试解析网页title,content,url是否正确?

6.编写建立索引的模块index

6.1.index模块的基本框架:

6.2.建立正派索引:

split()函数具体使用说明:

6.3.建立倒排索引:

7.编写搜索引擎模块searcher

7.1.基本代码框架:

7.2.建立摘要

问题:搜索结果出现重复文档的问题

7.3.修改后去重的代码:

7.4.测试:

8.编写 http_server 模块

9.简单的日志系统

10.前端代码

11.最终的测试结果(成品展示)

12.个人问题汇总:

问题一:

问题二:

问题三:

问题四:

问题五:

问题六:

问题七:

1、项目的相关背景

  • 公司:百度、搜狗、360搜索、头条新闻客户端,我们自己实现是不可能的!
  • 站内搜索:搜索的数据更垂直,数据量其实更小
  • boost的官网是没有站内搜索的,需要我们自己做⼀个

boost网站中是没有相关的搜索引擎的,我们自己实现一个!

boost 官网: https://www.boost.org/

我们使用最新的boost_1_86_0/doc/html⽬录下的html⽂件,⽤它来进⾏建⽴索引

2.搜索引擎的相关宏观原理

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

技术栈:C/C++ C++11, STL, 标准库Boost,Jsoncpp,cppjieba,cpp-httplib ,

选学: html5,css,js、jQuery、Ajax(前端) 

项目环境: Ubuntu 9.4.0云服务器,vim/gcc(g++)/Makefile ,vs2022 or vscode

4.正排索引vs倒排索引-搜索引擎具体原理

  • 文档1:雷军买了四斤小米
  • 文档2:雷军发布了小米手机

正排索引:就是从⽂档ID找到⽂档内容(⽂档内的关键字)

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

  • 文档1[雷军买了四斤小米]: 雷军/买/四斤/小米/四斤小米
  • 文档2[雷军发布了小米手机]:雷军/发布/小米/小米手机

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

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

模拟一次查找的过程:
用户输入:小米 ->倒排索引中查找->提取出文档ID(1,2)->根据正排索引->找到文档的内容 ->
title+conent(desc)+url 文档结果进行摘要->构建响应结果

倒排->正排->文档摘要

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

5.1.去标签

我们首先需要将boost网站里的站内资源进行下载,并压缩到我们的项目当中,作为初始数据进行保存

什么是标签?

<> : html的标签,这个标签对我们进⾏搜索是没有价值的,需要去掉这些标签,⼀般标签都是成 对出现的!

为什么要去标签?我们随便打开一个压缩好的网页资源,他是这样的:

大部分内容其实都是标签,对我们进行搜索是没有用的,所以我们要进行去标签。

目标:

把每个文档都去标签,然后写入到同一个文件中!每个文档内容不需要任何\n!文档和文档之间用\3 区分

parser文件的编写

5.2.代码的整体框架:

#include <iostream>
#include <string>
#include <vector>
#include<boost/filesystem.hpp>
#include"util.hpp"

const std::string src_path = "data/input/";        //这⾥放的是原始的html⽂档 
const std::string output = "data/raw_html/raw.txt";//这是放的是去标签之后的⼲净⽂档 


typedef struct DocInfo
{
    std::string title;  // 文档的标题
    std::string contnt; // 文档内容
    std::string url;    // 该文档在官网中的url
} DocInfo_t;

// const &: 输⼊
//*: 输出
//&:输⼊输出
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 error!" << std::endl;
        return 1;
    }
    // 第⼆步: 按照files_list读取每个⽂件的内容,并进⾏解析
    std::vector<DocInfo_t> results;
    if (!ParseHtml(files_list, &results))
    {
        std::cerr << "parse html error" << std::endl;
        return 2;
    }
    // 第三步: 把解析完毕的各个⽂件内容,写⼊到output,按照\3作为每个⽂档的分割符
    if (!SaveHtml(results, output))
    {
        std::cerr << "sava html error" << std::endl;
        return 3;
    }

    return 0;
}

EnumFile函数的实现:

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))
    {
        std::cerr << src_path << "not exists" << std::endl;
        return false;
    }

    //定义一个空的迭代器,用来判断递归结束
    fs::recursive_directory_iterator end;
    for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
    {
        //判断文件是否是普通文件,html都是普通文件
        if(!fs::is_regular_file(*iter))
        {
            continue;
        }
        if(iter->path().extension() != ".html")//判断文件名的后缀是否符合要求
        {
            continue;
        }
        //std::cout << "debug: " << iter->path().string() << std::endl;
        //当前的路径一定是一个合法的,以.html结束的普通网页文件
        files_list->push_back(iter->path().string());//将所有带路径的html保存在file_list,方便后续进行文本分析
    }
    return true;
}

EnumFile测试结果

如下,可以把所有的.html网页输出出来了

我们提取网页中的title和content都比较简单。

提取title是直接在网页内容中查找<title>,然后进行字符串的截取即可。

bool ParseTitle(const std::string &file, std::string *title)
{
    std::size_t begin = file.find("<title>");
    if(begin == std::string::npos)
    {
        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);
    return true;
}

提取content就是一个去标签的过程,我们这里采用的是基于简单的状态机进行去标签。

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;
}

如何提取网页的url呢?

boost库的官方文档,和我们下载下来的文档,是有路径的对应关系的
官网URL样例:
https://www.boost.org/doc/libs/1_86_0/doc/html/accumulators.html
我们下载下来的url样例:boost/1_86_0/doc/html/accumulators.html
我们拷贝到我们项目中的样例:data/input/accumulators.html //我们把下载下来的boost库doc/html/* copy data/input/
url head ="https://www.boost.org/doc/libs/1_86_0/doc/html";

url tail = [data/input](删除)/accumulators.html -> url tail =/accumulators.html
url = url_head + url_tail ;相当于形成了一个官网链接

bool ParseUrl(const std::string &file_path ,std::string *url)
{
    std::string url_head = "https://www.boost.org/doc/libs/1_86_0/doc/html/";
    std::string url_tail = file_path.substr(src_path.size());
    *url = url_head + url_tail;
    return true;
}

将解析内容写入文件中:

bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3'
    //按照二进制方式进行写入
    std::ofstream out (output, std::ios::out | std::ios::binary);
    if(!out.is_open())
    {
        std::cerr << "open " << output << " failed!" << std::endl;
        return false;
    }

    //可以开始进行文件内容的写入了
    for(auto &item : results)
    {
        std::string out_string;
        out_string = item.title;
        out_string += SEP;
        out_string += item.contnt;
        out_string += SEP;
        out_string += item.url;
        out_string += '\n';
        out.write(out_string.c_str(), out_string.size());
    }
    return true;
}

测试解析网页title,content,url是否正确?

vim data/input/mpi/history.html

在自己下载的文件里面进行验证,发现正确,没问题!

在网站中验证,也没问题!

最后将测试将结果内容填充到raw.txt

 

6.编写建立索引的模块index

6.1.index模块的基本框架:

#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <mutex>
#include <unordered_map>
#include "util.hpp"

namespace ns_index
{
    struct DocInfo
    {
        std::string title;   // 文档的标题
        std::string content; // 文档对应的去标签之后的内容
        std::string url;     // 官网文档url
        uint64_t doc_id;     // 文档的ID,暂时先不做过多理解
    };

    struct InvertedElem
    {
        uint64_t doc_id;
        std::string word;
        int weight; // 权重
    };

    // 倒排拉链
    typedef std::vector<InvertedElem> InvertedList;

    class Index
    {
    private:
        // 正排索引的数据结构用数组,数组的下标天然是文档的ID
        std::vector<DocInfo> forward_index; // 正排索引
        // 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
        std::unordered_map<std::string, InvertedList> inverted_index; // 倒排索引

    private:
        Index() {} // 但是一定要有函数体,不能delete
        Index(const Index &) = delete;
        Index &operator=(const Index &) = delete;

        static Index *instance;
        static std::mutex mtx;

    public:
        ~Index() {}

    public:
        static Index *GetInstance()
        {
            if (nullptr == instance)
            {
                mtx.lock();
                if (nullptr == instance)
                {
                    instance = new Index();
                }
                mtx.unlock();
            }
            return instance;
        }

    public:
        // 根据doc_id找到文档内容
        DocInfo *GetForwardIndex(uint64_t doc_id)
        {
            if (doc_id >= forward_index.size())
            {
                std::cerr << "doc_id out range, error!" << std::endl;
                return nullptr;
            }
            return &forward_index[doc_id];
        }
        // 根据关键字string,获得倒排拉链
        InvertedList *GetInvertedList(const std::string &word)
        {
            auto iter = inverted_index.find(word);
            if (iter == inverted_index.end())
            {
                std::cerr << word << " have no InvertedList" << std::endl;
                return nullptr;
            }
            return &(iter->second);
        }
        // 根据去标签,格式化之后的文档,构建正排和倒排索引
        bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
        {
            std::ifstream in(input, std::ios::in | std::ios::binary);
            if (!in.is_open())
            {
                std::cerr << "sorry, " << input << " open error" << std::endl;
                return false;
            }

            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 deubg
                    continue;
                }

                BuildInvertedIndex(*doc);
                count++;
                if(count % 50 == 0){
                std::cout <<"当前已经建立的索引文档: " << count <<std::endl;
                // LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count));
                }
            }
            return true;
        }

6.2.建立正派索引:

DocInfo *BuildForwardIndex(const std::string &line)
        {
            // 1. 解析line,字符串切分
            // line -> 3 string, title, content, url
            std::vector<std::string> results;
            const std::string sep = "\3"; // 行内分隔符
            ns_util::StringUtil::Split(line, &results, sep);
            // ns_util::StringUtil::CutString(line, &results, sep);
            if (results.size() != 3)
            {
                return nullptr;
            }
            // 2. 字符串进行填充到DocIinfo
            DocInfo doc;
            doc.title = results[0];            // title
            doc.content = results[1];          // content
            doc.url = results[2];              /// url
            doc.doc_id = forward_index.size(); // 先进行保存id,在插入,对应的id就是当前doc在vector中的下标!
            // 3. 插入到正排索引的vector
            forward_index.push_back(std::move(doc)); // doc,html文件内容
            return &forward_index.back();
        }

这里正排索引在切分字符串的时候,我采用了boost库中的split函数

    class StringUtil
    {
    public:
        static void Split(const std::string &target, std::vector<std::string> *out, const std::string &sep)
        {
            // boost split
            boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
        }
    };

split()函数具体使用说明:

boost 库中split函数用来字符串的切割

引用的头文件 <boost/algorithm/string.hpp>

boost::split()函数用于切割string字符串,将切割之后的字符串放到一个std::vector<std::string> 之中;

有4个参数:

以boost::split(type, select_list, boost::is_any_of(","), boost::token_compress_on);

(1)、type类型是std::vector<std::string>,用于存放切割之后的字符串

(2)、select_list:传入的字符串,可以为空。

(3)、boost::is_any_of(","):设定切割符为,(逗号)

(4)、 boost::token_compress_on:将连续多个分隔符默认为压缩一个!默认没有打开,当用的时候一般是要打开的。

测试代码:

最后输出就是三部分,没有空格!

6.3.建立倒排索引:

需要对 title && content都要先分词 --使⽤jieba分词,并且搜索内容不区分大小写,统一变成小写。

 使用jieba的时候有一个坑,需要我们手动将limonp这个头文件拷贝到include头文件当中,不然编译会报错!

        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);

            // if(doc.doc_id == 1572){
            //     for(auto &s : title_words){
            //         std::cout << "title: " << s << std::endl;
            //     }
            // }

            // 对标题进行词频统计
            for (std::string s : title_words)
            {
                boost::to_lower(s);      // 需要统一转化成为小写
                word_map[s].title_cnt++; // 如果存在就获取,如果不存在就新建
            }

            // 对文档内容进行分词
            std::vector<std::string> content_words;
            ns_util::JiebaUtil::CutString(doc.content, &content_words);
            // if(doc.doc_id == 1572){
            //     for(auto &s : content_words){
            //         std::cout << "content: " << s << std::endl;
            //     }
            // }

            // 对内容进行词频统计
            for (std::string s : content_words)
            {
                boost::to_lower(s);
                word_map[s].content_cnt++;
            }

#define X 10
#define Y 1
            // Hello,hello,HELLO
            for (auto &word_pair : word_map)
            {
                InvertedElem item;
                item.doc_id = doc.doc_id;
                item.word = word_pair.first;
                item.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt; // 相关性
                InvertedList &inverted_list = inverted_index[word_pair.first];
                inverted_list.push_back(std::move(item));
            }

            return true;
        }
    };

7.编写搜索引擎模块searcher

7.1.基本代码框架:

#include "index.hpp"
namespace ns_searcher{
 class Searcher{
 private:
 ns_index::Index *index; //供系统进⾏查找的索引 
 public:
 Searcher(){}
 ~Searcher(){}
 public:
 void InitSearcher(const std::string &input)
{
 //1. 获取或者创建index对象 
 //2. 根据index对象建⽴索引 
 }
 //query: 搜索关键字 
 //json_string: 返回给⽤⼾浏览器的搜索结果 
 void Search(const std::string &query, std::string *json_string)
 {
 //1.[分词]:对我们的query进⾏按照searcher的要求进⾏分词 
 //2.[触发]:就是根据分词的各个"词",进⾏index查找 
 //3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序 
 //4.[构建]:根据查找出来的结果,构建json串 -- jsoncpp 
 }
 };
}

7.2.建立摘要

为什么要建立摘要?
因为我们正常在搜索引擎搜到的内容,是不可能将网页的一整个内容显示给客户的,一定要将网页的摘要返回给客户,相当于提炼出主旨,那我们怎么实现呢?
找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)

注意定义start和end双指针的时候,要注意size_t类型与int类型的符号比较,很容易出错!

  1. 由于size_t是无符号类型,如果使用不当(比如使用负数做运算),可能会导致意想不到的结果。例如,将负数赋值给size_t会导致它变成一个很大的正数。

代码:

std::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 = 100;
                //1. 找到首次出现
                //不能使用find查找,可能因为大小写不匹配而报错
                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);

                //2. 获取start,end , std::size_t 无符号整数
                int start = 0; 
                int end = html_content.size() - 1;
                //如果之前有50+字符,就更新开始位置
                if(pos > start + prev_step) start = pos - prev_step;
                if(pos < end - next_step) end = pos + next_step;

                //3. 截取子串,return
                if(start >= end) return "None2";
                std::string desc = html_content.substr(start, end - start);
                desc += "...";
                return desc;
            }

问题:搜索结果出现重复文档的问题

比如我们在搜索“你是一个好人”时,jieba会将该语句分解为你/一个/好人/一个好人,在建立图的时候,可能会指向同一个文档,导致我们在搜索的时候会出现重复的结果。

现象:

我们将一个boost库中的文档修改内容为“你是一个好人”,我们在搜索你是一个好人的时候就会出现重复结果:

所以我们要做去重操作,如何判断相同呢?直接看文档id即可。并且要将权值修改,我们应该将搜索到的相同内容进行权值的累加,作为该文档的真正权值!

去重之后的效果:

7.3.修改后去重的代码:

#pragma once

#include "index.hpp"
#include "util.hpp"
#include "log.hpp"
#include <algorithm>
#include <unordered_map>
#include <jsoncpp/json/json.h>

namespace ns_searcher{

    struct InvertedElemPrint{
        uint64_t doc_id;
        int weight;
        std::vector<std::string> words;
        InvertedElemPrint():doc_id(0), weight(0){}
    };

    class Searcher
    {
        private:
            ns_index::Index *index; //供系统进行查找的索引
        public:
            Searcher(){}
            ~Searcher(){}
        public:
            void InitSearcher(const std::string &input)
            {
                //1. 获取或者创建index对象
                index = ns_index::Index::GetInstance();
                std::cout << "获取index单例成功..." << std::endl;
                //LOG(NORMAL, "获取index单例成功...");
                //2. 根据index对象建立索引
                index->BuildIndex(input);
                std::cout << "建立正排和倒排索引成功..." << std::endl;
                //LOG(NORMAL, "建立正排和倒排索引成功...");
            }
            //query: 搜索关键字
            //json_string: 返回给用户浏览器的搜索结果
            void Search(const std::string &query, std::string *json_string)
            {
                //1.[分词]:对我们的query进行按照searcher的要求进行分词
                std::vector<std::string> words;
                ns_util::JiebaUtil::CutString(query, &words);
                //2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索,关键字也需要
                //ns_index::InvertedList inverted_list_all; //内部InvertedElem
                std::vector<InvertedElemPrint> inverted_list_all;

                std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;

                for(std::string word : words){
                    boost::to_lower(word);

                    ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
                    if(nullptr == inverted_list){
                        continue;
                    }
                    //不完美的地方: 你/是/一个/好人 100
                    //inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
                    for(const auto &elem : *inverted_list){
                        auto &item = tokens_map[elem.doc_id]; //[]:如果存在直接获取,如果不存在新建
                        //item一定是doc_id相同的print节点
                        item.doc_id = elem.doc_id;
                        item.weight += elem.weight;
                        item.words.push_back(elem.word);
                    }
                }
                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 ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){
                //        return e1.weight > e2.weight;
                //        });
                  std::sort(inverted_list_all.begin(), inverted_list_all.end(),\
                          [](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
                          return e1.weight > e2.weight;
                          });
                //4.[构建]:根据查找出来的结果,构建json串 -- jsoncpp --通过jsoncpp完成序列化&&反序列化
                Json::Value root;
                for(auto &item : inverted_list_all){
                    ns_index::DocInfo * doc = index->GetForwardIndex(item.doc_id);
                    if(nullptr == doc){
                        continue;
                    }
                    Json::Value elem;
                    elem["title"] = doc->title;
                    elem["desc"] = GetDesc(doc->content, item.words[0]); //content是文档的去标签的结果,但是不是我们想要的,我们要的是一部分 TODO
                    elem["url"]  = doc->url;
                    //for deubg, for delete
                    elem["id"] = (int)item.doc_id;
                    elem["weight"] = item.weight; //int->string

                    root.append(elem);
                }

                Json::StyledWriter writer;
                //Json::FastWriter writer;
                *json_string = writer.write(root);
            }

             std::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 = 100;
                //1. 找到首次出现
                //不能使用find查找,可能因为大小写不匹配而报错
                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);

                //2. 获取start,end , std::size_t 无符号整数
                int start = 0; 
                int end = html_content.size() - 1;
                //如果之前有50+字符,就更新开始位置
                if(pos > start + prev_step) start = pos - prev_step;
                if(pos < end - next_step) end = pos + next_step;

                //3. 截取子串,return
                if(start >= end) return "None2";
                std::string desc = html_content.substr(start, end - start);
                desc += "...";
                return desc;
            }
        
    };
}

7.4.测试:

打出来的是不是按权值进行排序的呢?我们可以将weight打印出来看看

最大是16 ,最小是1,我们打开网站自己验证一下

这是16的,在文章内容中一共出现了16次,下面是1次的

一共出现1次正确!!!

8.编写 http_server 模块

我们这里不用自己去搭建轮子,直接用网上的cpp-httplib库即可搭建网络通信。

 httpserver的基本测试代码:

#include"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",8085);
    return 0;
}

没问题!

所以我们只要会使用基本的接口即可

9.简单的日志系统

#pragma once

#include <iostream>
#include <string>
#include <ctime>

#define NORMAL  1
#define WARNING 2
#define DEBUG   3
#define FATAL   4

#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)

void log(std::string level, std::string message, std::string file, int line)
{
    std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file << " : " << line << "]" << std::endl;
}

10.前端代码

因为我们的重点主要在于后端,所以前端的代码不讲解。

原码:

<!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>

11.最终的测试结果(成品展示)

首页:

当我们在搜索框搜索file system后,显示出来的搜索结果,因为之前已经验证其正确性,所以这里直接就展示了。

打开第一个网站,直接就跳转到boost库中相应的网页。

非常的完美!

12.个人问题汇总:

问题一:

首先就是项目本身存在的问题就是解决搜索结果出现重复文档的问题,在第7部分已经解决讲解完毕!

问题二:

单例模式加锁的时候,为什么是双判断

第一层判断是为了提效,后面的线程不需要再申请锁,提高效率;第二层判断是为了保证线程安全,保证了只创建1个对象


问题三:

如何使用lamdar表达式?

[](){}

记住方括号、圆括号、花括号

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。必写!

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。必写,用来书写如何进行比较

问题四:

建立软链接的目的是什么?

  1. 统一资源访问点:在统一的目录下创建软连接,可以将分布在不同位置的资源集中管理,提高工作效率。例如,在开发过程中,可以将多个项目所需的共享库文件链接到统一的目录下,方便程序链接和使用。
  2. 避免重复文件:通过软连接,可以避免相同内容的多个副本,从而减少了存储空间的浪费。这在数据备份和迁移过程中尤为重要,可以避免不必要的数据复制操作。

问题五:

index.hpp最后这两行到底有什么意义

静态成员变量在类定义中声明,并在类外部定义和初始化。

问题六:

Styledwriter和Fastwriter有什么区别

styledwriter输出有空行,更加美观,方便测试人员测试;而Fastwriter直接正常输出

问题七:

这里类型一定要多注意,不能使用size_t是无符号整型,会报错

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

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

相关文章

ComfyUI绘画|提示词反推工作流,实现自动化书写提示词

今天先分享到这里~ ComfyUI绘画|关于 ComfyUI 的学习建议

高频面试题(含笔试高频算法整理)基本总结回顾20

干货分享&#xff0c;感谢您的阅读&#xff01; &#xff08;暂存篇---后续会删除&#xff0c;完整版和持续更新见高频面试题基本总结回顾&#xff08;含笔试高频算法整理&#xff09;&#xff09; 备注&#xff1a;引用请标注出处&#xff0c;同时存在的问题请在相关博客留言…

【AI模型对比】AI新宠Kimi与ChatGPT的全面对比:技术、性能、应用全揭秘

文章目录 Moss前沿AI技术背景Kimi人工智能的技术积淀ChatGPT的技术优势 详细对比列表模型研发Kimi大模型的研发历程ChatGPT的发展演进 参数规模与架构Kimi大模型的参数规模解析ChatGPT的参数体系 模型表现与局限性Kimi大模型的表现ChatGPT的表现 结论&#xff1a;如何选择适合自…

性能测试基础知识jmeter使用

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;测试_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 性能指标 1. 并发数 (Con…

如何通过 Windows 自带的启动管理功能优化电脑启动程序

在日常使用电脑的过程中&#xff0c;您可能注意到开机后某些程序会自动运行。这些程序被称为“自启动”或“启动项”&#xff0c;它们可以在系统启动时自动加载并开始运行&#xff0c;有时甚至在后台默默工作。虽然一些启动项可能是必要的&#xff08;如杀毒软件&#xff09;&a…

基于PSO粒子群优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

STM32 Jlink Flash读写固件数据

目录 一、从单片机读数据 1.创建工程XX.jflash,已经有的工程不需要创建直接打开 2.创建完成&#xff0c;连接jlink 3.读取整个芯片的数据 4.读取完成后保存数据 5.选择保存的数据格式&#xff0c;以及位置&#xff0c;读数据完成 二、写固件数据到单片机 1.创建工程XX.j…

Scrapy解析JSON响应v

在 Scrapy 中解析 JSON 响应非常常见&#xff0c;特别是当目标网站的 API 返回 JSON 数据时。Scrapy 提供了一些工具和方法来轻松处理 JSON 响应。 1、问题背景 Scrapy中如何解析JSON响应&#xff1f; 有一只爬虫(点击查看源代码)&#xff0c;它可以完美地完成常规的HTML页面…

波动理论、传输线和S参数网络

波动理论、传输线和S参数网络 传输线 求解传输线方程 对于传输线模型&#xff0c;我们通常用 R L G C RLGC RLGC 来表示&#xff1a; 其中 R R R 可以表示导体损耗&#xff0c;由于电子流经非理想导体而产生的能量损耗。 G G G 表示介质损耗&#xff0c;由于非理想电介质…

鸿蒙开发——使用ArkTs处理XML文本

1、概 述 XML&#xff08;可扩展标记语言&#xff09;是一种用于描述数据的标记语言&#xff0c;旨在提供一种通用的方式来传输和存储数据&#xff0c;特别是Web应用程序中经常使用的数据。XML并不预定义标记。因此&#xff0c;XML更加灵活&#xff0c;并且可以适用于广泛的应…

微信小程序介绍-以及写项目流程(重要)

前言&#xff1a;本篇文章介绍微信小程序以及项目介绍&#xff1a; 文章介绍&#xff1a;介绍了微信小程序常用的指令、组件、api。tips&#xff1a;最好按照官方文档来进行学习&#xff0c;大致可以我的目录来学习&#xff0c;对于写项目是没有问题的 微信小程序官方文档https…

嵌入式蓝桥杯学习5 定时中断实现按键

Cubemx配置 打开cubemx。 前面的配置与前文一样&#xff0c;这里主要配置基本定时器的定时功能。 1.在Timer中点击TIM6&#xff0c;勾选activated。配置Parameter Settings中的预分频器&#xff08;PSC&#xff09;和计数器&#xff08;auto-reload Register&#xff09; 补…

特别分享!SIM卡接口功能及其电路设计相关注意事项

SIM卡接口功能及其电路设计相关注意事项对电子工程师来说非常重要。SIM卡接口用于连接SIM卡并读取SIM卡信息&#xff0c;以便在注册4G网络时进行鉴权身份验证&#xff0c;是4G通信系统的必要功能。 一、SIM卡接口功能描述 Air700ECQ/Air700EAQ/Air700EMQ系列模组支持1路USIM接…

OpenGL ES详解——文字渲染

目录 一、文字渲染 二、经典文字渲染&#xff1a;位图字体 1.概念 2.优缺点 三、现代文字渲染&#xff1a;FreeType 1.着色器 2.渲染一行文字 四、关于未来 一、文字渲染 当你在图形计算领域冒险到了一定阶段以后你可能会想使用OpenGL来绘制文字。然而&#xff0c;可能…

devops-Dockerfile+Jenkinsfile方式部署Java前后端应用

文章目录 概述部署前端Vue应用一、环境准备1、Dockerfile2、.dockerignore3、nginx.conf4、Jenkinsfile 二、Jenkins部署1、新建任务2、流水线3、Build Now 构建 & 访问 Springboot后端应用1. 准备工作2. 创建项目结构3. 编写 Dockerfile后端 Dockerfile (backend/Dockerfi…

VTK编程指南<三>:基于VTK入门程序解析来理解VTK基础知识

1、VTK入门程序 下面是一个完整的Vtk入门程序&#xff0c;我们基于这个程序来对VTK的基本知识进行一个初步了解。 #include <iostream>#include <vtkAutoInit.h> VTK_MODULE_INIT(vtkRenderingOpenGL2);// VTK was built with vtkRenderingOpenGL2 VTK_MODULE_INI…

十二、消息队列-MQ

文章目录 前言一、MQ介绍1. 背景2. 解决思路3. 解决方案 二、应用场景三、常见的MQ产品四、MQ选型总结五、相关知识1. AMQP2. JMS 五、如何设计实现一个消息队列1. 设计消息队列的思路2. 实现队列基本功能1. RPC通信协议2. 高可用3. 服务端承载消息堆积的能力4. 存储子系统的选…

新手如何做好一份技术文档

对于新手来说&#xff0c;编写技术文档可能是一项挑战&#xff0c;但这也是一个提升自己技术写作能力的绝佳机会。技术文档不仅仅是代码的补充说明&#xff0c;它更是团队协作和项目成功的基石。本文将为你提供一些实用的指导和建议&#xff0c;帮助你编写出一份高质量的技术文…

如何设置PPT以“只读方式”打开?3种简单方法!

在PPT中设置文件为“只读”模式&#xff0c;可以防止自己意外修改&#xff0c;也可以防止他人对文件内容进行修改&#xff0c;确保文件的安全性。根据需求不同&#xff0c;PPT可以设置3种不同的”只读方式“&#xff0c;一起来看看吧&#xff01; 方式1&#xff1a;设置文件为只…

DICOM医学影象应用篇——多平面重建(MPR)在DICOM医学影像中的应用详解

目录 MPR(多平面重建)概述 基本原理 具体实现 代码详解 总结 MPR(多平面重建)概述 多平面重建&#xff08;MPR, Multi-Planar Reconstruction&#xff09;是一项用于从三维医学影像数据集中生成不同平面的二维切片的技术。通常应用于CT或MRI数据集&#xff0c;MPR可以帮助医…