Lucene5.5.4入门以及基于Lucene实现博客搜索功能
前言
一直以來個人博客的搜索功能很蹩腳,只是自己簡單用數據庫的like %keyword%來實現的,所以導致經常搜不到想要找的內容,而且高亮顯示、摘要截取等也不好實現,所以決定采用Lucene改寫博客的搜索功能。先來看一下最終效果:
本文demo地址:https://github.com/liuxianan/lucene-demo (包括本文需要用到的jar包可以從這里面下載)
效果演示地址:http://blog.liuxianan.com/search?kw=%E7%AB%AF%E5%8F%A3%20%E5%8D%A0%E7%94%A8
Lucene 介紹
Lucene是一個用Java開發的開源全文檢索引擎,官網是:http://lucene.apache.org/ ,Lucene不是一個完整的全文索引應用(與之對應的是solr),而是是一個用Java寫的全文索引引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引/檢索功能,更多介紹大家自行搜索。
版本選擇
目前最新版是6.5.1(截止到2017-05-04),本來想直接用最新版的,但是下載下來之后發現老是提示找不到某些類,可我直接找到對應的jar包下去看卻是有的,不過卻無法用jd-gui反編譯,提示一個什么錯誤,盲目的我竟然以為是因為版本太新,apache在放出最新jar包時自己沒測試,后來試了幾個老一點的6.x版本發現都是這個錯誤,5.x就不會,好吧,這時才想起來應該是jdk版本不對,Lucene6.x需要jdk1.8以上,只能怪我太out了,畢竟確實好久沒怎么寫過Java代碼了。
由于本地、線上都是使用的jdk1.7,不好為了一個Lucene就升級到1.8,所以決定改用5.5.4版本。
正式開始
下載
從網上下載的包一般比較大,有70多M(官網目前只能下載最新版的,5.x的估計要到其它地方下載),一般人只用下面這幾個就夠了:
也就是這幾個:
其中IKAnalyzer2012_FF.jar是一個國人寫的中文分詞工具,Lucene自帶的分詞對中文支持不好。注意,這個jar包網上比較亂,隨便從網上下載的話可能不兼容,因為跟具體的Lucene版本有關,初學者建議直接用我demo里面整理好的jar包:https://github.com/liuxianan/lucene-demo/tree/master/WebContent/WEB-INF/lib
建立索引
特別注意,Lucene不同版本的API變化比較大,如果你用的是其它版本,注意代碼可能要變。
其實代碼比較簡單,我們先來一個搜索文件的例子(下面的FileUtil可以自己簡單實現)。
public static final String INDEX_PATH = "E:\\lucene"; // 存放Lucene索引文件的位置 public static final String SCAN_PATH = "E:\\text"; // 需要被掃描的位置,測試的時候記得多在這下面放一些文件/*** 創建索引*/ public void creatIndex() {IndexWriter indexWriter = null;try{Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));//Analyzer analyzer = new StandardAnalyzer();Analyzer analyzer = new IKAnalyzer(true);IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);indexWriter = new IndexWriter(directory, indexWriterConfig);indexWriter.deleteAll();// 清除以前的index// 獲取被掃描目錄下的所有文件,包括子目錄List<File> files = FileUtil.listAllFiles(SCAN_PATH);for(int i=0; i<files.size(); i++){Document document = new Document();File file = files.get(i);document.add(new Field("content", FileUtil.readFile(file.getAbsolutePath()), TextField.TYPE_STORED));document.add(new Field("fileName", file.getName(), TextField.TYPE_STORED));document.add(new Field("filePath", file.getAbsolutePath(), TextField.TYPE_STORED));document.add(new Field("updateTime", file.lastModified()+"", TextField.TYPE_STORED));indexWriter.addDocument(document);}}catch (Exception e){e.printStackTrace();}finally{try{if(indexWriter != null) indexWriter.close();}catch (Exception e){e.printStackTrace();}} }執行完之后就在指定目錄新建了索引文件,以后的搜索就靠他們了:
簡單的搜索
代碼比較簡單,具體可以看注釋,這里就不詳述了。
/*** 搜索*/ public void search(String keyWord) {DirectoryReader directoryReader = null;try{// 1、創建DirectoryDirectory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));// 2、創建IndexReaderdirectoryReader = DirectoryReader.open(directory);// 3、根據IndexReader創建IndexSearchIndexSearcher indexSearcher = new IndexSearcher(directoryReader);// 4、創建搜索的Query// Analyzer analyzer = new StandardAnalyzer();Analyzer analyzer = new IKAnalyzer(true); // 使用IK分詞// 簡單的查詢,創建Query表示搜索域為content包含keyWord的文檔//Query query = new QueryParser("content", analyzer).parse(keyWord);String[] fields = {"fileName", "content"}; // 要搜索的字段,一般搜索時都不會只搜索一個字段// 字段之間的與或非關系,MUST表示and,MUST_NOT表示not,SHOULD表示or,有幾個fields就必須有幾個clausesBooleanClause.Occur[] clauses = {BooleanClause.Occur.SHOULD, BooleanClause.Occur.SHOULD};// MultiFieldQueryParser表示多個域解析, 同時可以解析含空格的字符串,如果我們搜索"上海 中國" Query multiFieldQuery = MultiFieldQueryParser.parse(keyWord, fields, clauses, analyzer);// 5、根據searcher搜索并且返回TopDocsTopDocs topDocs = indexSearcher.search(multiFieldQuery, 100); // 搜索前100條結果System.out.println("共找到匹配處:" + topDocs.totalHits); // totalHits和scoreDocs.length的區別還沒搞明白// 6、根據TopDocs獲取ScoreDoc對象ScoreDoc[] scoreDocs = topDocs.scoreDocs;System.out.println("共找到匹配文檔數:" + scoreDocs.length);QueryScorer scorer = new QueryScorer(multiFieldQuery, "content");// 自定義高亮代碼SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter("<span style=\"backgroud:red\">", "</span>");Highlighter highlighter = new Highlighter(htmlFormatter, scorer);highlighter.setTextFragmenter(new SimpleSpanFragmenter(scorer));for (ScoreDoc scoreDoc : scoreDocs){// 7、根據searcher和ScoreDoc對象獲取具體的Document對象Document document = indexSearcher.doc(scoreDoc.doc);//TokenStream tokenStream = new SimpleAnalyzer().tokenStream("content", new StringReader(content));//TokenSources.getTokenStream("content", tvFields, content, analyzer, 100);//TokenStream tokenStream = TokenSources.getAnyTokenStream(indexSearcher.getIndexReader(), scoreDoc.doc, "content", document, analyzer);//System.out.println(highlighter.getBestFragment(tokenStream, content));System.out.println("-----------------------------------------");System.out.println(document.get("fileName") + ":" + document.get("filePath"));System.out.println(highlighter.getBestFragment(analyzer, "content", document.get("content")));System.out.println("");}}catch (Exception e){e.printStackTrace();}finally{try{if(directoryReader != null) directoryReader.close();}catch (Exception e){e.printStackTrace();}} }測試:
public static void main(String args[]) {FileSearchDemo demo = new FileSearchDemo();demo.creatIndex();demo.search("讀取 導出"); }稍微復雜一點的搜索
很多時候搜索時可能需要多個條件配合,就像我們的SQL查詢一樣,不然無法滿足我們的業務。Lucene可以將多個query通過BooleanQuery進行與或非處理得到最終的query。其實再復雜一點的我也沒試過,下面只是一個簡單的示例:
String[] fields = {"fileName", "content"}; // 要搜索的字段,一般搜索時都不會只搜索一個字段 // 字段之間的與或非關系,MUST表示and,MUST_NOT表示not,SHOULD表示or,有幾個fields就必須有幾個clauses BooleanClause.Occur[] clauses = {BooleanClause.Occur.SHOULD, BooleanClause.Occur.SHOULD}; // MultiFieldQueryParser表示多個域解析, 同時可以解析含空格的字符串,如果我們搜索"上海 中國" Query multiFieldQuery = MultiFieldQueryParser.parse(keyWord, fields, clauses, analyzer); Query termQuery = new TermQuery(new Term("content", keyWord));// 詞語搜索,完全匹配,搜索具體的域 Query wildqQuery = new WildcardQuery(new Term("content", keyWord));// 通配符查詢 Query prefixQuery = new PrefixQuery(new Term("content", keyWord));// 字段前綴搜索 Query fuzzyQuery = new FuzzyQuery(new Term("content", keyWord));// 相似度查詢,模糊查詢比如OpenOffica,OpenOffice BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); queryBuilder.add(multiFieldQuery, BooleanClause.Occur.SHOULD); queryBuilder.add(termQuery, BooleanClause.Occur.SHOULD); queryBuilder.add(wildqQuery, BooleanClause.Occur.SHOULD); queryBuilder.add(prefixQuery, BooleanClause.Occur.SHOULD); queryBuilder.add(fuzzyQuery, BooleanClause.Occur.SHOULD); BooleanQuery query = queryBuilder.build(); // 這才是最終的query TopDocs topDocs = indexSearcher.search(query, 100); // 搜索前100條結果復雜的搜索還有可能涉及多個索引目錄的搜索,不同結果的權重分配、排序,近義詞搜索,等等,這里就不多說了,本文只是入門而已。
數據庫搜索
其實和文件搜索差不多,只不過建立索引時是從數據庫讀取內容,我也寫了一個簡單的數據庫搜索示例,可以從前面提到的demo找到(https://github.com/liuxianan/lucene-demo/blob/master/src/com/test/DbSearchDemo.java ),這里不細述。
運行效果如下:
共找到匹配處:1 共找到匹配文檔數:1 ----------------------------------------- 文章標題:Android原生與JS交互總結 文章地址:http://blog.liuxianan.com/android-native-js-interactive.html 文章內容: test.testBoolean(false); // 輸出"boolean:null" 可以發現,如果<span style="backgroud:red">Android</span>這邊參數使用了包裝類型會導致參數接收不到,必須使用基本類型,把上面的基于Lucene實現博客搜索功能
前面都只是例子,下面要試著把它用于正式的項目中。
創建索引的時機
首先寫一個LuceneService類,這里面只有2個方法,一個是創建索引,一個是搜索,那么在什么時候創建索引呢?
我在SpringMVC的監聽器里面加入了段代碼,在系統啟動時主動創建一次索引,另外每24小時再自動更新一次,防止萬一。為保證實時更新,添加文章、修改文章、刪除文章之后也都立即更新一次索引。
/*** 更新Lucene索引* @param event*/ public void updateLuceneIndex(final ServletContextEvent event) {luceneTimer = new Timer("Lucene索引定時構建任務", true);log.debug("啟動Lucene索引構建定時任務!");ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(event.getServletContext());final LuceneService luceneService = context.getBean(LuceneService.class);// 系統啟動1分鐘之后主動建立一次Lucene索引luceneTimer.schedule(new TimerTask(){@Overridepublic void run(){luceneService.updateIndex(event.getServletContext());}}, 1000 * 60, 1000 * 60 * 60 * 24); }必須要開新線程執行
經過測試對于博文內容不是很多的情況下,一般建立索引都在數秒之內,雖然比較快,但還是要避免阻塞主線程,這里我偷懶簡單的用new Thread來實現:
/*** 創建索引,發布文章、修改文章、刪除文章之后都應記得更新索引*/ public void updateIndex(final ServletContext application) {new Thread(new Runnable(){@Overridepublic void run(){try{Thread.sleep(3000); // 由于新增、修改文章之后立即更新索引可能太數據庫還未寫入,所以延遲一段時間執行}catch (InterruptedException e){e.printStackTrace();}// 創建索引一般需要數秒種,為避免阻塞主線程影響業務,開啟新線程執行createIndexSingleThread(application);}}).start(); }如何搜索HTML或markdown
由于我的數據庫存放的是markdown,這里著重考慮一下后面這個問題,雖然markdown已經和純文本差不多了,但是在搜索摘要里面顯示一大堆類似# 這是一級標題這樣的東西也是不爽的,我沒有找到合適的將markdown過濾為純文本的工具類,只能自己簡單寫一個,真的是太簡單,簡單到我的博客里面主要哪種類型的markdown標記,我就過濾什么樣的標記,其它都沒管,這個方法肯定還有很多問題,目前只要能滿足我的需求就足夠了,如果有誰有好的工具歡迎推薦。另外一個就是注意替換HTML的<>標簽:
/*** 簡單地過濾markdown標記使之成為純文本,主要用在摘要和搜索的場景* @param md* @return*/ public static String markdownToText(String md) {if(StringUtil.isEmpty(md)) return "";md = md.replaceAll("(^|\n|\r\n)#{1,6} *", "$1"); // 去除 #md = md.replaceAll("(^|\n|\r\n)\\* *", "$1"); // 去除 *md = md.replaceAll("(^|\n|\r\n)> *", "$1"); // 去除 > (引用)md = md.replaceAll("(^|\n|\r\n)```\\w*?(\n|\r\n)([\\s\\S]+?)```", "$2$3"); // 去除代碼塊md = md.replaceAll("`([^`]+?)`", "$1"); // 去除行內 `code`md = md.replaceAll("!\\[(.*?)\\]\\(.+?\\)", "$1"); // 去除 imgmd = md.replaceAll("\\[(.*?)\\]\\(.+?\\)", "$1"); // 去除 超鏈接md = md.replaceAll("<", "<");md = md.replaceAll(">", ">"); // 替換HTML標簽return md; }如果是數據庫存放的是HTML,可以用一些開源庫把它轉換成純文本再建立索引,比如jsoup。
分頁
官方建議一次性全部查出來,然后再自己分頁,而且如果你要知道總頁數,也只能這么干。雖然還有一個searchAfter方法,但是對于這里沒啥用。
不同用戶顯示不同內容
比如有一些僅自己可見的文章,我希望當我登錄了時可以被搜索到,沒有登錄時不能搜索,可以這樣實現:
BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); queryBuilder.add(multiFieldQuery, BooleanClause.Occur.MUST); if(user == null) {// 未登錄用戶只能查詢公開的文章Query termQuery = new TermQuery(new Term("permission", "pub")); // term表示準確搜索queryBuilder.add(termQuery, BooleanClause.Occur.MUST); } BooleanQuery query = queryBuilder.build();效果體驗
可以訪問我的博客 http://blog.liuxianan.com 然后雙擊Ctrl即可搜索。
結束語
由于時間匆忙,目前草草地實現了搜索功能,后續發現問題再慢慢優化吧,畢竟這不是主業(已轉前端),沒那么多時間搞這東西。
搜索效果文章最前面已經給出了,仿百度做的,哈哈!
本文是面向入門級別的,想深入學習可以參考這位仁兄的系列文章:
http://blog.csdn.net/wuyinggui10000/article/category/3173543
總結
以上是生活随笔為你收集整理的Lucene5.5.4入门以及基于Lucene实现博客搜索功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: javascript原型链中 this
- 下一篇: curl liinux下http命