Boost是一个功能强大 , 构造精良 , 跨越平台 , 代码开源 , 完全免费的 C ++ 程序库。但是其主页并没有搜索功能,基于其开源的属性,我们可以为其建立一个搜索引擎。
是一个工具类,是在编写项目代码的时候不断进行完善的。
ReadFile
:读取文件path的内容,并将内容存放在字符串result中,成功返回true。 class FileUtil
{
public:
static bool ReadFile(const std::string& path,std::string* result)
{
std::ifstream in(path,std::ios::in);
if(!in.is_open())
{
std::cerr<<"打开文件失败"<<std::endl;
return false;
}
std::string out;
while(getline(in,out))
{
*result+=out;
}
in.close();
return true;
}
};
Split
:调用boost库中切分字符串的函数,根据sep将字符串line切割成多个子串,并放入result中。 class StringUtil
{
public:
static void Split(const std::string& line,std::vector<std::string>* results,const std::string& sep)
{
boost::split(*results,line,boost::is_any_of(sep),boost::token_compress_on);
return;
}
};
CutString
:调用jieba库,进行字符串分割操作 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 JieBa
{
public:
static cppjieba::Jieba jieba;
static void CutString(const std::string& str,std::vector<std::string>* result)
{
jieba.CutForSearch(str,*result);
}
};
cppjieba::Jieba JieBa::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);
其中我们使用的是jieba库中CutForSearch的方法。
将读取所有html文件,依次对其进行去标签的操作,并写入raw.txt中。
Enum_html
:通过筛选的方式筛去非html文件,并将各个html文件依次读取到file_list数组中。
//将所有html路径导入到files_list中
bool Enum_html(const std::string &path, std::vector<std::string> *files_list)
{
//定义一个路径对象,当路径不存在时返回false
namespace fs = boost::filesystem;
fs::path root_path(path);
if (!fs::exists(root_path))
{
std::cerr << "path doesn't exists!" << std::endl;
return false;
}
//定义空迭代器end作为结束,定义迭代器it初始化为路径对象,此时它指向的是该路径的第一个元素(文件)
fs::recursive_directory_iterator end;
for (fs::recursive_directory_iterator it(root_path); it != end; it++)//定义一个目录迭代器,它会遍历目录下的所有目录
{
if (!is_regular_file(*it)) //筛掉非普通文件
{
continue;
}
if (it->path().extension() != ".html") //筛掉后缀不为html的文件
{
continue;
}
//std::cout<<"debug "<path().string()<
files_list->push_back(it->path().string()); //向files_list指向的数组push_back这些文件路径
}
return true;
}
ParserTitle
:通过find方法解析单个文本的标题。
bool ParserTitle(const std::string& result,std::string* title)
{
std::size_t begin=result.find("" );
if(begin==std::string::npos)
{
std::cerr<<"can't find title's begin"<<std::endl;
return false;
}
std::size_t end=result.find("");
if(end==std::string::npos)
{
std::cerr<<"can't find title's end"<<std::endl;
return false;
}
begin+=std::string("" ).size();
if(begin>end)
{
std::cerr<<"title's begin>end"<<std::endl;
return false;
}
*title=result.substr(begin,end-begin);
//std::cerr<<*title<
return true;
}
ParserContent
:筛选出各个文本的内容,即去标签。
bool ParserContent(const std::string& result,std::string* content)
{
enum status//定义一个状态机
{
LABLE,
CONTENT
};
enum status st=LABLE;
for(char c:result)
{
switch(st)
{
case LABLE:
if(c=='>')
{
st=CONTENT;
}
break;
case CONTENT:
if (c == '<')
{
st = LABLE;
}
else
{
if (c == '\n')//不希望读入"\n"想将它作为文本的分隔符
{
c = ' ';
}
*content += c;
}
break;
default:
break;
};
}
//std::cout<<*content<
return true;
}
ParserUrl
:获取各个文本的url。
bool ParserUrl(const std::string& file,std::string* url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_80_0/doc/html";
std::string url_tail=file.substr(src.size());
*url=url_head+url_tail;
return true;
}
Parse_html
:将每一个文本中标题,内容以及url信息存放在result数组中的每一个doc中。
bool Parse_html(const std::vector<std::string> &files_list, std::vector<Doc> *results)
{
for (auto &file:files_list)
{
std::string result;
if (!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
Doc doc;
if(!ParserTitle(result,&doc.title))//提取一个file的标题
{
continue;
}
if(!ParserContent(result,&doc.content))//提取一个file的内容
{
continue;
}
if(!ParserUrl(file,&doc.url))//提取一个file的url
{
continue;
}
//std::cout<<"debug:success push"<
//std::cout<
results->push_back(std::move(doc));//移动构造提高效率
}
return true;
}
Save_html
:解析doc中内容,将每一个html的数据写入到raw.txt的一行中。
bool Save_html(const std::vector<Doc> &results, const std::string &path)
{
#define SEP '\3'
std::ofstream out(path,std::ios::out|std::ios::binary);
if(!out.is_open())
{
std::cerr<<"can't open dest file"<<std::endl;
return false;
}
std::string result;
for(auto& res:results)
{
result+=res.title;
result+=SEP;
result+=res.content;
result+=SEP;
result+=res.url;
result+='\n';
}
out.write(result.c_str(),result.size());
out.close();
return true;
}
建立正排和倒排索引。
//正排索引
struct DocInfo
{
std::string title;
std::string content;
std::string url;
uint64_t doc_id;
};
//倒排索引
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
BuildForwardIndexLine
:建立正排索引,传入raw.txt的每一行,根据分割符构建正排索引,并将构建出来的DocInfo使用forward_list管理起来,将对应的forward_list下标作为文档id,同时返回该正排索引结构,为构建倒排索引做准备。 DocInfo *BuildForwardIndexLine(const std::string &res) //正排索引的构建
{
//分割字符串
std::vector<std::string> results;
std::string sep = "\3";
ns_util::StringUtil::Split(res, &results, sep);
if (results.size() != 3)
{
std::cerr << "failed to cut string" << std::endl;
return nullptr;
}
//将分割后字符串写入doc中
DocInfo doc;
doc.title = results[0];
doc.content = results[1];
doc.url = results[2];
doc.doc_id = forward_index.size();
//正排索引列表插入doc
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
BuildInvertedIndex
:建立倒排索引,传入的是一个文档的正排索引
void BuildInvertedIndex(const DocInfo &doc) //倒排索引的构建
{
//分词
//统计词频
//插入
//通过正排索引的列表中元素构建倒排索引
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt() : title_cnt(0), content_cnt(0)
{
}
};
//标题分词,放在title_list中
std::vector<std::string> title_list;
std::unordered_map<std::string, word_cnt> word_map;
ns_util::JieBa::CutString(doc.title, &title_list);
//为标题中每一个词统计词频,放在word_map的second的title_cnt
for (auto &s : title_list)
{
boost::to_lower(s); //全部转换成小写
word_map[s].title_cnt++;
}
//内容分词,放在content_list中
std::vector<std::string> content_list;
ns_util::JieBa::CutString(doc.content, &content_list);
//为内容中每一个词统计词频,并放在word_map的second的content_cnt中
for (auto &s : content_list)
{
boost::to_lower(s); //全部转换成小写
word_map[s].content_cnt++;
}
//构建倒排索引
//将word_map中的每一个词构建一个InvertedElem,计算该词在该篇文章中所占的权重,插入到该词对应的倒排拉链中
for (auto &word_pair : word_map)
{
#define X 10
#define Y 1
InvertedElem inv;
inv.doc_id = doc.doc_id;
inv.word = word_pair.first;
inv.weight = X * (word_pair.second.title_cnt) + Y * (word_pair.second.content_cnt);
inverted_index[word_pair.first].push_back(std::move(inv));
}
return;
}
GetForwardIndex
:通过文档编号,即正排索引数组forward_index的下标,获取正排索引。
DocInfo *GetForwardIndex(uint64_t doc_id)
{
if (doc_id >= forward_index.size())
{
return nullptr;
}
return &forward_index[doc_id];
}
GetInvertedList
:根据关键词获取该词的倒排拉链。
InvertedList *GetInvertedList(const std::string &word)
{
auto it = inverted_index.find(word);
if (it == inverted_index.end())
{
std::cerr << "fail to find key word" << std::endl;
return nullptr;
}
return &(it->second);
}
BuildIndex
:传入raw.txt文件,建立正排和倒排索引。
//根据去标签后的数据,构建正排和倒排索引
bool BuildIndex(const std::string &input)
{
std::ifstream in(input, std::ios::in | std::ios::binary);
if (!in.is_open())
{
std::cerr << "fail to open raw.txt" << std::endl;
return false;
}
std::string res;
int count=0;//用于监控处理文档的数量
while (getline(in, res))
{
DocInfo *doc = BuildForwardIndexLine(res);
if (doc == nullptr)
{
std::cerr << "fail to read a line from raw.txt" << std::endl;
continue;
}
BuildInvertedIndex(*doc);
count++;
if(count%100==0)
{
// std::cout<<"当前已经处理的索引文档有:"<
LOG(NORMAL,"当前已经建立索引的文档"+std::to_string(count));
}
}
std::cout<<"建立正排和倒排索引成功"<<std::endl;
in.close();
return true;
}
对搜索内容进行分词,根据分词找到倒排拉链,根据倒排拉链的节点中的文档id找到文档内容,并将文档内容都输入到json_string中。
其中InvertedElemPrint的作用是去重。
struct InvertedElemPrint
{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0),weight(0)
{}
};
InitSearcher
:对所有文档建立索引。
void InitSearcher(const std::string &input)
{
//建立或者获取索引对象
//根据索引对象建立索引
index = ns_index::index::Getinstance();
LOG(NOMAL,"获取单例成功");
index->BuildIndex(input);
}
Search
:搜索并写入json串。
void Search(const std::string &query, std::string *json_string)
{
//对query进行分词
//根据分出来的词进行查找
//对查找结果进行排序
//返回一个json串
//对query进行分词
std::vector<std::string> words;
ns_util::JieBa::CutString(query, &words);
//将分词后的每个词的倒排拉链中的内容都保存在InvertedList_all中
//ns_index::InvertedList InvertedList_all;
std::vector<InvertedElemPrint> InvertedList_all;
std::unordered_map<uint64_t,InvertedElemPrint> tokens_map;
for (auto &word : words)
{
boost::to_lower(word);
ns_index::InvertedList *invertedlist = index->GetInvertedList(word);
if (invertedlist == nullptr)
{
continue;
}
for (const auto &elem : *invertedlist)
{
auto& item=tokens_map[elem.doc_id];
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
//InvertedList_all.insert(InvertedList_all.end(), invertedlist->begin(), invertedlist->end());
}
for(const auto& item:tokens_map)
{
InvertedList_all.push_back(std::move(item.second));
}
//对InvertedList_all中的每个InvertElem元素按权值进行排序
std::sort(InvertedList_all.begin(), InvertedList_all.end(),
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2)
{
return e1.weight > e2.weight;
});
Json::Value root;
for (auto &e : InvertedList_all)
{
ns_index::DocInfo *doc = index->GetForwardIndex(e.doc_id);
if (nullptr == doc)
{
continue;
}
Json::Value elem;
elem["title"] = doc->title;
// elem["desc"] = doc->content;
elem["desc"] = GetDesc(doc->content, e.words[0]); //摘录内容,待处理
elem["url"] = doc->url;
elem["id"] = (int)doc->doc_id;
elem["weight"] = e.weight;
root.append(elem);
}
Json::StyledWriter writer;
*json_string = writer.write(root);
}
GetDesc
:获取文档内容的摘要,并处理查找中大小写问题。
std::string GetDesc(const std::string &content, const std::string &word)
{
//取关键字前50个和关键字后50个字节作为摘要
// int pos=content.find(word);//大小写问题需要进行处理
auto iter = std::search(content.begin(), content.end(), word.begin(), word.end(), [](int x, int y)
{ return (std::tolower(x) == std::tolower(y)); });
if (iter == content.end())
{
return "NONE";
}
int pos = std::distance(content.begin(), iter);
// if(pos==std::string::npos)
// {
// return "NONE";
// }
int start = 0;
int end = content.size() - 1;
if ((pos - start) >= 100)
start = pos - start - 100;
if ((end - pos) >= 200)
end = pos + 200;
if (start > end)
{
return "Wrong";
}
return content.substr(start, end - start);
}
使用httplib库来进行网络通信,只需要调用searcher中的initSearcher来建立索引,以及Search来查找关键字构建的json串即可。
ns_searcher::Searcher search;
search.InitSearcher(raw_path);
httplib::Server svr;
svr.set_base_dir(root_path.c_str());
svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &res) {
//通过Request获得参数
//通过Reaponse构建响应
//res.set_content("hello world!","text/plain;charset=utf8");
if(!req.has_param("word"))
{
res.set_content("必须要有搜索关键字!","text/plain;charset=utf-8");//设置响应界面
return;
}
std::string word=req.get_param_value("word");//获取搜索内容
std::cout<<"用户正在搜索"<<word<<std::endl;
std::string json_string;
search.Search(word,&json_string);
res.set_content(json_string,"application/json");
});
svr.listen("0.0.0.0",8081);
https://gitee.com/selling-lonely-little-boys/my-world/tree/master/boost_searcher
首先用户向后端输入一个字符串,然后对这个字符串进行分词操作,获取每一个词的倒排拉链,并根据权重进行排序,然后通过倒排拉链进行正排索引找到文档内容,并返回给用户。
从boost官网获取文档之后,将文档内容放在一个数组中,数组下标代表哪一个文档。
将所有文档进行分词操作,因为一个词可以在多个文档中,这些文档就构成了该词的倒排拉链,根据不同文档该词出现的频率进行排序。
获取所有词的倒排拉链之后,遍历这些倒排拉链,这样就获取到了所有要返回的文档了,文档的权重就是不同词在该文档的权重之和。然后对这些文档排序,返回给用户。