【项目介绍】搜索引擎
文章目錄
- BoostSearch / 搜索引擎
- 開發(fā)語言
- 開發(fā)環(huán)境
- 依賴庫
- 項(xiàng)目簡介
- 項(xiàng)目特點(diǎn)
- 項(xiàng)目鏈接
- 演示
- 項(xiàng)目介紹與難點(diǎn)分析
- 解析
- 分詞
- 索引
- 正排索引
- 倒排索引
- 權(quán)重
- 包裝
BoostSearch / 搜索引擎
開發(fā)語言
C/C++,HTML,CSS,JavaScript
開發(fā)環(huán)境
CentOS7、vim、g++、gdb、git、Makefile
依賴庫
jsoncpp, cppjieba, http-lib, boost
項(xiàng)目簡介
BoostSearch是一個(gè)基于boost文檔的站內(nèi)搜索引擎,當(dāng)用戶在頁面上輸入查詢詞后,就會快速的查詢出相關(guān)的boost在線文檔,彌補(bǔ)了boost在線文檔中沒有搜索的缺陷。
項(xiàng)目特點(diǎn)
- 對離線版本的html文檔進(jìn)行解析,將解析結(jié)果整理為一個(gè)行本文文件。
- 讀取處理好的行文本文件進(jìn)行分詞、權(quán)重計(jì)算等操作,在內(nèi)存中構(gòu)造出正排索引和倒排索引。
- 對查詢詞進(jìn)行分詞、觸發(fā),依據(jù)相關(guān)度對查詢結(jié)果進(jìn)行排序,并以Json格式進(jìn)行包裝后序列化為字符串返回。
- 通過HTTP服務(wù)器搭載搜索頁面, 為外部提供服務(wù)。
項(xiàng)目鏈接
github
演示
項(xiàng)目介紹與難點(diǎn)分析
具體的模塊劃分以及功能分析我以上圖的形式展現(xiàn)了出來,本文主要說明項(xiàng)目中的一些難點(diǎn)以及問題分析,至于具體實(shí)現(xiàn)以及其他內(nèi)容請參考源代碼
https://github.com/HONGYU-LEE/BoostSearch
解析
無論是站內(nèi)搜索引擎還是全網(wǎng)搜索引擎,第一步都必須對獲取的文件進(jìn)行一個(gè)預(yù)處理。
預(yù)處理分為兩個(gè)步驟
無論是官方提供的文檔文件,還是通過爬蟲在網(wǎng)上爬取的文件,都會夾雜著許多除了HTML外的例如圖片、音樂、測試代碼等搜索引擎不需要的東西,所以第一步就是將從這些雜亂的文件中,找出所有的HTML。
而C++標(biāo)準(zhǔn)庫中并沒有提供一個(gè)能夠遍歷文件的方法,而linux的系統(tǒng)函數(shù)dirent也可以做到,但是在我上一個(gè)項(xiàng)目MiniFTP中獲取列表那一塊可以看到,這玩意并不好用,所以這里我用的是Boost庫中的filesystem模塊。它會通過一個(gè)迭代器來遞歸找到該目錄下的所有文件,來達(dá)到遍歷的效果
找到文件后,第二步就是將文章中的關(guān)鍵信息提取出來
我們需要的信息只有三個(gè),標(biāo)題、URL、正文。
標(biāo)題:標(biāo)題的提取方法很簡單,只需要找到HTML中title這個(gè)標(biāo)簽中的內(nèi)容即可
URL:URL的處理也很簡單,通過查詢官方在線文檔,可以看到URL的構(gòu)成就是官方文檔地址加上文件名,所以我們只需要提取出之前枚舉的文件的名字,將其和文檔地址進(jìn)行組合,就可以獲得到URL
正文:接下來我們需要做的,就是過濾掉所有的標(biāo)簽以及標(biāo)簽中的內(nèi)容,剩下的text內(nèi)容就是我們需要的正文。并且為了之后方便處理,將所有的換行符全部替換成為空格,將每篇的正文都單獨(dú)處理成一行。
提取出信息后,就需要對信息進(jìn)行劃分并保存。
我們所要做的,就是將每一篇文檔的信息全部匯總到一行中,即處理為一個(gè)行文本文件,使得每行數(shù)據(jù)就對對應(yīng)著一篇文檔,這樣我們在后續(xù)處理的時(shí)候只需要按行來提取出信息,就可以達(dá)到遍歷所有數(shù)據(jù)的效果。
接著,還需要通過某種方式來劃分開標(biāo)題、URL、正文。
考慮到這是一個(gè)行文本文件,換行、空格、回車等符號則直接pass,并且作為分隔符的符號必須要在文章中沒有出現(xiàn)過,考慮到這一點(diǎn),就可以使用不可見字符(文章中不可能出現(xiàn)的字符)
在這里我選擇的是\1(SOH符號),通過這個(gè)符號對所有的標(biāo)題、URL、正文進(jìn)行劃分。
?
搜索的本質(zhì)其實(shí)就是找到出現(xiàn)過用戶查詢內(nèi)容的文章
要做到這一點(diǎn)其實(shí)并不復(fù)雜,可以通過比對文章內(nèi)容以及用戶查詢詞來完成。
所以這時(shí)需要進(jìn)行兩個(gè)步驟,一個(gè)是分詞,一個(gè)是索引。
分詞
分詞很好理解,就是將用戶輸入的字符串,分解成為一個(gè)一個(gè)的關(guān)鍵詞,然后再通過關(guān)鍵詞去進(jìn)行內(nèi)容的查找。
例如對“今天的天氣很好啊”這句話進(jìn)行分詞
今天/的/天氣/很好/啊
對于我們?nèi)藖碚f,由于我們有自主思考的能力,加上了出生至今的經(jīng)驗(yàn)以及積累,分詞對于我們來說是很簡單的一件事,但是對于計(jì)算機(jī)而言,這并不是一件簡單的事情。
例如這句出自神雕俠侶中的一句話,需要結(jié)合其中的背景來理解,機(jī)器則很難理解
“我也想過過兒過過的生活” = 我/也想/過/過兒/過/過的/生活
并且有些句子可能會解析出多種結(jié)果
“在北京大學(xué)生活區(qū)喝進(jìn)口紅酒”
1.在/北京/大學(xué)/生活區(qū)/喝/進(jìn)口/紅酒
2.在/北京大學(xué)/生活區(qū)/喝/進(jìn)口紅酒
3.在/北京/大學(xué)生活區(qū)/喝/進(jìn)口紅酒
…
并且在句子中會經(jīng)常出現(xiàn)一些如“的”,“了”,“呢”,“嗎”,"像"等等等語氣詞或者結(jié)束詞會不可避免的大量出現(xiàn),但是這些詞又沒有什么實(shí)際上的意義,所以我們將其稱為**“暫停詞/停用詞”**,不會將其放入權(quán)重的計(jì)算中。
考慮到分詞的困難巨大,甚至比我們整個(gè)項(xiàng)目都要復(fù)雜,所以這里就不再自己造輪子,而是引入了第三方庫結(jié)巴分詞——https://github.com/yanyiwu/cppjieba
索引
接著就到了整個(gè)項(xiàng)目中最核心的部分,索引。
即使本項(xiàng)目是一個(gè)站內(nèi)的搜索引擎,只有8000多篇文檔, 但是實(shí)際算下來字符數(shù)量可能多達(dá)千萬甚至更多,如果直接進(jìn)行遍歷搜索,那么速度將會慢的嚇人。而在龐大的數(shù)據(jù)中想要快速的查詢到指定的數(shù)據(jù),那么馬上就能想到的方法就是建立索引。
索引就相當(dāng)與我們看書時(shí)文章的一個(gè)目錄,我們可以通過目錄上的頁數(shù)來快速定位到需要查詢的數(shù)據(jù),所以我們以下兩種索引,正排索引與倒排索引。
正排索引
正排索引的作用即根據(jù)文檔ID來查找文檔信息
//文章信息 struct DocInfo {int64_t doc_id; //文檔IDstring title; //文檔標(biāo)題string url; //文檔URLstring content; //文檔正文 };因?yàn)槲覀冏罱K需要查詢的結(jié)果只需要文檔的標(biāo)題、URL、正文,所以正排索引所存儲的文檔信息就使用上面這種結(jié)構(gòu)。
vector<DocInfo> forward_index; //正排索引接著,就需要建立起ID與文檔信息的映射關(guān)系,由于文檔本身并沒有規(guī)定固定的ID,所以這里我就直接使用數(shù)組下標(biāo)來作為文檔的ID,將其以vector的形式進(jìn)行存儲(如果規(guī)定了ID就使用unordered_map)
倒排索引
倒排索引的作用即根據(jù)文檔片段來查找相關(guān)的文檔ID
//權(quán)重信息 struct Weight {int64_t doc_id; //文檔IDint weight; //權(quán)重string word; //關(guān)鍵詞 }; typedef vector<Weight> InvertedList; //倒排拉鏈我使用上面的Weight結(jié)構(gòu)來存儲每個(gè)關(guān)鍵詞所對應(yīng)出現(xiàn)的文章與該文章的權(quán)重大小。
由于一個(gè)Weight對應(yīng)著該詞的其中一篇相關(guān)文章,所以為了方便查詢,我將該詞的所有相關(guān)文章以一個(gè)vector進(jìn)行存儲,構(gòu)成一個(gè)倒排拉鏈的結(jié)構(gòu),并將這個(gè)倒排拉鏈作為倒排索引的節(jié)點(diǎn)。這樣通過關(guān)鍵詞來查找到倒排拉鏈時(shí),就可以通過直接遍歷倒排拉鏈來獲取到所有的相關(guān)文檔。
unordered_map<string, InvertedList> invert_index; //倒排索引關(guān)鍵詞與相關(guān)文檔的映射使用哈希表(unordered_map)建立
下面介紹下索引的建立流程
假設(shè)我們此時(shí)有兩篇文章,首先進(jìn)行分詞
接著,根據(jù)文檔信息構(gòu)建出正排索引
文檔ID——》文檔信息
接著,依據(jù)每個(gè)關(guān)鍵詞出現(xiàn)過的文章,構(gòu)建出倒排索引
關(guān)鍵詞——》相關(guān)文檔ID
建立了這樣一個(gè)索引體系之后,在查詢時(shí),我們只需要對查詢詞進(jìn)行分詞,再通過倒排索引查詢到對應(yīng)的倒排拉鏈,權(quán)重信息,來為每個(gè)倒排拉鏈的文檔進(jìn)行排序,就可以根據(jù)相關(guān)度高低以此獲取到對應(yīng)的文檔ID,再通過正排索引就可以獲取到我們需要的文檔信息了。
權(quán)重
由于我們存儲的數(shù)據(jù)量不是很大,只有大概8000多個(gè)文檔,并且該項(xiàng)目只是用于學(xué)習(xí)搜索引擎原理,所以這里就使用線性公式來進(jìn)行權(quán)重的計(jì)算,計(jì)算的依據(jù)就是關(guān)鍵詞的詞頻。
首先用以下結(jié)構(gòu)來統(tǒng)計(jì)關(guān)鍵詞在文章的正文以及標(biāo)題中分別出現(xiàn)的次數(shù)
struct WordCount {WordCount() : title_count(0), content_count(0){} int title_count; //標(biāo)題出現(xiàn)頻率int content_count; //正文出現(xiàn)頻率 };關(guān)鍵詞在正文中和標(biāo)題中出現(xiàn)的權(quán)重應(yīng)該不一樣。
因?yàn)闃?biāo)題即對正文的高度概括,在標(biāo)題中出現(xiàn)的內(nèi)容往往與實(shí)際內(nèi)容相關(guān),而在正文即使出現(xiàn),也有可能只是順口一提或者引用,所以我認(rèn)為在標(biāo)題中出現(xiàn)的權(quán)重應(yīng)該要比在正文出現(xiàn)要高
所以推導(dǎo)的公式如下:權(quán)重 = 標(biāo)題出現(xiàn)次數(shù) * 10 + 正文出現(xiàn)次數(shù)
//權(quán)重 = 標(biāo)題出現(xiàn)次數(shù) * 10 + 正文出現(xiàn)次數(shù) weight.weight = word_pair.second.title_count * 10 + word_pair.second.content_count;包裝
對于查詢的結(jié)果,由于我們并不需要將所有的信息都展示在搜索頁面上,只需要展示關(guān)鍵信息,所以我們需要的內(nèi)容只有標(biāo)題、URL、描述。
前兩個(gè)已經(jīng)獲取了,但是描述還需要我們自己從正文中提取。
關(guān)于描述信息的提取,我的思路如下。
根據(jù)以上條件,我推導(dǎo)出以下描述信息提取公式
返回內(nèi)容 = 150字節(jié) = 關(guān)鍵詞往前50字節(jié) + 關(guān)鍵詞(包括關(guān)鍵詞)往后100個(gè)字節(jié)
這樣,我們就可以構(gòu)建出最后查詢出的結(jié)果。
但是我們最終查詢的結(jié)果并不能直接返回給調(diào)用者,還需要對其進(jìn)行包裝。
這里我采用的是JSON格式,借助了第三方庫JSONCPP(https://github.com/open-source-parsers/jsoncpp)來進(jìn)行包裝
JSON的格式如下
[{"title" : "標(biāo)題","url" : "url","desc" : "描述"}{"title" : "標(biāo)題","url" : "url","desc" : "描述"}{"title" : "標(biāo)題","url" : "url","desc" : "描述"}.................. ]之后將結(jié)果序列化為字符串回復(fù)給調(diào)用者
總結(jié)
以上是生活随笔為你收集整理的【项目介绍】搜索引擎的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【项目介绍】FTP服务器
- 下一篇: 海量数据处理(二) :常见海量数据处理方