Spring Boot 中使用 Java API 呼叫 lucene

搜雲庫技術團隊發表於2017-11-09

Lucene是apache軟體基金會4 jakarta專案組的一個子專案,是一個開放原始碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文字分析引擎(英文與德文兩種西方語言)。Lucene的目的是為軟體開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎

全文檢索概述

比如,我們一個資料夾中,或者一個磁碟中有很多的檔案,記事本、world、Excel、pdf,我們想根據其中的關鍵詞搜尋包含的檔案。例如,我們輸入Lucene,所有內容含有Lucene的檔案就會被檢查出來。這就是所謂的全文檢索。

因此,很容易的我們想到,應該建立一個關鍵字與檔案的相關對映,盜用ppt中的一張圖,很明白的解釋了這種對映如何實現。

倒排索引

倒排索引

有了這種對映關係,我們就來看看Lucene的架構設計。 下面是Lucene的資料必出現的一張圖,但也是其精髓的概括。

倒排

我們可以看到,Lucene的使用主要體現在兩個步驟:

1 建立索引,通過IndexWriter對不同的檔案進行索引的建立,並將其儲存在索引相關檔案儲存的位置中。

2 通過索引查尋關鍵字相關文件。

在Lucene中,就是使用這種“倒排索引”的技術,來實現相關對映。

Lucene數學模型

文件、域、詞元

文件是Lucene搜尋和索引的原子單位,文件為包含一個或者多個域的容器,而域則是依次包含“真正的”被搜尋的內容,域值通過分詞技術處理,得到多個詞元。

For Example,一篇小說(鬥破蒼穹)資訊可以稱為一個文件,小說資訊又包含多個域,例如:標題(鬥破蒼穹)、作者、簡介、最後更新時間等等,對標題這個域採用分詞技術又可以得到一個或者多個詞元(鬥、破、蒼、穹)。

Lucene檔案結構

層次結構

index 一個索引存放在一個目錄中

segment 一個索引中可以有多個段,段與段之間是獨立的,新增新的文件可能產生新段,不同的段可以合併成一個新段

document 文件是建立索引的基本單位,不同的文件儲存在不同的段中,一個段可以包含多個文件

field 域,一個文件包含不同型別的資訊,可以拆分開索引

term 詞,索引的最小單位,是經過詞法分析和語言處理後的資料。

正向資訊

按照層次依次儲存了從索引到詞的包含關係:index-->segment-->document-->field-->term。

反向資訊

反向資訊儲存了詞典的倒排表對映:term-->document

IndexWriter lucene中最重要的的類之一,它主要是用來將文件加入索引,同時控制索引過程中的一些引數使用。

Analyzer 分析器,主要用於分析搜尋引擎遇到的各種文字。常用的有StandardAnalyzer分析器,StopAnalyzer分析器,WhitespaceAnalyzer分析器等。

Directory 索引存放的位置;lucene提供了兩種索引存放的位置,一種是磁碟,一種是記憶體。一般情況將索引放在磁碟上;相應地lucene提供了FSDirectory和RAMDirectory兩個類。

Document 文件;Document相當於一個要進行索引的單元,任何可以想要被索引的檔案都必須轉化為Document物件才能進行索引。

Field 欄位。

IndexSearcher 是lucene中最基本的檢索工具,所有的檢索都會用到IndexSearcher工具;

Query 查詢,lucene中支援模糊查詢,語義查詢,短語查詢,組合查詢等等,如有TermQuery,BooleanQuery,RangeQuery,WildcardQuery等一些類。

QueryParser 是一個解析使用者輸入的工具,可以通過掃描使用者輸入的字串,生成Query物件。

Hits 在搜尋完成之後,需要把搜尋結果返回並顯示給使用者,只有這樣才算是完成搜尋的目的。在lucene中,搜尋的結果的集合是用Hits類的例項來表示的。

測試用例

Github 程式碼

程式碼我已放到 Github ,匯入spring-boot-lucene-demo 專案

github github.com/souyunku/sp…

新增依賴

<!--對分詞索引查詢解析-->
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-queryparser</artifactId>
	<version>7.1.0</version>
</dependency>

<!--高亮 -->
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-highlighter</artifactId>
	<version>7.1.0</version>
</dependency>

<!--smartcn 中文分詞器 SmartChineseAnalyzer  smartcn分詞器 需要lucene依賴 且和lucene版本同步-->
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-analyzers-smartcn</artifactId>
	<version>7.1.0</version>
</dependency>

<!--ik-analyzer 中文分詞器-->
<dependency>
	<groupId>cn.bestwu</groupId>
	<artifactId>ik-analyzers</artifactId>
	<version>5.1.0</version>
</dependency>

<!--MMSeg4j 分詞器-->
<dependency>
	<groupId>com.chenlb.mmseg4j</groupId>
	<artifactId>mmseg4j-solr</artifactId>
	<version>2.4.0</version>
	<exclusions>
		<exclusion>
			<groupId>org.apache.solr</groupId>
			<artifactId>solr-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>
複製程式碼

配置 lucene

private Directory directory;

private IndexReader indexReader;

private IndexSearcher indexSearcher;

@Before
public void setUp() throws IOException {
	//索引存放的位置,設定在當前目錄中
	directory = FSDirectory.open(Paths.get("indexDir/"));

	//建立索引的讀取器
	indexReader = DirectoryReader.open(directory);

	//建立一個索引的查詢器,來檢索索引庫
	indexSearcher = new IndexSearcher(indexReader);
}

@After
public void tearDown() throws Exception {
	indexReader.close();
}

**
 * 執行查詢,並列印查詢到的記錄數
 *
 * @param query
 * @throws IOException
 */
public void executeQuery(Query query) throws IOException {

	TopDocs topDocs = indexSearcher.search(query, 100);

	//列印查詢到的記錄數
	System.out.println("總共查詢到" + topDocs.totalHits + "個文件");
	for (ScoreDoc scoreDoc : topDocs.scoreDocs) {

		//取得對應的文件物件
		Document document = indexSearcher.doc(scoreDoc.doc);
		System.out.println("id:" + document.get("id"));
		System.out.println("title:" + document.get("title"));
		System.out.println("content:" + document.get("content"));
	}
}

/**
 * 分詞列印
 *
 * @param analyzer
 * @param text
 * @throws IOException
 */
public void printAnalyzerDoc(Analyzer analyzer, String text) throws IOException {

	TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(text));
	CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
	try {
		tokenStream.reset();
		while (tokenStream.incrementToken()) {
			System.out.println(charTermAttribute.toString());
		}
		tokenStream.end();
	} finally {
		tokenStream.close();
		analyzer.close();
	}
}
	
複製程式碼

建立索引

@Test
public void indexWriterTest() throws IOException {
	long start = System.currentTimeMillis();

	//索引存放的位置,設定在當前目錄中
	Directory directory = FSDirectory.open(Paths.get("indexDir/"));

	//在 6.6 以上版本中 version 不再是必要的,並且,存在無參構造方法,可以直接使用預設的 StandardAnalyzer 分詞器。
	Version version = Version.LUCENE_7_1_0;

	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	//建立索引寫入配置
	IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);

	//建立索引寫入物件
	IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);

	//建立Document物件,儲存索引

	Document doc = new Document();

	int id = 1;

	//將欄位加入到doc中
	doc.add(new IntPoint("id", id));
	doc.add(new StringField("title", "Spark", Field.Store.YES));
	doc.add(new TextField("content", "Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎", Field.Store.YES));
	doc.add(new StoredField("id", id));

	//將doc物件儲存到索引庫中
	indexWriter.addDocument(doc);

	indexWriter.commit();
	//關閉流
	indexWriter.close();

	long end = System.currentTimeMillis();
	System.out.println("索引花費了" + (end - start) + " 毫秒");
}
複製程式碼

響應

17:58:14.655 [main] DEBUG org.wltea.analyzer.dic.Dictionary - 載入擴充套件詞典:ext.dic
17:58:14.660 [main] DEBUG org.wltea.analyzer.dic.Dictionary - 載入擴充套件停止詞典:stopword.dic
索引花費了879 毫秒
複製程式碼

刪除文件

@Test
public void deleteDocumentsTest() throws IOException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	//建立索引寫入配置
	IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);

	//建立索引寫入物件
	IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);

	// 刪除title中含有關鍵詞“Spark”的文件
	long count = indexWriter.deleteDocuments(new Term("title", "Spark"));

	//  除此之外IndexWriter還提供了以下方法:
	// DeleteDocuments(Query query):根據Query條件來刪除單個或多個Document
	// DeleteDocuments(Query[] queries):根據Query條件來刪除單個或多個Document
	// DeleteDocuments(Term term):根據Term來刪除單個或多個Document
	// DeleteDocuments(Term[] terms):根據Term來刪除單個或多個Document
	// DeleteAll():刪除所有的Document

	//使用IndexWriter進行Document刪除操作時,文件並不會立即被刪除,而是把這個刪除動作快取起來,當IndexWriter.Commit()或IndexWriter.Close()時,刪除操作才會被真正執行。

	indexWriter.commit();
	indexWriter.close();

	System.out.println("刪除完成:" + count);
}

複製程式碼

響應

刪除完成:1
複製程式碼

更新文件

/**
 * 測試更新
 * 實際上就是刪除後新增一條
 *
 * @throws IOException
 */
@Test
public void updateDocumentTest() throws IOException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	//建立索引寫入配置
	IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);

	//建立索引寫入物件
	IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);

	Document doc = new Document();

	int id = 1;

	doc.add(new IntPoint("id", id));
	doc.add(new StringField("title", "Spark", Field.Store.YES));
	doc.add(new TextField("content", "Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎", Field.Store.YES));
	doc.add(new StoredField("id", id));

	long count = indexWriter.updateDocument(new Term("id", "1"), doc);
	System.out.println("更新文件:" + count);
	indexWriter.close();
}
複製程式碼

響應

更新文件:1
複製程式碼

按詞條搜尋

/**
 * 按詞條搜尋
 * <p>
 * TermQuery是最簡單、也是最常用的Query。TermQuery可以理解成為“詞條搜尋”,
 * 在搜尋引擎中最基本的搜尋就是在索引中搜尋某一詞條,而TermQuery就是用來完成這項工作的。
 * 在Lucene中詞條是最基本的搜尋單位,從本質上來講一個詞條其實就是一個名/值對。
 * 只不過這個“名”是欄位名,而“值”則表示欄位中所包含的某個關鍵字。
 *
 * @throws IOException
 */
@Test
public void termQueryTest() throws IOException {

	String searchField = "title";
	//這是一個條件查詢的api,用於新增條件
	TermQuery query = new TermQuery(new Term(searchField, "Spark"));

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

多條件查詢

/**
 * 多條件查詢
 *
 * BooleanQuery也是實際開發過程中經常使用的一種Query。
 * 它其實是一個組合的Query,在使用時可以把各種Query物件新增進去並標明它們之間的邏輯關係。
 * BooleanQuery本身來講是一個布林子句的容器,它提供了專門的API方法往其中新增子句,
 * 並標明它們之間的關係,以下程式碼為BooleanQuery提供的用於新增子句的API介面:
 *
 * @throws IOException
 */
@Test
public void BooleanQueryTest() throws IOException {

	String searchField1 = "title";
	String searchField2 = "content";
	Query query1 = new TermQuery(new Term(searchField1, "Spark"));
	Query query2 = new TermQuery(new Term(searchField2, "Apache"));
	BooleanQuery.Builder builder = new BooleanQuery.Builder();

	// BooleanClause用於表示布林查詢子句關係的類,
	// 包 括:
	// BooleanClause.Occur.MUST,
	// BooleanClause.Occur.MUST_NOT,
	// BooleanClause.Occur.SHOULD。
	// 必須包含,不能包含,可以包含三種.有以下6種組合:
	//
	// 1.MUST和MUST:取得連個查詢子句的交集。
	// 2.MUST和MUST_NOT:表示查詢結果中不能包含MUST_NOT所對應得查詢子句的檢索結果。
	// 3.SHOULD與MUST_NOT:連用時,功能同MUST和MUST_NOT。
	// 4.SHOULD與MUST連用時,結果為MUST子句的檢索結果,但是SHOULD可影響排序。
	// 5.SHOULD與SHOULD:表示“或”關係,最終檢索結果為所有檢索子句的並集。
	// 6.MUST_NOT和MUST_NOT:無意義,檢索無結果。

	builder.add(query1, BooleanClause.Occur.SHOULD);
	builder.add(query2, BooleanClause.Occur.SHOULD);

	BooleanQuery query = builder.build();

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

匹配字首

/**
 * 匹配字首
 * <p>
 * PrefixQuery用於匹配其索引開始以指定的字串的文件。就是文件中存在xxx%
 * <p>
 *
 * @throws IOException
 */
@Test
public void prefixQueryTest() throws IOException {
	String searchField = "title";
	Term term = new Term(searchField, "Spar");
	Query query = new PrefixQuery(term);

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

短語搜尋

/**
 * 短語搜尋
 * <p>
 * 所謂PhraseQuery,就是通過短語來檢索,比如我想查“big car”這個短語,
 * 那麼如果待匹配的document的指定項裡包含了"big car"這個短語,
 * 這個document就算匹配成功。可如果待匹配的句子裡包含的是“big black car”,
 * 那麼就無法匹配成功了,如果也想讓這個匹配,就需要設定slop,
 * 先給出slop的概念:slop是指兩個項的位置之間允許的最大間隔距離
 *
 * @throws IOException
 */
@Test
public void phraseQueryTest() throws IOException {

	String searchField = "content";
	String query1 = "apache";
	String query2 = "spark";

	PhraseQuery.Builder builder = new PhraseQuery.Builder();
	builder.add(new Term(searchField, query1));
	builder.add(new Term(searchField, query2));
	builder.setSlop(0);
	PhraseQuery phraseQuery = builder.build();

	//執行查詢,並列印查詢到的記錄數
	executeQuery(phraseQuery);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

相近詞語搜尋

/**
 * 相近詞語搜尋
 * <p>
 * FuzzyQuery是一種模糊查詢,它可以簡單地識別兩個相近的詞語。
 *
 * @throws IOException
 */
@Test
public void fuzzyQueryTest() throws IOException {

	String searchField = "content";
	Term t = new Term(searchField, "大規模");
	Query query = new FuzzyQuery(t);

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

萬用字元搜尋

/**
 * 萬用字元搜尋
 * <p>
 * Lucene也提供了萬用字元的查詢,這就是WildcardQuery。
 * 萬用字元“?”代表1個字元,而“*”則代表0至多個字元。
 *
 * @throws IOException
 */
@Test
public void wildcardQueryTest() throws IOException {
	String searchField = "content";
	Term term = new Term(searchField, "大*規模");
	Query query = new WildcardQuery(term);

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

分詞查詢

/**
 * 分詞查詢
 *
 * @throws IOException
 * @throws ParseException
 */
@Test
public void queryParserTest() throws IOException, ParseException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	String searchField = "content";

	//指定搜尋欄位和分析器
	QueryParser parser = new QueryParser(searchField, analyzer);

	//使用者輸入內容
	Query query = parser.parse("計算引擎");

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

多個 Field 分詞查詢

/**
 * 多個 Field 分詞查詢
 *
 * @throws IOException
 * @throws ParseException
 */
@Test
public void multiFieldQueryParserTest() throws IOException, ParseException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	String[] filedStr = new String[]{"title", "content"};

	//指定搜尋欄位和分析器
	QueryParser queryParser = new MultiFieldQueryParser(filedStr, analyzer);

	//使用者輸入內容
	Query query = queryParser.parse("Spark");

	//執行查詢,並列印查詢到的記錄數
	executeQuery(query);
}
複製程式碼

響應

總共查詢到1個文件
id:1
title:Spark
content:Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎!
複製程式碼

中文分詞器

/**
 * IKAnalyzer  中文分詞器
 * SmartChineseAnalyzer  smartcn分詞器 需要lucene依賴 且和lucene版本同步
 *
 * @throws IOException
 */
@Test
public void AnalyzerTest() throws IOException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = null;
	String text = "Apache Spark 是專為大規模資料處理而設計的快速通用的計算引擎";

	analyzer = new IKAnalyzer();//IKAnalyzer 中文分詞
	printAnalyzerDoc(analyzer, text);
	System.out.println();

	analyzer = new ComplexAnalyzer();//MMSeg4j 中文分詞
	printAnalyzerDoc(analyzer, text);
	System.out.println();

	analyzer = new SmartChineseAnalyzer();//Lucene 中文分詞器
	printAnalyzerDoc(analyzer, text);
}
複製程式碼

三種分詞響應

apache
spark
專為
大規模
規模
模數
資料處理
資料
處理
而設
設計
快速
通用
計算
引擎
複製程式碼
apache
spark
是
專為
大規模
資料處理
而
設計
的
快速
通用
的
計算
引擎
複製程式碼
apach
spark
是
專
為
大規模
資料
處理
而
設計
的
快速
通用
的
計算
引擎
複製程式碼

高亮處理

/**
 * 高亮處理
 *
 * @throws IOException
 */
@Test
public void HighlighterTest() throws IOException, ParseException, InvalidTokenOffsetsException {
	//Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文
	//Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞
	//Analyzer analyzer = new ComplexAnalyzer();//中文分詞
	//Analyzer analyzer = new IKAnalyzer();//中文分詞

	Analyzer analyzer = new IKAnalyzer();//中文分詞

	String searchField = "content";
	String text = "Apache Spark 大規模資料處理";

	//指定搜尋欄位和分析器
	QueryParser parser = new QueryParser(searchField, analyzer);

	//使用者輸入內容
	Query query = parser.parse(text);

	TopDocs topDocs = indexSearcher.search(query, 100);

	// 關鍵字高亮顯示的html標籤,需要匯入lucene-highlighter-xxx.jar
	SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<span style='color:red'>", "</span>");
	Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));

	for (ScoreDoc scoreDoc : topDocs.scoreDocs) {

		//取得對應的文件物件
		Document document = indexSearcher.doc(scoreDoc.doc);

		// 內容增加高亮顯示
		TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(document.get("content")));
		String content = highlighter.getBestFragment(tokenStream, document.get("content"));

		System.out.println(content);
	}

}
複製程式碼

響應

<span style='color:red'>Apache</span> <span style='color:red'>Spark</span> 是專為<span style='color:red'>大規模資料處理</span>而設計的快速通用的計算引擎!
複製程式碼

程式碼我已放到 Github ,匯入spring-boot-lucene-demo 專案

github github.com/souyunku/sp…

Contact

  • 作者:鵬磊
  • 出處:www.ymq.io
  • Email:admin@souyunku.com
  • 版權歸作者所有,轉載請註明出處
  • Wechat:關注公眾號,搜雲庫,專注於開發技術的研究與知識分享

關注公眾號-搜雲庫
搜雲庫

相關文章