基於Lucene的圖書全文搜索引擎
基於Lucene的圖書全文搜索引擎
Baofeng Zhang@zju
轉載請注明出處:https://blog.csdn.net/zbf8441372
背景介紹
這是一個關於圖書的多側麵,多粒度的搜索引擎。仿照“讀秀”(https://www.duxiu.com/)那樣的搜索方式和搜索結果呈現方式,可以根據書的一些基本屬性進行關鍵字搜索,展現的時候還附加進行了搜索結果的統計,也可以看到相關的全文信息。多側麵,多粒度的搜索和展示都是為了給用戶更好的體驗,方便用戶的各種搜索需求和閱讀需求。
這個搜索引擎是我前段時間開發的,前後台都是由我一個人開發完成的,沒有采用框架或者任何現成代碼。現在也隻是個可以看的demo,很多地方還需要很大的改善。下麵我會介紹下采用的技術,數據的說明,功能說明和設計,開發過程中遇到的難點問題以及他的不足和改進。
主要技術
前台:采用的是html + css + javascript(包括jquery庫)
後台:主要采用的是JSP和Struts2(Spring還沒有使用),還有Ajax(jquery來實現)
搜索:采用lucene包完成索引的建立和查詢,暫時用的是很老的2.0版本
切詞:切詞工具是je切詞器,可能比較老,切詞效果一般
數據說明
元數據
數據是1萬多本的工程類圖書(中途分類法中N字母及以後的那些類的圖書)。每本書是一個文件夾,裏麵的txt目錄內的所有.ok文件是OCR掃描得到的每一頁的書的全文內容。
meta目錄下的dc.xml文件裏是書的基本信息和屬性(書名,作者,id,出版時間等),Catalog.xml文件裏是書的每一章及每一節的標題和頁碼信息。
索引
現在索引的大小是1G多。meta目錄裏的兩個xml我都解析出來存在索引裏,並對需要的部分進行切詞(如書名要切詞,出版時間就不切詞,直接存在索引裏)。txt目錄下的是書的全文內容,我對所有的.ok進行讀取並切詞建立索引,但是文本本身不存索引,為了避免索引過大。在“設計實現”部分我會再具體說明索引的設計。
功能說明
現在的搜索都是精確匹配的搜索,即切詞器切的是什麼詞,我就隻能搜到完全一樣的詞,沒有模煳匹配之類的搜索。而且隻限於中文搜索,英文沒有進行切詞,不能進行搜索。
書名搜索
輸入關鍵字,得到書名中帶有該關鍵字的圖書,在中間顯示,現在的搜索結果是全部展示的,也可以設置隻顯示前10條。其實對搜索來說是一樣的,總是返回所有符合的書的,所以這樣對速度沒有拖累。
作者搜索
輸入作者全名,得到該作者的書。這個搜索不太好用,沒有實現好,因為xml裏的元數據的作者就很雜亂。
內容搜索
這是基於全文的檢索。和之前不同的地方在於。在本書目錄的部分出現了一些文字,這些文字是本書含有這個關鍵字的頁裏的第一個頁裏的內容。我好像截取了從關鍵字開始的後200個字,所以有的書沒有顯示出來,是因為之後可能沒有200個字了,這個地方我處理的不是很好。
左欄多了一個“相關數目列表”,點擊之後可以看到這本書裏含有這個關鍵字的每一頁的全文內容。並且也是有加亮的,也隻是顯示了前幾百個字。
目錄搜索
能檢索到每本書中的所有章節目錄裏含有該搜索關鍵字的目錄全文信息,顯示在每本書下麵。
左欄統計
左側是對分類,出版時間,作者,出版社的統計。隻顯示top 5。
右欄結果
右側是百度裏前五條搜索結果。(直接爬了過來)
其他
當“出版時間”的統計結果多於五條時, 可以通過“展開”,“收起”動態選擇查看全部或者前五條。
對於返回的記錄總數和搜索時間,可以在搜索框下麵看到。
其他還有分類瀏覽的功能,
是按照中圖分類法來做的。隻是這部分實現的不是很好,而且涉及到數據庫,就不展示了,這裏貼張圖。會列出一級分類,二級分類,三級分類以及某一分類下的符合的圖書。
設計實現
我盡量把設計實現介紹地簡單,到位,因為過多的表述可能沒有必要。
索引建立
索引的建立是邊解析文件係統裏的相應文件,邊建立的。代碼主要在index.writer的package內。Lucene建立索引方便,使用IndexWriter類並addCocument,會自動幫我建立倒排索引,並且按照他自己的方式建立一些特定文件存在我指定的索引文件夾下。
/*中文切詞器可以選擇別的*/ IndexWriter writer = new IndexWriter(this.indexDir, new MMAnalyzer(), true);
下麵介紹索引的設計。Lucene裏有documemt類,document類裏可以add很多field。可以把Document理解為數據庫的table,field是每個table的一行行屬性。在搜索的時候,是按照某個field的名字去搜索他的value,並且返回所有含有這樣的field的document的。所以我在建索引的時候,一本書當作一個document,書的作者,名字,等等信息都是這個document裏的field,我可以在field裏具體設置name, value, store_or_not,index_or_not。具體如下:

這是對“書”這個document建立的field。如對“title”,即書名這個field,我進行了切詞,並且存儲了內容,但是對“date”,即出版日期這個field,我沒有進行切詞,原因是用戶不需要對時間進行搜索,隻需要把這個屬性完整保留下來,存進索引就可以。而且這些屬性的內容都不多,全部存進索引有利於我在搜索的時候,直接通過document得到所有field的全文內容,被前台獲取並展現。
對於目錄,我建立了另外的document。每一本書的每一章的每一節,我都建立一個document,document裏包含了書的id和page的id,以及目錄catalog的全文內容,隻有目錄全文是要切詞的,其他的隻要直接存內容。目的是通過目錄內容得到相應的書,然後再次搜索bookid得到書的其他屬性。因為我不知道一本書有多少目錄,就不知道我該設置多少的field,所以采取這樣的方式,可以當作bookID這個field相當於是兩個document的外鍵。
對於全文,也是和目錄一樣,單獨給每一本書的每一頁書建立一個document,理由也是因為我不知道每本書有多少頁,所以不能一起建立在書的document內,需要用bookid來關聯起來。和目錄有區別的是,由於全文內容比較多,所以我隻選擇建立索引,不存儲全文內容。這樣的後果就是,我可以搜索得到哪本書,哪一頁有這個詞,但是我不能從索引文件裏得到這頁書裏的所有文本內容,所以我記錄下okfilename,去我的文件係統裏讀取這個.ok文件,而不是把全文存在索引裏,把索引變得很大。
搜索
搜索主要通過Term類和Query類組成查詢請求,用IndexSearcher類去搜索,最後得到Hits類,裏麵包含了所有含有關鍵字的document,讓我可以一一遍曆和取值(從索引中取)。
IndexSearcher is = new IndexSearcher(this.path); Term t = new Term(searchType, searchKey); Query q = new TermQuery(t); Hits hits = is.search(q); if (hits.length() == 0) { System.out.println("nobook~~~~~~~~~~~~~~"); } else { for (int i = 0; i < hits.length(); i ++) { Documentdoc = hits.doc(i); //… } }
針對不同類型的搜索,我在index.search package下的getBook.java內,設計了searchWithoutContent(String,String),searchWithContent(String)和searchCatalog (String)三個類,分別對應非全文內容的搜索(如作者,標題,主題詞的搜索),全文內容的搜索和目錄的搜索。這三種搜索其實分別對應了我之前的索引的建立裏三種document(書,page內容和目錄內容)。區別在於,像searchWithoutContent和searchCatalog兩個函數其實都需要通過得到的document裏的bookid再用searchWithoutContent去搜索得到這本book的別的信息,進行了二次搜索(保證這點的可實現性在於我上麵的索引設計,這麼設計的原因是我不知道一本書有多少目錄和書頁,所以不能包含在一個document裏解決所有問題,需要用bookid充當關聯)。
這樣對於Struts的action,隻要new getBook這個類,調用相應的搜索函數就可以滿足前台的搜索請求。
結果展現
由於在搜索結果的中間欄顯示的是書的基本信息,所以以上的三個關鍵搜索函數返回都是ArrayList<BookAttributes>。BookAttributes是每本書的實例類(在book.entity的package內),裏麵包含了書的基本信息和getter,setter函數。返回所有滿足條件(包含搜索關鍵字的field的document)書的list,讓前台獲取searchAciton的結果集裏的
publicList<BookAttributes>booklist =new ArrayList<BookAttributes>();
通過struts的<s:iterator>遍曆展現在頁麵上。
關鍵代碼如下:
<% int i = 0; pageContext.setAttribute("ba",request.getAttribute("booklist")); List<BookAttributes> ba = new ArrayList<BookAttributes>(); %> <s:iterator value="booklist" > <table cellSpacing=0 cellPadding=5> <tbody><tr> <td vAlign="top"> <div > <img height=127 alt=封麵 src="img/zkz.jpg" width=90 /></div> <div ></div> <div ></div> </td> <td vAlign=top width="88%"> <table border="0"cellspacing="0" cellpadding="0"width="100%"> <tr width="100%"> <td height="20"> <a href="#"target="_blank" >${BookAttributes.title}</a> <div > </div> </td> </tr> </table> 作者:${BookAttributes.creator} <br /> 分類:${BookAttributes.CRC} <br /> ISBN:${BookAttributes.ISBN} <br /> 出版日期:${BookAttributes.date} <br /> 本書目錄:<br /> <font color="blue">${BookAttributes.content}</font><br /> <br>主題詞:${BookAttributes.subject} <br> <b>分類</b>: <% // 通過循環變量i和request,從action讀取list,用java處理成正確的url。struts標簽沒辦法做雙重循環,也取不了list內的類的某個變量 //… %> <span id=m_fl><a href=<%=url1%>>${BookAttributes.firstCat}</a>-><a href=<%=url2%>>${BookAttributes.secondCat}</a>-> <a href=<%=url3%>>${BookAttributes.thirdCat}</a></span><br> </td> </tr> <tr> <td colspan="3"align="right"> <a title=收藏 href="javascript:subAdd_new('0');"><IMG border="0"alt=收藏 src="/images/shoucang.jpg" /></a> </td> </tr> </tbody> </table>
左側統計
左側是對搜索得到的書的分類,作者,出版日期等的統計,采用Ajax的異步調用方式,給addupAction來完成。實質上是對搜索關鍵字再進行一次搜索,而沒有通過request傳參的方式把searchAction裏的booklist直接傳給addupAction,因為lucene做一次搜索還是很快的。我是通過
$(document).ready(function(){ $("#booksearch").click(show()); }
的方式觸發Ajax的統計Action的。$("#booksearch")對應的就是頁麵上“中文搜索”這個submit的button。而show()這個js函數中,通過Jquery包裝過了的Ajax
$.getJSON(url, function(data,stat){}, “json”);
獲取前台表單裏的參數值傳給action,
sk = $("#searchkey").val(); type = $("#js2").val(); url= "ShowAddup?sk=" + sk + "&type=" + type;
並在回調函數中通過js變量得到action裏的參數通過DOM模型改變特定id標簽的內容在頁麵上顯示。這部分代碼都寫在了search.jsp內。
統計結果是通過tool package內的處理類處理之後返回一個字符串的list在頁麵上遍曆輸出實現的。
而對於當統計結果多於五條時的“展開”“收起”功能是通過jquery來實現的,展開的時候將全部list都遍曆,收起的時候隻顯示前五條遍曆結果。
// 顯示更多 $("#more").click(function(){ for (var i = yearlist.length-listnum-1; i >= 0; i --) { $("#yearadd").append("<li><a href='#'>" + yearlist[i] + "</a></li>" ); } $("#yearadd").show(); }); // 再隱藏 $("#hide").click(function(){ $("#yearadd").hide("slow"); });
右側結果
右側的結果是將搜索框內的輸入放到百度內搜索,抓取了前五條百度的原搜索內容進行呈現的。和左側的實現方式是一樣,也是用了Ajax異步的方式,對應的是baiduAction。兩個返回類型是json的action在struts.xml裏配置的時候如下:
<package name="test"extends="json-default"> <action name="ShowAddup" > <result type="json"></result> </action> <action name="ShowBaidu" > <result type="json"></result> </action> </package>
百度搜索的抓取在url.fetch的package下的GetBaidu.java類內,用URL類構建url,獲取百度搜索結果的html內容,對in.readLine()的內容進行判斷,得到前五條符合要求的搜索結果。
this.source= "https://www.baidu.com/s?tn=monline_5_dg&f=8&rsv_bp=1&rsv_spt=3&wd=" + this.strKey; URL url = new URL(this.source); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); // …
全文內容
在搜索“內容”的時候,在得到的結果中,可以點擊左側的書的標題,展示書中含有搜索關鍵字的每一頁的全文內容。這也是用ajax異步實現,對應的是allpagesAction。每次
先通過document.getElementById("pagecontent").innerHTML=" ";清空內容,再添加內容$("#pagecontent").append(…);。
具體的搜索類是index.search包下的BookContentSearch類。是去文件係統中讀取特定.ok文件的內容。
if (doc.getField("bookid").stringValue().equals(bookid)) { okfilename = doc.getField("okfilename").stringValue(); String tmp = loadFileToString(new File("E:\\06_1000" + "\\" + bookid + "\\txt\\txt\\" + okfilename)); contentsmap.put(okfilename, tmp); }
加亮處理
通過yit(document.body, k);這個加亮js函數,在需要的地方調用這個函數,k是傳入的需要加亮的關鍵字。我在異步返回的各個回調函數中都會使用這個加亮函數。下麵是第一次頁麵展現的時候也使用了加亮。
window.onload = function(){ var k=document.getElementById("searchkey").value; yit(document.body, k); }
技術難點
這裏簡單說明下我在開發過程中遇到的和解決的主要技術關鍵點。
主要的難點還是在struts的靈活運用上,因為這也是我第一次使用struts,邊學邊用。我自己也在博客的一篇文章裏(https://blog.csdn.net/zbf8441372/article/details/7602329)總結了開發經驗。主要是Java對象, Js對象, Action內的對象在同一個jsp頁麵內到底能怎樣互通的問題。
Ø struts的標簽,和jsp頁麵內js代碼,java代碼的結合和靈活使用
我現在理解,struts的標簽的實現,實質上應該也是servlet中request的setter函數(action裏配置)讀取到jsp上來的,但是struts標簽無法賦給js變量,無法賦給<% %>內的java變量,隻能這樣賦:
<input type=hidden value="<s:text name='sk'></s:text>" ></input> <input type=hidden value="<s:text name='type'></s:text>" ></input>
他永遠隻是個setter方法,當你的值是一個對象,比如一個List時,你這樣賦的結果,就是一個list.toString()的值,對象的屬性不能獲取,對象在標簽下就死掉了。所以當我需要在<s:iterator>遞歸下,同時遍曆兩個action的list時,我通過
pageContext.setAttribute("ba",request.getAttribute("booklist"));
將某個其中一個list額外先取到pageContext內,然後同時在外部初始一個計數器,在<s:iterator>內我的計數器也是一次次增加,可以實現兩個list的同時遍曆。
Ø jquery的一些使用
Jquery的ajax方法有四個:
$.get( url [, data] [, callback] ) $.post(url,[data],[callback],[type]) $.getJSON(url,[data],[callback]) $.ajax(options)
使用ajax,通過json形式包裝的時候,struts的action在配置文件中的配置需要改變,包括兩個地方,extends="json-default",<resulttype="json"></result>
靈活使用jquery,對DOM模型進行操作,對標簽內容進行添改刪藏,並可以設置簡單的動畫。這些也都是邊學邊用。
Ø 索引的設計
建立索引和搜索索引並不困難,lucene都進行了很好的封裝。但是索引的設計需要考慮。如前麵所說的,我的設計是基於全文,目錄和普通搜索三塊內容,設計了三種ducoment。在搜索的時候會出現三種document之間的互相關聯,二次搜索,form表單的同步執行和異步返回,讓每種類型的搜索分別對應一個action,不同的action以同步或者異步的形式返回在前台頁麵上,避免在呈現中間欄結果的時候,由於統計結果和百度搜索而延長了搜索時間。
缺點不足
代碼
後台代碼還是比較亂的,package的層次也分得不清晰,沒有進行過重構和優化。
搜索性能
索引由於超過了一個G,搜索結果變慢很多。無論從lucene版本,代碼重構方麵還是索引設計方麵都需要改進以減少搜索時間,提高用戶體驗。
界麵
搜索結果的展現還需要改進,有些地方不太舒服,用戶體驗可能會不好。界麵也沒有進行過美工,最多達到了層次清晰,結構簡潔。左右側異步請求,明顯延遲的時候沒有一個load之類的提醒用戶等待。
總結和未來工作
代碼
提高lucene版本(新的版本性能方麵會有不同),重構代碼,合理使用ajax和struts,提高速度。
功能
分頁功能還沒有完成。有些統計數據的鏈接還沒有完成。分類瀏覽還需要完善。
索引
1. 將索引分開建立,減小每份索引大小。圖書原信息存一份索引,圖書全文的索引存在另一份,圖書目錄的索引也存在另一份。這樣將原本的一整個索引分需求和搜索類型的不同分開建立,減小索引大小,提高搜索速度。
2. 分布式索引。以後打算建立分布式索引,一個query轉發多台服務器,多台機器搜索結果進行合並。還可以結合Hadoop的HDFS文件係統,HBase或者別的NoSQL把圖書的全文txt內容合理存儲,提高數據的讀取速度。
3. IndexSearcher池。雖然lucene的IndexSearcher,IndexWriter,IndexReader這些類都是線程安全的,但是open,close的過程中還是造成了比較大的開銷。所以除了數據庫需要DBpool之外,這些lucene的關鍵類也需要池來維護,減少不必要開銷。
4. 其他很多關於搜索引擎的性能優化和代碼,運行環境參數調優。我打算將這個搜索引擎不斷完善和深入,其實現在處於打通了一些技術問題,真正的性能還沒有開始著手,隻是暢想以後要做的是一個分布式的搜索。
最後更新:2017-04-02 17:09:29