[项目] Boost搜索引擎

news2024/11/26 5:52:51

 

目录

1.项目相关背景

2.项目宏观原理

3.技术栈和项目环境

4.正排索引&&倒排索引

5.去标签与数据清洗

6.构建索引模块Index

6.1正排索引

6.2 建立倒排

jiebacpp使用

建立分词

7.搜索引擎模块Searcher

Jsoncpp -- 通过jsoncpp进行序列化和反序列化

处理Content

8.引入http-lib

9.Web前端代码编写

10.项目日志编写

11.项目测试


1.项目相关背景

由于boost官网是没有站内搜索的,因此我们需要自己做一个。我们所做的是站内搜索,所谓站内搜索,其搜索的数据更垂直,数据量更小。

 

2.项目宏观原理

3.技术栈和项目环境

技术栈:C/C++ C++11, STL, 准标准库Boost,Jsoncpp,cppjieba,cpp-httplib , 选学: html5,css,js、jQuery、Ajax.

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

4.正排索引&&倒排索引

文档举例:

文档ID

文档内容

1

雷军买了四斤小米

2

雷军发布了小米手机

正排索引:从文档ID找到文档内容

分词:方便建立倒排索引和查找

雷军买了四斤小米:雷军/买/了/四斤/小米

雷军发布了小米手机:雷军/发布/了/小米/手机

倒排索引:根据关键字,找到文档ID的内容

关键字

文档ID

雷军

1,2

1

四斤

1

小米

1,2

四斤小米

1

发布

2

小米手机

2

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

用户输入:小米->倒排索引中查找到文档ID->提取出文档ID(1,2)->根据正排索引->找到文档ID->构建响应结果

5.去标签与数据清洗

我们只需要boost下doc/html/中文件建立索引

作用:原始数据->去标签->把结果放在同一个行文本文档中

Parser.cc

Parser.cc中有主要有三个函数EnumFile,ParseHtml,SaveHtml

5.1 EnumFile():

作用:递归式把每个html文件名带路径,保存到files_list中

步骤:

  1. 判断路径是否存在
  2. 判断文件是不是普通文件,因为.html文件是普通文件
  3. 判断后缀是否符合要求,必须是.html结尾

要使用Boost库中filesystem

boost开发库安装:sudo yum install -y boost-devel

递归遍历:使用boost库中recursive_directory_iterator

必须是.html文件才可以被遍历插入

iter->path().extension() == ".html"
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;
        }
        //一定是一个普通文件  判断文件后缀 只需要html
        if(iter->path().extension() != ".html")
        {
            continue;
        }
        //std::cout<<"debug: "<<iter->path().string()<<std::endl;
        //当前的路径一定是一个合法的,以.html结束的普通网页文件
        files_list->push_back(iter->path().string());
    }
    return true;
}

5.2 ParseHtml()

作用:读取每个文件的内容,解析文件内容建立DocInfo_t

步骤:

  1. 读取文件
  2. 解析指定文件,提取title -> <title> </title>
  3. 解析指定文件,提取content
  4. 解析指定文件,提取url
typedef struct DocInfo
{
    std::string title;  //文档的标题
    std::string content;//文档内容
    std::string url;    //文档在官网中的url
}DocInfo_t;

bool ParseHtml(const std::vector<std::string>& files_list,std::vector<DocInfo_t>*results)
{
    //遍历文件 解析文件
    for(const std::string &file : files_list)
    {
        //1.读取文件 Read()
        std::string result;
        if(!ns_util::FileUtil::ReadFile(file,&result))
        {
            continue;
        }
        //2.解析指定的文件,提取title
        DocInfo_t doc;
        //解析title
        if(!ParseTitle(result,&doc.title))
        {
            continue;
        }
        //3.解析指定的文件,提取content 本质是去标签
        if(!ParseContent(result,&doc.content))
        {
            continue;
        }
        //4.解析指定的文件,提取url
        if(!ParseUrl(file,&doc.url))
        {
            continue;
        }
        //这里一定是完成了解析任务,当前文档的相关结果都保存在了doc里面
        results->push_back(std::move(doc));//细节 本质会发生拷贝 效率可能比较低
    }
    return true;
}

5.2.1 读取文件

static bool ReadFile(const std::string &file_path,std::string *out)
{
    std::ifstream in(file_path,std::ios::in);
    if(!in.is_open())
    {
        std::cerr<<"open file"<<file_path<<" error " << std::endl;
        return false;
    }
    //文件打开成功
    std::string line;
    //如何理解getline读取到文件结束呢?
    //getline的返回值是一个& 
    //while(bool) 本质是因为返回的对象重载了强制类型转换
    while(std::getline(in,line))
    {
        *out += line;
    }

    in.close();
    return true;
}

5.2.2 解析指定文件,提取title

由于title标题都是在<title> </title>标签之间,因此我们可以使用字符串操作来进行提取

//找到<title> </title>位置,然后选取中间的位置
static 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;
}

5.2.3 去掉标签

去标签是基于一个状态机来读取的,在进行遍历的时候,一旦碰到'>',说明该标签处理完毕了,我们不想保留原始文本中的'\n',就设定为空字符

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解析之后文本的分隔符4
                    if(c == '\n') c= ' ';
                    content->push_back(c);
                }
                break;
            default:
                break;
        }
    }
    return true;
}

5.2.4 拼接url

我们在观察boost官方库的url可以发现,boost库下的官方文档和下载下来的文档有路径对应关系的。

官网URL样例: https://www.boost.org/doc/libs/1_78_0/doc/html/accumulators.html

其中url是由url_head和url_tail拼接而成。而url_head为固定的字符串构成:

"https://www.boost.org/doc/libs/1_81_0/doc/html"

而url_tail正式我们html文件的文件路径名,只保留文件名

//构建url boost库的官方文档,和我们下载下来的文档是有路径对应关系的
static bool ParseUrl(const std::string & file_path,std::string *url)
{ 
    const std::string url_head = "https://www.boost.org/doc/libs/1_81_0/doc/html";
    std::string url_tail = file_path.substr(src_path.size());

    *url = url_head + url_tail;
    return true;
}

5.3 SaveHtml函数

作用:将解析内容写入文档中,一定要考虑下一次在读取的时候也方便操作。因此我们这里采用的格式如下:

类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...

这样方便我们以后使用getline(ifsream, line),直接获取文档的全部内容:title\3content\3url

//每一个文档包含3个部分 title\3 content\3 url \3 \n title\3 content\3 url \3 
//每个文档和文档之间用'/n'分隔开
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;
    }
    //begin
    for(auto &item : results)
    {
        std::string out_string;
        out_string = item.title;
        out_string += SEP;
        out_string += item.content;
        out_string += SEP;
        out_string += item.url;
        out_string += '\n';

        //写入文件
        out.write(out_string.c_str(),out_string.size());
    }
    out.close();
    return true;
}

6.构建索引模块Index

这一步我们要构建正排索引和倒排索引。

    struct DocInfo
    {
        std::string title;  //文档的标题
        std::string content;//文档对应去标签之后的内容
        std::string url;    //文档的url
        uint64_t doc_id;         //文档id
    };

6.1正排索引

//正排索引的数据结构用数组,数组的下标天然是文档的ID

std::vector<DocInfo> forward_index; //正排索引

正排索引是根据doc_id找到文档内容

步骤:

  1. 字符串切分,解析line
  2. 字符串进行填充到DocInfo
  3. 将DocInfo插入到正排索引的vector中
//namespace ns_util 中,类class FileUtil的成员函数
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);
        }
};

DocInfo *BuildForwardIndex(const std::string&line)
{
    //1.解析line,做字符串切分
    std::vector<std::string> results;
    const std::string sep = "\3";//行内分隔符
    ns_util::StringUtil::split(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();//先进行保存id,再插入,对应的id就是当前doc在vector中的下标!
    //3.插入到正排索引的vector
    forward_index.push_back(std::move(doc));//doc.html文件
    return &forward_index.back();
}

这里使用了boost库中的split方法。如果有多个"\3"分割符时,要将第三个参数设置为boost::token_compress_on

注意:先进行保存id,再插入,对应的id就是当前doc在vector中的下标!

6.2 建立倒排

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

根据文档内容,行成一个或者多个InvertedElem(倒排拉链),因为当前我们是在一个文档进行处理的,一个文档中会包含多个"词",都对应到当前doc_id。

  1. 需要对title和content进行分词
  2. 设置词和文档的相关性 -- 我们这里使用词频统计 使用weight,因此我们需要定义一个倒排拉链的结构体,这里做一个简单处理。
  3. 自定义相关性,让在标题出现的关键字的weight值更高。因此weight = 10*title + content。

安装cpp-jieba:

获取链接:cppjieba: cppjieba cppjieba

下载成功后rz -E 加入到我们的项目路径下。之后我们建立软连接到当前的目录之下

ln -s cppjieba/dict dict -- 词库

ln -s cppjieba/include/cppjieba/ cppjieba --相关头文件

注意:在使用cppjieba时有一个坑,比如把deps/limonp 下的文件拷贝到include/cppjieba/ 下才能正常使用

cp deps/limonp include/cppjieba/ -rf

jiebacpp使用

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <mutex>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include "cppjieba/Jieba.hpp"
#include "log.hpp"

namespace ns_util{
    const char* const DICT_PATH = "./dict/jieba.dict.utf8";
    const char* const HMM_PATH = "./dict/hmm_model.utf8";
    const char* const USER_DICT_PATH = "./dict/user.dict.utf8";
    const char* const IDF_PATH = "./dict/idf.utf8";
    const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";

    class JiebaUtil{
        private:
            //static cppjieba::Jieba jieba;
            cppjieba::Jieba jieba;
            std::unordered_map<std::string, bool> stop_words;
        private:
            JiebaUtil():jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH)
            {}
            JiebaUtil(const JiebaUtil&) = delete;

            static JiebaUtil *instance;
        public:

            static JiebaUtil* get_instance()
            {
                static std::mutex mtx;
                if(nullptr == instance){
                    mtx.lock();
                    if(nullptr == instance){
                        instance = new JiebaUtil();
                        instance->InitJiebaUtil();
                    }
                    mtx.unlock();
                }

                return instance;
            }

            void InitJiebaUtil()
            {
                std::ifstream in(STOP_WORD_PATH);
                if(!in.is_open()){
                    LOG(FATAL, "load stop words file error");
                    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()){
                        //说明当前的string 是暂停词,需要去掉
                        iter = out->erase(iter);
                    }
                    else{
                        iter++;
                    }
                }
            }

        public:
            static void CutString(const std::string &src, std::vector<std::string> *out)
            {
                ns_util::JiebaUtil::get_instance()->CutStringHelper(src, out);
                //jieba.CutForSearch(src, *out);
            }
    };

    JiebaUtil *JiebaUtil::instance = nullptr;
    //cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
}

获取单例时可能会有线程安全的问题,我们对其进行加锁

至此我们引入了jieba分词,我们可以正是编写倒排索引了

建立分词

首先对title和content进行分词,因此当title和content分完词后我们要对词和词频建立映射表。我们对title和content进行分词是想统计各个词出现的词频。我们建立vector来保存分出来的词语。

当这个词语出现在title时认为其权重较重,在content出现时认为其权重较轻。

注意:由于搜索的时候本身是不区分大小写的,因此我们在分词结束之后将出现的词语全部转换成小写,然后进行统计。

 bool BuildInvertedIndex(const DocInfo& doc)
            {
                
                //DocInfo{title,content,url,doc_id}
                //word-> 
                //需要对title和content进行分词
                //example: 吃/葡萄/不吐/葡萄皮
                 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(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);
                //对内容进行词频统计
                for(std::string s : content_words){
                    boost::to_lower(s);
                    word_map[s].content_cnt++;
                }
#define X 10
#define Y 1
                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

我们编写Searcher首先需要获取或穿件index对象,然后根据index对象建立索引

建立正排和倒排索引成功之后,用户要进行搜索了。首先我们要先将用户输入的关键字进行分词,然后根据分词的各个词进行index查找,建立index时要忽略大小写。然后进行合并排序,汇总查找结果,按照相关性进行降序排序,将权重高的排在前面,最后我们根据查找出来的结果,构建Json串

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;
                    }
                    for(const auto&elem : *inverted_list)
                    {
                        auto &item = tokens_map[elem.doc_id];
                        //item一定是doc_id相同的节点
                        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));
            }
            std::sort(inverted_list_all.begin(),inverted_list_all.end(),
            [](const InvertedElemPrint &e1,const InvertedElemPrint& e2)
            {
                return e1.weight > e2.weight;
            });
            //4.构建 根据查找出来的结果 构建json -- 通过第三方库
            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是文档的去标签的结果 但是不是我们想要的,我们要的是一部分*/
                elem["url"] = doc->url;
                
                root.append(elem);
            }
            //Json::StyledWriter writer;
            Json::FastWriter writer;
            *json_string = writer.write(root);
        }

查找时我们首先需要获取倒排拉链。

Jsoncpp -- 通过jsoncpp进行序列化和反序列化

jsoncpp的安装:sudo yum install -y jsoncpp-devel

那我们如何使用Jsoncpp呢?我们做一下演示

#include <iostream>
#include <string>
#include <vector>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    Json::Value item1;
    item1["key1"] = "value1";
    item1["key2"] = "value2";

    Json::Value item2;
    item2["key1"] = "value1";
    item2["key2"] = "value2";

    root.append(item1);
    root.append(item2);

    //进行序列化
    Json::StyledWriter writer;
    std::string s = writer.write(root);
    std::cout<<s<<std::endl;
    return 0;
} 

这里注意我们在编译时是需要链接json库的否则会报连接时错误

需要 -ljsoncpp

我们发现打印的结果进行了序列化。我们还有另一种形式FastWriter,这种形式更加简洁

做好这些准备工作之后我们进行构建Json串

这里还有一个需要注意的地方是content是文档的去标签结果,但是不是我们想要的,我们只需要一部分,因此这里需要进行处理。

处理Content

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. 找到首次出现
            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;
        }

8.引入http-lib

引入http-lib:cpp-httplib: cpp-httplib - Gitee.com

注意:在引入http-lib的时候需要较新版本的gcc,使用gcc -v便可查看gcc版本

如果您当前是较低版本,请先升级至较新版本,升级方法:升级GCC-Linux CentOS7

当GCC版本更新之后我们在当前文件下创建httplib的软连接

ln -s /home/Lxy/cpp-httplib-v0.7.15 cpp-httplib

#include "searcher.hpp"
#include "cpp-httplib/httplib.h"
#include "log.hpp"
const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";

int main()
{
    ns_searcher::Searcher search;
    search.InitSearcher(input);
    httplib::Server svr;
    svr.set_base_dir(root_path.c_str());
    svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp)
    {
        if(!req.has_param("word"))
        {
            rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
            return;
        }
        std::string word = req.get_param_value("word");
        //std::cout << "用户在搜索:" << word << std::endl;
        LOG(NORMAL,"用户搜索的: "+word);
        std::string json_string;
        search.Search(word, &json_string);
        rsp.set_content(json_string, "application/json");
        //rsp.set_content("你好,世界!", "text/plain; charset=utf-8");
    });
    LOG(NORMAL,"服务器启动成功....");
    svr.listen("0.0.0.0", 8081);
    return 0;
}

9.Web前端代码编写

了解html,css,js

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>

10.项目日志编写

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

项目部署到Linux服务器上

nohup ./http_server > log/log.txt 2>&1 &

11.项目测试

项目结果:boost 搜索引擎

项目代码地址:project-boost-search-engine

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

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

相关文章

Vscode中Vue文件保存格式化、 ElementUI、Font Awesome俩大插件使用

Vscode中Vue文件老一片红色出现格式错误&#xff1f;&#xff1f;如何运行别人的项目&#xff08;没有node_modules文件&#xff09;&#xff1f;&#xff1f;选用组件与图标&#xff1f;&#xff1f; 解决问题一 前提有&#xff1a;Prettier ESLint插件、ESLint插件 1.打开s…

Git图解-常用命令操作-可视化

目录 一、前言 二、初始化仓库 2.1 设置用户名与邮箱 2.2 初始化仓库 三、添加文件 四、查看文件状态 五、查看提交日志 六、查看差异 七、版本回退 八、删除文件 九、分支管理 9.1 创建分支 9.2 切换分支 9.3 查看分支 9.4 合并分支 十、文件冲突 十一、转视…

ROS从入门到精通系列(二十九)-- linux封装ROS 自启动脚本及可执行程序App

机器人分布式处理架构ROS分布式架构,HiBot架构主要使用C/S架构,其中HibotServer为服务器,Muqutte为消息服务器中间件,HiBotClient为运行在机器人上的客户端。主要实现了机器人任务的远程部署、监控、控制三大功能,机器人平台依赖于ROS。其架构如下图所示 Img Src: https:/…

第53篇-某商城sign参数分析-webpack【2023-03-07】

声明:该专栏涉及的所有案例均为学习使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!如有侵权,请私信联系本人删帖! 文章目录 一、前言二、网站分析三、完整代码一、前言 今天再来试一个webpack的例子吧,网址: aHR0cHM6Ly9tLnlxYi5jb20vYmFuay9…

【NLP相关】XLNET原理以及案例

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️&#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

类成员的方法

初识对象 生活中或是程序中&#xff0c;我们都可以使用设计表格、生产表格、填写表格的形式组织数据进行对比&#xff0c;在程序中&#xff1a; 设计表格&#xff0c;称之为&#xff1a;设计类&#xff08;class&#xff09; 打印表格&#xff0c;称之为&#xff1a;创建对象 …

C语言--一维数组

数组概念 数组&#xff1a;是一种构造数据类型&#xff0c;用以处理批量的同种类型的数据。 主要特点&#xff1a;数据量大 &#xff0c;类型相同 一维数组的定义 语法&#xff1a; 类型说明符 数组名[整型常量表达式]&#xff1b; 注意&#xff1a; 方括号里面的内容用于指…

css3横向无限公告消息滚动功能

html部分 {{item}}css部分 .boxingeds{ display: flex; flex-wrap: wrap; width: 150%; position: relative; left: 1000rpx; padding: 30rpx 0; position: absolute; top: 23%; z-index: 2; -webkit-animation: myfirst 30s linear 2s infinite; .textname{ display: inlin…

数字三角形 购物单

题目&#xff1a; 题目描述 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径&#xff0c;把路径上面的数加起来可以得到一个和&#xff0c;你的任务就是找到最大的和。 路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右 边…

从0开始自制解释器——实现简单的加法计算器

为什么要学习编译器和解释器呢&#xff1f;文中的作者给出的答案有下面几个&#xff1a; 为了深入理解计算机是如何工作的&#xff1a;一个显而易见的道理就是&#xff0c;如果你不懂编译器和解释器是如何工作的那么你就不明白计算机是如何工作的编译器和解释器用到的一些原理…

InnoDB——详细说明索引中B+树的操作和原理

本内容针对Mysql5.x&#xff1b; 索引是应用程序设计和开发的一个重要方面。 若索引太多&#xff0c;应用程序的性能可能会收到影响。 而索引太少&#xff0c;对查询性能又会产生影响。 索引的注意事项&#xff1a; 如果知道数据的使用&#xff0c;从一开始就应该在需要处添加…

车企数据分类分级的实践指南出炉!“数据安全推进计划”发布,奇点云参编

日前&#xff0c;“数据安全推进计划”&#xff08;DSI&#xff09;正式发布《智能网联汽车数据分类分级实践指南》&#xff08;下文简称“指南”&#xff09;&#xff0c;旨在以合规为主要导向&#xff0c;明确智能网联汽车数据分类分级的方法论&#xff0c;为数据全生命周期的…

每日学术速递3.7

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Dropout Reduces Underfitting 标题&#xff1a;Dropout 减少欠拟合 作者&#xff1a;Zhuang Liu, Zhiqiu Xu, Joseph Jin, Zhiqiang Shen, Trevor Darrel 文章链接&#xff1a;h…

SpringBoot(Tedu)—DAY01——环境搭建

SpringBoot(Tedu)—DAY01——环境搭建 目录SpringBoot(Tedu)—DAY01——环境搭建零、今日目标一、IDEA2021项目环境搭建1.1 通过 ctrl鼠标滚轮 实现字体大小缩放1.2 自动提示设置 去除大小写匹配1.3 设置参数方法自动提示1.4 设定字符集 要求都使用UTF-8编码1.5 设置自动编译二…

controller-runtime搭建operator开发环境

目录 基本结构 注入CRD 基本结构 首先下载相应的go pkg go get -u sigs.k8s.io/controller-runtime 接下来需要创建控制器和Manager Operator的本质是一个可重入的队列编程模式&#xff0c;而Manager可以用来管理Controller、Admission Webhook&#xff0c;包括访问资源对…

MATLAB算法实战应用案例精讲-【优化算法】樽海鞘群算法(SSA)及其算法变种(附matlab代码实现)

目录 前言 算法原理 算法思想 数学模型 &#xff08;1&#xff09;种群初始化 &#xff08;2&#xff09;领导者位置更新 &#xff08;3&#xff09;跟随者位置更新 代码实现 算法流程图 算法步骤 伪代码 SSA伪代码 MSSA伪代码 面向全局搜索的自适应领导者樽海鞘群算…

同模块设置不同应用主题方案

有时候公司内部会有不同应用但是有部分模块功能一样&#xff0c;只根据应用角色有些细节逻辑区分的场景。这时候往往采用模块化采用以应用至不同的APP。如果APP主题不一致&#xff0c;该如果解决。 方案&#xff1a; 在不同应用的config.gradle 下面根据不同应用定义不同的a…

基于SSM+SpringBoot《CRM客户关系管理系统》实战开发教程(附文档及源码)

1.项目简介 客户关系管理&#xff08;Customer Relationship Management&#xff0c;简称CRM&#xff09;&#xff0c;是指企业为提高核心竞争力&#xff0c;利用相应的信息技术以及互联网技术协调企业与顾客间在销售、营销和服务上的交互&#xff0c;从而提升其管理方式&…

AUTOSAR知识点Com(一):CANIf入门知识

目录 1、概述 2、上下层关系 2.1 上层 2.2 下层 3、链接 4、记录项 1、概述 下面主要是规范方面的描述&#xff1a; 参考文档《AUTOSAR_SWS_CANInterface.pdf》 CAN接口模块&#xff08;下文简“CanIf”&#xff09;位于底层CAN驱动&#xff08;CanDrv&#xff09;、CA…

华为机试题:HJ103 Redraiment的走法(python)

文章目录&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;Python3实现&#xff08;3&#xff09;知识点详解1、input()&#xff1a;获取控制台&#xff08;任意形式&#xff09;的输入。输出均为字符串类型。1.1、input() 与 list(input()) 的区别、及其相互转换方…