前言
大家平時(shí)肯定都有用過全文檢索工具,最常用的百度谷歌就是其中的典型萨惑。如果自己能夠做一個(gè)那是不是想想就逼格滿滿呢更舞。Apache就為我們提供了這樣一個(gè)框架,以下就是在實(shí)際開發(fā)中加入Lucene的一個(gè)小Demo拙吉。
獲取Maven依賴
首先看一下實(shí)際運(yùn)行的效果圖:
這個(gè)項(xiàng)目是基于之前使用IDEA搭建的SSM的基礎(chǔ)上進(jìn)行增加的潮孽,建議小白先看下一我。上一篇博客筷黔,以及共享在Github上的源碼往史。
以下是Lucene所需要的依賴:
<!--加入lucene-->
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lucene.version}</version>
</dependency>
<!--lucene中文分詞-->
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-smartcn -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>${lucene.version}</version>
</dependency>
<!--lucene高亮-->
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-highlighter -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
具體的用途我都寫有注釋。
在IDEA中修改了Pom.xml文件之后只需要點(diǎn)擊如圖所示的按鈕即可重新獲取依賴:
編寫Lucene工具類
這個(gè)工具類中的具體代碼我就不單獨(dú)提出來說了佛舱,每個(gè)關(guān)鍵的地方我都寫有注釋椎例,不清楚的再討論。
package com.crossoverJie.lucene;
import com.crossoverJie.pojo.User;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import com.crossoverJie.util.*;
/**
* 博客索引類
* @author Administrator
*
*/
public class LuceneIndex {
private Directory dir=null;
/**
* 獲取IndexWriter實(shí)例
* @return
* @throws Exception
*/
private IndexWriter getWriter()throws Exception{
/**
* 生成的索引我放在了C盤请祖,可以根據(jù)自己的需要放在具體位置
*/
dir= FSDirectory.open(Paths.get("C://lucene"));
SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer();
IndexWriterConfig iwc=new IndexWriterConfig(analyzer);
IndexWriter writer=new IndexWriter(dir, iwc);
return writer;
}
/**
* 添加博客索引
* @param user
*/
public void addIndex(User user)throws Exception{
IndexWriter writer=getWriter();
Document doc=new Document();
doc.add(new StringField("id",String.valueOf(user.getUserId()), Field.Store.YES));
/**
* yes是會(huì)將數(shù)據(jù)存進(jìn)索引订歪,如果查詢結(jié)果中需要將記錄顯示出來就要存進(jìn)去,如果查詢結(jié)果
* 只是顯示標(biāo)題之類的就可以不用存肆捕,而且內(nèi)容過長(zhǎng)不建議存進(jìn)去
* 使用TextField類是可以用于查詢的刷晋。
*/
doc.add(new TextField("username", user.getUsername(), Field.Store.YES));
doc.add(new TextField("description",user.getDescription(), Field.Store.YES));
writer.addDocument(doc);
writer.close();
}
/**
* 更新博客索引
* @param user
* @throws Exception
*/
public void updateIndex(User user)throws Exception{
IndexWriter writer=getWriter();
Document doc=new Document();
doc.add(new StringField("id",String.valueOf(user.getUserId()), Field.Store.YES));
doc.add(new TextField("username", user.getUsername(), Field.Store.YES));
doc.add(new TextField("description",user.getDescription(), Field.Store.YES));
writer.updateDocument(new Term("id", String.valueOf(user.getUserId())), doc);
writer.close();
}
/**
* 刪除指定博客的索引
* @param userId
* @throws Exception
*/
public void deleteIndex(String userId)throws Exception{
IndexWriter writer=getWriter();
writer.deleteDocuments(new Term("id", userId));
writer.forceMergeDeletes(); // 強(qiáng)制刪除
writer.commit();
writer.close();
}
/**
* 查詢用戶
* @param q 查詢關(guān)鍵字
* @return
* @throws Exception
*/
public List<User> searchBlog(String q)throws Exception{
/**
* 注意的是查詢索引的位置得是存放索引的位置,不然會(huì)找不到。
*/
dir= FSDirectory.open(Paths.get("C://lucene"));
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher is=new IndexSearcher(reader);
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer();
/**
* username和description就是我們需要進(jìn)行查找的兩個(gè)字段
* 同時(shí)在存放索引的時(shí)候要使用TextField類進(jìn)行存放眼虱。
*/
QueryParser parser=new QueryParser("username",analyzer);
Query query=parser.parse(q);
QueryParser parser2=new QueryParser("description",analyzer);
Query query2=parser2.parse(q);
booleanQuery.add(query, BooleanClause.Occur.SHOULD);
booleanQuery.add(query2, BooleanClause.Occur.SHOULD);
TopDocs hits=is.search(booleanQuery.build(), 100);
QueryScorer scorer=new QueryScorer(query);
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
/**
* 這里可以根據(jù)自己的需要來自定義查找關(guān)鍵字高亮?xí)r的樣式喻奥。
*/
SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color='red'>","</font></b>");
Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer);
highlighter.setTextFragmenter(fragmenter);
List<User> userList=new LinkedList<User>();
for(ScoreDoc scoreDoc:hits.scoreDocs){
Document doc=is.doc(scoreDoc.doc);
User user=new User();
user.setUserId(Integer.parseInt(doc.get(("id"))));
user.setDescription(doc.get(("description")));
String username=doc.get("username");
String description=doc.get("description");
if(username!=null){
TokenStream tokenStream = analyzer.tokenStream("username", new StringReader(username));
String husername=highlighter.getBestFragment(tokenStream, username);
if(StringUtil.isEmpty(husername)){
user.setUsername(username);
}else{
user.setUsername(husername);
}
}
if(description!=null){
TokenStream tokenStream = analyzer.tokenStream("description", new StringReader(description));
String hContent=highlighter.getBestFragment(tokenStream, description);
if(StringUtil.isEmpty(hContent)){
if(description.length()<=200){
user.setDescription(description);
}else{
user.setDescription(description.substring(0, 200));
}
}else{
user.setDescription(hContent);
}
}
userList.add(user);
}
return userList;
}
}
查詢Controller的編寫
接下來是查詢Controller:
@RequestMapping("/q")
public String search(@RequestParam(value = "q", required = false,defaultValue = "") String q,
@RequestParam(value = "page", required = false, defaultValue = "1") String page,
Model model,
HttpServletRequest request) throws Exception {
LuceneIndex luceneIndex = new LuceneIndex() ;
List<User> userList = luceneIndex.searchBlog(q);
/**
* 關(guān)于查詢之后的分頁我采用的是每次分頁發(fā)起的請(qǐng)求都是將所有的數(shù)據(jù)查詢出來,
* 具體是第幾頁再截取對(duì)應(yīng)頁數(shù)的數(shù)據(jù)捏悬,典型的拿空間換時(shí)間的做法撞蚕,如果各位有什么
* 高招歡迎受教。
*/
Integer toIndex = userList.size() >= Integer.parseInt(page) * 5 ? Integer.parseInt(page) * 5 : userList.size();
List<User> newList = userList.subList((Integer.parseInt(page) - 1) * 5, toIndex);
model.addAttribute("userList",newList) ;
String s = this.genUpAndDownPageCode(Integer.parseInt(page), userList.size(), q, 5, request.getServletContext().
getContextPath());
model.addAttribute("pageHtml",s) ;
model.addAttribute("q",q) ;
model.addAttribute("resultTotal",userList.size()) ;
model.addAttribute("pageTitle","搜索關(guān)鍵字'" + q + "'結(jié)果頁面") ;
return "queryResult";
}
其中有用到一個(gè)genUpAndDownPageCode()
方法來生成分頁的Html代碼过牙,如下:
/**
* 查詢之后的分頁
* @param page
* @param totalNum
* @param q
* @param pageSize
* @param projectContext
* @return
*/
private String genUpAndDownPageCode(int page,Integer totalNum,String q,Integer pageSize,String projectContext){
long totalPage=totalNum%pageSize==0?totalNum/pageSize:totalNum/pageSize+1;
StringBuffer pageCode=new StringBuffer();
if(totalPage==0){
return "";
}else{
pageCode.append("<nav>");
pageCode.append("<ul class='pager' >");
if(page>1){
pageCode.append("<li><a href='"+projectContext+"/q?page="+(page-1)+"&q="+q+"'>上一頁</a></li>");
}else{
pageCode.append("<li class='disabled'><a href='#'>上一頁</a></li>");
}
if(page<totalPage){
pageCode.append("<li><a href='"+projectContext+"/q?page="+(page+1)+"&q="+q+"'>下一頁</a></li>");
}else{
pageCode.append("<li class='disabled'><a href='#'>下一頁</a></li>");
}
pageCode.append("</ul>");
pageCode.append("</nav>");
}
return pageCode.toString();
}
代碼比較簡(jiǎn)單诈豌,就是根據(jù)的頁數(shù)、總頁數(shù)來生成分頁代碼抒和,對(duì)了我前端采用的是現(xiàn)在流行的Bootstrap矫渔,這個(gè)有不會(huì)的可以去他官網(wǎng)看看,比較簡(jiǎn)單易上手摧莽。接下來只需要編寫顯示界面就大功告成了庙洼。
顯示界面
我只貼關(guān)鍵代碼,具體的可以去Github上查看镊辕。
<c:choose>
<c:when test="${userList.size()==0 }">
<div align="center" style="padding-top: 20px"><font color="red">${q}</font>未查詢到結(jié)果油够,請(qǐng)換個(gè)關(guān)鍵字試試!</div>
</c:when>
<c:otherwise>
<div align="center" style="padding-top: 20px">
查詢<font color="red">${q}</font>關(guān)鍵字征懈,約${resultTotal}條記錄石咬!
</div>
<c:forEach var="u" items="${userList }" varStatus="status">
<div class="panel-heading ">
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-12">
<b>
<a href="<%=path %>/user/showUser/${u.userId}">${u.username}</a>
</b>
<br/>
${u.description}
</div>
</div>
</div>
<div class="col-md-4 col-md-offset-2">
<p class="text-muted text-right">
${u.password}
</p>
</div>
</div>
</div>
<div class="panel-footer">
<p class="text-right">
<span class="label label-default">
<span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
${u.password}
</span>
</p>
</div>
</c:forEach>
</c:otherwise>
</c:choose>
利用JSTL
標(biāo)簽即可將數(shù)據(jù)循環(huán)展示出來,關(guān)鍵字就不需要單獨(dú)做處理了卖哎,在后臺(tái)查詢的時(shí)候已經(jīng)做了修改了鬼悠。
總結(jié)
關(guān)于全文檢索的框架不止Lucene
還有solr
,具體誰好有什么區(qū)別我也不太清楚亏娜,準(zhǔn)備下來花點(diǎn)時(shí)間研究下焕窝。哦對(duì)了,最近又有點(diǎn)想做Android
開發(fā)了维贺,感覺做點(diǎn)東西能夠?qū)崒?shí)在在的摸得到逼格確實(shí)要高些(現(xiàn)在主要在做后端開發(fā))它掂,感興趣的朋友可以關(guān)注下。哦對(duì)了溯泣,直接運(yùn)行我代碼的朋友要下注意:
-
首先要將數(shù)據(jù)庫倒到自己的MySQL上
- 之后在首次運(yùn)行的時(shí)候需要點(diǎn)擊
LuceneIndex
類中的addIndex
方法傳入實(shí)體即可,再做更新栏尚、刪除操作的時(shí)候也同樣需要對(duì)索引做操作起愈。
個(gè)人博客地址:http://crossoverjie.top只恨。
GitHub地址:https://github.com/crossoverJie。