C/C++,STL,准标准库,Boost库,Jsoncpp(客户端和服务器端进行交互的时候所需要进行序列化和反序列化),cppjieba(给搜索关键字进行分词),cpp-httplib(一个开源的http的开源库,可以直接构建http服务器) html5,css,js,jQuery,Ajax
Centos 7云服务器
,vim/g++/Makefile,vscode/vs2019
文档ID | 文档内容 |
---|---|
1 | 新海诚上映了新电影 |
2 | 新海诚的新电影是铃芽户缔 |
对目标文档进行分词(目的:方便建立倒排索引和查找):
- 文档1:新海诚/上映/了新电影
- 文档2:新海诚/的新电影/是/铃芽户缔
停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑
关键字(具有唯一性) | 文档ID,weight(权重高的在前) |
---|---|
新海诚 | 文档1,2 |
上映 | 文档1 |
新电影 | 文档1,2 |
铃芽户缔 | 文档2 |
这样就可以模拟一次查找的过程:
用户输入:新海诚-->倒排索引中查找-->找到之后提取出文档ID(1,2)-->根据正排索引--->找到文档内容 -->title+conent(desc) +url 文档结果进行摘要--->构建响应结果
boost官网:https://www.boost.org/
//目前只需要boost_1_81_0/doc/html目录下的html文件,用它来建立索引
之所以选doc/html文件也是因为官网之中绝大部分使用的都是这里面的文件
[xifeng@VM-16-14-centos MyBoostSearch]$ touch parser.cc
//想要做数据处理就需要有原始数据-->去标签之后的数据
//标签也就是<>之中包含的,标签对搜索没有价值,需要被去除
//一般标签都是成对出现的
//html放的是原始文档
[xifeng@VM-16-14-centos data]$ html
[xifeng@VM-16-14-centos data]$ purify_html
//去除掉标签之后的html我们保存在puritf_html中(puritf提纯/去除)
[xifeng@VM-16-14-centos html]$ ls -Rl | grep -E '*.html' | wc -l
8429 //一共有8429个html文件
目标: 把每个文档都去标签,然后写入到同一个文件中,文档内容不需要换行(\n),文档和文档之间用\3区分
类似于:xxxxxxx\3xxxxxxxxx\3xxxxxxxxxxxx\3 但是这种方法虽然可行,可是后续处理的时候会比较麻烦,因为还需要去区分title,content,url的内容
---------------------------------------------------
所以我选择的是这种操作:title\3content\3url \n title\3content\3url \n....
就是同一个文档之间不同的数据之间采用\3来区分,文档文档之间采用\n来区分,这样既可以通过getline获取到一个文档的全部信息,又方便区分文档中的不同信息
之所以选择\3也是有一定的理由的:首先是因为它是控制字符,不会显示到文件中,其次就是\3也有正文结束的意思
主要有三大块:
首先要将所有的文件给拿到,可以将带路径的文件名存放到一个数组中(Enum File通过这个函数来枚举各个文件的路径)
其次就是对文件进行读取和解析(Parser File通过这个函数来解析)
解析数据要解析成什么样子的呢
可以定义一个结构体
typedef struct Docinfo
{
std::string title;//标签
std::string content;//内容
std::string url;//官网所对应的url
}Docinfo_t;
最后就是将解析到的数据存放在purity.txt中(Save Data通过这个函数来存储数据)
//代码的大致框架
#include
#include
#include
#include
const std::string src_path = "./data/html";
const std::string save_path= "./data/purify_html/purify.txt";
typedef struct Docinfo
{
std::string title;//文档的标签
std::string content;//文件的内容
std::string url; //文件对应官网中的url
}Docinfo_t;
//注意:输入型参数我使用:const &
//输出型参数:*
//输入输出型参数:&
bool EnumFile(const std::string& src_path,std::vector* file_list);
bool ParserFile(const std::vector& file_list,std::vector* data_list);
bool SaveData(const std::string& save_path,const std::vector& data_list);
int main()
{
//第一步:获取到文件中的所有的html路径,并将其存放到一个数组当中
std::vector file_list;//文件列表
if(!EnumFile(src_path,&file_list))
{
//如果遍历失败,后续就没有意义了,直接退出
std::cerr<<"EnumFile error"< data_list;//数据列表
if(!ParserFile(file_list,&data_list))
{
//同样解析失败也就是退出
std::cerr<<"ParserFile error"<
sudo yum install -y boost-devel
,我安装的是1.53版本的boost库,这意味的是用1.53的库去写代码,我搜索的文档还是1.81的文档,这两个并不冲突
首先需要通过boost库了解一些相关的接口,这里对文件的操作使用的是boost库中提供的filesystem(头文件:
在官方文档的使用样例中,对于命名空间这一块他使用的是namespace fs = boost::filesystem;
,所以在我的代码中也就不全局放开了,与文档保持一致,在使用的时候通过域作用限定符去写,不去无脑放开还是很有好处的,能很大程度的减少接口上的冲突
//一下是需要了解的部分类或者接口
//path类里面定义了一个root_path
path root_path() const;
Returns: root_name() / root_directory()
-----------------------------------------
path operator/ (const path& lhs, const path& rhs);
Returns: path(lhs) /= rhs.
// /=是被重载了的,作用:将优选目录分隔符附加到包含的路径名,除非:
//一个添加的分离器将是多余的,或者会将相对路径更改为绝对路径,或P.Empty()或*p.native()。cbegin()是目录分离器。
//root_name() 如果包含根名则返回根名
//root_directory() 如果包含根目录返回根目录
------------------------------------------
//exists用来判断文件是否存在
bool exists(const path& p);
//递归遍历
Class recursive_directory_iterator
//类型的对象提供标准库 对目录内容进行合规迭代,包括递归到其子目录
//进行迭代的时候需要考虑要遍历的文件必须是普通文件,必须是.html的,所以这就需要对文件名进行筛选
//判断是否是常规文件
bool is_regular_file(const path& p);
//用来判断后缀的是path类内部的一个成员函数
path extension(const path& p) const;
//可以看一下文档举的例子
std::cout << path("/foo/bar.txt").extension(); // outputs ".txt"
//这里可以发现调用返回的是".txt",这样就可以进行判断
//当判断完之后我们就需要将其push到存放地址的数组中去,path类中提供了一个string方法
template
String string(const codecvt_type& cvt=codecvt()) const;
//返回的是string类型的路径
EnumFile具体的实现:
#Makefile编写时,如果不加 -lboost_system和-lboost_filesystem会报错的
#因为boost不是标准库,第三方库链接时需要指明库名称
cc=g++
parser:parser.cc
$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem .PHONY:clean
clean:
rm -rf parser
//作用类似于typedef,跟文档样例保持一致,写代码时最好不要遇到命名空间就全部展开
namespace fs = boost::filesystem;
bool EnumFile(const std::string& src_path,std::vector* file_list)
{
//boost库中定义的成员,简单理解就是将我们传入的字符串转换成boost能够识别的路径
fs::path root_path(src_path);
if(!exists(root_path))
{
//代表目标文件不存在
return false;
}
//到这里就是已经找到了目标文件,可以进行迭代查找了
fs::recursive_directory_iterator end;//定义一个结束的迭代器
//构建一个从root_path开始的迭代器
for(fs::recursive_directory_iterator it(root_path);it!= end;++it)
{
//要查找首先得是一个普通文件
if(!fs::is_regular_file(*it))
{
continue;
}
//其次需要的是.html后缀的文件
if(!(it->path().extension()==".html"))
{
continue;
}
//到这里就是后缀名为.html的普通文件了,将其存放进vector即可
//如果直接push_back(*it)是不正确的,因为那个已经不是string类型了
//我们可以通过boost提供的string接口来将其转化一下
file_list->push_back((it->path()).string());
}
return true;
}
总体思路:首先已经拿到了所有的带路径的文件名,接下来要做的就是对这些文件进行遍历,打开每一个文件进行读写,将其解析成Docinfo_t
这样的结构将其插入到vector中
#include
ifstream
是输入流,其构造函数explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in);
注意一下ios和ios_base没有区别,都可以用open和close
分别对应着打开文件和关闭文件;void open (const char* filename, ios_base::openmode mode = ios_base::in);
void close();
is_open
是判断文件是否被打开bool is_open();
getline
是按行读取,需要将读到的字符拼接成一个长字符串istream& getline (istream& is, string& str);
//以下是具体代码
//这里对c++提供的接口进行了简单的封装
bool Open(const std::string &file_path, std::string *out)
{
std::ifstream in(file_path, std::ios_base::in);
if (!in.is_open()) // 如果文件没有被打开则返回false
{
// 文件如果打开失败打印失败的文件名
std::cerr << "Open file error:" << file_path << std::endl;
return false;
}
//文件能够被成功打开
std::string str;
//理解getline读取到文件结尾:getline的返回值是一个&,while判断的是一个bool类型,本质就是返回的类型中重载了强制类型转换
while(std::getline(in,str))//按行读,最后拼接在一起
{
*out +=str;
}
in.close();
return true;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADIrHGnD-1679760493442)(C:/Users/yangyr0206/AppData/Roaming/Typora/typora-user-images/image-20230314143742011.png)]
通过STL容器string提供的find函数可以去查找字符串"
size_t find (const string& str, size_t pos = 0) const;
//如果找不到就会返回std::npos;
//找到就返回找到的第一个字符对应的位置下标,比如找到就返回<的下标
通过substr去获取中间的片段
string substr (size_t pos = 0, size_t len = npos) const;
//pos是从什么位置开始,len是取多长
//实现
bool ParserTitle(const std::string &result, std::string *title)
{
// 要提取的就是 之间的数据
size_t begin = result.find("");
size_t end = result.find(" ");
if (begin == std::string::npos || end == std::string::npos || begin > end)
{
std::cerr << "find error" << std::endl;
return false;
}
// 两个都找到了,且begin < end
begin += 7;//为了跳过
*title = result.substr(begin, end - begin); // 左闭右开区间
return true;
}
在进行遍历时,无论是单标签还是双标签,只要碰到了>
,就意味着,当前的标签被处理完毕;只要读到了<
意味着新的标签开始了
//实现
bool ParserContent(const std::string &result, std::string *content)
{
enum Status // 状态机的状态码
{
START, // 开始
OVER // 结束
};
// 去标签,当遇到>我们认为一个标签结束了,遇到<表示一个标签刚刚开始
// 由此可以设置一个简单的状态机
enum Status st = START; // 默认是标签的开始
for (char c : result)
{
switch (st)
{
case START:
if (c == '>')
st = OVER;
break;
case OVER:
if (c == '<')
st = START;
else // 正文内容
{
if (c == '\n')
c = ' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
boost库的官方文档,和我们下载下来的文档,是有路径的对应关系的
官网的URL样例:https://www.boost.org/doc/libs/1_81_0/doc/html/accumulators.html
下载下来的URL样例:boost_1_81_0/doc/html/accumulators.html
我项目中的URL样例:MyBoostSearch/data/html/accumulators.html
//本质就是我将下载下来doc/html/* cp data/html/
----------------------------------------------------------------
url_head = "https://www.boost.org/doc/libs/1_81_0/doc/html";
url_tail = [data/html](delete) /accumulators.html--->"/accumulators.html";
url = url_head + url_tail;//相当于形成了一个官网链接
const std::string head_url = "https://www.boost.org/doc/libs/1_81_0/doc/html";
bool ParserUrl(const std::string &head_url, const std::string &file, std::string *url)
{
// 就是进行平接只需要把我的路径中的./data/html-->也就是之前定义的src_path去除与head_url拼接即可
std::string tail_url = file.substr(src_path.size());
*url = head_url + tail_url;
return true;
}
bool ParserFile(const std::vector &file_list, std::vector *data_list)
{
for (const std::string &file : file_list)
{
// 1.打开文件
// result(结果)用来存放文件读出来的数据
std::string result;
if (!Tool::Open(file, &result))
{
continue;
}
// 2.解析成Docinfo_t类型的数据
// 提取标签,提取内容,拼接url
Docinfo_t doc;
// 获取标签
if (!ParserTitle(result, &doc.title))
{
continue;
}
// 获取内容
if (!ParserContent(result, &doc.content))
{
continue;
}
const std::string head_url = "https://www.boost.org/doc/libs/1_81_0/doc/html";
// 拼接url
if (!ParserUrl(head_url, file, &doc.url))
{
continue;
}
//move函数主要作用就是数据的移动,也就是说moved-from对象处于有效但未指定的状态。这意味着,在这样的操作之后,移出对象的值只应被销毁或分配一个新值;否则,访问它会生成未指定的值。在这里就是doc里面的数据就不再是有效数据了
//如果不加move会发生拷贝,而一个网页数据有的还是挺大的,拷贝的话需要浪费很多时间
data_list->push_back(std::move(doc));
}
return true;
}
就是将前面得到的data_list里面的数据全部按照制定的规则存放到purify.txt文件中即可
bool SaveData(const std::string &save_path, const std::vector &data_list)
{
std::ofstream out(save_path, std::ios_base::out | std::ios_base::binary);
//要实现的就是同一网页的title,content,url通过\3分隔,不同网页通过\n分隔
//在文件中'\3'是以^C的形式体现的
for(auto & data : data_list)
{
//打开文件,以二进制的形式写入
if(!out.is_open())
{
std::cerr<<"open savefail error"<8429行也就是说里面有8429个文件
首先需要建立index.hpp文件,在其中定义一个数据的数据结构Docinfo
需要建立倒排索引的节点(文档id,权重,关键字)
正排索引的数据结构选择数组,这样的话当知道文档id的话就可以根据下表直接找到(时间复杂度就为O(1))
倒排索引一定会存在一个关键字和多个文档id(一个)有关联,所以倒排索引天然的就适合使用unordered_map
字符串切分虽然用stl容器提供的接口也能够去写,可是比较繁琐,可以使用boost库中的split(
boost::split(type,str,boost::is_any_of("\3"),boost::token_compress_on)
//第一个参数type就是一个用来存放切分后数据的数据结构
//str就是要切分的字符串
//boost::is_any_of()里面设置的是分隔符
//boost::tocken_compress_on:将连续多个分隔符压缩成一个,默认没打开,一般用的时候打开
// 这里是构建索引模块
#pragma once
#include
#include
#include
#include
namespace MyIndex
{
// 文档信息
struct Docinfo
{
std::string title;
std::string content;
std::string url;
uint64_t file_id; // 因为有正排和倒排索引,所以文档id是不可或缺的
};
// 倒排元素
struct InvertedElement
{
std::string key_word;
uint64_t file_id;
int weight; // 权重,之后显示的先后顺序需要与权重挂钩
};
//重命名为倒排拉链
typedef std::vector InvertedList;
class Index
{
private:
// 正排索引的数据结构
// 因为正排索引需要的是根据文档id去找文档内容,文档id可以当做数组下标能够实现O(1)的查找
std::vector forward_index;
// 倒排索引的数据结构
// 根据关键字去找文件id,这天然就是一对多(一对一)的关系
// 所以用unordered_map最为合适
std::unordered_map inverted_index;
public:
Index();
~Index();
public:
// 获取正排-->返回的是文档信息
Docinfo *GetForward(const uint64_t &id)
{
//这类比较简单就直接写了
if(id > forward_index.size())
{
//id越界
std::cerr<<"id cross the border error"<返回的是倒排拉链,也就是一个关键字对应的一组文件id
InvertedList* GetInverted(const std::string &key_word)
{
auto it = inverted_index.find(key_word);
if(it==inverted_index.end())
{
std::cerr<<"not found"<second);
}
// 构建索引 --->根据的就是解析后的./data/purify_html/purify.txt里面的内容
//const std::string target_path = "./data/purify_html/purify.txt"; // 目标文件的路径
bool BuildIndex(const std::string &input)//在这里面去构建对应的索引模块
{
return true;
}
};
};
接下来就是对上述函数的实现
要构建索引就需要数据,而这数据就是Parser解析出来的存放在./data/purify_html/purify.txt里面的内容
所以理所应当的在Build Index中需要进行文件操作
bool BuildIndex(const std::string &input)
{
// 之前是二进制形式写,这里也是二进制形式读
std::ifstream in(input.c_str(), std::ios_base::in | std::ios_base::binary);
if (!in.is_open())
{
std::cerr << "open error" << std::endl;
return false;
}
std::string line;
while (std::getline(in, line))
{
// 构建正排-->返回的是Docinfo然后需要通过返回值去构建倒排索引
Docinfo *doc = BuildForward(line);
if (doc == nullptr)
{
std::cerr << "BuildForward error" << std::endl;
continue; // 一个文档的正排构建失败就没有必要再去构建倒排了
}
// 构建倒排
BuildInverted(*doc);
}
in.close();
return true;
}
需要使用到之前提到的boost库中提供的split函数
void SlicingString(const std::string& line,std::vector*result, const std::string& separator)
{
//可以通过stl提供的容器接口来实现,可是实现起来比较繁琐
//所以这里直接使用boost库中的 split
boost::split(*result,line,boost::is_any_of(separator),boost::token_compress_on);
//*result 存放切分的数据的结构vector
//line 要切分的数据
//is_any_of() 构建切分符
//token_compress_on将连续的分隔符压缩成一个分隔符(默认关闭,需要手动打开)
}
Docinfo *BuildForward(const std::string &line)
{
std::vector result;
const std::string separator = "\3";
MyTool::StringTool::SlicingString(line, &result, separator);
if (result.size() != 3)
{
// 如果切分后的数据没有三部分则切分出错
return nullptr; // 切分失败返回空
}
// 将切分的数据填充到doc中
Docinfo doc;
doc.title = result[0];
doc.content = result[1];
doc.url = result[2];
doc.file_id = forward_index.size(); // 先保存在push,这样可以让id和数组下标对应,比如一开始什么都没有size=0,文档id=0,当这个doc push进去之后它所在的下标也是0
// 将doc 插入到正排索引的vector中
forward_index.push_back(std::move(doc));// 通过拷贝效率太低,直接move可以提升效率
return &forward_index.back();
}
//原理:就是需要构建出多个这样的结构,之前已经拿到了Docinfo的数据
struct InvertedElement
{
std::string key_word;
uint64_t doc_id;
int weight; // 权重,之后显示的先后顺序需要与权重挂钩
};
//倒排拉链
typedef std::vector InvertedList;
//倒排索引一定是一个key_word和一组(一个)InvertedElement对应
//通过正排索引可以拿到的内容
struct Docinfo
{
std::string title;
std::string content;
std::string url;
uint64_t file_id;
};
//举例文档:
title:新电影铃芽户缔
content:新海诚出了一部新的电影名叫铃芽户缔
url:http://XXX
doc_id: 324
根据文档内容形成一个或多个InvertendElement(倒排拉链)
因为是一个一个的对文档进行处理的,一个文档会包含多个词,都对应到当前的doc_id
1. 需要对title和content进行分词 --这里使用jieba分词-->CutForSearch(s,words)
titile:新/电影/新电影/铃芽/户缔/铃芽户缔 title_words
content:新/海诚/新海诚/出/一部/新/电影/电影名/叫/铃芽/户缔/铃芽户缔 content_words
词和文档的相关性(相关性的实现并不是一个简单的技术,这里用最简单的方式去实现)
2. 词频统计
这里我实现相关性的方案就是根据词频来设置,同时我认为在标题中出现的词相关性会更高一些,在内容中出现相关性低一些
struct word_count{
int title_count;//标题中key_word出现的次数
int content_count;///内容中key_word出现的次数
};
//建立关键字和出现次数的映射关系
unordered_map words_count;
for &word : title_word{
word_count[word].title_count++;
}
for &word : content_word{
word_count[word].content_count++;
}
知道了文档内容中词的出现次数
3. 自定义相关性
for &word : words_count{
InvertendElement elem;
//因为处理的是同一个文档的内容,所以文档的id是可知的
elem.doc_id = 324;
elem.key_word = word.first;
elem.weight = 10*word.second.title.count + 1* word.second.content_count;
inverted_index[word.first].push_back(elem);
}
//s要切部分
//words切后存储的结构
//string s;
//boost::to_lower(s)将字符串转化成小写
//c也提供了一些函数接口:toupper()//转换为大写,tolower()//转换为小写
//c++提供的接口可以用:transform(s.begin(),s.end(),s.begin(),::toupper);
//几个参数简单理解就是从哪开始,到哪结束,结果储存到哪,转成大写还是小写
获取链接:git clone https://gitcode.net/mirrors/yangyiwu/cppjieba.git
如何使用:需要的是include/cppjieba/Jieba.hpp
文件中有个test/demo.cpp的文件里面是jieba分词的用法
如果想要正确编过,需要将deps/limonp里面的所有文件都拷贝进include/cppjieba中,我这里使用切词的函数是里面的:CutForSearch(s,words) ---->s表示要切分的数据,words表示存放的数据
//在下载后的cppjieba文件中,有一个test文件里面有个demo.cpp文件,里面记录了各种接口函数的用法以及样例
//接下来来看看CutForSearch(s,words)函数在文档中的用法
#include "../include/cppjieba/Jieba.hpp"
#include
#include
#include
using namespace std;
//这些const定义的是词库的所在路径,自己使用的时候需要注意调整这些路径
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";
int main(int argc, char** argv) {
cppjieba::Jieba jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
string s;
vector words;
s = "小明硕士毕业于中国科学院计算所,后在日本京都大学深造";
cout << s << endl;
cout << "[demo] CutForSearch" << endl;
jieba.CutForSearch(s, words);
cout << limonp::Join(words.begin(), words.end(), "/") << endl;
return 0;
}
//我这里采用ln -s软链接的方式去调整路径
//结果:
//小明硕士毕业于中国科学院计算所,后在日本京都大学深造
//[demo] CutForSearch
///小明/硕士/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造
#include "./cppjieba/Jieba.hpp" //我在当前目录设置了一个cppjieba的软连接
//lrwxrwxrwx cppjieba -> data/cppjieba/include/cppjieba
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 Cppjieba
{
private:
static cppjieba::Jieba jieba;//要定一个全局的静态成员变量,这样不需要每一次调用CutForSearch都去先创建一个jieba对象,大大的节省了时间
//因为jieba分词在index建立索引中会非常频繁的使用,所以一旦在函数里面定义jieba对象就会让整个程序变的很慢
public:
static void CutForSearch(const std::string &s, std::vector *words)
{
jieba.CutForSearch(s, *words);
}
};
cppjieba::Jieba Cppjieba::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
bool BuildInverted(const Docinfo &doc)
{
#define T_weight 10
#define C_weight 1
struct word_count{
int title_cnt;//标题key_word出现次数
int content_cnt;//内容key_word出现次数
};
//1.需要对标题和内容进行切分 && 2.进行词频统计
std::vector title_result;//存放切分后的数据
std::unordered_map word_cnt;//建立key_word和词频的映射
MyTool::Cppjieba::CutForSearch(doc.title,&title_result);
for(auto word : title_result)//遍历标题分词
{
//这里有个细节,Hello hello HELLO这些词是算一个,还是算三个-->通过百度浏览器的搜索结果可以发现搜索时是不区分大小写的
//这里就定个规定:文档的标题和正文分词全部按照小写来分词,同时用户输入之后也将其转换成小写
//可以用c提供的toupper()--->转换为大写,tolower()---->转换为小写 /c++ 中的transform等等
//我选择的就是boost库提供的一个接口to_lower()
boost::to_lower(word);//因为我不想改变doc里面的数据,所以没有用引用
word_cnt[word].title_cnt++;//unordered_map重载了[],如果key存在时返回的就是value的引用,如果不存在就插入key
}
std::vector content_result;
MyTool::Cppjieba::CutForSearch(doc.content,&content_result);
for(auto word : content_result)
{
boost::to_lower(word);
word_cnt[word].content_cnt++;
}
//3.构建相关性
for(auto& word : word_cnt)
{
//word 在这里是unordered_map>
InvertedElement elem;
//这里处理的都是一个文档的分词,所以id就是doc里面的file_id
elem.file_id = doc.file_id;
elem.key_word = word.first;
elem.weight = T_weight* word.second.title_cnt + C_weight* word.second.content_cnt;//设置相关性
//unordered_map> inverted_index;
InvertedList& inverted_list = inverted_index[word.first];
inverted_list.push_back(std::move(elem));//这里加不加move都还好,里面数据比较小
}
return true;
}
基本代码结构:
//安装jsoncpp:yum install -y jsoncpp-devel
void InitSearcher(const std::string &path)
{
// 1.构建/获取index对象--->因为对于index,建立完之后主要就是查找,并不会修改里面的内容,所以index只需要一份即可
// 也就是把index设计成一个单例即可
}
//这个就是通过用户的搜索信息去索引中去查找相关文档,并输出序列化之后的字符串
void Search(const std::string &Inquire, std::string *json_string)
{
// 1.分词:将搜索Inquire进行分词
// 2.查询:分词之后用关键词去索引表中查找,如果有会拿到倒排拉链InvertedList-->vector
// 3.合并排序:将查找后的结果通过weight权重进行降序排序
// 4.构建:根据查询结果构建json字符串--->也就是序列化,需要jsoncpp
}
原因:我所实现的搜索引擎只是对boost进行搜索,所以理论上并不需要多份索引,构建索引也需要消耗资源,只需要构建一份之后其他的共用即可,这里就需要将index改为单例,具体改法如下:
//在index类中设置一个staitc的index对象,和一把锁(解决线程安全问题,如果是多线程不加锁,对单例的获取存在线程安全问题)
static Index *singleton_index;
static std::mutex mtx; // 创建一把锁---->#include
//之后就是说构造函数私有化-->必须要有,因为这样才能够在类内new一个index对象
//禁止构造和拷贝构造函数
Index(const Index &in) = delete; // 禁止拷贝构造
Index &operator=(const Index &in) = delete; // 禁止赋值拷贝
//之后就是在public中定义一个静态的成员函数
static Index *GetIndex()// 静态成员函数才能访问静态成员变量
{
// 两个if是为了提高效率,多个线程竞争锁也是需要消耗资源的,
if (singleton_index == nullptr)
{
// 不加锁的话多线程情况下不是线程安全的
mtx.lock(); // 加锁
if (singleton_index == nullptr)
{
singleton_index = new Index;
}
mtx.unlock();
}
return singleton_index;
}
//构建好单例之后直接使用接口即可
index = MyIndex::Index::GetIndex();
index->BuildIndex(path);
struct Compare//定义一个降序的仿函数用于sort的第三个参数
{
bool operator()(const MyIndex::InvertedElement &e1, const MyIndex::InvertedElement &e2)
{
return e1.weight > e2.weight;
}
};
void Search(const std::string &Inquire, std::string *json_string)
{
// 1.分词:将搜索Inquire进行分词
std::vector words;
MyTool::Cppjieba::CutForSearch(Inquire, &words);
// 2.查询:分词之后用关键词去索引表中查找,如果有会拿到倒排拉链InvertedList-->vector
MyIndex::InvertedList result;//different
for (std::string key_word : words)
{
boost::to_lower(key_word); // 建立索引的时候默认全是小写,搜索的时候也同样需要
// 先查倒排,再根据查询后的结果查正排
MyIndex::InvertedList *inverted_list = index->GetInverted(key_word);
if (inverted_list == nullptr) // 如果词不存在,就继续下一次查找
{
continue;
}
// 这个就是通过迭代器将InvertedList里面的InvertedElement全部插入到result中
result.insert(result.end(), inverted_list->begin(), inverted_list->end());
}
// 3.合并排序:将查找后的结果通过weight权重进行降序排序
std::sort(result.begin(), result.end(), Compare());
// 4.构建:根据查询结果构建json字符串--->也就是序列化,需要jsoncpp
Json::Value root; //Value是json里面的万能类
//json序列化通常使用Wright类(FastWriter,StyledWriter) 反序列化通常是Reader类
for(auto& elem : result)
{
//根据result里面的数据,去正排查找文档并且把查找到的文档序列化
MyIndex::Docinfo* doc = index->GetForward(elem.file_id);
Json::Value tmp;
tmp["title"] = doc->title;//标题
tmp["desc"] = GetDesc(doc->content, elem.key_word);//描述
tmp["url"] = doc->url;//要跳转的网页
root.append(tmp);
}
//要注意无论是FastWriter还是StylenWriter它们都是类,而write是它们的成员函数,所以需要先构建一个匿名对象来使用它的成员函数
//*json_string = Json::FastWriter().write(root);//StyleWriter方便调试,所以先用,后面没有问题之后再用这个
*json_string = Json::StyledWriter().write(root);//这样用户就获得了通过权重排序之后的文档信息
}
注意:jsoncpp
是第三方库所以使用g++编译的时候需要指定库-ljsoncpp
struct GetCompare//定义了一个仿函数
{
bool operator()(int x, int y)
{
return std::tolower(x) == std::tolower(y);
}
};
std::string GetDesc(const std::string &content, const std::string &key_word)
{
// 获取描述---->这里就简易实现一下
// 获取第一次出现的key_word的前50个字符,获取key_word的后100个字符,如果不够就从头开始,或者截取到尾部
const int prev = 50; // 因为size_t是无符号整数,所以为了方便就直接设置为int
const int next = 100;
// int pos = content.find(key_word); // 这里查找会出现None1的情况,主要是因为在之前的代码中我对切分后的数据统一to_lower了,但是find函数在查找的时候不会自己进行大小写转化
// 这里使用C++的提供的算法search
auto it = std::search(content.begin(), content.end(), key_word.begin(), key_word.end(), GetCompare());
if (it == content.end())
return "None"; // 如果到结尾都没有查到就返回None---不会发生
int pos = std::distance(content.begin(), it);
int start = 0;
int end = content.size() - 1;
// 代表前面有50个字符
if (pos - prev > start)
start = pos - prev;
// 代表后面有100个字符
if (end - next > pos)
end = pos + next;
if (start >= end) // 同理
return "None";
std::string desc = content.substr(pos, end - start);
desc += "......";
return desc;
}
C++的#include
提供的search函数
template
ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,ForwardIterator2 first2, ForwardIterator2 last2,BinaryPredicate pred);
//简单点理解就是1,2两个参数是要查找数据的迭代器:意味着从哪开始查到哪结束比如:string str = "hello word";
//3,4两个参数是查找目标的迭代器string key = "word"; 在str中去查找key
//第5个参数可以理解为传入查找方法,我这里是传入的一个GetCompare的仿函数
#include
提供的函数destance
可以用来查看迭代器相比于begin走了多远
template
typename iterator_traits::difference_type
distance (InputIterator first, InputIterator last);
//用法
std::list mylist;
for (int i=0; i<10; i++) mylist.push_back (i*10);
std::list::iterator first = mylist.begin();
std::list::iterator last = mylist.end();
std::cout << "The distance is: " << std::distance(first,last) << '\n';
return 0; //result:The distance is 10
如果想使用cpp-httplib库
注意:
centos 7 默认的gcc编译器的版本是4.8.5版本的,如果想要使用cpp-httplib是需要更新gcc编译器的,如果用老的会编译不通过,或者运行时报错
更新gcc方法,搜索关键字: scl gcc devsettool升级gcc
1.安装scl源:
yum install centos-release-scl scl-utils-build
2.安装新版本gcc
sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
安装好之后,工具集在ls /opt/rh中
//启动新版本的gcc:命令行启动只能在本会话有效
scl enable devtoolset-7 bash
//建议将上面的启动命令添加到~/.bash_profile这个登录脚本里面
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
#每次启动的时候都会执行scl这个命令
scl enable devtoolset-7 bash
最新的cpp-httplib在使用的时候,如果gcc不是特别新的话可能会有运行时错误
我这里使用的是cpp-httplib 0.7.15版本
通过gitee搜索cpp-httplib,下载zip文件上传到服务器即可
inline bool Request::has_param(const char *key) const {
return params.find(key) != params.end();//他这里面的params是一个multimap类型
}
//Server端的使用
//http
httplib::Server svr;
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
});
svr.lesten("0.0.0.0",8080);
#include "./cpp-httplib/httplib.h"//安装的cpp-httplib库的位置
#include "searcher.hpp"
const std::string input = "./data/purify_html/purify.txt";//建立索引的数据源
const std::string root_path = "./wwwroot";//web根目录
int main()
{
MySearcher::Searcher searcher;
searcher.InitSearcher(input);
httplib::Server svr;
//设置web根目录
svr.set_base_dir(root_path.c_str());
//Lambda表达式想要引用外部的对象,需要在[] 中&+对象---->[&searcher]
svr.Get("/s",[&searcher](const httplib::Request& req, httplib::Response& res){
//res.set_content("hello word!","text/plain");//里面是页面的显示内容,以文本形式显示
if(!req.has_param("word"))//has_param--->判断是否有参数
{
res.set_content("需要有搜索关键字!", "text/plain; charset=utf-8");
//设置返回内容"text/plain" 对应的是http中的Content-Type设置charset=utf-8的时候中间不能带空格
return ;
}
std::string key_word = req.get_param_value("word");//获取参数
std::cout<<"用户在搜索:"<json的Content-Type是application/json
res.set_content(json_string,"application/json");
});
//将其设置为listen状态
svr.listen("0.0.0.0",8080);
return 0;
}
了解vscode
它是一个编辑器
安装插件
1.Chinese(汉化)
2.open in browser//写好的网页可以直接单击右键用浏览器打开
3.Remote -SSH //用来链接Linux
在命令行输入remote -ssh之后就开始登录了跟xshell的登录是一样的
html: 是网页的骨骼---负责网页结构
css: 负责网页美观
js(javascript):网页的灵魂--网页的动态效果,前后端交互
//我是对着教程:w3cschool这个网站去写的一些前端模块
div 元素是块级元素,它是可用于组合其他 HTML 元素的容器。div 元素没有特定的含义
input 是设置表单数据
button元素定义可点击的按钮
a标签,href 属性规定链接的目标。开始标签和结束标签之间的文字被作为超级链接来显示。
<a href="http://www.w3school.com.cn/">Visit W3Schoola>
<p>这个就是一个普通的标签p>
<i>这个是斜体标签i>
有很多种方法将html和css进行关联起来,这里我就直接采用style将其内联到html中
设置样式的本质:找到要设置的标签,设置它的属性
1.选择特定的标签:类选择器,标签选择器,复合选择器
2.设置指定标签的属性:具体见代码(很多属性也可以去参考已有的网页他们的属性设置)
<style>
/*去掉网页中的所有默认内外边距*/
* {
/*设置外边距*/
margin: 0;
/*设置内边距*/
padding: 0;
}
/*设置body内的内容和html的呈现是1:1的*/
html,
body {
height: 100%;
}
/*.开头的一般叫类选择器*/
.container {
/*设置div的宽度*/
width: 800px;
/*通过设置外边距达到居中效果*/
margin: 0px auto;
/* 设置外边距的上边距,保持元素和网页的上部距离 */
margin-top: 15px;
}
/* 复合选择器,选择container下的search */
.container .search {
/* 宽度与父标签一致 */
width: 100%;
/* 高度设置为52xp */
height: 52px;
}
/* 选中input标签 单看input就是标签选择器,不需要加.*/
.container .search input {
float: left;
/*给input和button设置left浮动就可以让两个盒子之间的边距清零就可以拼在一起了*/
width: 600px;
height: 50px;
/* input在设置的时候没有考虑边框的问题,所以同height的情况下button会比input小一点 */
/* 设置边框宽度(1px),边框的样式(实线) ,边框颜色(black) */
border: 1px solid black;
/* 右边框给去除 */
border-right: none;
/* 设置内边距,让默认文字不要紧贴左侧边框 */
padding-left: 10px;
/* 设置input内部字体的颜色和字体大小、样式*/
color: #ccc;
font-size: 17px;
font-family: Georgia, serif;
}
.container .search button {
float: left;
width: 150px;
height: 50px;
/*设置button的背景颜色,可以通过f12去查看百度或者其他浏览器的颜色设置*/
background-color: #4e6ef2;
/*设置button中的字体颜色*/
color: #FFF;
/*设置button字体大小*/
font-size: 20px;
/* 设置字体样式 */
font-family: Georgia, serif;
}
.container .result {
width: 100%;
}
.container .result .docinfo {
/*上边距*/
margin-top: 10px;
}
/*a标签的设置,a和i属于行内元素,为了防止显示错误需要加上display:block*/
.container .result .docinfo a {
/*设置块级元素,可以独占一行*/
display: block;
/*去除a标签的下划线*/
text-decoration: none;
/*设置a标签的字体大小,颜色*/
font-size: 18px;
color: #4e6ef2;
}
/*a标签的光标事件*/
.container .result .docinfo a:hover
{
/*设置鼠标放在a标签上的动态效果*/
text-decoration: underline;
}
/*p标签属性设置*/
.container .result .docinfo p {
/*就是让p标签内容与a标签内容有点空位*/
margin-top: 3px;
font-size: 15px;
font-family: Georgia, serif;
}
/*i标签属性设置*/
.container .result .docinfo i {
/*设置块级元素,可以独占一行*/
display: block;
/*取消标签斜体风格*/
font-style: normal;
font-size: 13;
font-family: Georgia, serif;
color: greenyellow;
}
style>
直接使用原生的js成本比较高,这里我使用的是JQuery
//有点像c++语言和STL库之间的关系
//这里我直接网页引入微软的CDN
JQuery
中会遇到的一些函数或者用法:
//这样就可以提取input里面的value数据
//()里面填写的是要提取谁里面的数据,提input里面的value
len query = $(".container .search input").val();
console.log("query = " +query);
//console 是浏览器的对话框,可以用来查看js数据
//具体可以通过F12然后选择控制器去查看
//发送http请求,ajax:属于一个和后端进行数据交互的函数,JQuery中的
ajax({});//具体的使用见下面的代码
还有一些的使用不好单独举例,见下面的代码注释
<!--js代码-->
<script>
function Search() {
//是浏览器的弹出框,当点击搜索一下会弹出alert的内容
//alert("hello js");
//1.提取数据,$可以理解成JQuery的别称
//()里面填写的是要提取谁里面的数据,提input里面的value
let query = $(".container .search input").val();
console.log("query = " + query);//console 是浏览器的对话框,可以用来查看js数据
//2.发送http请求,ajax:属于一个和后端进行数据交互的函数
//type是请求的方法(GET,POSE)这里用GET,url就是自己的url后面加上/s?word=query
//success:function(data)请求成功返回的参数就存在data中,相当于设置了一个回调
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: function(data) {
console.log(data);
BuildHtml(data);
}
});
}
function BuildHtml(data) {
//想重新构建一个网页信息,data是从http_server中返回的json_string
let result_tag = $(".container .result");//获取网页中的result标签
result_tag.empty();//清空历史搜索结果
//类似于c++里面的for(auto e : result)
for(let elem of data){
let a_tag = $("", {
text: elem.title, //text就代表标签内容
href: elem.url, //设置跳转网页的url
target: "_blank" //设置跳转,即跳转到一个新的网页中
});
let p_tag = $(""
, {
text: elem.desc
});
let i_tag = $("", {
text: elem.url
});
let doc_tag = $("", {
class: "docinfo"
});
a_tag.appendTo(doc_tag);//表示a_tag添加到doc_tag中
p_tag.appendTo(doc_tag);
i_tag.appendTo(doc_tag);
doc_tag.appendTo(result_tag);//doc_tag要添加到result标签中
}
}
</script>
10.细节优化
文档重复显示问题
首先是之前在searcher.hpp中有一处的问题具体暴露出来就是以下问题,当一个文档同时出现几个关键词时,会重复的将其打印出来
接下来就是去优化这段代码,其实也很简单,我这里选择重新定义一个类
struct DedupElem
{
uint64_t doc_id;
int wight;//权值进行加和
vector key_word;//存放一组关键字
};
具体实现见下面代码:
void Search(const std::string &Inquire, std::string *json_string)
{
std::vector words;
MyTool::Cppjieba::CutForSearch(Inquire, &words);
//-------------------------begin--------------------------------- 这是需要的两个变量
std::unordered_map dedup_map; // 建立一个文档id与Dedupmlem的映射
std::vector list_all;//用来存dedup_map中的second
//--------------------------end--------------------------------
for (std::string key_word : words)
{
boost::to_lower(key_word);
MyIndex::InvertedList *inverted_list = _index->GetInverted(key_word);
if (inverted_list == nullptr)
{
continue;
}
//------------------------------begin-----------------------------------------这之间的就是优化的代码
// 优化代码,遍历InvertedList
for (const auto &elem : *inverted_list)
{
auto &item = dedup_map[elem.file_id]; // 如果文档id没有就构建,有了就返回second的引用
item.doc_id = elem.file_id; // 构建文档id
item.weight += elem.weight; // 将关键字的权重全部相加
item.key_words.push_back(elem.key_word); // 同一文档的关键字全部插入到vector中
}
}
for (auto &elem : dedup_map)
{
list_all.push_back(std::move(elem.second));
}
//-------------------------------end-------------------------------------------到这结束
std::sort(list_all.begin(), list_all.end(), Compare());
Json::Value root;
for (auto &elem : list_all)
{
// 根据result里面的数据,去正排查找文档并且把查找到的文档序列化
MyIndex::Docinfo *doc = _index->GetForward(elem.doc_id);
Json::Value tmp;
tmp["title"] = doc->title; // 标题
tmp["desc"] = GetDesc(doc->content, elem.key_words[0]);
// 描述--->这里就取vector里面的第一个词作为关键字了
tmp["url"] = doc->url; // 要跳转的网页
root.append(tmp);
}
*json_string = Json::FastWriter().write(root);
}
添加一些日志信息
具体见代码,这个主要目的就是为了看着方便,只要是之前代码用cerr
或者cout
打印的都可以换成LOG();
#pragma once
#include
#include
#include
#include
#include
#define NORMAL 0 //正常
#define WARNING 1 //警告
#define DEBUG 3 //调试
#define CRITICAL 4 //严重错误
#define LOG(STATE,MESSAGE) log(#STATE,MESSAGE,__FILE__,__LINE__)
void log(const std::string &state,const std::string& message,const std::string& file,int line)
{
//看日志的话一般看处于什么状态是正常还是错误以及输入的信息,其次看什么文件出问题了,在哪一行
time_t t = time(nullptr);//获取当前的时间戳
//下面是将时间戳转换成北京时间
//struct tm *gmtime(const time_t *timep);
struct tm* my_tm = gmtime(&t);
//因为有时差,所以小时需要加上8
my_tm->tm_hour+=8;
std::string format = "%Y-%m-%d:%H:%M:%S";
//size_t strftime(char *s, size_t max, const char *format,const struct tm *tm);
//char* s 输出型参数
//size_t max 是s的大小
//format就是要按照什么形式输出
char buff[25];
memset(buff,0,sizeof(buff));
strftime(buff,sizeof(buff),format.c_str(),my_tm);
std::cout<<"["<
添加去暂停词代码
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 Cppjieba
{
private:
// 增加一个去暂停词的功能---->这个功能会让创建变的非常的慢
cppjieba::Jieba jieba;
std::unordered_map stop_word; // 同时这些只需要同时存在一份,所以直接设置为单例
static Cppjieba *singloten;
private:
Cppjieba()
: jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH){}
Cppjieba(const Cppjieba &ba) = delete;
Cppjieba &operator=(const Cppjieba &ba) = delete;
public:
static Cppjieba *GetJieba() // 获取单例
{
static std::mutex mtx;
if (singloten == nullptr)
{
mtx.lock();
if (singloten == nullptr)
{
singloten = new Cppjieba();
singloten->InitJieba(); // 构建完进行初始化
}
mtx.unlock();
}
return singloten;
}
bool InitJieba()
{
std::ifstream in(STOP_WORD_PATH);
if (!in.is_open())
{
std::string st = "open file error:";
LOG(CRITICAL, st += STOP_WORD_PATH);
return false;
}
std::string line;
while (std::getline(in, line))
{
stop_word[line] = true; // 插入暂停词到stop_word中
}
in.close();
return true;
}
void CutForSearchHelper(const std::string &s, std::vector *words)
{
jieba.CutForSearch(s, *words);
for (auto item = words->begin(); item != words->end();) // 就是遍历words然后里面的每个元素都去stop_find里面去找
{
auto it = stop_word.find(*item);
if (it != stop_word.end())
{
// item是暂停词需要去除,同时需要考虑迭代器失效问题
item = words->erase(item);
}
else
{
item++;
}
}
}
public:
static void CutForSearch(const std::string &s, std::vector *words)
{
MyTool::Cppjieba::GetJieba()->CutForSearchHelper(s, words); // 直接调用上面的分词助手
// 好处就是无需修改其他的代码
}
};
Cppjieba *Cppjieba::singloten = nullptr;
};
11.将其部署到云服务器上
部署
部署其实很简单
将日志信息输出到log.txt中 ,把标准错误重定向到标准输出 最后的&就是以守护进程的方式 ---这样即使关闭xshell,这个服务依旧是存在的
nohup ./Http_Server > log.txt 2>&1 &
如果想要关闭服务,可以通过ps axj | grep Http_Server 查看pid然后通过 kill命令将其关闭