Lucene5.5.4入門以及基於Lucene實現博客搜索功能


前言

一直以來個人博客的搜索功能很蹩腳,只是自己簡單用數據庫的like %keyword%來實現的,所以導致經常搜不到想要找的內容,而且高亮顯示、摘要截取等也不好實現,所以決定采用Lucene改寫博客的搜索功能。先來看一下最終效果:

Lucene搜索效果

本文demo地址:https://github.com/liuxianan/lucene-demo (包括本文需要用到的jar包可以從這里面下載)

效果演示地址:http://blog.liuxianan.com/search?kw=端口 占用

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、創建Directory
		Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));
		// 2、創建IndexReader
		directoryReader = DirectoryReader.open(directory);
		// 3、根據IndexReader創建IndexSearch
		IndexSearcher 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就必須有幾個clauses
		BooleanClause.Occur[] clauses = {BooleanClause.Occur.SHOULD, BooleanClause.Occur.SHOULD};
		// MultiFieldQueryParser表示多個域解析, 同時可以解析含空格的字符串,如果我們搜索"上海 中國" 
		Query multiFieldQuery = MultiFieldQueryParser.parse(keyWord, fields, clauses, analyzer);
		
		// 5、根據searcher搜索並且返回TopDocs
		TopDocs 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()
	{
		@Override
		public void run()
		{
			luceneService.updateIndex(event.getServletContext());
		}
	}, 1000 * 60, 1000 * 60 * 60 * 24);
}

必須要開新線程執行

經過測試對於博文內容不是很多的情況下,一般建立索引都在數秒之內,雖然比較快,但還是要避免阻塞主線程,這里我偷懶簡單的用new Thread來實現:

/**
 * 創建索引,發布文章、修改文章、刪除文章之后都應記得更新索引
 */
public void updateIndex(final ServletContext application)
{
	new Thread(new Runnable()
	{
		@Override
		public 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"); // 去除 img
	md = 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM