Gitee倉庫:boost庫搜索引擎
0. 前言
市面上有很多搜索引擎例如Google、百度、360等,這些都是特別大的項目。
對于個人學習我們可以寫一個站內搜索,這個搜索的內容更加垂直,數據量更小,例如C++的文檔The C++ Resources Network
Google搜索顯示內容:
1. 搜索引擎原理
客戶端使用瀏覽器搜索向服務器發(fā)起請求(GET方法上傳)。
服務器上面有一個搜索的服務軟件,在搜索之前,它要在全網抓取網頁(爬蟲)。
將網頁抓下來之后:
- 去標簽和數據清理
- 建立索引
之后這個搜索的軟件檢索索引得到相關的html
,拼接多個網頁的title、description、url
,構建出一個新的網頁返回給客戶端
2. 技術棧和項目環(huán)境
技術棧:
-
后端:
C/C++、C++11、STL、boost準標準庫、Jsoncpp、cppjieba、cpp-http
-
前端(選學):
html5、css、JavaScript、jQuery、Ajax
項目環(huán)境:
- Centos 7云服務器、VS Code、 gcc(g++)、Makefile
3. 正排索引和倒排索引
比如說我們有2個文檔:
- 文檔1:我好喜歡睡覺啊,因為睡覺很舒服呀
- 文檔2:你喜歡睡覺嗎?喜歡和誰一起睡覺啊
3.1 正排索引
正排索引中每個文檔都有唯一標識符,然后就是從文檔ID找到文檔內容
文檔ID | 文檔內容 |
---|---|
1 | 我好喜歡睡覺啊,因為睡覺很舒服噢 |
2 | 你喜歡睡覺嗎?喜歡和誰一起睡覺啊 |
3.2 倒排索引
倒排索引則是文檔分詞,通過整理不重復的各個關鍵字來找到文檔ID
有些停止詞在分詞的時候一般不考慮,搜索引擎
stopwords
大全:https://github.com/goto456/stopwords
關鍵字 | 文檔ID,weight(權重) |
---|---|
睡覺 | 文檔1、文檔2 |
舒服 | 文檔1 |
喜歡 | 文檔1、文檔2 |
一起 | 文檔2 |
總的來說,正排索引適用于需要快速訪問整個文檔內容的場景,而倒排索引則適用于需要快速檢索包含特定詞項的文檔的場景。兩者常常結合使用,在信息檢索系統(tǒng)中發(fā)揮各自的優(yōu)勢。
3.3 模擬查找
查找:喜歡 -> 倒排索引查找 -> 提取文檔1、文檔2 -> 正排索引 -> 找到文檔內容 -> 文檔內容摘要(title、desc、url) -> 構建響應結果
4. 獲取數據源
進入boost官網:Boost C++ Libraries,然后下載文檔:
下載完畢之后傳入服務器
rz -E
? 傳入之后解壓:
tar xzf boost_1_84_0.tar.gz
這些html
文件就是我們的數據源了,我們只需要這里面的內容,所以將這里面的內容拷貝到要存放數據源的文件夾當中。
5. 數據清洗
html
中用的<>
就是標簽,一般都是成對出現,所以我們要將這些內容給去掉,留下里面正文的內容
ls -Rl | grep -E '*.html' | wc -l
這里查看到差不多有8500個文檔,我們的目標就是要將這八千多個文檔的標簽全部去掉,然后保存到一個文件中。
5.1 保存路徑
bool EnumFiles(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;
}
//判斷后綴
if(iter->path().extension() != ".html")
{
continue;
}
//保存獲取到的文件路徑
//std::cout << iter->path().string() << std::endl;
//保存路徑
files_list->push_back(iter->path().string());
}
return true;
}
我們這里先將路徑保存下來,方便后續(xù)的讀取
這里需要用到
boost
庫的文件操作,需要提前安裝boost
庫boost庫安裝:
sudo yum install -y boost-devel
5.2 解析文件
首先將文件的內容讀取上來,然后根據讀取的內容對標題、內容進行提取,最后構造出對應的url
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
for(const std::string &file : files_list)
{
//讀取文件內容
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
DocInfo_t doc;
//解析文件,提取標題
if(!ParseTitle(result, &doc.title))
{
continue;
}
//解析文件,提取內容
if(!ParseContent(result, &doc.content))
{
continue;
}
//解析路徑,構建url
if(!ParseUrl(file, &doc.url))
{
continue;
}
//解析完畢
//results->push_back(doc); //直接拷貝, 效率較低
results->push_back(std::move(doc));
//Debug
//ShowDoc(doc);
}
return true;
}
提取標題
標題的標簽是<title>內容</title>
,需要拿到中間的內容,string
中的find
方法,找到的是起始位置,也就是<
,所以截取的起始位置,還需要加上"<title>"
的長度
static bool ParseTitle(const std::string &file, std::string *title)
{
size_t begin = file.find("<title>");
if(begin == std::string::npos)
return false;
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;
}
提取內容
提取內容并不是拿取html
網頁的內容,而是要去掉標簽,也就是去除<>
里面的內容去掉
這里采用簡易的狀態(tài)機來進行判斷:
static bool ParseContent(const std::string &file, std::string *content)
{
//簡易狀態(tài)機
enum status
{
LABEL,
CONTENT
};
status s = LABEL; //起始肯定是遇到標簽
for(char c : file)
{
switch (s)
{
case LABEL:
if(c == '>') s = CONTENT;
break;
case CONTENT:
if(c == '<') s = LABEL;
else
{
if(c == '\n') c = ' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
起始狀態(tài)肯定是遇到標簽<
,所以將狀態(tài)機的其狀態(tài)設為標簽狀態(tài)
當遇到>
就表明狀態(tài)結束,此時我們將狀態(tài)設置為CONTENT
,但是也可能下一個又是標簽,所以還行進行判斷,當遇到<
,就表明新的狀態(tài)又開始了。
這里沒有保留原文件中的
\n
,因為之后\n
用于文本的分隔符
構造url
boost
庫的官網文檔,和我們獲取的數據源是有對應的路徑關系的
而我們將html文件放到data/input
當中,所以相當官網的路徑+我們的文檔路徑訪問到指定的文檔
url_head
:https://www.boost.org/doc/libs/1_84_0/doc/html
(切記,帶上前綴https://
,踩bug之談)我們的路徑為
data/input/array.html
,但是我們并不需要~~/data/input
~~
url_tail
:array.html
url
=url_head + url + tail
這樣拼接起來就是指定文檔的網址了
5.4 保存內容
文檔之間如何區(qū)分:
文檔和文檔之間可以用\3
區(qū)分,例如:
AAAAAAA\3BBBBBB\3CCCCCC
為什么是
\3
?文檔當中能夠顯示的字符,一般都是屬于打印字符;而像
\3
這樣的叫做控制字符,是不顯示的,就不會污染我們清洗之后的文檔
但是getline
函數,是以行讀取的內容,所以我們可以采用\3
來作為title
、content
、url
的分隔符,而\n
作為文檔的分隔符,即:title\3content\3url\3\n\...
,這樣會方便之后的文件讀取
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
std::ofstream out(output, std::ios::out | std::ios::binary);
if(!out.is_open())
{
std::cerr << "open " << output << " error..." << std::endl;
return false;
}
//寫入
for(auto &item : results)
{
std::string out_str;
out_str += item.title;
out_str += SEP;
out_str += item.content;
out_str += SEP;
out_str += item.url;
out_str += '\n';
out.write(out_str.c_str(), out_str.size());
}
out.close();
return true;
}
運行之后得到的內容和從官網獲取的文檔內容數量一樣:
6. 建立索引
建立正排索引是要將文檔的內容讀取之后放到數組當中,而建立倒排是需要在正排之后,根據正排之后的文檔再建立。
struct DocInfo
{
std::string title;
std::string content;
std::string url;
uint64_t doc_id; //文檔id
};
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
typedef std::vector<InvertedElem> InvertedList; //倒排拉鏈
class Index
{
public:
Index()
{}
~Index()
{}
public:
//根據文檔id找文檔內容
DocInfo* GetForwardIndex(const uint64_t &id)
{
//...
return nullptr;
}
//根據關鍵字找倒排拉鏈
InvertedList* GetInvertedList(const std::string &word)
{
//...
return nullptr;
}
//根據parse之后的數據,建立正排倒排索引
bool BuildIndex(const std::string &input)
{
//...
return true;
}
private:
std::vector<DocInfo> forward_index; //數組的下標就是天然的文檔id
std::unordered_map<std::string, InvertedList> inverted_index; //一個關鍵字對應一組(個)倒排元素
};
6.1 建立正排索引
DocInfo *BuildForwardIndex(const std::string &line)
{
//1. 切割字符串
std::vector<std::string> results;
const std::string sep = "\3"; //內容分隔符
ns_util::StringUtil::SplitString(line, &results, sep);
if(results.size() != 3)
return nullptr;
//2.填充內容
DocInfo doc;
doc.title = results[0];
doc.content = results[1];
doc.url = results[2];
doc.doc_id = forward_index.size(); //當時的大小就是 文檔id
//3. 插入正排
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
切分字符串采用
boost
庫的split
函數:class StringUtil { public: static void SplitString(const std::string &target, std::vector<std::string> *ret, const std::string &sep) { boost::split(*ret, target, boost::is_any_of(sep), boost::token_compress_on); } };
*ret
:目標容器的指針,這個容器用于存儲分割后的子串。target
:需要分割的源字符串。boost::is_any_of(sep)
:指定的分隔符,可以是單個字符、字符串或者字符范圍。boost::token_compress_on
:表示開啟壓縮模式。開啟后,連續(xù)的分隔符將被視為一個分隔符,并且不會產生空字符串。
6.2 建立倒排索引
原理:
一個文檔里面的
title
和content
是包含很多詞的,需要將這些詞進行分詞,根據這些內容形成倒排拉鏈這里就需要將詞和文檔標題/內容中出現次數進行關聯,采用
unordered_map
jieba庫
:
獲取cppjieba
:GitHub - yanyiwu/cppjieba: "結巴"中文分詞的C++版本,然后克隆到服務器
git clone https://github.com/yanyiwu/cppjieba.git
demo
代碼:
#include "cppjieba/Jieba.hpp"
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";
這些就是詞庫,就是根據這些詞庫進行分詞
我們稍后要用,肯定不是在這個庫文件里面創(chuàng)建文件使用,所以我們需要建立軟連接讓程序能找到詞庫,然后它包含的頭文件還要包含Jieba.hpp
,也是需要建立軟鏈接的
不了解的查看此篇文章:Linux軟硬鏈接
這里還有一個小坑:
我們編譯的會報錯,說缺少
limonp/Logging.hpp
,我們需要手動拷貝limonp
這個目錄到cppjieba/include
當中
bool BuildInvertedIndex(const DocInfo &doc)
{
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;
std::vector<std::string> content_words;
ns_util::JiebaUtil::CutString(doc.title, &title_words, STOP_WORD_FLAG); //標題分詞
ns_util::JiebaUtil::CutString(doc.content, &content_words, STOP_WORD_FLAG); //內容分詞
//統(tǒng)計標題詞頻
for(std::string s : title_words)
{
word_map[s].title_cnt++;
}
//統(tǒng)計內容詞頻
for(std::string s : content_words)
{
word_map[s].content_cnt++;
}
#define X 10
#define Y 1
//構建倒排索引
for(auto &word_pair : word_map)
{
InvertedElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = word_pair.second.title_cnt * X + word_pair.second.content_cnt * Y; //標題出現權重更高
//inverted_index[word_pair.first].push_back(std::move(item));
InvertedList &invertedlist = inverted_index[word_pair.first];
invertedlist.push_back(std::move(item));
}
return true;
}
6.3 構建索引
bool BuildIndex(const std::string &input)
{
std::ifstream in(input, std::ios::in | std::ios::binary);
if(!in.is_open())
{
ns_log::log(Fatal, "%s open error", input.c_str());
//std::cerr << input << "open error..." << std::endl;
return false;
}
std::string line;
int cnt = 0;
int len = lable.size();
while(std::getline(in, line))
{
//建立正排
DocInfo *doc = BuildForwardIndex(line);
if(doc == nullptr)
{
ns_log::log(Warning, "build %s error", line.c_str());
//std::cerr << "build " << line << " error" << std::endl;
continue;
}
//建立倒排
BuildInvertedIndex(*doc);
cnt++;
ns_log::log(Info, "build document %d", cnt);
// std::cout << "build doc: " << cnt << "..." << std::endl;
}
ns_log::log(Info, "total document %d ...", cnt);
return true;
}
建立索引的本質是將去標簽化的數據加載到內存當中,這個體積是很大的,而且索引只有一份,所以構建成單例模式
class Index { //... private: Index() {} Index(const Index&) = delete;//拷貝構造去掉 Index& operator=(const Index&) = delete; //賦值語句去掉 static Index *instance; //單例 static std::mutex mtx; public: ~Index() {} public: static Index* GetInstance() { if (nullptr == instance) { mtx.lock(); if (nullptr == instance) { instance = new Index(); } mtx.unlock(); } return instance; } //... }; Index* Index::instance = nullptr; std::mutex Index::mtx; //構造函數
7. 搜索
搜索服務思維導圖:
#pragma once
#include<algorithm>
#include<unordered_map>
#include<jsoncpp/json/json.h>
#include"index.hpp"
#include"util.hpp"
#include"Log.hpp"
namespace ns_searcher
{
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint()
:doc_id(0),weight(0)
{}
};
class Searcher
{
public:
Searcher()
{}
~Searcher()
{}
private:
std::string GetAbstract(const std::string &content, const std::string &word)
{
// 往前找50字節(jié),往后找100字節(jié) 直接硬編碼
size_t prev_pos = 50;
size_t next_pos = 100;
//size_t pos = content.find(word);
// if (pos == std::string::npos)
// return "Not Found";
auto it = std::search(content.begin(), content.end(), word.begin(), word.end(), [](int x, int y){
return std::tolower(x) == std::tolower(y);
});
if(it == content.end()) return "Not Found1";
size_t pos = std::distance(content.begin(), it);
size_t start = 0; // 默認起始位置為begin
size_t end = content.size() - 1; // 結束位置為end
if (pos > start + prev_pos)
start = pos - prev_pos; // 用加法,防止無符號溢出
if (pos + next_pos < end)
end = pos + next_pos;
if (start >= end)
return "Not Found2";
return content.substr(start, end - start)+ "...";
}
public:
void InitSearcher(const std::string &input)
{
//獲取index對象
index = ns_index::Index::GetInstance();
//std::cout << "get instance" << std::endl;
ns_log::log(Info, "Get instance success");
//建立索引
index->BuildIndex(input);
//std::cout << "build index success" << std::endl;
ns_log::log(Info, "build index success");
}
//keyword:搜索關鍵字 json_string:返回給用戶瀏覽器的搜索結果(序列化)
void Search(const std::string &keyword, std::string *json_string)
{
//對關鍵字進行分詞
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(keyword, &words, 1);
//觸發(fā),根據關鍵字分詞進行查找
//ns_index::InvertedList inverted_list_all;
std::vector<InvertedElemPrint> inverted_list_all;
std::unordered_map<uint16_t, InvertedElemPrint> tokens_map;
for(std::string s : words)
{
boost::to_lower(s); //忽略大小寫(轉小寫)
//先倒排,再正排
//倒排
ns_index::InvertedList *inverted_list = index->GetInvertedList(s);
if(inverted_list == nullptr)
{
continue;
}
//inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
for(const auto &elem : *inverted_list)
{
auto &item = tokens_map[elem.doc_id]; //去重
item.doc_id = elem.doc_id; //去重之后賦值
item.weight += elem.weight; //累加權重值
item.words.push_back(elem.word);
}
for(const auto &e : tokens_map)
{
inverted_list_all.push_back(std::move(e.second));
}
//排序
// std::sort(inverted_list_all.begin(), inverted_list_all.end(),
// [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){
// return e1.weight > e2.weight;
// });
std::sort(inverted_list_all.begin(),inverted_list_all.end(),
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
return e1.weight > e2.weight;
});
//正排
Json::Value root;
for(auto &e : inverted_list_all)
{
ns_index::DocInfo *doc = index->GetForwardIndex(e.doc_id);
if(doc == nullptr)
{
continue;
}
Json::Value item;
item["title"] = doc->title;
//item["abstract"] = doc->content;
item["content"] = GetAbstract(doc->content, e.words[0]);
item["url"] = doc->url;
//debug
//item["id"] = (int)e.doc_id; //json自動轉換int -> string
//item["weight"] = e.weight;
root.append(item);
//
}
//序列化
Json::StyledWriter writer;
*json_string = writer.write(root);
}
}
private:
ns_index::Index *index;
};
}
8. 服務端
使用第三方網絡庫cpp-httplib
,這個庫需要使用較新的編譯器:
-
安裝
scl
源:sudo yum install centos-release-scl scl-utils-build
-
更新gcc/g++:
sudo yum install -y devtoolset-7gcc devtoolset-7-gcc-c++
安裝好之后的路徑:
這里用的時候要啟動,使用指令:
scl enable devtoolset-7 bash
但是這個只是每次會話有效,如果想每次啟動都有效,可以將上面的指令加入
.bash_profile
配置文件
#include"cpp-httplib/httplib.h"
#include"search.hpp"
#include"Log.hpp"
const std::string root_path = "./wwwroot";
const std::string input = "data/raw_html/raw.txt";
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)
{
//rsp.set_content("hello httplib", "text/plain; charset=utf-8");
if(!req.has_param("word"))
{
rsp.set_content("please enter keyword!", "text/plain; charset=utf-8");
return;
}
std::string word = req.get_param_value("word");
//std::cout << "user is searching: " << word << std::endl;
ns_log::log(Info, "User search content is \"%s\"", word.c_str());
std::string json_str;
search.Search(word, &json_str);
rsp.set_content(json_str, "application/json");
});
ns_log::log(Info, "server start success");
svr.listen("0.0.0.0", 8080);
return 0;
}
9. 服務部署
nohup ./http_server > log/log.txt 2>&1 &
將服務后臺運行部署,如果要要查看,可以直接看log/log.txt
,也可以tail -f log/log.txt
文章來源:http://www.zghlxwxcb.cn/news/detail-845531.html
默認設置了去掉暫停詞,第一次啟動可能會較慢,如果不想去掉暫停詞,可以將
STOP_WORD_FLAG
設置為0文章來源地址http://www.zghlxwxcb.cn/news/detail-845531.html
到了這里,關于boost庫搜索引擎的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!