目錄
項目相關背景
搜索引擎原理技術棧和項目環境
導入數據到自己的本地
數據去標簽與數據清洗模塊
Enumfile(src_path, &file_list)遞歸式寫入
Parsehtml(file_list, &results)去標簽
bool Parsetitle(const string& file, string* title)拆分標題
bool Parsecontent(const string& file, string* content)拆分內容
bool Parseurl(const string& file, string* url)
bool Savehtml(const vector& results, const string& output)
main函數編寫
建立正排/倒排索引模塊
正排/倒排索引是什么以及編寫思路
儲存索引的結構
Build_index(const string &input)建立索引
Docinfo *ret = Build_forward_index(line)
bool Build_inverted_index(const Docinfo &doc)
設計單例模式
Docinfo *getforwardindex(uint64_t doc_id)
invertedlist_t *getinvertedlist(const string &word)
使用搜索引擎返回搜索內容模塊
編寫思路
Initsearcher(const string& input)調用建立索引
void Search(const string& query, string* out)
相同id的去重邏輯
方法1:
方法2:
構建輸出json串序列化
string Getdesc(const string& content, const string& word)截取摘要
search.cc本地測試模塊
http_server模塊
編寫前端模塊
引入日志
項目總結與擴展
分享我遇到的困難
項目相關背景
? ? ? Boost搜索引擎并非指某個商業化的通用搜索引擎,而是一個專為Boost C++庫設計的站內搜索項目。
搜索引擎原理技術棧和項目環境
? ? ? ?我們可以先將整個boost庫導入到我們當前目錄下的某個文件里面,從全網導入可以使用爬蟲,也可以去官網下載然后re -E,再tar解包導入。形成我們本次boost的數據源,數據源很混亂而且要查找是需要構建引索的,也就是下標,且所有的數據都在.html文件里面展示也就是我們接著需要將得來的數據源進行去標簽,數據清洗,然后建立索引。建立索引的過程分為正排索引和倒排索引。將這些索引鏈存在內存。等客戶端通過瀏覽器什么的,通過get/post方法訪問時拼接多個網頁的title+desc(索引鏈中的內容)構建一個新的網頁返還給用戶。這樣就完成了我們這次小項目的框架和邏輯。
導入數據到自己的本地
? ? ? ?首先我們下載最新的1_88_0的庫,我推薦你們使用google進行下載,然后將其rz到我們的項目目錄里面,這里我的是boost_searcher,然后由于所有的庫函數等的數據信息都在boost_searcher/boost_1_88_0/doc/html這個路徑下,所以我們需要將里面的所有的文件(.html)都拷貝到我們專門放數據的地方當前目錄下的data/input,但是其實里面不是全部都是.html結尾的,可能還有圖片啥的,這就是我們需要進行數據清洗的原因,所以在data/里面再新建一個.txt文件用來保存清洗過后的信息,每行代表一條.html結尾的文件。這樣數據的準備就完成了!
數據去標簽與數據清洗模塊
? ? ? ?在parser.cc里面進行數據的清洗與去標簽,我們需要先清洗再去html里面的標簽,在執行前先將數據路徑保存體現。
//是一個目錄,下面放的是所有的html網頁
const string src_path = "data/input";
const string output = "data/raw_html/raw.txt";
? ? ? 我們分三步完成我們的模塊,第一步:我們先遞歸式的把每一個帶有.html文件名帶路徑,保持在一個數組files_list中,方便后期一個一個的文件進行讀取,對應函數Enumfile(src_path, &file_list),第二步:按照files_list讀取每個文件的內容,并進行解析,對應函數Parsehtml(file_list, &results),第三步:把解析完畢的各個文件內容,寫入output,按照\3作為每個文檔的分隔符對應函數Savehtml(results, output),我們提取的格式為每個結構體要由標題+正文+url組成如下:
typedef struct Docinfo{
? ? string title; ?//文檔的標題
? ? string content; ?//文檔的正文
? ? string url; ? ? //文檔在官網的url
}Docinfo_t;
Enumfile(src_path, &file_list)遞歸式寫入
? ? ? ?std庫里面的ifstream是很難做到只寫入.html的且很難做到遞歸,所以我們使用boost庫里面的文件操作,包含頭文件#include<boost/filesystem.hpp>。引入命名空間boost::filesystem,我們先使用之前導入的src_path創建一個由src_path初始化完成的path對象,此時這個對象就是src_path,然后定義一個空的迭代器,用來判斷遞歸結束fs::recursive_directory_iterator end; 他能自動幫你深入所有子目錄的迭代器,然后初始就是src_path,不斷for循環遍歷的時候,進行兩個判斷,is_regular_file判斷指向的文件是否為普通文件,以及iter->path().extension()判斷后綴是否為.html,然后判斷都通過的路徑就push_back到file_list里面,當然我推薦使用move進行右值轉換因為可以提高效率。提醒一下:需判斷文件是否打開。
Parsehtml(file_list, &results)去標簽
? ? ?將file_list里面的數據再存入results數組里面,此時results里面的元素就是之前定義好的去標簽的Docinfo_t。首先需要對file_list進行按行遍歷,之后使用std的ifstream進行讀取html里面的內容,常規的讀取操作getline全部讀取完存入string result里面,這時候應該html里面的信息(含標簽)都在里面了,之后我分別做的三個函數來切分標題,內容,url。得到的值構造Docinfo_t,push_back入results,之后全部html讀取完畢,result就是所有的拆分后的boost庫內源文件的內容了。
bool Parsetitle(const string& file, string* title)拆分標題
? ? ? 首先我很明確的告訴你,標題只有一個,也就是只有一個<title>......</title>標簽,這就好辦了,在string file里面使用兩次find方法找到兩個左<括號的下標位置f1,f2,然后<title>的左<位置向右平移<title>長度為f3,再substr截取f3到f2的距離返回就是標題了。注意判斷不成立情況,find返回值需判斷,f3的值是否會大于f2也需要判斷。這種常規的判斷我后面就不一一寫出來了,應該成為一種自覺。
bool Parsecontent(const string& file, string* content)拆分內容
? ? ? 要提取內容其實就是在去標簽,首先內容不可避免的會把標題也算進去,我們這里認為標題也屬于內容,這樣簡單點,我們先構建一個狀態機(enum)就兩個狀態,LABLE和CONTENT,因為處在string里面的要么是標簽要么就是標簽以外的內容,所以status s默認初始值為標簽狀態,因為最開始遍歷的時候就是先遇到標簽,當遇到>時說明單標簽結束或者雙標簽的左標簽結束,剩下的接著就是正文s=CONTENT,當s=CONTENT遇到<時說明這段內容結束了,接著的就是新的標簽或者右標簽了。什么時候開始push呢,在沒有遇到<且s等于正文狀態時push僅content。這里注意,當讀取到'\n'時,我們將其變成讀取空格,以防止之后getline從中再讀取分詞的影響,getline是不會讀取\n的,遇到\n就停下了。
enum status{
? ? ? ? LABLE, //標簽狀態
? ? ? ? CONTENT ?//正文狀態
? ? };
bool Parseurl(const string& file, string* url)
? ? ? ?提取url就需要結合官網了,官網的所有的html總的文件夾的地址是"https://www.boost.org/doc/libs/1_88_0/doc/html",而我們查找到的對應的.html文件的地址是這個file比如是data/input/xxx.html,而data/input是src_path,所以我們只需要從file中截取從src_path長度下標到最后位置再拼接到官網url就完成了。依次判斷以上三個部分的函數之后,將Docinfo_t插入我們實現準備好的容器results里面就完成了構建。
bool Savehtml(const vector<Docinfo_t>& results, const string& output)
? ? ? ?將所構建好的results按行寫入output,但是寫入格式得是string,所以我們整個提取格式作為string寫入時也應有辦法分辨哪里是title,哪里是url,所以string寫入的格式我們用\3作為分隔符,兩個string之間使用\n方便getline分開。為什么\3進行分割整合呢,因為再ascll碼里面\3屬于不可轉義字符不會被打印顯示處理干擾文本。格式如下:
results[i].title \3 results[i].content \3 results[i].url \n
? ? ? 就這樣逐行使用ofstream寫入output就完成了,注意ios必須如下編寫ios::binary | ios::app。最后記得關上ofstream養成好習慣。
main函數編寫
? ? ? 依次對上面三個部分的函數進行if判斷即可。
建立正排/倒排索引模塊
正排/倒排索引是什么以及編寫思路
? ? ? 首先這兩個索引都是哈希的結構,正排索引是由一個從0開始的不重復的序號分別映射不同的搜索文檔,而倒排索引是由一個唯一關鍵字作為k值映射所有可以包含其關鍵字的文檔id(內部含有權值),權值就是這個關鍵字被包含了幾次。所以對于倒排索引一個關鍵字可以映射多個文檔id,一個文檔id可以被多個關鍵字映射。對于倒排索引,每個關建字的每個文檔是按權重排成倒序的,大的在前面。
? ? ? 也就是說我們對于一個文檔需要先建立其的正排索引然后再進行分詞,對所有分好的詞建立倒排索引,為了保證k值的唯一性,我們使用unordered_map進行完成。并且需要先進行正排索引地道某個文檔id,然后再進行倒排索引。我們的index.hpp完成建立索引
儲存索引的結構
struct Docinfo {
? string title; ? ?//文檔標題
? string content; ?//文檔內容
? string url; ? ? ?//文檔url
? uint64_t doc_id; //文檔id ? //存入方便倒排索引找到id
};
struct Invertedelem {
? uint64_t doc_id;
? string word; //關鍵字
? int weight; ?//權重
};
typedef vector<Invertedelem> invertedlist_t;
//正派索引用的是數組,索引值就是下標
? vector<Docinfo> forward_index;
? //倒排索引使用哈希表映射多個值
? unordered_map<string, invertedlist_t> inverted_index;
? ? ? ?為什么Invertedelem要體現關鍵詞呢,這個是為了方便調試以及后續的查找工作。
Build_index(const string &input)建立索引
? ? ? ?從input就是我們存放全部html解析完后的由\3分割完后的文件 data/raw_html/raw.txt中使用ifstream配合getline讀取每一行,然后一行一行的先建立正排索引以及倒排索引。
Docinfo *ret = Build_forward_index(line)
? ? ? ?我們根據/3分段line,將其分成3斷,分別為title,content,url,然后將其存入Docinfo,然后push_back入forward_index就完事了,要得到最新的插入id可以相當于得到最新插入的整個元素,直接調用.back方法就可以了。這里的難點就是分段,可以使用C語言的strtok,也可以string::find直接寫,但是都比較麻煩,我們直接使用boost庫的split。
?static void Cutstring(const string &target, vector<string> *out, string sep) {
? ? boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
? }
bool Build_inverted_index(const Docinfo &doc)
? ? ? ? 因為建立倒排索引需要得到權值,所以我們還需要一個結構體統計分出的詞在doc.content和doc.title里面的出現次數,這個可能有誤差,但不大的。然后整合起來成為一個哈希表。
struct word_cnt //統計在標題和內容里面的詞頻
? ? {
? ? ? word_cnt() : title_cnt(0), content_cnt(0) {}
? ? ? int title_cnt;
? ? ? int content_cnt;
? ? };
? ? unordered_map<string, word_cnt> word_map; //詞頻統計
? ? ? ? 第一步使用cppjieba分詞工具,有機會去gitcode平臺下載一下,然后調用jieba.Cutstring進行對title和content的分詞然后分完的詞會自動存入一個數組中,這里注意一下在const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";這個路徑有很多需要分詞屏蔽的暫停詞,暫停詞就是is這種連接詞以及&這種符號等等,然后在分詞時需要去除不然搜索時會有影響。然后進行詞頻統計存入哈希表word_map,然后算權值,這個權值我們可以自己規定算法,我們權值的計算是使用權值= title_cnt * 10 +?content_cnt * 1,因為我們的content是包括了title里面的內容了,所以content_cnt會大一點點。因為全部的分詞都存在了 word_map里面,所以我們只需遍歷 word_map就可以了,然后逐個存入 invertedlist_t,進而純如哈希表inverted_index就可以了。這里邊遍歷其實Invertedelem里面的信息都是清晰的。
? ? ? ? 因為我們的搜索引擎是不區分大小寫的,所以inverted_index的k值需要先進行小寫轉化再存入,確保大寫關鍵字和小寫的關鍵字匹配插入到同一個k值,這樣查找時都用小寫的就可以連同大寫的一起查到了。對于小寫轉化我們可以使用boost::tolower就地轉換,也可以使用std::transform 。
?std::transform(ret.begin(), ret.end(), ret.begin(),
? ? ? ? ? ? ? ? ? ? ?[](unsigned char c) { return std::tolower(c); });
設計單例模式
? ? ? ?這里主要是我們不希望正排/倒排拉鏈,以及切詞組合的vector是每人一份的,所以都將這兩個類設計成單例模式。注意我們設計的單例模式是需要加鎖的,并且我們使用if雙循環判斷更保險。
class Index {
private:
? Index() {}
? Index(const Index&) = delete;
? Index& operator=(const Index&) = delete;
? static Index* instance;
? static std::mutex mtx;
public:
? ~Index() {}
? //構建單例
? static Index* Getinstance()
? {
? ? //雙層判斷進行二次分流
? ? if (nullptr == instance)
? ? {
? ? ? mtx.lock();
? ? ? if (nullptr == instance)
? ? ? {
? ? ? ? instance = new Index();
? ? ? }
? ? ? mtx.unlock();
? ? }
? ? return instance;
? }
class Jiebautil {
private:
? ? cppjieba::Jieba jieba;
? ? unordered_map<string, bool> stop_words;
? ? Jiebautil():jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH,
? ? ? ? ? ? ? ? ? ? ? STOP_WORD_PATH)
? ? {}
? ? Jiebautil(const Jiebautil& ) = delete;
? ? Jiebautil& operator=(const Jiebautil& ) = delete;
? ? static Jiebautil* instance;
public:
? ? static Jiebautil* get_instance()
? ? {
? ? ? ? static std::mutex mtx;
? ? ? ? if (instance == nullptr)
? ? ? ? {
? ? ? ? ? mtx.lock();
? ? ? ? if (instance == nullptr)
? ? ? ? {
? ? ? ? ? ?instance = new Jiebautil();
? ? ? ? ? ?instance->initjiebautil();
? ? ? ? }
? ? ? ? mtx.unlock();
? ? ? ?}
? ? ? ?return instance;
? ? }
? ? ? ? 接著為了查找,我們還需要編寫根據id查找正排和根據關鍵字查找倒排的函數。
Docinfo *getforwardindex(uint64_t doc_id)
? ? ? ? 正常的返回unordered_map的v值很簡單。
invertedlist_t *getinvertedlist(const string &word)
? ? ? ?先調用find方法,看是否查找得到,然后查找并返回返回值的second就可以了,注意這個second值是個vector,別忘了。
使用搜索引擎返回搜索內容模塊
編寫思路
? ? ? ?搜索模塊我們就分為初始化或者說是在通過初始化調用建立索引,以及獲取用戶輸入的string轉成小寫后的search函數。最后search得到的結果寫成json形式序列化返回就可以了,3個部分。
Initsearcher(const string& input)調用建立索引
? ? ? 將Index* index;設置入searcher類中,建立索引時只需要index = Index::Getinstance();創建單例對象,然后調用index的Build_index(input)方法,此時倒排索引就在searcher類中了。
void Search(const string& query, string* out)
? ? ? ?我們傳入的用戶搜索詞是query對吧,這個可能是短語可能是單詞,短語在匹配時就等價于匹配多個詞,然后將多個詞的結果整合,排序,再去重,因為多個詞可能存在映射一個文檔的情況。
也就是說我們需要先對query進行分詞,統計分詞結果,使用jieba::Cutstring,存在vector::words里面,然后遍歷words,接著運用我們前面getinvertedlist方法得到每個詞的倒排拉鏈,這里要判斷是否存在(nullptr),如果存在再另起一個vector裝入全部的結果,注意這里另起的裝倒排拉鏈的vector不設計成二維的,我們僅僅尾插倒排拉鏈,所以只是拉長的整個一維數組。使用insert尾插。
?inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
std::vector::insert
是 C++ 標準庫<vector>
提供的成員函數,用來在任意位置插入一個或多個元素。由于vector
的底層是一段連續內存,插入點之后的所有元素都必須向后搬移,因此平均復雜度為 O(n),在尾部插入才是 O(1)。
? ? ? ?在inverted_list_all.end()位置插入inverted_list->begin()到inverted_list->end()區間長度的數據。但是相同的id不能留下超過一個呀,而且相同id的文檔屬于重匹配,權重得相加呀,怎么辦。
相同id的去重邏輯
方法1:
我們可以再創建一個struct Invertedelem,以及對應的新的 unordered_map<string, invertedlist_t> inverted_index,只是這時對應一個id是多個關鍵字(數組什么的)了,然后遍歷整個之前插入好的全部的vector,把id相同的word存入新的 Invertedelem,權值在這個時候相加,多少個加入就加對應的值就可以了。就看成一個id映射多個關鍵字的問題,哈希表前面的關鍵字的映射就被打破了,此時不是很重要了,多余的映射被剪掉了。
方法2:
這種方法不需要改動原本的結構,僅僅只是在后面編寫json串,以json串的形式發送時進行去重和加權值。我們如下采用兩個哈希表進行去重, Id負責去掉重復的元素,讓相同的id所對應的內容不插入,Id_weight負責統計相同id累加的權值,先運行Id_weight,這樣最后編寫json串時權值到Id_weight里面去取就可以了。
///讀取倒排拉鏈去重邏輯///
? ? ? ? unordered_map<int, vector<int>> Id_weight;
? ? ? ? unordered_map<int, int> Id;
? ? ? ?不知道這樣講你們能不能聽懂。
? ? ? ?如果是采用了第一種方法進行先去重再構建json格式返回,就還需要對整個倒排拉鏈進行sort自定義weight排序。
sort(inverted_list_all.begin(), inverted_list_all.end(), [](const Invertedelem& s1, const Invertedelem& s2){
? ? ? ? ? ? return s1.weight > s2.weight; //不要加上=
? ? ? ? });
接著構建json串
構建輸出json串序列化
? ? ? ?在Linux上下載完json的庫后就可以使用了,包含一下頭文件
#include <json/json.h>
#include <json/value.h>
#include <json/writer.h>
? ? ? 構建Json::Value root的哈希結構(可以自己指定k值),作為總的json容器,內部裝的也是Json::Value,我們這里叫做elem,然后遍歷總的倒排拉鏈,通過id調用index->getforwardindex(e.doc_id)獲得正排拉鏈,然后元素都存在且找得到的做如下插入即可:
elem["title"] = doc->title;
elem["desc"] = Getdesc(doc->content, e.word);//我們只要content的一部分作為摘要
elem["url"] = doc->url;
?elem["weight"] = e.weight(或者weight的總數ret)
root.append(elem);
? ? ? 插入時content不能插入全部內容,作為網頁展示應為content的部分內容,也就是要獲取摘要。在這個部分獲取摘要不是主要問題,如果去重做法采用的是如上方法1:那此時root里面就是有序的,直接如下write寫入即可。
Json::FastWriter write; //提供打印出來的類對象
*out = write.write(root); //調用write方法進行打印, 打印格式是有個string
? ? ? ?這里為了調試好看可以使用Json::StyledWriter,使用Json::FastWriter打印出來比較干脆,沒有那么雜。
? ? ? ?如果你使用的是如上方法2進行的去重,那這時root里面就是亂序的,也就是你還需要多做一步對root里面的Value進行排序再調用Json::StyledWriter。由于json不支持sort那種通過迭代器進行的指定排序,所以比較麻煩,需要先將root里面的內容那出來存入vector中,然后在對vector使用sort指定weight排序后,再寫回root,你如果不這么做,代碼本身沒錯報錯但是將來編譯的時候會報錯一堆,你看不懂的,所以寫法如下:
// 1. 先把 root 里的元素搬到 vector
? ? ? ? vector<Json::Value> vec;
? ? ? ? for (const auto &v : root)
? ? ? ? ? vec.push_back(v);
? ? ? ? // 2. 排序
? ? ? ? std::sort(vec.begin(), vec.end(),
? ? ? ? ? ? ? ? ? [](const Json::Value &a, const Json::Value &b) {
? ? ? ? ? ? ? ? ? ? return a["weight"] > b["weight"];
? ? ? ? ? ? ? ? ? });
? ? ? ? // 3. 清空原數組并重新填充
? ? ? ? root.clear();
? ? ? ? for (auto &v : vec)
? ? ? ? {
? ? ? ? ? ? root.append(std::move(v));
? ? ? ? }
string Getdesc(const string& content, const string& word)截取摘要
? ? ? ?我們首先要截取需要知道一下關鍵詞在指定content的哪個位置,找到word在content首次出現的位置,然后往前讀取50個字節,然后往后讀取100個字節,無法讀取這么多就默認從開頭或者結尾開始。但是由于我們的word如果原本是有大寫字符的,已經被之前boost::toslower變全小寫了,但是content里面可能匹配這個詞的原本是大寫的,這樣就可能會匹配不到從而無法找到在哪里。
? ? ? ?我們就需要在查找content的同時,邊對content進行小寫轉化,邊匹配。直接先將content轉成全小寫的代價比較大,所以更喜歡引入匹配規則,規則:逐個字符先轉小寫再匹配,剛好std::search庫函數可以滿足我們的需求。
std::search
是 C++<algorithm>
中的序列匹配算法,用來在一段區間中查找第一次出現的子序列(subsequence),返回指向該子序列首元素的迭代器。
// 帶自定義比較器的版本 template<class ForwardIt1, class ForwardIt2, class BinaryPredicate> ForwardIt1 search(ForwardIt1 first1, ForwardIt1 last1,ForwardIt2 first2, ForwardIt2 last2,BinaryPredicate pred);
#include <boost/algorithm/string/case_conv.hpp>? //頭文件
auto seq = search(content.begin(), content.end(), word.begin(), word.end(), [](char a, char b){return std::tolower(a) == std::tolower(b);});
? ? ? 返回值就是word在content剛開始位置的迭代器位置,也就是指向該位置的指針。如果不存在word則會返回查找序列的尾端指針,也就是xxx.end(),作為判斷條件可以如下書寫。
if (seq == content.end()){return "None1 未找到word";}
? ? ? ?然后如下幾步就是利用指針-指針獲得下標位置,利用我們之前定義的截取長度進行向前向后substr截取就可以了,沒有什么好說的了,很簡單,最后在截取的摘要后面添加......就完成了。
search.cc本地測試模塊
#include"searcher.hpp"
using namespace ns_searcher;
const string input = "data/raw_html/raw.txt";
int main()
{shared_ptr<Searcher> search = make_shared<Searcher>();search->Initsearcher(input);string query;string json_string;while (true){cout << "Please Enter Your Search Query! ";getline(cin, query);search->Search(query, &json_string);cout << json_string << endl;}return 0;
}
? ? ? 使用智能指針更安全,按照邏輯完成各種準備即可,比較簡單,直接給了。如果本地測試那些內容索引什么的,每個權值都正確(要去官網找計算比對)就可以進入下一個模塊編寫http_server了。
http_server模塊
? ? ? ?自行在gitee中輸入cpp_httplib,然后下載對應的庫。然后導入在文件目錄里面,成功后我們就可以用了。如果你的g++版本太低了這個庫就運行不起來,我沒有這個問題,如果你的centos有這個問題另行在CSDN或者別的地方找解決辦法,有解決辦法的,我沒有學,真的很抱歉不能幫到你。
? ? ? ?然后我們包含頭文件#include"cpp-httplib-master/httplib.h",注意cpp-httplib-master是我的庫導入時的名字,你們不一定跟我一樣。我們用 C++ 的 HTTP 庫 httplib
在本地啟動一個“搜索服務器”,客戶端通過瀏覽器或 curl 訪問 http://ip:8082/s?word=xxx
就能拿到搜索結果(JSON),先創建 HTTP 服務器對象Server server,接著如本地測試那些得先建立索引,然后調用server.Get方法,注冊 GET 路由 /s
,收到請求后執行后面的 Lambda。Lambda 捕獲列表 [&search]
把前面創建的 Searcher
智能指針以引用方式捕獲進來,供后續使用。這里不是服務器調用get/post方法,是處理客戶端的get/post請求的函數。
server.Get("/s", [&search](const httplib::Request& req, httplib::Response& rep)
? ? ? 捕獲search用以后面調用,"/s"指定搜索路徑的前綴必須帶/s,再接?。const httplib::Request& req, httplib::Response& rep
這兩個形參出現在 httplib 的 路由處理函數(handler)里,它們是 httplib 庫與業務代碼之間的“接口對象”,req
用來“讀”請求,rep
用來“寫回”響應。接著在Get函數里面的Lambda操作中先檢查 URL 是否帶 word
參數,例如 /s?word=C++。
rep.set_content("必須要有搜索關鍵字!", "text/plain: charset=utf-8");
? ? ? set_content有rep調用用于返回應答(寫入指定內容可以被瀏覽器渲染在瀏覽器上),后面的是content_Type,是解讀類型,文件解讀類型如上,如果你不知道某個你希望的被解讀的類型是什么,可以在瀏覽器上查找content_Type表,有對應映射關系。
? ? ? 接著在lambda中,調用req.get_param_value("word"),從 HTTP 請求行(GET 時即 URL,POST 時即表單)里取出名為 "word" 的參數值(獲取V值),把這個值以 std::string
的形式賦給局部變量 words
,后續業務邏輯就能直接使用它,事先寫成json串后,調用rep.set_content,把 JSON 作為 HTTP 響應體發回給客戶端,并聲明 Content-Type: application/json
,瀏覽器/前端就能直接解析。當然我們還需要指定訪問的跟目錄,所以在當前路徑下建立wwwhu(自定義的),然后內部編寫根目錄體現的.html,將來直接如果沒有指定搜索內容裸給一個ip就會自動訪問跟目錄./wwwhu,瀏覽器自動運行里面的html體現一個界面給我們,這個就是搜索界面。使用set_base_dir可以指定訪問的根目錄。
const string root_path = "./wwwhu";
server.set_base_dir(root_path);
? ? ? 最后server.listen("0.0.0.0", 8082);開始監聽本機所有網卡的 8082 端口。現在任何人訪問http://<服務器IP>:8082/s?word=C++
都會得到一段 JSON 搜索結果。"0.0.0.0"的意思是可以監聽所有ip發來的請求。
? ? ? 這邊肯定有人有一個小疑問,為什么是先Get處理再Listen監聽呢,這里是因為,要先注冊處理方法,有了處理方法才能監聽信息,跟socket編程不一樣的點就是socket是在寫之前已經有方法了才,listen自然在前面了。server.Get不是處理結果用的而是向底層注冊處理的方法還是要等到listen成功不阻塞之后底層才調用處理的。
編寫前端模塊
前端代碼如下:
<!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();if (query == '' || query == null){return;}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){if (data == '' || data == null){document.write("搜索內容為空");return;}// 獲取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>
? ? ? ? 就是相當于前后端建立頁面的鏈接,使得點擊搜索按鈕可以通過js聯系到后端從而得到搜索內容。
引入日志
? ? ? ? 可以使用我之前使用觀察者模式寫好的Log.hpp,這個日志很不錯直接拿來用就完了,效果如下,感興趣的可以私信我!!!
logmodule::LOG(loglevel::INFO)<< "當前已經建立的索引文檔:" << count << '\n';
項目總結與擴展
? ? ? ? 我們這個項目的基礎版本就做完了,之前我們說了獲取數據源可以使用爬蟲工具,所以數據源可以使用爬蟲進行摘取,然后用信號進行實時更新,我們會發現在數據源中不只有doc/html里面存在.html文件其他目錄里面也有,但是由于boost庫太大了,所以未來有機會可以做全站搜索的。另外,在瀏覽器搜索中,搜索條目一般都是有競價排名的,我們可以添加競價排名來有意抬高某條內容的優先級(可以將權值搞得很大)。我們還可以添加搜索框的熱詞統計,只能在前端頁面顯示搜索關鍵詞(可以使用字典樹,優先級隊列這種結構完成)。最后可以考慮添加登入注冊界面,通過http提交/login信息完成讀取登入。反正學有余力者自行擴展,擴展的這部分我回來之后再好好考慮一下!!!
分享我遇到的困難
1。boost庫下載那個tar.gz,我用的是最新版的1.88.0的,用edge下不了,以我現成的瀏覽器只能使用google,然后導入linux時rz指令啥也不帶的導入會亂碼,必須rz?-E才行,如果說你命令行輸入rz -E導入還是會很多亂碼,先重新下載rz,然后不用命令行了,手動使用xshell8的上排按鈕導入。
2。關于cppjieba這個庫,GitHub和gitee上面的limonp配置文件都不全不要下載,gitee的里面還沒有demo.cc(教你如何使用的),gitcode里面的已經沒有了,如果有需要cppjieba的私信我,我免費發完整的。
3。關于jieba分詞邏輯,由于如果不做任何的干預,jieba分詞會將一些連接詞,符號也算進去,filesystem_filesystem就會被分成filesystem/_/filesystem,這個_會影響這個給短句的匹配,會多匹配很多文檔的下標(從0-8千多)當你發現某個短語匹配時中間匹配了所有下標,就是jieba沒有去連接詞的原因。如果加上連接詞的判斷那就建立引索就會很慢,這時如果你的虛擬機或者云服務器配置很低的話很容易崩潰。
4。對于http與瀏覽器鏈接的問題,如果你使用的是虛擬機,那要確保你的虛擬機要有處在公網中的ip,這樣才可以通過瀏覽器訪問,如果瀏覽器報錯標號是502或者超時的話,如果多次更換端口還是顯示502或者超時,那我也沒有找到很好的方法解決了,使用云服務器就沒有這個問題。
5。如果在搜索時摘要返回的是None1的話,就是word是原本有大寫的,轉成小寫之后去匹配content匹配不上的問題。
6。如果導入boost庫進行tar解壓時,報錯了但是內容大寫大概4096字節就沒有問題不用管了,反正所有的html總共2000多萬字節吧。