当前位置: 首页 > news >正文

[项目] Boost搜索引擎

 

目录

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;}//一定是一个普通文件  判断文件后缀 只需要htmlif(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.解析指定的文件,提取titleDocInfo_t doc;//解析titleif(!ParseTitle(result,&doc.title)){continue;}//3.解析指定的文件,提取content 本质是去标签if(!ParseContent(result,&doc.content)){continue;}//4.解析指定的文件,提取urlif(!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解析之后文本的分隔符4if(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;}//beginfor(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;    //文档的urluint64_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 splitboost::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.字符串进行填充DocInfoDocInfo doc;doc.title = results[0];doc.content = results[1];doc.url = results[2];doc.doc_id = forward_index.size();//先进行保存id,再插入,对应的id就是当前doc在vector中的下标!//3.插入到正排索引的vectorforward_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 1for(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;//内部放的是InvertedElemstd::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. 截取子串,returnif(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

相关文章:

[项目] Boost搜索引擎

目录 1.项目相关背景 2.项目宏观原理 3.技术栈和项目环境 4.正排索引&&倒排索引 5.去标签与数据清洗 6.构建索引模块Index 6.1正排索引 6.2 建立倒排 jiebacpp使用 建立分词 7.搜索引擎模块Searcher Jsoncpp -- 通过jsoncpp进行序列化和反序列化 处理Cont…...

解决新版QGIS找不到Georeferencer插件

目录1. 问题分析1.1 去 Raster 找&#xff0c;没找到1.2 去插件搜&#xff0c;未搜到1.3 插件库里也搜不到2. 解决办法在 QGIS 3.30中&#xff0c;按常规办法&#xff0c;找不到 Georeferencer插件&#xff0c;它并没有被安装&#xff0c;在库中也找不到它&#xff0c; 请问问题…...

c---冒泡排序模拟qsort

一、冒泡排序 二、冒泡排序优化排各种类型数据 文章目录一、冒泡排序二、冒泡排序优化排各种类型数据冒泡排序 冒泡排序原理&#xff1a;两两相邻元素进行比较 初级版 void bulle_sort(int* a, int sz) {int i 0;for (int i 0; i < sz-1; i){int j 0; for (j 0; j…...

Java知识复习(十四)JS

1、数据类型 基本数据类型&#xff1a;null、undefinde、boolean、string、number、symbol&#xff08;ES6新增&#xff09;引用数据类型&#xff1a;Function、Array、Object、Map和Set&#xff08;ES6新增&#xff09; 2、let、var和const的区别 var定义的变量&#xff0c…...

代码随想录刷题-数组-移除元素

文章目录写在前面习题我的想法暴力解法双指针写在前面 本节对应代码随想录中&#xff1a;代码随想录 习题 题目链接&#xff1a; 27. 移除元素- 力扣&#xff08;LeetCode&#xff09; 给你一个数组 nums 和一个值 val&#xff0c;你需要原地移除所有数值等于 val 的元素&a…...

聚观早报 |拼多多跨境电商业务正式登陆澳洲;中国加快6G网络研发

今日要闻&#xff1a;拼多多跨境电商业务正式登陆澳洲&#xff1b;全球自动驾驶公司排名特斯拉垫底&#xff1b;中国将加快 6G 网络研发&#xff1b;B站再次“崩”上热搜&#xff01;已闪电修复&#xff1b;微软将必应AI聊天每次对话上限增加至8条拼多多跨境电商业务正式登陆澳…...

MDK Keil5 创建Stm32工程-理论篇(这里以Stm32F103Zet6为例)

一、文件夹创建与文件说明整个工程可以粗略的划分为几个文件夹&#xff1a;BSP底层驱动比如GPIO\Timer等驱动文件CMSIS内核相关的文件Firmware生成的固件下载文件Mycode用户编写的相关文件&#xff0c;主要编写的文件都在这个文件夹里Project工程文件startup芯片启动文件STM32F…...

应届大学生学什么技术好?哪些技术适合年轻人?

到了毕业季&#xff0c;应届大学生面临的就是就业问题&#xff0c;很多专业的大学生难以找到对口的工作&#xff0c;或是不得已随便就业&#xff0c;或者是学个技术高薪就业&#xff0c;那么&#xff0c;问题来了&#xff0c;应届大学生学什么技术好&#xff1f;哪些技术适合年…...

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

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

Nginx学习 (2) —— 虚拟主机配置

文章目录虚拟主机原理域名解析与泛域名解析&#xff08;实践&#xff09;配置文件中ServerName的匹配规则技术架构多用户二级域名短网址虚拟主机原理 为什么需要虚拟主机&#xff1a; 当一台主机充当服务器给用户提供资源的时候&#xff0c;并不是一直都有很大的用户量&#…...

Java 动态代理简述和实例

Java动态代理是一种在运行时动态创建代理对象的技术。它可以让我们在不修改原始代码的情况下&#xff0c;对原始对象进行增强或者添加额外的行为。这种代理方式可以用于很多场景&#xff0c;例如AOP编程、RPC框架等。动态代理是基于Java反射机制实现的&#xff0c;它允许程序在…...

Unity编译器扩展(Advanced Editor Scripting)

Untiy编译器扩展允许我们对编译器的增加自己编写的的功能菜单栏MenuItemContextMenu和ContextMenuItemContextMenuContextMenuItemMenuItem 该属性允许您将菜单项添加到主菜单和检查器窗口上下文菜单。 该属性将任何静态函数转换为菜单命令。只有静态函数可以使用该属性。 Men…...

AFR机制及流程介绍

AFR(Auto Fast Return)不符合3GPP协议标准,因此终端默认是disable状态。如果运营商有要求可以配置开启。 AFR有两种场景 2G或者3G AFR到4G4G AFR到5G3G AFR TO 4G AFR到LTE功能的作用就是终端从LTE Handover或者重定向到3G进行业务,等业务做完后能够快速回到LTE网络。...

9.Hbase 部署

9.Hbase部署 注意事项&#xff1a; 1&#xff1a;必须事先安装 Hadoop分布式集群&#xff0c;zookeeper分布式集群 2&#xff1a;查看版本号&#xff1a; hbase version1、解压文件并改名 tar -zxvf /opt/software/hbase-2.2.3-bin.tar.gz -C /usr/app/ mv hbase-2.2.3/ hba…...

【maven 学习记录】

maven 学习记录一、maven基础1. maven是什么2. maven的作用3. maven的下载安装4. maven仓库5. maven坐标6. 第一个maven项目 手工实现7. maven插件8. 依赖管理9. 生命周期二、maven进阶一、maven基础 1. maven是什么 maven的本质是一个项目管理工具&#xff0c;将项目开发和管…...

NB-IOT宣传这么多年,这次总算用好了吧

一、方案概述随着实体经济快速发展&#xff0c;石化、港口、货场、工地等区域规模日益扩大&#xff0c;厂区面积广阔、环境复杂、作业人员和车辆众多&#xff0c;如无法实时掌握工作人员状态及外来人员位置、外来车辆情况等问题&#xff0c;将存在非常大的安全隐患。今天小编介…...

sort函数对结构体|pair对组|vector容器|map排序|二维数组的第x列 的排序

目录 sort对 vector容器 sort对 vector<pair<int,int>>对组 sort对 结构体 结构体外部规定排序 结构体内部运算符重载 map容器的排序 map的键排序 map的值排序 sort对二维数组的排序 sort对 vector容器 sort()函数可以用于对vector容器进行排序。具体来…...

Java定时器Timer的使用

一、Timer常用方法 Timer应用场景&#xff1a; 1、每隔一段时间执行指定的代码逻辑&#xff08;即按周期执行任务&#xff09; 2、指定时间执行指定的代码逻辑 为方便测试并查看运行效果&#xff0c;首先先建一个类并继承TimerTask&#xff0c;代码如下: package timerTest…...

MySQL安装和配置

下载官网下载mysql解压版本&#xff1a;配置环境变量下载完成后直接解压到需要放的文件夹&#xff0c;根据文件夹来配置环境变量&#xff1b;新建系统变量&#xff0c;变量名自取&#xff0c;值是MySQL的目录编辑path环境变量&#xff0c;加上MySQL的bin目录 %MYSQL_HOME%\bin配…...

openpnnp - 载入板子后,要确定板子的放置角度

文章目录openpnnp - 载入板子后,要确定板子的放置角度概述用openpnp提供的功能来确定被夹住的板子的左下角原点位置和板子的角度备注ENDopenpnnp - 载入板子后,要确定板子的放置角度 概述 设备是有夹具的, 用百分表打过, 夹具本身在Z方向的平行度是没问题的. 但是, PCB板子的…...

HCIP知识点(前三天)

复习HCIA&#xff1a; 一、TCP/IP模型&#xff0c;OSI模型 OSI 开放式系统互联参考模型 应用层 抽象语言—>编码 表示层 编码—>二进制 会话层 应用程序内部的区分地址&#xff08;无标准格式&#xff09; 传输层 TCP/UDP – 分段&#xff08;受MTU限制&#xff09;、端…...

模板学堂丨妙用Tab组件制作多屏仪表板并实现自动轮播

DataEase开源数据可视化分析平台于2022年6月正式发布模板市场&#xff08;https://dataease.io/templates/&#xff09;。模板市场旨在为DataEase用户提供专业、美观、拿来即用的仪表板模板&#xff0c;方便用户根据自身的业务需求和使用场景选择对应的仪表板模板&#xff0c;并…...

C++:初识函数模板和类模板

目录 一. 泛型编程 二. 函数模板 2.1 什么是函数模板 2.2 函数模板的实例化 2.2.1 函数模板的隐式实例化 2.2.1 函数模板的显示实例化 2.3 函数模板实例化的原理 2.4 模板函数调用实例化原则 三. 类模板 3.1 什么是类模板 3.2 类模板的实例化 一. 泛型编程 泛型编程…...

3.8妇女节如何做好TikTok网红营销?

3月8日是国际妇女节&#xff0c;这一节日已经成为全球关注女性权益和平等的标志性日子&#xff0c;TikTok上话题#internationalwomensday累计播放超10亿次&#xff0c;话题#WomensDay2023累计播放量也将近300万次。 这个特别的日子为品牌提供了一个很好的营销机会。据Nox聚星了…...

使用Advanced Installer打包程序及运行环境

Advanced Installer 工具版本&#xff1a;20.1.1 设置产品信息 选中右侧【Product Details】输入产品信息 设置文件和文件夹 添加使用VS发布之后的程序文件夹 设置文件夹刷新 选中文件夹&#xff0c;右键选择属性&#xff0c;选中Synchronize标签。启用“Synchronize conten…...

华为OD机试真题Python实现【计算堆栈中的剩余数字】真题+解题思路+代码(20222023)

计算堆栈中的剩余数字 题目 向一个空栈中依次存入正整数 假设入栈元素N(1<=N<=2^31-1) 按顺序依次为Nx … N4、N3、N2、N1, 当元素入栈时,如果N1=N2+...Ny(y的范围[2,x],1 <= x <= 1000) 则 N1 到 Ny 全部元素出栈,重新入栈新元素M(M=2*N1) 如依次向栈存储6、1、…...

企业文件数据泄露防护(DLP)

什么是数据丢失防护 数据丢失防护 &#xff08;DLP&#xff09; 是保护数据不落入坏人之手的做法。如今&#xff0c;数据传输的主要问题是使大量数据容易受到未经授权的传输。通过设置足够的安全边界&#xff0c;您可以控制数据在网络中的移动。由于您的数据非常有价值&#x…...

不考虑分配与合并情况下,GO实现GCMarkSweep(标记清楚算法)

观前提醒 熟悉涉及到GC的最基本概念到底什么意思&#xff08;《垃圾回收的算法与实现》&#xff09;我用go实现&#xff08;因为其他的都忘了&#xff0c;(╬◣д◢)&#xff91;&#xff77;&#xff70;!!&#xff09; 源码地址&#xff08;你的点赞&#xff0c;是我开源的…...

利用HGT聚类单细胞多组学数据并推理生物网络

单细胞多组学数据允许同时对多种组学数据进行定量分析&#xff0c;以捕捉复杂的分子机制和细胞异质性。然而现有的工具不能有效地推断不同细胞类型的活性生物网络以及这些网络对外部刺激的反应。 来自&#xff1a;Single-cell biological network inference using a heterogen…...

杂记——18.VSCode的下载及使用

这篇文章&#xff0c;我们来讲一下VSCode&#xff0c;讲一下如何下载及使用VSCode 目录 1.VSCode的下载 1.1VSCode的简介 1.2VSCode的下载与安装 1.2.1下载 1.2.2安装 2.VSCode的使用 2.1界面 2.2基础设置 2.3禁用自动更新 2.3自动保存设置 2.4Vscode更换主题 2.5…...

厦门外贸网站建设公司/西安霸屏推广

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼怎样转换下面的二维数组到一维数组&#xff0c;然后输入数字&#xff0c;然后显示出输入数字在数组中相对应的词char commonWords[13][60] {"C is a structured, procedural programming","language that has been …...

河南网络洛阳网站建设河南网站建设/软文广告怎么写

在中国做企业&#xff0c;“人”永远都是绕不过去的一道坎&#xff0c;你不可能就事论事&#xff0c;也不可能把“事业”做的那么纯粹。中国企业最难逾越的不是企业本身&#xff0c;而是复杂的中国人性&#xff01;一个很古老的故事&#xff1a;从前有座山&#xff0c;山里有座…...

免费logo设计网址/seosem是什么职位

为什么80%的码农都做不了架构师&#xff1f;>>> 安装 添加安装源 rpm -Uvh https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm安装 yum install mysql-community-server设置 防火墙设置 firewall-cmd --zonepublic --permanent --add-servic…...

淘宝网站优化实例/什么是电商

先上软件效果图 代码如下1.根据Url地址得到网页的html源码 1 public static string GetWebContent(string Url)2 {3 string strResult "";4 try5 {6 HttpWebRequest request (HttpWebRequest)WebReq…...

免费素材网站哪个最好/百度引擎的搜索方式是什么

写查询语句的时候有时候会突然来这么一句提示&#xff0c;仔细看sql还没什么错误。 报错代码 SQL> select l.awuname,g.aultext,d.aulword2 from aw_usercotrl l,aw_userlog g,aw_userlog_lword d3 where l.awuid g.awuid and g.aulid d.fkaulid4 ;select from whereO…...

国外设计最漂亮的网站/深圳广告公司

使用 bootstrap 框架制作的创意和现代应用程序登陆页面模板&#xff0c;它是具有创意设计的单页 html 模板&#xff0c;您可以在此模板中展示您的任何应用&#xff0c;Applook 在所有现代浏览器&#xff0c;平板电脑和手机上看起来都很完美&#xff0c;您可以根据需要自定义每个…...