文章目录
- 1.背景
- 2.宏观原理
- 3.相关技术与开发环境
- 4. 实现原理
- 1.下载
- 2.加载与解析文件
- 2.1获取指定目录下的所有网页文件
- 2.2. 获取网页文件中的关键信息
- 2.3. 对读取文件进行保存
- 3.索引
- 3.1正排与倒排
- 3.2获取正排和倒排索引
- 3.3建立索引
- 3.3.1正排索引
- 3.3.2倒排索引
- 4.搜索
- 4.1 初始化
- 4.2 搜索功能
- 5. http_server
- 5.1 升级gcc
- 5.2 安装cpp-httplib
- 5.3编写http_server.cc
- 6. 编写前端模块
- 6.1 HTML
- 6.2 CSS
- 6.4 JavaScript
- 总结 与 拓展
- 尾序
- 效果图:
- 项目源码:链接
- 搜索引擎测试链接:点击进入
1.背景
- 百度,360,谷歌等搜索引擎的实现门槛过高,几乎不可能由个人进行实现。
- 站内搜索,其中的资源相对比较垂直,适合个人进行实现,并借此达到管中窥豹的效果。
- Boost库是没有站内搜索的,实现更有意义。
搜索相关内容:
- 共性:都含有标题,摘要,网站的网址。
说明:有些网站还有图片,广告等信息,但由于我们做的是Boost库的搜索,这些信息知道即可。
2.宏观原理
3.相关技术与开发环境
- 技术栈:C/C++,C++11,STL,JsonCpp,Boost,Cpp-Httplib,Jquery,正排与倒排索引。
- 开发环境: Centos,云服务器,vim/g++/gcc/Makefile,VsDode/VS2019。
4. 实现原理
说明: 为了方便理解代码,博主将项目的目录进行贴出,因为下面我们include包含使用的库,使用的是绝对路径。
项目的路径为:
/home/shun_hua/practical-projects/Boost_Search
原因:相对目录是基于进程的工作目录,进程的工作目录在项目的路径下,而我们写的代码是在
项目的子目录的路径
下,因此要么把所有的文件都放在项目的路径下
,要么就用绝对路径,虽然长,但是可以把文件整理分类,看着比较简洁。
1.下载
- 进入boost官网:点击进入
- 第一步:
- 第二步:
- 第三步:下载完成之后,在对应的Linux操作系统系统上,输入上传文件,进行上传。
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$rz -E
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ ls
boost_1_84_0.tar.gz
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ tar xzf boost*
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ ls
boost_1_84_0 boost_1_84_0.tar.gz
说明:
- rz - E是上传较大文件时进行使用。
- tar -xzf 是对文件进行解压缩。
- boost_1_84_0是解压缩之后的文件。
- 安装rz : sudo yum install -y rz
- 第四步:找到解压缩文件中的html提取出来用于作为搜索引擎的数据。
- 路径:/boost_1_84_0/doc/html
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ ls
boost_1_84_0
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ cp -r ./boost_1_84_0/doc/html ./input
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ Boost_Search]$ ls
boost_1_84_0 input
说明:cp -r [指定路径的目录] [目标路径 + 重命名]
[shun_hua@iZ2zebfc5jur5cm0zu2n3gZ input]$ ls -R | grep -E ".html" | wc -l
8586
说明:
- ls -R 显示所有文件,目录递归显示所有文件。
- grep -E [字符串] [文件], 显示出带有指定字符的信息。
- wc -l [文件], 显示出文件的行数。
2.加载与解析文件
基本框架:
2.1获取指定目录下的所有网页文件
-
引入文件库:Boost文件库,具体使用里面的filesystem里面的接口。
-
接口:文档
命名空间:boost::filesystem
类:class path
string string(const codecvt_type& cvt=codecvt()) const; ————将path对象转换为string类。 path extension() const; ——文件的后缀。
迭代器:class recursive_directory_iterator
recursive_directory_iterator() noexcept;//默认构造,其实执向的是end explicit recursive_directory_iterator(const path& p, directory_options opts\ = directory_options::none);//用根目录初始化,即整个多叉树根。
接口:
bool is_regular_file(const path& p); //判断是否是普通的文件,目录不是普通文件。 bool exists(const path& p); //判断是否文件的目录是否存在。
- 实现代码:
bool GetPathFiles(const string& path,vector<string>* files)
{
//首先将path转化为boost库的path便于处理
//防止命名污染的情况
namespace fs = boost::filesystem;
fs::path root_path(path);
if(!exists(root_path))
{
lg(CRIT,"path is not exist!");
return false;
}
fs::recursive_directory_iterator end;
for(fs::recursive_directory_iterator cur(root_path); cur != end; cur++)
{
//如果不是普通文件,例如目录。
if(!is_regular_file(cur->path()))
{
continue;
}
string suffix = cur->path().extension().string();
if(suffix != ".html")
{
continue;
}
string path = cur->path().string();
// cout << path << endl;
files->push_back(cur->path().string());
}
return true;
}
2.2. 获取网页文件中的关键信息
核心:
- 标题——title
- 内容——content
- 网址——url
网页的大致内容:
说明:
- …. 之间的为标题,即网页窗口显示的内容。
- 除去<…> 之间的内容其余的都为内容。
- 网址,根据网页的基本内容结合本地的相对目录,获取到具体boost库的网址。
解析文件的基本流程:
- 读取网页文件的内容
//使用命名空间,避免命名污染
namespace util
{
namespace filesystem
{
bool ReadFiles(const std::string& file_path,std::string *text)
{
std::ifstream fin(file_path);
if(!fin.is_open())
{
lg(WARNNING,"open file fail!");
return false;
}
string line;
while(getline(fin,line))
{
*text += line;
}
return true;
}
}
}
- 获取标题
bool PraseTile(const string& text,string *title)
{
string prefix = "<title>";
string suffix = "</title>";
auto begin = text.find(prefix);
auto end = text.find(suffix);
if(begin == string::npos || end == string::npos) return false;
begin += prefix.size();
*title = text.substr(begin,end - begin);
return true;
}
- 获取内容
- 此处采用的是状态机的实现方式,即除了<….> 都是内容。
bool PraseContent(const string& text,string *content)
{
State s = LABLE;
for(char ch : text)
{
switch (s)
{
case LABLE:
if(ch == '>')
s = CONTENT;
break;
case CONTENT:
if(ch == '<')
{
s = LABLE;
break;
}
//把换行符去掉。
*content += ch == '\n' ? ' ' : ch;
}
}
return true;
}
-
解析网站的url
实现原理:
- 我们用的是
/doc/html
下的所有*.html
文件。 - 因此网站的前缀为:
https://www.boost.org/doc/libs/1_84_0/doc/html/
- 根据对应的*.html文件,再去掉本地目录的前缀:
/home/shun_hua/practical-projects/Boost_Search
,得到资源的后缀。 - 前缀与后缀拼接出来的结果,即为搜索网站的Url。
实现代码:
- 我们用的是
bool ParseUrl(const string& path,string* url)
{
string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";
// /doc/html文件中存放的是帮助文档的html,用于搜索引擎的查找。
// 其它目录下的html先暂时不做考虑。
auto pos = path.find(src_path);
if(pos == string::npos) return false;
pos += src_path.size();
string url_tail = path.substr(pos);
*url = url_head + url_tail;
return true;
}
因此,我们的解析网页的实现代码为:
bool PraseHtmls(const vector<string>& files,vector<HtmlInfor>* contents)
{
//先读取文件的内容
int cnt = 5;
for(auto file_path : files)
{
//1.打开文件读取对应的内容。
string text;
namespace u_fs = util::filesystem;
if(!u_fs::ReadFiles(file_path,&text))
{
lg(WARNNING,"read files content fail!");
continue;
}
//2.解析网站的标题
HtmlInfor htm;
if(!PraseTile(text,&htm.title))
{
lg(WARNNING,"Prase html title content fail!");
continue;
}
//3.解析网站的内容
if(!PraseContent(text,&htm.content))
{
lg(WARNNING,"parse content fail!");
continue;
}
if(!ParseUrl(file_path,&htm.url))
{
lg(WARNNING,"parse url fail!");
continue;
}
//for debug:
// cout << htm.title << endl;
// cout << htm.url << endl;
// cout << htm.content << endl;
// break;
contents->push_back(htm);
}
return true;
}
2.3. 对读取文件进行保存
- 保存信息的结构体为:
struct HtmlInfor
{
string title;
string content;
string url;
};
-
保存方式:
- 在进行读取时,我们希望一次能读取一个文件的内容。因此文件与文件之间用 ‘\n’ 进行划分。
- 在分析一个文件的内容时,我们需要获取到标题,内容,url信息,因此这之间需要用一个控制字符 ‘\3’划分即可。
-
实现代码:
bool SaveHtmls(const vector<HtmlInfor>& contents,const string& path)
{
std::fstream out(path,ios_base::binary | ios_base::out);
if(!out.is_open())
{
lg(ERRO,"open file %s fail!",path.c_str());
return false;
}
int rate;
int cur = 0;
for(const HtmlInfor& infor : contents)
{
rate = 100 * (++cur) / contents.size();
processbar(rate);
string mes = infor.title + "\3" + infor.url + \
"\3" + infor.content + '\n';
out.write(mes.c_str(),mes.size());
}
return true;
}
此处用之前学到的进度条程序,显示保存文件的进度,实现可视化。
进度条代码:
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAX 102
#define STYLE '#'
#define RIGHT '>'
typedef void (*call_back)(int);
void processbar(int rate);
void init();
char buf[MAX];
char signal[5] = {'-','\\','|','/','\0'};
void init()
{
memset(buf,'\0',sizeof(buf));
}
void processbar(int rate)
{
if(rate > 100 || rate < 0)
{
return;
}
int len = strlen(signal);
if(rate == 100)
{
printf("[%-100s][%-3d%%] sourse load done!\r\n",buf,rate);
usleep(1000);
buf[rate++] = STYLE;
return;
}
printf("[%-100s][%-3d%%][%c]\r",buf,rate,signal[rate%len]);
usleep(10);
//刷新缓冲区
fflush(stdout);
//更新存储的进度条
buf[rate++] = STYLE;
if(rate < 100)
{
buf[rate] = RIGHT;
}
}
3.索引
3.1正排与倒排
采用技术:正排索引与倒排索引。
举例:
-
假设针对如下三个标题建立正排和倒排索引。
-
乔布斯买苹果手机。
-
乔布斯吃苹果。
-
乔布斯看手机。
-
- 建立正排索引的文档
文档ID | 文档内容 |
---|---|
1 | 乔布斯买苹果手机。 |
2 | 乔布斯吃苹果。 |
3 | 乔布斯看手机。 |
- 提取关键字
- 乔布斯 苹果手机 苹果 手机 买 吃 看
- 根据关键字建立倒排索引
关键字 | 文档ID |
---|---|
乔布斯 | 1,2,3 |
苹果手机 | 1 |
苹果 | 2 |
手机 | 1,3 |
吃 | 2 |
看 | 3 |
买 | 1 |
总结一下:
- 正排索引即对读取的文件内容进行编号。
- 倒排索引是针对关键字找到文件的编号,从而找到文件的内容。
基本框架:
typedef std::vector<int> Interved_List;
//正排索引
struct DocInfor
{
DocInfor()
{}
DocInfor(int Idx,string Title,string Url,string Content)
:idx(Idx),title(Title),url(Url),content(Content)
{};
int idx;//文档ID
std::string title;
std::string url;
std::string content;
};
//索引类,实现正排和倒排索引
class Index
{
//Document
public:
Index()
{}
~Index()
{}
DocInfor* GetDocInfor(int doc_idx);
Interved_List* GetInterList(const string& keyword);
//从文件中读取内容,建立正排和倒排索引
bool BuildForwardIndex(const string& path);
private:
//这里的vector的数组的下标天然就可以当做DocInfor的idx
std::vector<DocInfor> forward_index;//正排索引
std::unordered_map<std::string,Interved_List> interved_index;//倒排索引
};
3.2获取正排和倒排索引
- 正排索引根据文档ID进行获取
- 倒排索引根据关键词,获取对应的倒排拉链。
实现代码:
DocInfor* GetDocInfor(int doc_idx)
{
if(doc_idx > forward_index.size())
{
lg(ERRO,"doc_idx:%d,out of range.",doc_idx);
return nullptr;
}
return &forward_index[doc_idx];
}
Interved_List* GetInterList(const string& str)
{
auto it = interved_index.find(str);
if(it == interved_index.end())
{
lg(ERRO,"keyword is not exist.");
return nullptr;
}
return &it->second;
}
3.3建立索引
3.3.1正排索引
- 说明: 我们可以采用string 容器的 find 与 substr接口实现文档内容的截取,但是基于学习Boost文件库的目的,这里直接使用现成的截取文档的接口。
接口:
// In header: <boost/algorithm/string/string.hpp>
//函数声明
template<typename SequenceSequenceT, typename RangeT, typename PredicateT>
SequenceSequenceT &
split(SequenceSequenceT & Result, RangeT & Input, PredicateT Pred,
token_compress_mode_type eCompress = token_compress_off);
/*
参数
1:vector<type>类型的,用于存放切割后的内容。
2:切割的内容。
3:分割符。
4:切割的模式,一般设置为token_compress_on,意为将连续的分割符看成一个。
*/
//例:
#include<iostream>
#include<boost/algorithm/string.hpp>
#include<string>
#include<vector>
int main()
{
std::vector<std::string> res;
std::string text = "aaaaaaaa\3\3bbbbbbbbbbb\3cccccccccc";
std::string split_str = "\3";
boost::split(res,text,boost::is_any_of(split_str),boost::token_compress_on);
for(auto &str : res)
{
std::cout << str << std::endl;
}
return 0;
}
/*
output:
aaaaaaaa
bbbbbbbbbbb
cccccccccc
*/
接口:
static DocInfor* GetForwardIndex(const string& split,const string &line,\
vector<DocInfor>* forward_index)
{
vector<string> tmp;
boost::split(tmp,line,boost::is_any_of(split),boost::token_compress_on);
if(tmp.size() != 3)
{
lg(ERRO,"split fail:GetForwardIndex");
return nullptr;
}
forward_index->push_back(DocInfor(forward_index-\
>size(),move(tmp[0]),move(tmp[1]),move(tmp[2])));
return &forward_index->back();
}
//说明:此函数封装在命名空间util的struct String内
3.3.2倒排索引
- 建立倒排的索引的结构体对象。如:文档ID,关键词,相关系数。
struct InterElem
{
//默认构造
InterElem()
{}
//写了构造,编译器就不会自动生成默认构造函数。
InterElem(int id,std::string key,int rate)
:idx(id),word(key),weight(rate)
{}
int idx;//文档id
std::string word;//关键词
int weight;//权重
};
- 对文档内容和标题进行分词。
- 说明:倒排需要对内容进行分词,而分词的工作有现成的库,因此我们采用jieba库分词即可。
-
安装jieba工具:
-
网址链接:jieba分词
-
使用
git clone https://gitcode.com/yanyiwu/cppjieba.git
克隆到本地。 -
将库进行调整:使用cp命令将
cppjieba/deps/limonp
拷贝到cppjieba/include/jieba
目录下。 -
将
cppjieba/test/demo.cpp
,拷贝到与cppjieba同级目录下进行测试。 -
在与cppjieba同级目录下建立软连接:
ln -s cppjieba/include/jieba jieba
,ln -s cppjieba/dict dict
-
测试代码:
#include "jieba/Jieba.hpp"
using namespace std;
std::string prefix = "/home/shun_hua/practical-projects/Boost_Search/Modules/Utils/";
std::string DICT_PATH = prefix + "dict/jieba.dict.utf8";
std::string HMM_PATH = prefix + "dict/hmm_model.utf8";
std::string USER_DICT_PATH = prefix + "dict/user.dict.utf8";
std::string IDF_PATH = prefix + "dict/idf.utf8";
std::string STOP_WORD_PATH = prefix + "dict/stop_words.utf8";
int main()
{
cppjieba::Jieba jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
vector<string> words;
vector<cppjieba::Word> jiebawords;
string s;
string result;
s = "小明硕士毕业于中国科学院计算所,后在日本京都大学深造";
cout << s << endl;
jieba.CutForSearch(s, words);
cout << "[demo] CutForSearch" << endl;
cout << limonp::Join(words.begin(), words.end(), "/") << endl;
}
说明:我们的目的是针对关键词进行搜索,因此使用
CutForSearch
接口即可。
我们用类进行封装:
#include "jieba/Jieba.hpp"
std::string prefix = "/home/shun_hua/practical-projects/Boost_Search/Modules/Utils/";
std::string DICT_PATH = prefix + "dict/jieba.dict.utf8";
std::string HMM_PATH = prefix + "dict/hmm_model.utf8";
std::string USER_DICT_PATH = prefix + "dict/user.dict.utf8";
std::string IDF_PATH = prefix + "dict/idf.utf8";
std::string STOP_WORD_PATH = prefix + "dict/stop_words.utf8";
//这里的JieBa类是用于封装的,而cppjieba::Jieba是一个类型,请注意进行区分。
struct JieBa
{
private:
static cppjieba::Jieba jieba;
public:
static void CutString(const std::string& content,std::vector<std::string>& words)
{
jieba.CutForSearch(content,words);
}
};
cppjieba::Jieba JieBa::jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
- 补充:中文和英文的分词jieba都支持,博主已经测试过。
- 对关键词次数进行分析。对标题和内容都要进行分析。
struct Word_Cnt
{
int title_cnt = 0;//标题中关键词的出现次数。
int content_cnt = 0;//内容中关键词的出现次数。
};
- 计算相关系数,将获取到完整的InterElem元素,打散到倒排索引中。
说明:实际相关系数要考虑到多个维度,且要基于数据进行分析,这里我们不做那么复杂,使用标题与内容中关键词的出现次数进行分析即可。
- 系数公式:
N*word_cnt.title_cnt + M * word.content_cnt
, 这里的N, M姑且就分别设置为10 与 1,即标题的相关性占比较大。
- 细节:
这里我们在对网页去标签时,内容中也含有标签,因此标签中的关键字在内容中被重复计算了一次。
jieba分词可能没有将我们所搜索的关键词在内容中分出来,因此可能会跟实际有一点点偏差,比实际的小一点。
实现代码:
void GenerateIntervedIndex(const DocInfor &doc)
{
// 首先文档id是已经有的。
// 剩余需要的是:
// 1.分析出关键词。
std::vector<std::string> title_words;
std::vector<std::string> content_words;
// 进行jieba分词.....
util::JieBa::CutString(doc.title, title_words);
util::JieBa::CutString(doc.content, content_words);
std::unordered_map<std::string, Word_Cnt> kv;
// 2.记录关键词的出现次数。
// 注意:在实际搜索的过程中是忽略大小写的,因此的对标题和内容都忽略大小写。
for (auto word : title_words)
{
boost::to_lower(word);
kv[word].title_cnt++;
}
for (auto word : content_words)
{
boost::to_lower(word);
kv[word].content_cnt++;
}
// 3.计算相关系数,打散到interverd_index中。
for (auto &it : kv)
{
auto &key_word = it.first;
auto &cnt = it.second;
interved_index[key_word].push_back({doc.idx, move(key_word),\
cnt.content_cnt + 10 * cnt.title_cnt});
}
}
说明:
- 在实际的搜索过程中是忽略大小写的,因此这里我们采用了boost库中的to_lower,统一转换为小写。
- 由于to_lower会对传进去的内容本身进行修改,因此使用范围for时,应使用拷贝,不可使用引用获取。
- 我们并不需要对文档内容进行大小写转换,只需要将索引的关键词和搜索的关键词进行大小写转化,即可完成忽略大小写。
索引构建代码:
bool BuildIndex(const string &path)
{
std::ifstream in(path, std::ios_base::in | std::ios_base::binary);
if (!in.is_open())
{
lg(ERRO, "open file fail,path is %s.", path);
return false;
}
string line;
const int sum = 8586;
int cnt = 0;
// getline的默认分割符为'\n'
string split_str = "\3";
while (getline(in, line))
{
cnt++;
processbar(cnt * 100 / sum );
// 获取正排索引
DocInfor *doc = GetForwardIndex(split_str,line);
if (doc == nullptr)
{
lg(WARNNING, "doc is not exist.");
continue;
}
// 获取倒排索引
GenerateIntervedIndex(*doc);
}
return true;
}
说明:
文档在建立时可能会比较慢,因此写了一个进度条,实现进度可视化。
sum为读取文件的数目,如果处理文件的方式不变的话,一般sum的值是不变的。
4.搜索
基本框架:
#pragma once
#include "../Index/index.hpp"
#include <jsoncpp/json/json.h>
namespace bs_search
{
const string data_path = "/home/shun_hua/practical-\
projects/Boost_Search/DataSource/output/data_processed.txt";
class Searcher
{
private:
bs_index::Index *index;
public:
Searcher()
{}
~Searcher()
{}
void InitSeacher()
{
//1.对单例index进行获取。
//2.对index进行构建。
}
//对查询进行搜索
void Search(const string &query,string* json_str)
{
//1.分词,即对query进行分词,便于查询。
//2.根据关键词在倒排索引中进行查找。
//3.根据相关系数,即weight对查找的内容进行降序排序。
//4.根据排序之后的结果,构建对应的字符串。
}
};
}
说明:
- 因为索引的构建的文件过大,如果反复进行过程较慢,因此我们这里的使用指针的形式,并且将Index类设为单例(懒汉)。
- 我们输入的查询语句也需要进行分词,下面博主贴一个例子进行举例。且分词之后的结果需要进行大小写转换与倒排索引对应。
- 我们查询之后的结果使用现成的json串,内容基本情况的获取,并且由于由于内容过大,返回有关键词的摘要即可。
补充:Index类
- 单例模式
static Index *instance()
{
if (index_ptr == nullptr)
{
mtx.lock();
if (index_ptr == nullptr)
{
index_ptr = new Index();
}
mtx.unlock();
}
return index_ptr;
}
说明:
- index_ptr 与 mtx的类型分别为Index * 与 mutex——C++的锁,头文件为mutex。
- 都需要设置为静态变量,便于进行外部通过类域进行获取。
- 这里外面的第一层的if是为了提高并发度, 因为多线程访问大多数情况是index不为空的情况。
- 获取关键描述
实现代码:
std::string GetDesc(const std::string& content,\
const std:: string& word)
{
auto pos = content.find(word);
if(pos == std::string::npos)
{
return "word is not in content";
}
//返回里面含有关键词的描述。
int presize = 50,sufsize = 50;
//获取pos前50个字节和后50个字节当做内容的描述
int pre = pos - presize,suf = pos + sufsize;
int begin = pre > 0 ? pre : 0;
int end = suf < content.size() ? suf : content.size();
//返回摘要即可。
return content.substr(begin,end - begin + 1);
}
- 说明: 这个函数我设置的很简单,只需要对内容进行搜索关键词的位置,返回附近的内容即可,如果没有我们设置一个默认值进行返回即可。
4.1 初始化
- Index类提供了 instance 和 BuildIndex进行初始化,因此直接调用接口即可。
实现代码:
void InitSeacher()
{
//1.对单例index进行获取。
index = bs_index::Index::instance();
//2.对index进行构建。
index->BuildIndex(data_path);
}
4.2 搜索功能
- 对查询进行分词。
说明:
- 索引的句子也进行了分词,使用关键词查找,并呈现对应的内容。
- 在之前我们在
util::Jieba
即命名空间的对应类域中使用了jieba库的封装的接口CutString
。
- 获取倒排索引。
- 在Index我们内含成员存有正排和倒排索引,并实现了对应的接口——GetInterList,直接用即可。
- 注意:对分词的结果也要忽略大小写,即为了与上面的索引模块对应,统一转换为小写即可。
- 说明: 转换为小写,使用boost库中的
to_lower
接口即可。
- 降序排序
- 对获取的倒排索引进行降序排序,使用algorithm 库里的sort,使用lambda表达式自定义对应的排序规则即可。
- 说明:自定义对象为我们Index中实现的
struct InterElem
struct InterElem
{
// 默认构造
InterElem()
{
}
// 写了构造,编译器就不会自动生成默认构造函数。
InterElem(int id, std::string key, int rate)
: idx(id), word(key), weight(rate)
{
}
int idx; // 文档id
std::string word; // 关键词
int weight; // 权重
};
- 获取文档内容,封装为Json串
- demo:
#include<iostream>
#include<jsoncpp/json/json.h>
#include<string>
int main()
{
int age = 18;
std::string name = "Shun_Hua";
int id = 12314213;
Json::Value root;
Json::StyledWriter wri;
root["age"] = age;
root["name"] = name;
root["id"] = id;
std::string json_str = wri.write(root);
std::cout << json_str << std::endl;
return 0;
}
说明:
- g++ 编译时,需要用 -l 选项包含对应的库名。
- 编译指令:
g++ json_demo.cc -std=c++11 -ljsoncpp
- CenOs按照json库的指令:
sudo yum install -y jsoncpp-devel
执行结果:
搜索功能实现代码:
void Search(const string &query,string* json_str)
{
//1.分词,即对query进行分词,便于查询。
std::vector<string> words;
util::JieBa::CutString(query,words);
//2.根据关键词在倒排索引中进行查找。
//获取与关键词相关的所有倒排拉链
bs_index::Interved_List lists;
for(auto& word : words)
{
//在此之前,我们需要对关键词进行大小写转换
boost::to_lower(word);
bs_index::Interved_List* list = index->GetInterList(word);
if(nullptr == list)
{
continue;
}
//说明:这里的内容可能会有大量的重复,最后可以保留较大的权值的文档ID,进行排序。
//template <class InputIterator>
//void insert (iterator position, InputIterator first, InputIterator last);
//在指定的迭代器位置插入对应的容器的迭代器区间。
lists.insert(lists.end(),list->begin(),list->end());
}
//将list的文档ID可能会有大量的重复,我们采用unordered_map进行去重,用文档ID作为索引值,
//保留权值较高的元素或者将权值进行累加即可。
std::unordered_map<int,bs_index::InterElem> kv;
for(auto& elem : lists)
{
int id = elem.idx;
if(kv.count(id))
{
//保留文档权值较高的即可。
//kv[id] = elem.weight > kv[id].weight ? elem : kv[id];
//对权值累加。
kv[id] += elem.weight;
}
else
{
kv[id] = elem;
}
}
bs_index::Interved_List Deduplication;
for(auto &pair : kv)
{
Deduplication.push_back(std::move(pair.second));
}
//去重之后的结果,进行赋值。
lists = move(Deduplication);
//3.根据相关系数,即weight对查找的内容进行降序排序。
sort(lists.begin(),lists.end(),[&](const bs_index::InterElem& x,\
const bs_index::InterElem& y){
return x.weight > y.weight;
});
//4.根据排序之后的结果,构建对应的字符串。
//是根据对应的索引内容进行构建的。
Json::Value root;
Json::StyledWriter write;
//这里我们还可以用:Json::FastWriter writer; 没有上面那一种美观。
for(auto& interved : lists)
{
bs_index::DocInfor* doc = index->GetDocInfor(interved.idx);
Json::Value val;
val["title"] = doc->title;
val["url"] = doc->url;
val["content"] = index->GetDesc(doc->content,interved.word);
//追加在root后面。
root.append(val);
}
*json_str = write.write(root);
}
- 说明:去重时,我们采用unordered_map<int,InterElem>,相同文档ID时,保留权值较大的文档即可,便于后面的排序,另外这里我们再赋值时,使用move减少拷贝的次数。
测试:
- 编写索引与搜索模块完毕,我们创建一个search.cc文件进行测试。
#include<iostream>
#include"search.hpp"
int main()
{
//获取搜索服务的对象。
bs_search::Searcher sear;
sear.InitSeacher();
std::string query;
while(true)
{
cout << "Please Enter query@";
std::getline(std::cin,query);
string json_str;
sear.Search(query,&json_str);
cout << json_str << endl;
}
return 0;
}
效果:
说明:由于查询的关键词反馈的内容可能过多,此处就不再显示了。
5. http_server
5.1 升级gcc
原因:CentOs 7 的默认版本较老,使用http_server的库会编译出错,因此需要对gcc/g++进行升级。
查看gcc/g++ 版本:
gcc -v
说明:升级到7以上的版本即可。
- 安装扩展源:scl
sudo yum install -y http://mirror.centos.org/centos/7/extras/x86_64/Packages/centos-release-scl-rh-2-3.el7.centos.noarch.rpm
说明:
可用su命令输入root密码,切换至root进行安装。
普通用户添加至信任白名单后, 输入用户密码进行安装。具体操作链接:详见文章开头
- 安装devtoolset
sudo yum install devtoolset-9-gcc-c++
说明: 这里的 -9 意为按照 g++/gcc 9的版本,这里我们使用7以上的即可。
- 激活devtooset
scl enable devtoolset-9 bash
说明:
- 这里的-9与第二步的意思相同,你上一步输入了几,这里就还输入几。
- 在每次启动会话时,版本就回退到原先的,所以我们还需要输入,为了避免重复输入,我们可以放在用户对应的配置文件中。
具体步骤:
- 命令行输入:vim ~/.bash_profile。
- 将激活代码贴到最后一行,标好注释即可。
5.2 安装cpp-httplib
- 安装稳定版本的cpp-httplib, 这里推荐 v0.7.15 版本的。
在命令行输入如下命令进行安装。
git clone https://gitee.com/linzhipong/cpp-httplib.git
- 安装网站对应的样例进行测试。
- 测试代码:
#include"./cpp-httplib-v0.7.15/httplib.h"
int main()
{
httplib::Server svr;
svr.Get("/hi", [](const httplib::Request & req, httplib::Response &res)
{
res.set_content("Hello World!", "text/plain charset=utf-8");
//说明:charset=utf-8是支持编码的格式,即避免中文在网页中显示乱码。
});
svr.listen("0.0.0.0", 8080);
return 0;
}
在网站上输入:
http:// ip地址:8080/hi
会显示如下的效果:
5.3编写http_server.cc
- 基本思路:
在此之前,我们还需对cpp-httplib的接口有一定的了解。
- 我们需要创建一个http类型的server对象,用于接收http请求。假设对象名为
svr
。 - 对象的Get方法,参数需要构建即获取url的路径,接收和处理http请求。
- 请求对象设为 req, 其中有
has_param
方法,判断url中的是否有搜索的word。 - 其中的
get_param_value(”word“)
, 可解析出word的内容。 - 比如url为
/s?word=XXX
,has_param(“word”)
即判断其中是否有word; - 而
get_param_value(“word”)
可将XXX提取出来。
-
提取出请求之后,我们可以用之前写的search对象,提供对应的搜索服务。
- 使用响应对象,设为其名称为rep,其中的
set_content
方法,可帮助我们返回搜索内容,上有详细例子可辅助进行理解。
- 使用响应对象,设为其名称为rep,其中的
-
最后别忘了设置服务器的状态为监听状态哦!不然服务器可起不来。
实现代码:
#include"/home/shun_hua/practical-projects/Boost_Search/Modules\
/Server/cpp-httplib/httplib.h"
#include"/home/shun_hua/practical-projects/Boost_Search/Modules\
/Search/search.hpp"
#include<string>
const std::string root_path = "/home/shun_hua/practical-projects/\
Boost_Search/Modules/Server/wwwroot";
int main()
{
bs_search::Searcher search;
search.InitSeacher();
httplib::Server ser;
ser.set_base_dir(root_path.c_str());
ser.Get("/s", [&](const httplib::Request& req, httplib::Response& res)
{
// res.set_content("Hello world!","text/plain");
if(!req.has_param("word"))
{
res.set_content("必须要有搜索内容","text/plain; charset=utf-8");
return;
}
std::string word = req.get_param_value("word");
cout << "搜索内容为:" << word << endl;
std::string json_str;
search.Search(word,&json_str);
res.set_content(json_str,"application/json,charset=utf-8");
}
);
ser.listen("0.0.0.0",8080);
return 0;
}
6. 编写前端模块
说明:
- HTML : 确定网页的骨骼。
- CSS: 决定网页的皮肉,即是否好看。
- JavaScirpt: 决定网页的灵魂,即动态效果,此项目指的是实现前后端交互。
开发环境:vscode,编写网页较为轻松,且可连云服务器,比较轻量化。
6.1 HTML
基本知识:
<meta charset="UTF-8">
<!-- 字符编码形式为UTF-8的形式-->
<html lang="en">
<!-- 设置网站站点为英文 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 设置网页的显示形式,确保能够正确的显示。-->
<title>XXXX</title>
<!-- 设置标题为XXXX-->
<html>....</html>用
<!-- 中间用来放置 网页文档的内容-->
<body>....</body>
<!--中间放置网页的可见内容-->
<div class="XXXX"></div>
<!--设置一个类名为XXX的div元素,方便进行选择和设置格式。-->
<h1 align="center">XXXX</h1>
<!--设置一个一级标题XXXX,并且居中显示。-->
<input type="text" value="XXXXX" >
<!--设置一个输入框,内容为XXXX-->
<button onclick = "XXXX">XXXXX</button>
<!--设置一个button按钮,点击执行XXXX的javascript的函数动作-->
实现代码:
<!-- 在vscode下编写,只需要按下! 与 Tab键,就可生成网页的基本骨架-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boost 搜索引擎</title>
</head>
<body>
<div class="container">
<!-- 居中显示-->
<h1 align="center">Boost</h1>
<div class="search">
<input type="text" value="请输入关键词" >
<button onclick="Search()">搜索一下</button>
</div>
<div class = "search_res">
<!-- 注释部分主要用于CS网页进行测试-->
<!--下面是显示对应的搜索结果,对应搜索结果的对应的具体格式。-->
<!-- <div class="elem">
<h3><a href="">标题:XXXXXX</a></h3>
<p>这是摘要XXXXXXXXXXXXXXXXXXXXXXXXXX</p>
<i>https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin</i>
</div>
<div class="elem">
<h3><a href="">标题:XXXXXX</a></h3>
<td align="left"><p>XXXXXXXXXXXXXXXX</p>
<i>https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin</i>
</div>
<div class="elem">
<h3><a href="">标题:XXXXXX</a></h3>
<td align="left"><p>XXXXXXXXXXXXXXXX</p>
<i>https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin</i>
</div>
<div class="elem">
<h3><a href="">标题:XXXXXX</a></h3>
<td align="left"><p>XXXXXXXXXXXXXXXX</p>
<i>https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin</i>
</div>
<div class="elem">
<h3><a href="">标题:XXXXXX</a></h3>
<td align="left"><p>XXXXXXXXXXXXXXXX</p>
<i>https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin</i>
</div> -->
</div>
</div>
</body>
</html>
6.2 CSS
基础知识:
/*拓展:盒子模型,流动模型。*/
/*选择网页的所有内容*/
*{}
/*选择指定的一些标签*/
[标签],[标签]{}
/*选择类名,和标签对指的的内容框架进行页表修改。*/
.[类名] [标签]{}
/*外边距,单位em,即相对于父元素的外边距的大小。*/
margin: xxem;
/*内边距,同外边距。*/
padding: xx em;
/*设置高度为 父元素的比例,即100%继承父元素*/
height: xxx %;
/*设置高度为 xx 像素点,即绝对长度。px是最常用的长度单位。*/
height: xx px;
/*宽度同理*/
width: xxx %;
width: xxx px;
/*设置居中对齐*/
margin:0px auto;
/*设置盒子与顶部的距离*/
margin-top: xx px;
/*设置盒子与底部的距离*/
margin-bottom: xx px
/* 设置边框的颜色和样式,参数为2个像素点,实体灰边框*/
border: 2px solid grey;
/*设置右边框的样式:这里的无。*/
border-right: none;
/*设置字体的大小*/
font-size: xx px;
/*设置为字体的样式*/
font-family:xxx;
/*将字体的风格设置为普通字样*/
font-style: normal;
/*设置字体距离内边框的左边距*/
padding-left: xxpx;
/*设置字体的颜色, 参数为可选的或者颜色对应的参数。*/
color: xxx;
/*设置背景颜色*/
background-color: xxx;
/*设置元素为块级元素,独占一行,便于调参和修改*/
display: block;
/*将下划线设置为无*/
text-decoration: none;
实现代码:
<!-- 网页的CSS部分-->
<style>
/*对所有内容去重内外边距 */
*
{
margin:0em; /*外边距*/
padding:0; /*内边距*/
}
/*网页body 和 html的内容完全吻合*/
html,body
{
height: 100%;
}
/*这是类选择器,即选择class container*/
.container
{
/*设置盒子的宽度*/
width: 700px;
/*通过设置内外边距来达到居中对齐的效果*/
margin:0px auto;
/*设置盒子与顶部的距离,达到美化的效果*/
margin-top: 20px;
}
.container h1
{
/*设置盒子与顶部的距离,达到美化的效果*/
width: 600px;
margin-bottom: 20px;
}
/* 复合选择器,先选择第一个类,再选择第二个类,中间用空格隔开。 */
.container .search
{
/*宽度与父标签保持一致*/
width: 100%;
/*高度设置*/
height: 50px;
}
/*选择form表单中的input标签设置搜索框, 直接选中设置标签的属性*/
.container .search input
{
/*设置left左浮动*/
float: left;
width: 480px;
height: 50px;
/* 设置边框的颜色和宽度*/
border: 2px solid grey;
/* 将右边框设为无*/
border-right: none;
/* 设置字体的大小*/
font-size: 15px;
/* 设置字体距离内边框的左边距 */
padding-left: 10px;
color: #ccc;
}
.container .search button
{
/*设置left左浮动*/
float: left;
width: 120px;
height: 54px;
border: 2px solid #4e6ef2;
border-left: none;
/* background-color: blue;
*/
/* 设置按钮颜色 */
background-color: #4e6ef2;
/* 设置字体颜色 */
color: white;
/* 设置字体大小 */
font-size: 20px;
/* font-family:'Times New Roman', Times, serif; */
/* font-family:Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; */
font-family:cursive;
/* font-family:monospace; */
/* font-family:serif; */
}
.container .search_res
{
margin-top: 30px;
}
.container .search_res .elem
{
margin-top: 20px;
}
.container .search_res .elem a
{
/*设置为块级元素,只占一行*/
display: block;
/* 将标题的下划线去掉*/
text-decoration: none;
/* 设置标题的大小 */
font-size: 20px;
/* 设置标题的颜色 */
color: #4e6ef2;
}
/*设置光标的动作,即选中链接显示下划线。*/
.container .search_res .elem a:hover
{
text-decoration: underline;
}
.container .search_res .elem i
{
/*设置为块级元素,只占一行*/
display: block;
margin-top: 8px;
font-style: normal;
font-size: 15px;
color: green;
}
.container .search_res .elem p
{
margin-top: 8px;
font-family: 'Times New Roman', Times, serif;
}
</style>
6.4 JavaScript
说明:由于我们不是前端,所以为了降低开发难度,所以要引入Jquery;
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
实现思路:
- 提取button中的内容。
- 将内容转换为查询,发送给后端。
- 后端返回对应搜索的json串。
- 前端根据返回的json串构建网页内容。
基础知识:
/*提取指定标签的内容*/
let query = $("xxxxxxx").val()
/*在网页按下F12,显示网页的前端界面,点击上面的console运行网页时,可看到对应的内容,便于进行测试*/
console.log(query);
$.ajax({
type: "Get",
url: "" /*请求网页内容的资源,对应后端的服务于Get请求的界面*/
success: function(data)
{
//请求成功时对应的数据
//对数据进行处理。
}
error: function(data)
{
//请求失败时对应的内容。
}
});
//选择指定的类里面存放搜索的内容
let res = $("xxxxx");
//循环遍历data中的元素,实际在提取时,这里的emem为json串,可供参数的选择。
for(let elem of data)
{
/*这里我们设置一个a标签,并用元素填充其内容。*/
let a_lable = $("<a>",
text:elem.title,
href:elem.url,
target: "_blank"
);
//其余的标签内容,放在实现中.
let div_lable = $("<div>",
class: "elem");
div_lable.appendTo(a_lable)
res.appendTo(div_lable)
}
实现代码:
function Search()
{
// 获取.container .search input中的输入的关键字。
let query = $(".container .search input").val();
$.ajax(
{
type: "Get",
url: "/s?word=" + query,
success:function(data)
{
//构建对应的网页
BulidHtml(data);
}
}
)
}
function BulidHtml(data)
{
let result = $(".container .search_res");
result.empty();
for(let elem of data)
{
let a_lable = $("<a>",{
text: elem.title,
href: elem.url,
//点击链接会跳转到新的网页。
target: "_blank"
});
let p_lable = $("<p>",
{
text:elem.content
});
let i_lable = $("<i>",
{
text:elem.url
}
);
let div_lable= $("<div>",
{
class:"elem"
}
);
a_lable.appendTo(div_lable);
p_lable.appendTo(div_lable);
i_lable.appendTo(div_lable);
div_lable.appendTo(result);
}
}
总结 与 拓展
-
总结
- 此项目我们只是实现了一个基础的版本,难度并不是很大,并且我们使用了相应的库来降低开发难度。
-
拓展
- 暂停词,比如is,a,an诸如此类,可以进行过滤,以便于提高搜索的效率。
- URL,我们可以读取所有的URL,这里我们只读取到了/doc/html目录下的html文件。
- 爬虫程序,我们这里是通过官方下载,也可以通过爬虫程序获取到 html文件。
- 普通的网页,可以看到广告的网页,我们也可以通过设置系数,来完成竞价功能。
- 在搜索界面,当我们输入一部分词时,会显示相应的可能满足我们搜索要求的,即智能显示。
- 设置登录注册,引入MySQL。
- 对于扩展内容,博主在这里挖个坑,后面有时间和精力会进行补充的。
尾序
我是舜华,期待与你的下次相遇!