搜索引擎(Lucene介紹、分詞器詳解)

Lucene介紹

Lucene簡介java

最受歡迎的java開源全文搜索引擎開發工具包。提供了完整的查詢引擎和索引引擎,部分文本分詞引擎(英文與德文兩種西方語言)。Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便在目標系統中實現全文檢索功能,或者是以此爲基礎創建起完整的全文檢索引擎。 是Apache的子項目,網址:http://lucene.apache.org/git

Lucene用途github

爲軟件開發人員提供一個簡單易用的工具包,以方便在目標系統中實現全文檢索功能,或者是以此爲基礎創建起完整的全文檢索引擎。算法

Lucene適用場景數據庫

在應用中爲數據庫中的數據提供全文檢索實現。apache

開發獨立的搜索引擎服務、系統架構

Lucene的特性dom

一、穩定、索引性能高maven

每小時可以索引150GB以上的數據。
對內存的要求小——只須要1MB的堆內存
增量索引和批量索引同樣快。
索引的大小約爲索引文本大小的20%~30%ide

二、高效、準確、高性能的搜索算法

良好的搜索排序。
強大的查詢方式支持:短語查詢、通配符查詢、臨近查詢、範圍查詢等。
支持字段搜索(如標題、做者、內容)。
可根據任意字段排序
支持多個索引查詢結果合併
支持更新操做和查詢操做同時進行
支持高亮、join、分組結果功能
速度快
可擴展排序模塊,內置包含向量空間模型、BM25模型可選
可配置存儲引擎

三、跨平臺

純java編寫。
做爲Apache開源許可下的開源項目,你可在商業或開源項目中使用。
Lucene有多種語言實現版可選(如C、C++、Python等),不光是JAVA。

Lucene架構

1.數據收集 
2.建立索引 
3.索引存儲 
4.搜索(使用索引)

Lucene集成

選用的Lucene版本

選用當前最新版 7.3.0 : https://lucene.apache.org/

系統要求

JDK1.8 及以上版本

集成:將lucene core的jar引入到你的應用中
方式一:官網下載 zip,解壓後拷貝jar到你的工程 
方式二:maven 引入依賴

Lucene 模塊說明

core: Lucene core library   核心模塊:分詞、索引、查詢
analyzers-*: 分詞器

facet: Faceted indexing and search capabilities  提供分類索引、搜索能力
grouping: Collectors for grouping search results.  搜索結果分組支持
highlighter: Highlights search keywords in results   關鍵字高亮支持
join: Index-time and Query-time joins for normalized content  鏈接支持
queries: Filters and Queries that add to core Lucene   補充的查詢、過濾方式實現
queryparser: Query parsers and parsing framework  查詢表達式解析模塊

spatial: Geospatial search  地理位置搜索支持
suggest: Auto-suggest and Spellchecking support  拼寫檢查、聯想提示

先引入lucene的核心模塊

<!-- lucene 核心模塊  -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>7.3.0</version>
</dependency>

瞭解核心模塊的構成

分詞器詳解

Lucene分詞器API

1.org.apache.lucene.analysi.Analyzer

分析器,分詞器組件的核心API,它的職責:構建真正對文本進行分詞處理的TokenStream(分詞處理器)。經過調用它的以下兩個方法,獲得輸入文本的分詞處理器。

public final TokenStream tokenStream(String fieldName, Reader reader) 
public final TokenStream tokenStream(String fieldName, String text)

這兩個方法是final方法,不能被覆蓋的,在這兩個方法中是如何構建分詞處理器的呢?

問題1:從哪裏獲得了TokenStream?
問題2:方法傳入的字符流Reader 給了誰?
問題3: components是什麼?components的獲取邏輯是怎樣?
問題4:createComponents(fieldName) 方法是個什麼方法?
問題5:Analyzer能直接建立對象嗎?
問題6:爲何它要這樣設計?
問題7:請看一下Analyzer的實現子類有哪些?
問題8:要實現一個本身的Analyzer,必須實現哪一個方法?

TokenStreamComponents  createComponents(String fieldName)

是Analizer中惟一的抽象方法,擴展點。經過提供該方法的實現來實現本身的Analyzer。參數說明:fieldName,若是咱們須要爲不一樣的字段建立不一樣的分詞處理器組件,則可根據這個參數來判斷。不然,就用不到這個參數。
返回值爲 TokenStreamComponents  分詞處理器組件。
咱們須要在createComponents方法中建立咱們想要的分詞處理器組件。

2.TokenStreamComponents

分詞處理器組件:這個類中封裝有供外部使用的TokenStream分詞處理器。提供了對source(源)和sink(供外部使用分詞處理器)兩個屬性的訪問方法。

問題1:這個類的構造方法有幾個?區別是什麼?從中能發現什麼?
問題2:source 和 sink屬性分別是什麼類型?這兩個類型有什麼關係?
問題3:在這個類中沒有建立source、sink對象的代碼(而是由構造方法傳人)。也就是說咱們在Analyzer.createComponents方法中建立它的對象前,需先建立什麼?
問題4:在Analyzer中tokenStream() 方法中把輸入流給了誰?獲得的TokenStream對象是誰?TokenStream對象sink中是否必須封裝有source對象?若是必須有,這個封裝是否也得在Analyzer.createComponents方法中完成?

3,org.apache.lucene.analysis.TokenStream

分詞處理器,負責對輸入文本完成分詞、處理。

問題1:接上一頁,TokenStream中有沒有對應的給入方法?
問題2:TokenStream是一個抽象類,有哪些方法,它的抽象方法有哪些?它的構造方法有什麼特色?
問題3:TokenStream的具體子類分爲哪兩類?有什麼區別?
問題4:TokenStream繼承了誰?它是幹什麼用的?

概念說明:Token: 分項,從字符流中分出一個一個的項.  Token  Attribute: 分項屬性(分項的信息):如 包含的詞、位置等

4.TokenStream 的兩類子類

Tokenizer:分詞器,輸入是Reader字符流的TokenStream,完成從流中分出分項
TokenFilter:分項過濾器,它的輸入是另外一個TokenStream,完成對從上一個TokenStream中流出的token的特殊處理。

問題1:請查看Tokenizer類的源碼及註釋,這個類該如何使用?要實現本身的Tokenizer只須要作什麼?
問題2:Tokenizer的子類有哪些?
問題3:請查看TokenFilter類的源碼及註釋,如何實現本身的TokenFilter?
問題4:TokenFilter的子類有哪些?
問題5:TokenFilter是否是一個典型的裝飾器模式?若是咱們須要對分詞進行各類處理,只須要按咱們的處理順序一層層包裹便可(每一層完成特定的處理)。不一樣的處理須要,只需不一樣的包裹順序、層數。

5.TokenStream 繼承了 AttributeSource

問題1:咱們在TokenStream及它的兩個子類中是否有看到關於分項信息的存儲,如該分項的詞是什麼、這個詞的位置索引?

概念說明:Attribute  屬性     Token Attribute  分項屬性(分項信息),如 分項的詞、詞的索引位置等等。這些屬性經過不一樣的Tokenizer /TokenFilter處理統計得出。不一樣的Tokenizer/TokenFilter組合,就會有不一樣的分項信息。它是會動態變化的,你不知道有多少,是什麼。那該如何實現分項信息的存儲呢?
答案就是 AttributeSource、Attribute 、AttributeImpl、AttributeFactory
一、AttribureSource 負責存放Attribute對象,它提供對應的存、取方法
二、Attribute對象中則能夠存儲一個或多個屬性信息
三、AttributeFactory 則是負責建立Attributre對象的工廠,在TokenStream中默認使用了AttributeFactory.getStaticImplementation  咱們不須要提供,遵照它的規則便可。

6.AttributeSource使用規則說明

一、某個TokenStream實現中如要存儲分項屬性,經過AttributeSource的兩個add方法之一,往AttributeSource中加入屬性對象。

<T extends Attribute> T addAttribute(Class<T> attClass)
該方法要求傳人你須要添加的屬性的接口類(繼承Attribute),返回對應的實現類實例給你。從接口到實例,這就是爲何須要AttributeFactory的緣由。
void addAttributeImpl(AttributeImpl att)

二、加入的每個Attribute實現類在AttributeSource中只會有一個實例,分詞過程當中,分項是重複使用這一實例來存放分項的屬性信息。重複調用add方法添加它返回已存儲的實例對象。

三、要獲取分項的某屬性信息,則需持有某屬性的實例對象,經過addAttribute方法或getAttribure方法得到Attribute對象,再調用實例的方法來獲取、設置值

四、在TokenStream中,咱們用本身實現的Attribute,默認的工廠。當咱們調用這個add方法時,它怎麼知道實現類是哪一個?這裏有必定規則要遵照:
     一、自定義的屬性接口 MyAttribute  繼承 Attribute
     二、自定義的屬性實現類必須繼承 Attribute,實現自定義的接口MyAttribute
     三、自定義的屬性實現類必須提供無參構造方法
     四、爲了讓默認工廠能根據自定義接口找到實現類,實現類名需爲 接口名+Impl 。
請查看lucene中提供的Attribute實現是不是這樣的。

7.TokenStream 的使用步驟。

咱們在應用中並不直接使用分詞器,只需爲索引引擎和搜索引擎建立咱們想要的分詞器對象。但咱們在選擇分詞器時,會須要測試分詞器的效果,就須要知道如何使用獲得的分詞處理器TokenStream,使用步驟:
一、從tokenStream得到你想要得到分項屬性對象(信息是存放在屬性對象中的)
二、調用 tokenStream 的 reset() 方法,進行重置。由於tokenStream是重複利用的。
三、循環調用tokenStream的incrementToken(),一個一個分詞,直到它返回false
四、在循環中取出每一個分項你想要的屬性值。
五、調用tokenStream的end(),執行任務須要的結束處理。
六、調用tokenStream的close()方法,釋放佔有的資源。

思考:tokenStream是裝飾器模式,這個reset / incrementToken / end / close是如何工做的?  請查看 tokenStream / Tokenizer / TokenFilter的源碼。

8.簡單實現一個咱們本身的Analyzer,需求說明

Tokenizer:
實現對英文按空白字符進行分詞。
須要記錄的屬性信息有: 詞
TokenFilter:
要進行的處理:轉爲小寫

說明:Tokenizer分詞時,是從字符流中一個一個字符讀取,判斷是不是空白字符來進行分詞。

思考:Tokenizer是一個 AttributeSource對象,TokenFilter 又是一個AttributeSource對象。在這兩個咱們本身的實現類中,咱們都調用了addAttribute方法,怎麼會只有一個 attribute對象? 請查看源碼找到答案。

public interface MyAttribute1 extends Attribute {
	String getAttr();
}
public class MyAttribute1Impl extends AttributeImpl {

	int value = 0;
	Random rd = new Random();

	public int getAttr() {
		return value;
	}

	@Override
	public void clear() {
		value = rd.nextInt(1000);
	}

	@Override
	public void reflectWith(AttributeReflector reflector) {
		// TODO Auto-generated method stub

	}

	@Override
	public void copyTo(AttributeImpl target) {

	}
}
public class MyWhitespaceAnalyzer extends Analyzer {

	@Override
	protected TokenStreamComponents createComponents(String fieldName) {
		Tokenizer source = new MyWhitespaceTokenizer();
		TokenStream filter = new MyLowerCaseTokenFilter(source);
		return new TokenStreamComponents(source, filter);
	}

	static class MyWhitespaceTokenizer extends Tokenizer {

		// 須要記錄的屬性
		// 詞
		MyCharAttribute charAttr = this.addAttribute(MyCharAttribute.class);

		// 存詞的出現位置

		// 存放詞的偏移

		//
		char[] buffer = new char[255];
		int length = 0;
		int c;

		@Override
		public boolean incrementToken() throws IOException {
			// 清除全部的詞項屬性
			clearAttributes();
			length = 0;
			while (true) {
				c = this.input.read();

				if (c == -1) {
					if (length > 0) {
						// 複製到charAttr
						this.charAttr.setChars(buffer, length);
						return true;
					} else {
						return false;
					}
				}

				if (Character.isWhitespace(c)) {
					if (length > 0) {
						// 複製到charAttr
						this.charAttr.setChars(buffer, length);
						return true;
					}
				}

				buffer[length++] = (char) c;
			}
		}

	}

	public static class MyLowerCaseTokenFilter extends TokenFilter {

		public MyLowerCaseTokenFilter(TokenStream input) {
			super(input);
		}

		MyCharAttribute charAttr = this.addAttribute(MyCharAttribute.class);

		@Override
		public boolean incrementToken() throws IOException {
			boolean res = this.input.incrementToken();
			if (res) {
				char[] chars = charAttr.getChars();
				int length = charAttr.getLength();
				if (length > 0) {
					for (int i = 0; i < length; i++) {
						chars[i] = Character.toLowerCase(chars[i]);
					}
				}
			}
			return res;
		}

	}

	public static interface MyCharAttribute extends Attribute {
		void setChars(char[] buffer, int length);

		char[] getChars();

		int getLength();

		String getString();
	}

	public static class MyCharAttributeImpl extends AttributeImpl
			implements MyCharAttribute {
		private char[] chatTerm = new char[255];
		private int length = 0;

		@Override
		public void setChars(char[] buffer, int length) {
			this.length = length;
			if (length > 0) {
				System.arraycopy(buffer, 0, this.chatTerm, 0, length);
			}
		}

		public char[] getChars() {
			return this.chatTerm;
		}

		public int getLength() {
			return this.length;
		}

		@Override
		public String getString() {
			if (this.length > 0) {
				return new String(this.chatTerm, 0, length);
			}
			return null;
		}

		@Override
		public void clear() {
			this.length = 0;
		}

		@Override
		public void reflectWith(AttributeReflector reflector) {

		}

		@Override
		public void copyTo(AttributeImpl target) {

		}
	}

	public static void main(String[] args) {

		String text = "An AttributeSource contains a list of different AttributeImpls, and methods to add and get them. ";

		try (Analyzer ana = new MyWhitespaceAnalyzer();
				TokenStream ts = ana.tokenStream("aa", text);) {
			MyCharAttribute ca = ts.getAttribute(MyCharAttribute.class);
			ts.reset();
			while (ts.incrementToken()) {
				System.out.print(ca.getString() + "|");
			}
			ts.end();
			System.out.println();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

}

9.小結

經過API及源碼的學習,你是否感覺到了做者的一些設計思想。

他是如何處理變與不變的,如Analyzer、TokenStream類的設計?
他是如何處理不一樣分詞器有不一樣的處理邏輯的問題的?

Lucene提供的分詞器

Lucene core模塊中的 StandardAnalyzer  英文分詞器

看看它都分析存儲哪些屬性信息 試試它的英文分詞效果 中文分詞效果

Lucene  的中文分詞器 SmartChineseAnalyzer

<!-- Lucene提供的中文分詞器模塊,lucene-analyzers-smartcn -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-smartcn</artifactId>
    <version>7.3.0</version>
</dependency>

試試它的中英文分詞效果

Lucene-Analyzers-common包中提供的分詞器

看看都有些什麼分詞器

public class AnalizerTestDemo {

	private static void doToken(TokenStream ts) throws IOException {
		ts.reset();
		CharTermAttribute cta = ts.getAttribute(CharTermAttribute.class);
		while (ts.incrementToken()) {
			System.out.print(cta.toString() + "|");
		}
		System.out.println();
		ts.end();
		ts.close();
	}

	public static void main(String[] args) throws IOException {
		String etext = "Analysis is one of the main causes of slow indexing. Simply put, the more you analyze the slower analyze the indexing (in most cases).";
		String chineseText = "張三說的確實在理。";
		// String chineseText = "中華人民共和國簡稱中國。";
		try (Analyzer ana = new StandardAnalyzer();) {
			TokenStream ts = ana.tokenStream("coent", etext);
			System.out.println("標準分詞器,英文分詞效果:");
			doToken(ts);
			ts = ana.tokenStream("content", chineseText);
			System.out.println("標準分詞器,中文分詞效果:");
			doToken(ts);
		} catch (IOException e) {

		}

		// smart中文分詞器
		try (Analyzer smart = new SmartChineseAnalyzer()) {
			TokenStream ts = smart.tokenStream("content", etext);
			System.out.println("smart中文分詞器,英文分詞效果:");
			doToken(ts);
			ts = smart.tokenStream("content", chineseText);
			System.out.println("smart中文分詞器,中文分詞效果:");
			doToken(ts);
		}

		// IKAnalyzer 細粒度切分
		try (Analyzer ik = new IKAnalyzer4Lucene7();) {
			TokenStream ts = ik.tokenStream("content", etext);
			System.out.println("IKAnalyzer中文分詞器 細粒度切分,英文分詞效果:");
			doToken(ts);
			ts = ik.tokenStream("content", chineseText);
			System.out.println("IKAnalyzer中文分詞器 細粒度切分,中文分詞效果:");
			doToken(ts);
		}

		// IKAnalyzer 智能切分
		try (Analyzer ik = new IKAnalyzer4Lucene7(true);) {
			TokenStream ts = ik.tokenStream("content", etext);
			System.out.println("IKAnalyzer中文分詞器 智能切分,英文分詞效果:");
			doToken(ts);
			ts = ik.tokenStream("content", chineseText);
			System.out.println("IKAnalyzer中文分詞器 智能切分,中文分詞效果:");
			doToken(ts);
		}
	}
}

IKAnalyzer集成

須要作集成,是由於Analyzer的createComponents方法API改變了。

集成步驟

一、找到 IkAnalyzer包體提供的Lucene支持類,比較IKAnalyzer的createComponets方法。

二、照這兩個類,建立新版本的, 類裏面的代碼直接複製,修改參數便可。

IKAnalyzer提供兩種分詞模式:細粒度分詞和智能分詞,看它的構造參數。

集成後,請測試細粒度分詞、智能分詞的效果

測試語句:張三說的確實在理

請比較IKAnalyzer和SmartChineseAnalyzer

測試語句:
張三說的確實在理。
Lucene是一個開源的,基於java的搜索引擎開發工具包。

擴展 IKAnalyzer的停用詞

Ik中默認的停用詞不多,咱們每每須要擴展它。可從網址: https://github.com/cseryp/stopwords    下載一份比較全的停用詞。
Ik中停用詞的擴展步驟:
一、在類目錄下建立IK的配置文件:IKAnalyzer.cfg.xml
二、在配置文件中增長配置擴展停用詞文件的節點:
 <entry key=「ext_stopwords」>my_ext_stopword.dic</entry>  
   若有多個,以「;」間隔
三、在類目錄下建立咱們的擴展停用詞文件 my_ext_stopword.dic
四、編輯該文件加入停用詞,一行一個

注意文件要是UTF-8編碼

擴展 IKAnalyzer的詞典

每一年都有不少的新詞產生,往分詞器的詞典中添加新詞的步驟: 
一、在類目錄下IK的配置文件:IKAnalyzer.cfg.xml 中增長配置擴展詞文件的節點:
    <entry key="ext_dict">ext.dic</entry>
   若有多個,以「;」間隔
二、在類目錄下建立擴展詞文件 ext.dic
四、編輯該文件加入新詞,一行一個

測試語句: 厲害了個人國一經播出,受到各方好評,強烈激發了國人的愛國之情、自豪感! 新詞:厲害了個人國

IKAnalyzer.cfg.xml 文件示例

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
<properties>  
<comment>IK Analyzer 擴展配置</comment>
<!--用戶能夠在這裏配置本身的擴展字典 -->
<entry key="ext_dict">ext.dic</entry> 
<!--用戶能夠在這裏配置本身的擴展中止詞字典-->
<entry key="ext_stopwords">my_ext_stopword.dic</entry>
</properties>
相關文章
相關標籤/搜索