前言
大家平時肯定都有用過全文檢索工具,最常用的百度谷歌就是其中的典型。如果自己能夠做一個那是不是想想就逼格滿滿呢。Apache就為我們提供了這樣一個框架,以下就是在實際開發中加入Lucene的一個小Demo。
獲取Maven依賴
首先看一下實際運行的效果圖:
這個項目是基于之前使用IDEA搭建的SSM的基礎上進行增加的,建議小白先看下一我。上一篇博客,以及共享在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文件之后只需要點擊如圖所示的按鈕即可重新獲取依賴:
編寫Lucene工具類
這個工具類中的具體代碼我就不單獨提出來說了,每個關鍵的地方我都寫有注釋,不清楚的再討論。
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實例
* @return
* @throws Exception
*/
private IndexWriter getWriter()throws Exception{
/**
* 生成的索引我放在了C盤,可以根據自己的需要放在具體位置
*/
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是會將數據存進索引,如果查詢結果中需要將記錄顯示出來就要存進去,如果查詢結果
* 只是顯示標題之類的就可以不用存,而且內容過長不建議存進去
* 使用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(); // 強制刪除
writer.commit();
writer.close();
}
/**
* 查詢用戶
* @param q 查詢關鍵字
* @return
* @throws Exception
*/
public List<User> searchBlog(String q)throws Exception{
/**
* 注意的是查詢索引的位置得是存放索引的位置,不然會找不到。
*/
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就是我們需要進行查找的兩個字段
* 同時在存放索引的時候要使用TextField類進行存放。
*/
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);
/**
* 這里可以根據自己的需要來自定義查找關鍵字高亮時的樣式。
*/
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);
/**
* 關于查詢之后的分頁我采用的是每次分頁發起的請求都是將所有的數據查詢出來,
* 具體是第幾頁再截取對應頁數的數據,典型的拿空間換時間的做法,如果各位有什么
* 高招歡迎受教。
*/
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","搜索關鍵字'" + q + "'結果頁面") ;
return "queryResult";
}
其中有用到一個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();
}
代碼比較簡單,就是根據的頁數、總頁數來生成分頁代碼,對了我前端采用的是現在流行的Bootstrap,這個有不會的可以去他官網看看,比較簡單易上手。接下來只需要編寫顯示界面就大功告成了。
顯示界面
我只貼關鍵代碼,具體的可以去Github上查看。
<c:choose>
<c:when test="${userList.size()==0 }">
<div align="center" style="padding-top: 20px"><font color="red">${q}</font>未查詢到結果,請換個關鍵字試試!</div>
</c:when>
<c:otherwise>
<div align="center" style="padding-top: 20px">
查詢<font color="red">${q}</font>關鍵字,約${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
標簽即可將數據循環展示出來,關鍵字就不需要單獨做處理了,在后臺查詢的時候已經做了修改了。
總結
關于全文檢索的框架不止Lucene
還有solr
,具體誰好有什么區別我也不太清楚,準備下來花點時間研究下。哦對了,最近又有點想做Android
開發了,感覺做點東西能夠實實在在的摸得到逼格確實要高些(現在主要在做后端開發),感興趣的朋友可以關注下。哦對了,直接運行我代碼的朋友要下注意:
-
首先要將數據庫倒到自己的MySQL上
- 之后在首次運行的時候需要點擊
LuceneIndex
類中的addIndex
方法傳入實體即可,再做更新、刪除操作的時候也同樣需要對索引做操作。
個人博客地址:http://crossoverjie.top。
GitHub地址:https://github.com/crossoverJie。