Springboot下的Lucene(詳細版)

1.搜索大數據

1.1 爲什麼要搜索

在當前百萬級數據的面前,如果全部放在同一個表或者某幾個表中,經常搜索數據庫特別模糊搜索會爆嗎?

答案是沒必,但結果可以預測是很慢很慢!

類似:select * from 表名 where 字段名 like ‘%關鍵字%’

例如:select * from article where content like ’%here%’

當關鍵字複雜的話,難道還 like ‘%關鍵字①%’ or like ‘%關鍵字②%’ ?

多個表關聯就更復雜了......

 

1.2 搜索引擎的必要

百度、google這些搜索引擎可以肯定不是直接搜數據庫的,

例如,在百度搜索“spring boot spring的區別”

從結果可以看出,百度搜索具備以下明顯特點:

1、即使在相關結果數量接近500萬時,也能快速得出結果。

2、搜索的結果不僅僅侷限於完整的“spring boot spring”這一短語,而是將此短語拆分成,“spring”,“springboot ”,“的區別”,“spring區別”等關鍵字。

3、對拆分後的搜索關鍵字進行標紅顯示。

4、…

有沒有發現這裏處理多個單詞的搜索,單詞在這裏是可以分開搜索的,

問題:上述功能,使用大家以前學過的數據庫搜索能夠方便實現嗎?數據庫sql可能實現嗎?

 

1.3 搜索索引原理

問題的結果,是建立索引,著名的數據庫就是oracle,就是建立大量的索引提高搜索速度。

在實際中,我們可以對數據庫中原始的數據結構(左圖),在業務空閒時事先根據左圖內容,創建新的倒排索引結構的數據區域(右圖)。

用戶有查詢需求時,先訪問倒排索引數據區域(右圖),得出文檔id後,通過文檔id即可快速,準確的通過左圖找到具體的文檔內容。

what the mean?

就是文檔內容有5個,5個數據

我們把數據的內容拆開一個個單詞然後記錄起對應的索引id

然後你輸入匹配單詞的時候,快速找到單詞對應的列表id集合返回給你

這一過程,可以通過我們自己寫程序來實現,也可以借用已經抽象出來的通用開源技術來實現,例如Lucene,

分佈式服務的話solr、elasticsearch。

 

2.Lucene、Solr、Elasticsearch關係

Lucene:底層的API,工具包

Solr:基於Lucene開發的企業級的搜索引擎產品

Elasticsearch:基於Lucene開發的企業級的搜索引擎產品

 

2.1 Lucene

Lucene是一套用於全文檢索和搜尋的開源程序庫,由Apache軟件基金會支持和提供

Lucene提供了一個簡單卻強大的應用程序接口(API),能夠做全文索引和搜尋,在Java開發環境裏Lucene是一個成熟的免費開放源代碼工具

ps:全文檢索意思就是對於文章中的每一個單詞都建立索引,然後對這些單詞索引進行排序,當查詢時根據事先建立的索引進行查找。

 

2.2 關於Elasticsearch 與 Solr 的比較總結

  • Solr 利用 Zookeeper 進行分佈式管理,而 Elasticsearch 自身帶有分佈式協調管理功能;
  • Solr 支持更多格式的數據,而 Elasticsearch 僅支持json文件格式;
  • Solr 官方提供的功能更多,而 Elasticsearch 本身更注重於核心功能,高級功能多有第三方插件提供;
  • Solr 在傳統的搜索應用中表現好於 Elasticsearch,但在處理實時搜索應用時效率明顯低於 Elasticsearch。
  • Solr 是傳統搜索應用的有力解決方案,但 Elasticsearch 更適用於新興的實時搜索應用。

 

3.創建索引

 

文檔Document:數據庫中一條具體的記錄

字段Field:數據庫中的每個字段

目錄Directory:物理存儲位置

寫出器的配置對象:需要分詞器Analyer和lucene的版本(lucene的大版本每次更新都有很大的變化

 

3.1 文檔Document

其實這個很理解相當於數據庫裏一條數據。

Id(主鍵) title(標題) content(內容) author(作者) createTime(創作時間)
1 Lucene你好 Lucene的探索大數據課堂,跟我喊123456789.... Joker 2019-9-19 00:00:00

這一個整個id爲1的數據就是Document

 

3.2 字段Field

一個Document中可以有很多個不同的字段,每一個字段都是一個Field類的對象。

一個Document中的字段其類型是不確定的,因此Field類就提供了各種不同的子類,來對應這些不同類型的字段。

這些子類有一些不同的特性:

1)DoubleField、FloatField、IntField、LongField、StringField、TextField這些子類一定會被創建索引,但是不會被分詞,而且不一定會被存儲到文檔列表。要通過構造函數中的參數Store來指定:如果Store.YES代表存儲,Store.NO代表不存儲

2)TextField即創建索引,又會被分詞。(多用於內容)

StringField會創建索引,但是不會被分詞。(多用於主鍵)

3)StoreField一定會被存儲,但是不一定創建索引(多用於不想再去數據庫拿的字段,或者一些重要字段,不想再去找數據庫顯示的,也有用於記錄表名用於知道是哪張表的數據)

如果不分詞,會造成整個字段作爲一個詞條,除非用戶完全匹配,否則搜索不到:

// 創建一個存儲對象
Document doc = new Document();
// 添加字段
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new TextField("title", title, Field.Store.YES));
doc.add(new TextField("content", content, Field.Store.YES));

問題1:如何確定一個字段是否需要存儲?

如果一個字段要顯示到最終的結果中,那麼一定要存儲,否則就不存儲

問題2:如何確定一個字段是否需要創建索引?

如果要根據這個字段進行搜索,那麼這個字段就必須創建索引。

問題3:如何確定一個字段是否需要分詞?

前提是這個字段首先要創建索引。然後如果這個字段的值是不可分割的,那麼就不需要分詞。例如:ID

 

3.3 目錄Directory

指定索引要存儲的位置,就是索引文件放去哪裏存放

FSDirectory:文件系統目錄,會把索引庫指向本地磁盤。

特點:速度略慢,但是比較安全,也方便遷移

RAMDirectory:內存目錄,會把索引庫保存在內存。

特點:速度快,但是不安全

3.4 Analyzer

lucene提供很多分詞算法,可以把文檔中的數據按照算法分詞

但是這些分詞器,並沒有合適的中文分詞器,因此一般我們會用第三方提供的分詞器:

庖丁,IK-Analyzer、mmseg4j(MMSegAnalyzer)等

說一下mmseg4j

<dependency>
	<groupId>com.chenlb.mmseg4j</groupId>
	<artifactId>mmseg4j-core</artifactId>
	<version>1.10.0</version>
</dependency>

這個是支持擴展詞典和停用詞典的

代碼是支持的

詞典可以參考源碼放在data文件夾下的dic文件

源碼:https://github.com/chenlb/mmseg4j-core

可以看到分詞的寫法其實很簡單,我們按需增加就好

3.5 IndexWriterConfig(索引寫出器配置類)

設置配置信息

String direct = "D:/test/luceneData";
Directory directory = FSDirectory.open(Paths.get(direct));	
IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
// 設置創建索引模式(在原來的索引的基礎上創建或新增)
iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);

//添加索引,在之前的索引基礎上追加 
//iwConfig.setOpenMode(OpenMode.APPEND); 
//創建索引,刪除之前的索引
//iwConfig.setOpenMode(OpenMode.CREATE);

3.6 IndexWriter(索引寫出器類)

當分詞算法弄好,配置也好了,就是要把索引找地方記錄保存起來了

/**
	 * 創建索引並進行存儲
	 * 
	 * @param title
	 * @param content
	 */
	public static void createIndex(String id,String title, String content) throws IOException {
		Directory directory = FSDirectory.open(Paths.get(direct));
		
		IndexWriterConfig iwConfig = new IndexWriterConfig(analyzer);
		// 設置創建索引模式(在原來的索引的基礎上創建或新增)
		iwConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);

		IndexWriter iwriter = new IndexWriter(directory, iwConfig);
		// 創建一個存儲對象
		Document doc = new Document();
		// 添加字段
		doc.add(new StringField("id", id, Field.Store.YES));
		doc.add(new TextField("title", title, Field.Store.YES));
		doc.add(new TextField("content", content, Field.Store.YES));
		// 新添加一個doc對象
		iwriter.addDocument(doc);
		// 創建的索引數目
		int numDocs = iwriter.numRamDocs();
		System.out.println("共索引了: " + numDocs + " 個對象");
		// 提交事務
		iwriter.commit();
		// 關閉事務
		iwriter.close();
	}

好了,有人問爲什麼有事務?

這個跟數據庫一樣,中間有一個報錯了,可以回滾不保存。

 

4.大數據查詢(核心)

好了,在這之前我們做了那麼多操作,爲了就是這個時刻把我們想要的結果給拿出來

對應的

查詢解析器:QueryParser(單字段解析器)、MultiFieldQueryParser(多字段的查詢解析器)

查詢對象Query,要查詢的關鍵詞信息:TermQuery(詞條查詢)、WildcardQuery(通配符查詢)、FuzzyQuery(模糊查詢)、NumericRangeQuery(數值範圍查詢)、BooleanQuery(組合查詢,這個大數據常用,將前面都可以一起用,想象一下鏈家房產的搜索數據有多個條件單價啊區域啊學區啊等等)

索引搜索對象IndexSearch(執行搜索功能)

查詢結果對象TopDocs(用於分頁)

4.1 查詢

/**
	 * 查詢方法
	 * 
	 * @param text
	 * @return
	 * @throws IOException
	 */
	public static List<Map<String, Object>> search(String text)throws Exception {
 
		// 得到存放索引的位置
		Directory directory = FSDirectory.open(Paths.get(direct));
		DirectoryReader ireader = DirectoryReader.open(directory);
		IndexSearcher searcher = new IndexSearcher(ireader);
		// 在content中進行搜索
		QueryParser parser = new QueryParser("content", analyzer);
		// 搜索含有text的內容
		Query query = parser.parse(text);
		// 搜索標題和顯示條數(10)
		TopDocs tds = searcher.search(query, 10);
        // 獲取總條數
        System.out.println("本次搜索共找到" + tds.totalHits + "條數據");
		List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
		Map<String, Object> map = null;
 
		// 在內容中查獲找
		for (ScoreDoc sd : tds.scoreDocs) {
			// 獲取title
			String id = searcher.doc(sd.doc).get("id");
			// 獲取title
			String title = searcher.doc(sd.doc).get("title");
			// 獲取content
			String content = searcher.doc(sd.doc).get("content");
			// 內容添加高亮
			QueryParser qp = new QueryParser("content", analyzer);
			// 將匹配到的text添加高亮處理
			Query q = qp.parse(text);
			String HighlightContent = displayHtmlHighlight(q, "content", content);
 
			map = new HashMap<String, Object>();
			map.put("id", id);
			map.put("title", title);
			map.put("content", content);
			map.put("highlight", HighlightContent);
			list.add(map);
		}
 
		return list;
	}
	
	/**
	 * 高亮處理
	 * 
	 * @param query
	 * @param fieldName
	 * @param fieldContent
	 * @return
	 */
	public static String displayHtmlHighlight(Query query, String fieldName, String fieldContent)
			throws IOException, InvalidTokenOffsetsException {
		// 設置高亮標籤,可以自定義,這裏我用html將其顯示爲紅色
		SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<font color='red'>", "</font>");
		// 評分
		QueryScorer scorer = new QueryScorer(query);
		// 創建Fragmenter
		Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
		// 高亮分析器
		Highlighter highlight = new Highlighter(formatter, scorer);
		highlight.setTextFragmenter(fragmenter);
		// 調用高亮方法
		String str = highlight.getBestFragment(analyzer, fieldName, fieldContent);
		return str;
	}

4.2 關於排序

 // 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10,sort);

4.3 分頁

// 實際上Lucene本身不支持分頁。因此我們需要自己進行邏輯分頁。我們要準備分頁參數:
int pageSize = 2;// 每頁條數
int pageNum = 3;// 當前頁碼
int start = (pageNum - 1) * pageSize;// 當前頁的起始條數
int end = start + pageSize;// 當前頁的結束條數(不能包含)
// 搜索數據,查詢0~end條
TopDocs topDocs = searcher.search(query, end);
System.out.println("本次搜索共" + topDocs.totalHits + "條數據");


ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i = start; i < end; i++) {
	ScoreDoc scoreDoc = scoreDocs[i];
	// 獲取文檔編號
	int docID = scoreDoc.doc;
	Document doc = reader.document(docID);
	System.out.println("id: " + doc.get("id"));
	System.out.println("title: " + doc.get("title"));
}

以上就是lucene的使用

補充一下,這裏使用的lucene版本

<lucene.version>7.7.2</lucene.version>

<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-core</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-queryparser</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-analyzers-common</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-queries</artifactId>
	<version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
	<artifactId>lucene-highlighter</artifactId>
	<version>${lucene.version}</version>
</dependency>

最後,謝謝大家堅持到這裏,代碼路上共勉之~!~

下篇會說說solr和elasticsearch的使用和選擇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章