1.什么是搜索引擎?
如圖所示:
我用的是谷歌瀏覽器,但是我的搜索引擎可以跟換 。切換到bing主頁
在搜索框中我們輸入一段話,跳到一個帶有搜索結(jié)果的頁面如下:
搜索引擎的核心功能:查找用戶輸入的詞/一句話 相關(guān)聯(lián)的網(wǎng)頁。?
搜索結(jié)果頁一條記錄包含的信息如下:
?當我們點擊標題,會跳轉(zhuǎn)到落地頁,如下圖:
相信大家對搜索引擎都有一定的了解了。
2.實現(xiàn)搜索引擎的核心思路:
- 首先我需要很多網(wǎng)頁。
- 再根據(jù)用戶輸入的詞/一句話,在這些網(wǎng)頁中進行查找。
搜索引擎的網(wǎng)頁又該如何獲取呢?
像百度,搜狗,這樣的大型搜索引擎,每天會有很多爬蟲去爬取大量網(wǎng)頁,在存儲起來。
我們這里先直接去官網(wǎng)下載jdk文檔(之后抽取時間改進利用爬蟲獲取網(wǎng)頁)。
用戶輸入查詢詞之后,如何去讓查詢詞語和當前的這些網(wǎng)頁進行匹配呢?
搜索引擎工作原理
利用這些網(wǎng)頁(.html文件解析過后,生成的文檔DocInfo)構(gòu)建正排索引和倒排索引。
3.正排索引和倒排索引原理:
正排,倒排原理理解
1.正排索引:文檔 id? ? ? ? ->? ? ? ? 文檔內(nèi)容? ? ? ? ? ? ? ? ????????(1個id對應(yīng)1個文檔內(nèi)容)
2.倒排索引:詞? ? ? ? ? ? ? ? ->? ? ? ? 和詞相關(guān)聯(lián)的文檔id? ? ? ? (1個詞對應(yīng)一個/多個文檔id)
舉個例子:
//正排索引
1? ? ? ? ? ? ? ? ? ? ? ? 我上街買了一份炸雞
2? ? ? ? ? ? ? ? ? ? ? ? 我晚飯吃了一份牛肉
//倒排索引(先分詞)
我? ? ? ? ? ? ? ? ? ? ? ? ????????1,2
上街? ? ? ? ? ? ? ? ? ? ? ? ? ? 1
買? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1
了? ? ? ? ? ? ? ? ? ? ? ? ????????1,2
一份? ? ? ? ? ? ? ? ? ? ????????1,2
炸雞? ? ? ? ? ? ? ? ? ? ? ? ????1?
晚飯? ? ? ? ? ? ? ? ? ? ? ??????2
吃? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2
牛肉? ? ? ? ? ? ? ? ? ? ? ? ????2
4.我要把搜索引擎做到什么程度?
?我們還是先做一個“站內(nèi)搜索”,類似于嗶哩嗶哩里的搜索框,搜索站內(nèi)的東西。我們要做一個java,jdk8文檔的搜索引擎,比如輸入一個ArrayList,會在頁面顯示一系列關(guān)于ArrayList文檔的信息,通過點擊標題或者utl跳轉(zhuǎn)到官網(wǎng)jdk8文檔。
5.獲取文檔(網(wǎng)頁)
Java Downloads | Oracle
先看看在線文檔的內(nèi)容以及url
?https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
?在對比看一下離線后的文檔路徑
?file:///C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html
本地鏈接和在線網(wǎng)站鏈接相比得出結(jié)論:
我們可以基于離線文檔來制作索引,實現(xiàn)搜索?,當用戶在搜索結(jié)果頁面點擊具體的搜索結(jié)果的時候,就自動跳轉(zhuǎn)到在線文檔的頁面。
6.模塊粗略劃分:
1.索引模塊:
- 掃描下載的文檔,分析文檔內(nèi)容,構(gòu)造出正排索引+倒排索引。并且把索引內(nèi)容保存到文件中
- 加載制作好的索引,并提供一些API實現(xiàn)查正排和查倒排這樣的功能。
2.搜索模塊:
- 調(diào)用索引模塊,實現(xiàn)一個搜索的完整過程
- 輸入:用戶輸入詞/一句話
- 輸出:完整的搜索結(jié)果(包含了很多條記錄,每個記錄都有標題,描述,展示url,并且點擊能夠跳轉(zhuǎn))
3.web模塊:
- 需要實現(xiàn)一個簡單的web程序,能夠通過網(wǎng)頁的形式來和用戶交互~
7.創(chuàng)建項目
用到的工具:
idea專業(yè)版2022.2.1
webstorm專業(yè)版2022.2.1
8.介紹一下什么是分詞
用戶在使用時侯,往往會輸入一句話,比如:
會出現(xiàn)一下結(jié)果頁:
用戶輸入的話被分成好多個詞,每個詞又對應(yīng)很多文檔id,所以結(jié)果頁面會有很多條記錄,每條記錄都包括標題,url,描述這三個基本信息?。
我上街買了一份炸雞? ? ? ? 可分詞為:
我
上街
買
了
一份
炸雞
我們要使用第三方庫ansj來進行分詞。ansj地址
說一下分詞的原理,有助于編寫代碼。
1.基于詞庫(跟不上時代發(fā)展,每隔一段時間會出現(xiàn)新詞)
- 嘗試把所有詞都進行窮舉~把這些窮舉結(jié)果放到詞典文件中。
- 然后就可以依次的取句子中的內(nèi)容,每隔一個字,在詞典里查一下,每隔倆個字,查一下.....
2.基于統(tǒng)計(該方法更加科學,用的多)
- 收集到很多很多的“語料庫” => "人工標注/直接統(tǒng)計? => "也就知道了哪些字放在一起的概率比較大~";
- 分詞的實現(xiàn),就是屬于“人工智能”典型應(yīng)用場景~
分詞原理
寫個測試類用一下ansj這個第三方庫
說一下爆紅的原因:
在分詞的時候,會加載一些詞典文件,通過這些詞典文件能夠加快分詞速度和準確率,但是沒有這些詞典文件ansj仍然能快速分出詞。?(可配置詞典文件)
我們在來看一下分詞結(jié)果:
9.實現(xiàn)索引模塊Parser類的整體流程?
- 指定本地jdk8文檔的路徑INPUT_PATH
- Parser類作為入口,main函數(shù)調(diào)用run方法
- run函數(shù)包含三個功能:
- 1.根據(jù)指定路徑,羅列出該文件夾下所有的.html文件,包括子文件夾中的.html的路徑
- 2.根據(jù)上邊羅列的所有html文件路徑,打開文件,讀取文件,解析文件,構(gòu)建索引(在內(nèi)存中)
- 3.把在內(nèi)存中構(gòu)造好的索引數(shù)據(jù)結(jié)構(gòu),保存到指定的文件中(即序列化,反序列化)
10.遞歸枚舉文件
import java.io.File;
import java.util.ArrayList;
public class Parser {
public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";
public void run() {
//1.枚舉出所有的.html文件,包括子目錄中的文件
ArrayList<File> filelist = new ArrayList<>();
enumFile(INPUT_PATH, filelist);
// System.out.println(filelist);
// System.out.println(filelist.size());
//2.針對上面羅列出的文檔路徑,打開文件,讀取文件內(nèi)容,進行解析,構(gòu)建索引
//3.把在內(nèi)存中構(gòu)造好的索引數(shù)據(jù)結(jié)構(gòu),保存到指定的文件中
}
private void enumFile(String inputPath, ArrayList<File> filelist) {
File rootPath = new File(inputPath);
File[] files = rootPath.listFiles();
for (File f : files) {
if(f.isDirectory()){
enumFile(f.getAbsolutePath(),filelist);
}else {
filelist.add(f);
}
}
}
public static void main(String[] args) {
Parser parser = new Parser();
parser.run();
}
}
現(xiàn)在有一個問題,我要的是html文件,其他文件例如css,js等其他文件是我不想要它出現(xiàn)在結(jié)果里邊,用戶也看不懂,所以filelist里邊要去掉這些前端文件。
11.除去不是html的文件?
我怎么實現(xiàn)?
我的思路是得到所有文件的目錄,它的后綴不是html的就不加到那個filelist里邊
private void enumFile(String inputPath, ArrayList<File> filelist) {
File rootPath = new File(inputPath);
File[] files = rootPath.listFiles();
for (File f : files) {
if(f.isDirectory()){
enumFile(f.getAbsolutePath(),filelist);
}else {
if(f.getAbsolutePath().endsWith(".html")){
filelist.add(f);
}
}
}
}
這里我想說的是,后續(xù)的開發(fā)中會遇到各種各樣的問題,但是只要思想不滑坡,辦法總比困難多。
浩大的工程,都是由一個一個小小的函數(shù)堆積起來的。好了,現(xiàn)在我們把所有的html文件都加載到filelist里邊了,現(xiàn)在的問題是,我怎么根據(jù)上邊的路徑打開文件去解析里邊的文件呢?解析,我要解析什么?(1.標題,2.url,3.描述(描述是根據(jù)正文來的,所以因該是解析正文)),我為什么要這三個東西?我要把這三個東西用一個弄一個實體類來表示,即一個DocInfo,里邊有id,title,url,content,然后通過一個ArrayList<DocInfo>構(gòu)建出一個正排索引,然后根據(jù)這個正排索引去構(gòu)建倒排索引,目前先考慮這么多。
12.解析html
我們打開文件發(fā)現(xiàn)html的名字和文件中的title里的名字相似,但是html的名字更加獲取,代碼實現(xiàn)上更加容易,我們就以html前邊的單詞作為標題 。
?
這樣的話,我們解析html的標題的功能完成了。我們繼續(xù)往下做。
13.解析url
我們所期望的結(jié)果就是,用戶點擊搜索結(jié)果,就能夠跳轉(zhuǎn)到對應(yīng)的線上文檔的頁面。
思路:
把本地路徑的后半段提取出來作為后綴? ? ? ? ?java/util/ArrayList.html
以在線路徑前半部分為前綴? ? ? ? ? ? ?https://docs.oracle.com/javase/8/docs/api/
拼接后就是完整的url?????????https://docs.oracle.com/javase/8/ docs/api/java/util/ArrayList.html
寫一個測試代碼:
import java.io.File;
public class TestUrl {
public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";
public static void main(String[] args) {
File f = new File("C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html");
String part1 = "https://docs.oracle.com/javase/8/docs/api/";
String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
System.out.println(part1+part2);
}
}
這里雖然是反斜杠,但是粘貼在瀏覽器上還能打開,體現(xiàn)出瀏覽器的魯棒性。
我們優(yōu)化一下吧,把\都替換成/.
具體方法:
注意:
?雖然沒看懂,但是能用!接下來我們開始解析正文,先講講解析正文的思路:
14.解析正文
我們先打開一個html文件觀察觀察
這里邊有script的代碼,還有css的代碼style,還有html的標簽
這里我們使用正則表達式去除一下這些標簽
具體代碼:
public String parseContent(File f) {
try (FileReader fileReader = new FileReader(f)) {
StringBuilder content = new StringBuilder();
while (true) {
int ret = fileReader.read();
if (ret == -1) {
break;
}
char c = (char) ret;
content.append(c);
}
String scriptRegex = "<script[^>]*?>[\\s\\S]*?<\\/script>";
//定義style的正則表達式,去除style樣式,防止css代碼過多時只截取到css樣式代碼
String styleRegex = "<style[^>]*?>[\\s\\S]*?<\\/style>";
//定義HTML標簽的正則表達式,去除標簽,只提取文字內(nèi)容
String htmlRegex = "<[^>]+>";
//定義回車,換行符,制表符
String spaceRegex = "\t|\r|\n";
String content_final = content.toString();
content_final = content_final.replaceAll(scriptRegex, "");
content_final = content_final.replaceAll(styleRegex, "");
content_final = content_final.replaceAll(htmlRegex, "");
content_final = content_final.replaceAll(spaceRegex,"");
return content_final;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
效果展示:
?嗯不錯不錯~(蜜汁自信)
現(xiàn)在的問題是,我們已經(jīng)把東西解析出來了,我們現(xiàn)在需要一個索引類去把解析出來的東西加入到索引,然后將制作好的索引保存到指定文件中去。
15.實現(xiàn)索引模塊
先創(chuàng)建一個Index類,這個類的主要實現(xiàn)方法:
//給定一個docId,在正排索引中,查詢文檔的詳細信息 //給定一個詞,在倒排索引中,查哪些文檔和這個詞相關(guān)聯(lián) //往索引中新增一個文檔 //把內(nèi)存中的索引結(jié)構(gòu)保存到磁盤中 //把磁盤中的索引數(shù)據(jù)加載到內(nèi)存中
然后創(chuàng)建一個DocInfo類,這個實體類來描述docId,title,url,content.
這里有一個問題,返回值是List<Integer>行不行?用戶輸入一個詞/一句話,我返回了文章的id難道不對嗎?如過這樣做的話,就體現(xiàn)不出文章的相關(guān)性了,排在前邊的永遠是docId小的文章。為了解決這樣的問題,修改一下返回值。
創(chuàng)建一個類Weight,權(quán)重的意思,Weight里邊包含docId,和weight這倆個屬性,weight值越大,我們就表示,用戶輸入的值和文章的相關(guān)性越強,在瀏覽器顯示的越靠前。這個Weight類就是把文檔id和文檔與詞的相關(guān)性進行一個包裹。
具體實現(xiàn)代碼:
import java.util.List;
//通過這個類在內(nèi)存中構(gòu)造出索引
public class Index {
//給定一個docId,在正排索引中,查詢文檔的詳細信息
public DocInfo getDocInfo(int docId) {
return null;
}
//給定一個詞,在倒排索引中,查哪些文檔和這個詞相關(guān)聯(lián)
public List<Weight> getInverted(String term) {
return null;
}
//往索引中新增一個文檔
public void addDoc(String title, String url, String content) {
}
//把內(nèi)存中的索引結(jié)構(gòu)保存到磁盤中
public void save(){
}
//把磁盤中的索引數(shù)據(jù)加載到內(nèi)存中
public void load(){
}
}
我們現(xiàn)在還需要倆個結(jié)構(gòu)表示正排索引和倒排索引:
現(xiàn)在在想一下,正排索引是通過一個docId返回一個DocInfo,而倒排索引可以通過一個詞返回一組docId,為了讓詞和docId有相關(guān)性,所以進行如下編碼:
private ArrayList<DocInfo> forwardIndex = new ArrayList<>();
private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();
現(xiàn)在我有一個問題,查正排,和查倒排效率高嗎?時間復(fù)雜度是多少?
查正排,我們用的是ArrayList,時間復(fù)雜度是O(1)
查倒排,我們用的是HashMap,時間復(fù)雜度是O(1)
所以說,查詢的很快,我們用這樣的結(jié)構(gòu)來保證查詢的夠快,夠高效。
還有就是,我們現(xiàn)在的數(shù)據(jù)都是放在內(nèi)存上的,這是查詢快的第二個原因。
目前,實現(xiàn)的代碼如下:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
//通過這個類在內(nèi)存中構(gòu)造出索引
public class Index {
private ArrayList<DocInfo> forwardIndex = new ArrayList<>();
private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();
//給定一個docId,在正排索引中,查詢文檔的詳細信息
public DocInfo getDocInfo(int docId) {
return forwardIndex.get(docId);
}
//給定一個詞,在倒排索引中,查哪些文檔和這個詞相關(guān)聯(lián)
public List<Weight> getInverted(String term) {
return invertedIndex.get(term);
}
//往索引中新增一個文檔
public void addDoc(String title, String url, String content) {
//構(gòu)建正排索引
DocInfo docInfo = buildForward(title,url,content);
//構(gòu)建倒排索引
buildInverted(docInfo);
}
private void buildInverted(DocInfo docInfo) {
}
private DocInfo buildForward(String title, String url, String content) {
DocInfo docInfo = new DocInfo();
docInfo.setDocId(forwardIndex.size());
docInfo.setTitle(title);
docInfo.setUrl(url);
docInfo.setContent(content);
forwardIndex.add(docInfo);
return docInfo;
}
//把內(nèi)存中的索引結(jié)構(gòu)保存到磁盤中
public void save(){
}
//把磁盤中的索引數(shù)據(jù)加載到內(nèi)存中
public void load(){
}
}
目前,我們新增文檔的功能還沒實現(xiàn)完,正排索引構(gòu)建好了,現(xiàn)在實現(xiàn)構(gòu)建倒排索引,怎么構(gòu)建?
怎么做?倒排索引就是詞與docId的映射,我準備,把傳入的docInfo,把它的title,和content進行分詞,我倒排索引用的是HashMap<String,ArrayList<Weight>>,也就是詞和權(quán)重的鍵值對。權(quán)重里邊包括docId,weight,我們又如何確定這個權(quán)重的值, (權(quán)重這個值,描述了詞個文檔之間的相關(guān)性,權(quán)重值越大,我們認為詞和文章id相關(guān)性越強,在網(wǎng)頁上顯示的也就越靠前)。
在真實的搜索引擎中,這里的相關(guān)性,是一個非常復(fù)雜的邏輯,往往是一個專門的算法團隊來進行負責的。根據(jù)文檔中提取的特征,訓練模型,最終借助機器學習的方式來衡量相關(guān)性。
現(xiàn)在我對構(gòu)建倒排索引做如下規(guī)劃:
1.針對文檔標題進行分詞
2.遍歷分詞結(jié)果,統(tǒng)計每個詞出現(xiàn)的次數(shù)
3.針對正文進行分詞
4.遍歷分詞結(jié)果,統(tǒng)計每個詞出現(xiàn)的次數(shù)
5.把上面的結(jié)果匯總到一個HashMap里面。
6.遍歷剛才這個HashMap,依次來更新倒排索引中的結(jié)構(gòu)。
現(xiàn)在,我們想一想,標題中出現(xiàn)的詞,和正文出現(xiàn)的詞相比,標題出現(xiàn)的詞是不是權(quán)重更大一些?
標題的詞少,但是這里的詞更能表達文章的中心思想,
?正文的詞多,但是這里的詞更不能表達文章的中心思想。
一句話:最終文檔的權(quán)重,就設(shè)定成 : 標題中出現(xiàn)的次數(shù) * 10 + 正文中出現(xiàn)的次數(shù)。
這里的權(quán)重公式可以改進。如何改進?
點擊率 = 點擊次數(shù)/展示次數(shù)
在實際開發(fā)中,比如我們的服務(wù)器,每天有一億訪問量,然后可以把這一億訪問量拆成若干份,
30%? ? ? ? 使用A公式
30%? ? ? ? 使用B公式
30%? ? ? ? 使用C公式
10%? ? ? ? 使用D公式
分別統(tǒng)計,這些情況的點擊率如何
公式會越來越復(fù)雜,同時也會讓點擊效果提升
最終我們看看這幾個公式哪個更好,就會留下哪個公式繼續(xù)迭代。
實際的搜索引擎,這里的計算公式非常復(fù)雜,并且要持續(xù)調(diào)整,反復(fù)迭代。
畫個圖理解一下倒排索引的數(shù)據(jù)結(jié)構(gòu)
?具體代碼實現(xiàn):
private void buildInverted(DocInfo docInfo) {
class WordCnt {
public int titleCount;
public int contentCount;
}
HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();
List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
for (Term term : terms) {
String word = term.getName();
WordCnt wordCnt = wordCntHashMap.get(word);
if (wordCnt == null) {
WordCnt newWordCnt = new WordCnt();
newWordCnt.titleCount = 1;
wordCntHashMap.put(word, newWordCnt);
} else {
wordCnt.titleCount += 1;
}
}
terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
for (Term term : terms) {
String word = term.getName();
WordCnt wordCnt = wordCntHashMap.get(word);
if (wordCnt == null) {
WordCnt newWordCnt = new WordCnt();
newWordCnt.contentCount = 1;
wordCntHashMap.put(word, newWordCnt);
} else {
wordCnt.contentCount += 1;
}
}
for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
//倒排拉鏈 invertedList
ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());
if (invertedList == null) {
ArrayList<Weight> newInvertedList = new ArrayList<>();
Weight weight = new Weight();
weight.setDocId(docInfo.getDocId());
weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
newInvertedList.add(weight);
invertedIndex.put(entry.getKey(), newInvertedList);
} else {
Weight weight = new Weight();
weight.setDocId(docInfo.getDocId());
weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
invertedList.add(weight);
}
}
}
????????
目前倒排索引構(gòu)建好了,但這些索引都保存在內(nèi)存中。而且構(gòu)建索引的過程是比較耗時間的。我們的文檔大概也有一萬多條,addDoc這個比較耗時間。
因此,我們不因該在服務(wù)器啟動的時候才構(gòu)建索引(啟動服務(wù)器會被拖慢很多很多)
我的想法是:
把這些耗時間的操作,單獨去執(zhí)行,單獨執(zhí)行完了之后,然后讓線上服務(wù)器直接加載構(gòu)造好的索引。
因此才要實現(xiàn)save和load的操作。
我們接下來就去把內(nèi)存中的索引結(jié)構(gòu),變成一個字符串,然后寫入文件,也就是序列化的過程,當我們加載文檔也就是反序列化的過程。
?序列化與反序列化也有很多方法,此處我們就用json格式來進行序列化與反序列化
我們先把jackson庫引入
具體實現(xiàn)代碼:
public void save() {
System.out.println("保存索引開始!");
File indexPathFile = new File(INDEX_PATH);
if (!indexPathFile.exists()) {
indexPathFile.mkdirs();
}
File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
try {
objectMapper.writeValue(forwardIndexFile, forwardIndex);
objectMapper.writeValue(invertedIndexFile, invertedIndex);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("保存索引完成!");
}
現(xiàn)在說一下json的格式:
?當我們實現(xiàn)加載功能的時候,jackson庫提供了TypeReference這樣的類來幫助我們解決,json格式的數(shù)據(jù)轉(zhuǎn)換成什么類型的數(shù)據(jù),由于forwardIndex 的返回值類型是ArrayList<DocInfo>,所以進行以下編碼:
public void load() {
System.out.println("加載索引開始!");
File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
try {
forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("加載索引結(jié)束!");
}
現(xiàn)在我們的索引模塊基本完成了,我們看看效果,在看看能否進行一下優(yōu)化。
16.優(yōu)化運行時間
先運行一下項目:保存索引大概是560ms這個樣子,也就是save的運行時間
?
文件大小第一個大約是70MB,第二個文件大小大約是64MB?
我們前邊的解析html也是比較消耗時間的,我們先寫個日志打印一下消耗的時間。
public void run() {
long begin = System.currentTimeMillis();
System.out.println("索引制作開始!");
//1.枚舉出所有的.html文件,包括子目錄中的文件
ArrayList<File> filelist = new ArrayList<>();
enumFile(INPUT_PATH, filelist);
long endEnumFile = System.currentTimeMillis();
System.out.println("枚舉文件完成!消耗時間: " + (endEnumFile - begin) + "ms");
// System.out.println(filelist);
// System.out.println(filelist.size());
//2.針對上面羅列出的文檔路徑,打開文件,讀取文件內(nèi)容,進行解析,構(gòu)建索引
for (File f : filelist) {
System.out.println("開始解析:" + f.getAbsolutePath());
parseHtml(f);
}
long endFor = System.currentTimeMillis();
System.out.println("遍歷文件完成!消耗時間:" + (endFor - endEnumFile) + "ms");
//3.把在內(nèi)存中構(gòu)造好的索引數(shù)據(jù)結(jié)構(gòu),保存到指定的文件中
index.save();
long end = System.currentTimeMillis();
System.out.println("索引制作完成!消耗時間: " + (end - begin) + " ms");
}
?可以清楚的看到制作索引需要消耗18s的時間,還是挺長的。我們該怎么辦?
說一下性能優(yōu)化:
要想優(yōu)化一段程序的性能,就需要先通過測試的手段找到其中的“性能瓶頸”?,F(xiàn)在問題主要出現(xiàn)在
for (File f : filelist) {
System.out.println("開始解析:" + f.getAbsolutePath());
parseHtml(f);
}
這一段代碼上,也就是
每次循環(huán)都要針對一個文件進行解析,讀文件+分詞+解析內(nèi)容(這里面主要還是卡在cpu運算上)
單個線程的情況下,這些任務(wù),都是串行執(zhí)行的,多個線程,這些任務(wù)就可以并發(fā)執(zhí)行了。
我們的速度就會有較大提升。
現(xiàn)在我們修改一下代碼,改成多線程制作索引。
什么時候會有線程安全問題?
多個線程同時修改一個對象,就會出現(xiàn)線程安全問題。
目前碰到一個問題,如何保證線程安全??
線程安不安全主要在parseHtml(f);這個函數(shù)
parseTitle不涉及多個線程同時修改一個對象,
parseUrl也同樣不涉及多個線程同時修改一個對象,
parseContent也不涉及多個線程同時修改一個對象,
上邊三個函數(shù)都是多個線程玩各自的對象。
問題出現(xiàn)在addDoc這個函數(shù),畫個圖理解一下。
八個線程同時玩forwardIndex,invertedIndex這倆個對象,勢必會出現(xiàn)線程不安全問題。
那你為啥不給addDoc加個鎖,這樣不就行了嗎?
我們引入線程的目的是為了讓串行變成并發(fā),你給addDoc加上鎖不就又成了串行執(zhí)行了嗎?
通俗點來說,脫褲子放屁,多費手續(xù)。
我們的目的是,串行的讓它并發(fā),實在并發(fā)不了的讓它串行。讓加鎖的粒度在小一些。
所以如果直接把synchronized加到parseHTML或者addDoc上,這樣做鎖的粒度太粗了,并發(fā)程度不高,就是提高的效率有限。
現(xiàn)在發(fā)現(xiàn),buildForward 和 buildInverted其實是在操作不同的對象。也就不存在鎖競爭。
新建倆個鎖對象,針對倆個鎖對象進行加鎖。
多線程的效果:
?多線程的使用極大的提高了,索引制作時間。
那么線程池中的線程個數(shù)設(shè)置成幾合適呢?我們只能通過實驗來進行確定。
不同的代碼并發(fā)程度是不一樣的。
并不是線程數(shù)目越多越好,線程多確實可以提升效率,但是太多的話,提升效果就不明顯了,在多加線程,白白浪費計算機資源。
目前又出現(xiàn)一個問題:
?程序執(zhí)行完了,進程沒有關(guān)閉。
這個問題的主要原因就是:
如果要是一個線程是守護線程,此時這個線程的運行狀態(tài),不會影響到進程結(jié)束。
如果要是一個線程不是守護線程,這個線程的運行狀態(tài),就會影響到進程結(jié)束。
默認創(chuàng)建出來的都是非守護線程,需要通過setDaemon方法手動設(shè)置,才能成為守護線程。
通過線程池創(chuàng)建的線程,并不是守護線程,
當main方法執(zhí)行完了,這些線程仍然在工作。(仍然在等待新任務(wù)的到來)
我們可以使用線程池提供的方法,讓線程停止。
目前又碰到一個問題,首次制作索引比較慢,尤其是開機之后,首次制作索引特別慢,后面就會快了,重啟機器,第一次制作就會特別慢。
通過排查parseHtml函數(shù)定位到,parseContent這個函數(shù)有問題,
計算機讀取文件,是一個開銷比較大的操作。
我為什么選擇atomiclong來計時間?
多線程環(huán)境下,可以簡單使用AtomicXXX 使代碼變得線程安全。?
atomiclong相當于加了synchronized的long。
開機第一次運行:
第二次運行:
可以明顯看到,t1的時間變化,????
????
注意此處是八個線程累加的時間。?
(第一次開機運行,t2用了51s ,第二次運行t2用了25s,搞不懂為啥出現(xiàn)這種情況)
這種情況的話,我猜測是因為addDoc之前,你就得把title,url,content解析出來,parsecontent沒解析完,adddoc就得等著,t2的時間前后也是2倍關(guān)系。
接下來我們可以優(yōu)化一下parsecontent這個函數(shù)。
public String parseContent(File f) {
try (FileReader fileReader = new FileReader(f)) {
StringBuilder content = new StringBuilder();
while (true) {
int ret = fileReader.read();
if (ret == -1) {
break;
}
char c = (char) ret;
content.append(c);
}
我們讀取文件用的是FileReader這個方法,fileReader.read()每次從文件中讀取一個字符,每次read都是在讀磁盤,它的速度就會很慢。
思路:我們可以先讓filereader提前把文件讀取到內(nèi)存里面,然后每次調(diào)用一次bufferedReader.read() 就可以從內(nèi)存中讀取.
BufferedReader 可以搭配FileReader來使用。
BufferedReader內(nèi)部就會內(nèi)置一個緩沖區(qū)。就能夠自動的把FileReader中的一些內(nèi)容預(yù)讀到內(nèi)存中,從而減少直接訪問磁盤的次數(shù)。
其實FileReader也有緩沖區(qū),只是BufferedReader對緩沖支持更好。
此刻的心情:優(yōu)化了個寂寞。
但是,開機第一次運行的效果肯定比沒優(yōu)化前好,我們開機重新試一下。
此時用了19秒?比前開機少用了10來秒,只能說還行。
17.實現(xiàn)搜索模塊
首先,搜索模塊就是調(diào)用索引模塊,來完成搜索的核心過程~
1.分詞。針對用戶輸入的詞/句進行分詞
2.觸發(fā),拿著每個分詞結(jié)果,去倒排索引中查,找到具有相關(guān)性的文檔,
3.排序,針對上邊觸發(fā)出來的結(jié)果,進行排序(按照相關(guān)性,降序排序)
4.包裝結(jié)果。根據(jù)排序后的結(jié)果,依次去查正排,獲取到每個文檔的詳細信息,包裝成一定的數(shù)據(jù)結(jié)構(gòu)返回出去。
目前遇到一個問題,返回結(jié)果中的描述該如何返回?
我們只是得到了正文。
說一下描述包含什么?描述是正文的一段摘要,這個摘要來源于正文,同時要包含查詢詞或者查詢詞的一部分。
思路:
目前我們可以獲取所有的查詢詞的分詞結(jié)果
遍歷分詞結(jié)果,看看哪個結(jié)果在正文中出現(xiàn)
for (Weight weight : allTermTesult) {
DocInfo docInfo = index.getDocInfo(weight.getDocId());
Result result = new Result();
result.setTitle(docInfo.getTitle());
result.setUrl(docInfo.getUrl());
result.setDesc(GenDesc(docInfo.getContent(), terms));
results.add(result);
}
這里抽象出了一個方法GenDesc,根據(jù)正文和分詞結(jié)果來生成描述。
針對當前文檔來說,不一定包含所有的分詞結(jié)果
就針對這個被包含的分詞結(jié)果,去正文中查找,找到對應(yīng)的位置
就以這個詞的位置為中心,往前截取60個字符,作為描述的開始。
然后再從這個描述開始,截取160個字符,作為整個描述。
如果用戶查詢的時候輸入了Hello,我們的分詞結(jié)果都是小寫的,所以用戶不管輸入什么,都會被轉(zhuǎn)換成小寫字母,那么正文中的大寫字母也得轉(zhuǎn)換成小寫字母,不然就查不到。我們這里統(tǒng)一弄成小寫的。
private String GenDesc(String content, List<Term> terms) {
int firstPos = -1;
for (Term term : terms) {
String word = term.getName();
firstPos = content.toLowerCase().indexOf(word);
if (firstPos >= 0) {
//找到了位置
break;
}
}
}
還有一個問題,如果用戶輸入的查詢詞是List,而正文中出現(xiàn)的是ArrayList,在生成描述的時候,此處拿著這個List去正文中indexOf,此時是否會把ArrayList當作結(jié)果呢?
這就會導致生成的描述,里面就是帶ArrayList的,而不是帶List的了
我要查List結(jié)果描述里邊是ArrayList 和 List,這樣設(shè)計就不科學。
類似這樣的情況,在查倒排的時候,是否會存在呢?
倒排索引中的key都是分詞結(jié)果,ArrayList不會被分成Array + List ,仍然把ArrayList視為是一個單詞。List和ArrayList并不能匹配,使用List這個詞不能查出包含ArrayList的結(jié)果。這是科學的
因此我們希望,在生成描述的過程中能夠找到整個詞都匹配的結(jié)果,才算是找到了。而不是只找到詞的一部分。
如何讓word獨立成詞?而不是只作為正文詞的一部分。
我的想法是直接在word的前后加個空格,讓這個詞獨立出來。
?僅僅是這么一處代碼,卻需要思考這么多的東西。
驗證一下docsearcher類
?
結(jié)果符合我們的預(yù)期要求。?
18.實現(xiàn)web模塊
提供一個web接口,最終以網(wǎng)頁的形式,把我們的程序呈現(xiàn)給用戶。
前端(html+css+js)后端(servlet/spring)先用servlet實現(xiàn),在升級成spring
現(xiàn)在約定一下前后端的通信接口。
請求:
GET/searcher?query=[查詢詞] HTTP/1.1
響應(yīng):(json格式的數(shù)據(jù))
Http/1.1 200 ok
[
? ? ? ? {
? ? ? ? ? ? ? ? title:"這是標題1",
? ? ? ? ? ? ? ? url:"這是url1",
? ? ? ? ? ? ? ? desc:"這是描述1",
????????},
? ? ? ? {?? ??
????????????????title:"這是標題2",
? ? ? ? ? ? ? ? url:"這是url2",
? ? ? ? ? ? ? ? desc:"這是描述2",
? ? ? ? },
]
具體實現(xiàn):
package api;
import com.fasterxml.jackson.databind.ObjectMapper;
import searcher.DocSearcher;
import searcher.Result;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class DocSearcherServlet extends HttpServlet {
//全局唯一,用static修飾一下
private static DocSearcher docSearcher = new DocSearcher();
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//拿到用戶提交的查詢詞
String query = req.getParameter("query");
if (query == null || query.equals("")) {
String msg = "您的參數(shù)非法,沒有獲取到query的值!";
System.out.println(msg);
resp.sendError(404, msg);
return;
}
System.out.println("query=" + query);
List<Result> results = docSearcher.search(query);
//把結(jié)果進行打包
resp.setContentType("application/json;charset=utf-8");
objectMapper.writeValue(resp.getWriter(),results);
}
}
驗證一下效果:
后端這邊我們已經(jīng)做好了,我們接下來做一個好看點的前端頁面。
19.前端頁面的制作?
我們利用ajax進行前后端交互,
當用戶點擊搜索按鈕的時候,瀏覽器就會獲取到搜索框的內(nèi)容,基于ajax構(gòu)造HTTP請求,然后發(fā)給搜索服務(wù)器。瀏覽器獲取到搜索結(jié)果之后,在根據(jù)結(jié)果的json數(shù)據(jù),把頁面生成出來。
目前遇到一個巨坑,嗚嗚嗚~調(diào)試了2個小時終于讓我給逮住了。
ajax構(gòu)造請求的路徑?jīng)]有理清楚,造成時間上的浪費。
我把index.?html放在了html這個文件夾中,所以ajax構(gòu)造請求url的路徑得修改一下,
?如果我放在webapp下,就不用加../了
我是看了這個博客解決的:
ajax關(guān)于url路徑問題
目前又遇到一個問題:
內(nèi)容太多,超出了一個屏幕,該怎么解決?
可以通過修改css代碼,
你可以定位到代碼問題出現(xiàn)在哪里嗎?
最大的div類名是container.,就讓在這個div內(nèi)部滾動。
又遇到一個問題:
第一次搜索和第二次搜索結(jié)果會累加到一起
?
出現(xiàn)這個問題的原因是啥?
?每次點擊按鈕,都是把結(jié)果往.result里面進行追加,沒有清理過內(nèi)容
更科學的方法,應(yīng)該是在每次搜索之前,都把之前的舊的結(jié)果給清空掉。
現(xiàn)在我們在實現(xiàn)一下標紅邏輯。
1.修改后端代碼,生成搜索結(jié)果的時候,就需要把其中包含查詢詞的部分,給加上一個標記。
例如,給這個部分套上一層<i>標簽~
2.然后在前端這里針對<i>標簽設(shè)置樣式,然后瀏覽器就可以根據(jù)<i>標簽的樣式來進行顯示了,
(比如給<i>標簽的字體顏色設(shè)置為紅色即可)
具體實現(xiàn):
private String GenDesc(String content, List<Term> terms) {
int firstPos = -1;
for (Term term : terms) {
String word = term.getName();
firstPos = content.toLowerCase().indexOf(" " + word + " ");
if (firstPos >= 0) {
//找到了位置
break;
}
}
if (firstPos == -1) {
//所有的分詞結(jié)果都不在正文中存在 我們返回空的描述就不合適,所以我們返回正文的前160個字符
return content.substring(0, 160) + "...";
}
//
String desc = "";
int descBeg = firstPos < 60 ? 0 : firstPos - 60;
if (descBeg + 160 > content.length()) {
desc = content.substring(descBeg);//從起始位置截到末尾
} else {
desc = content.substring(descBeg, descBeg + 160) + "...";
}
for (Term term : terms) {
String word = term.getName();
//此處進行全字匹配,也就是當查詢詞為list的時候不能把Arraylist中的List給單獨標紅。(?i) 不區(qū)分大小寫替換
desc = desc.replaceAll("(?i) " + word + " ", "<i> " + word + " </i>");
}
return desc;
}
現(xiàn)在測試一下更加復(fù)雜的情況:
?倆次的搜索結(jié)果一樣,
服務(wù)器500,原因是代碼執(zhí)行過程中拋出異常了。
就需要找到?
?
?數(shù)組越界異常:
排查發(fā)現(xiàn)如果正文沒有160個字符,還要截取160個字符必然會導致錯誤。
改正:
又出現(xiàn)一個問題:
下邊的itemDiv都沒有標紅的字?
?我想是因為分詞把空格也看成一個詞了,導致代碼會拿空格去查找倒排索引,我們想個辦法把空格去掉。
這里用停用詞表,修改一下代碼。
思路:
讓搜索程序加載這個停用詞表,到內(nèi)存中,
使用一個HashSet把這些詞都存起來,
在針對分詞結(jié)果,在停用詞中進行篩選,
如果某個結(jié)果在詞表中存在,就直接去掉。
驗證結(jié)果:
符合預(yù)期要求。?
又發(fā)現(xiàn)一個bug
還是有不標紅的情況,出現(xiàn)這種情況的原因
我點進去一個不標紅的文檔,搜索關(guān)鍵字,結(jié)果發(fā)現(xiàn)該文檔確實存在這個關(guān)鍵字,哪里出現(xiàn)了問題?
前邊后邊都有空格才去匹配這個firstPos,那要是沒有空格就匹配不到了,關(guān)鍵是全文就只有這一個字?,所以firstPos直接返回的是-1,我們要解決這里的問題,就得使用正則表達式。
成功解決bug。
目前又發(fā)現(xiàn)一個問題:
1576 + 1350? = 2926 = 2926
同一個文檔即出現(xiàn)了array 又出現(xiàn)了list,也就是說,ArrayList出現(xiàn)了倆次,重復(fù)了。
前面算權(quán)重,針對分詞結(jié)果依次進行促發(fā),
array => 觸發(fā)一組docId
list => 觸發(fā)一組docId?
同時包含多個分詞結(jié)果,意味著這個文檔的相關(guān)性更高,
像這種情況,就應(yīng)該提高這個文檔的權(quán)重,既然相關(guān)性更高,提高權(quán)重之后就能排的更靠前。
把權(quán)重進行相加。把多個分詞結(jié)果觸發(fā)的文檔,按照docId進行去重,同時進行權(quán)重合并。
去重思路:?
合并倆個有序列表,合并倆個數(shù)組。
先把分詞結(jié)果進行一個排序處理(按照docId升序排序)在進行合并,合并的時候就可以根據(jù)docId值相同的情況,做權(quán)重相加。
可是用戶輸入的僅僅就是倆個詞嗎?
所以應(yīng)該是N個數(shù)組合并。
對比多個數(shù)組值的大小關(guān)系,找到小的,插入到結(jié)果中。
誰對應(yīng)的值越小,就把這個值取出來,插入到結(jié)果數(shù)組中 ,同時針對這個下標進行++。
我們這里使用堆/優(yōu)先級隊列?
end.擴散一下思維,那么一些小型網(wǎng)站的搜索引擎我們是否就會做了呢?
我們是否還能通過es來做一下?文章來源:http://www.zghlxwxcb.cn/news/detail-410636.html
爬蟲能否實現(xiàn)一下?文章來源地址http://www.zghlxwxcb.cn/news/detail-410636.html
到了這里,關(guān)于站內(nèi)搜索引擎的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!