分析器,分詞器組件的核心API,它的職責:構建真正對文本進行分詞處理的TokenStream(分詞處理器)。經過調用它的以下兩個方法,獲得輸入文本的分詞處理器。java
public final TokenStream tokenStream(String fieldName, Reader reader) public final TokenStream tokenStream(String fieldName, String text)
這兩個方法是final方法,不能被覆蓋的,在這兩個方法中是如何構建分詞處理器的呢?git
對應源碼分析:github
public final TokenStream tokenStream(final String fieldName, final Reader reader) { TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName); final Reader r = initReader(fieldName, reader); if (components == null) { components = createComponents(fieldName); reuseStrategy.setReusableComponents(this, fieldName, components); } components.setReader(r); return components.getTokenStream(); }
問題1:從哪裏獲得了TokenStream?apache
從components.getTokenStream()獲得了TokenStreammaven
問題2:方法傳入的字符流Reader 給了誰?ide
方法傳入的字符流Reader 最終給了Tokenizer的inputPending(類型:Reader):initReader(fieldName, reader)-components.setReader(r)-source.setReader(reader)-this.inputPending = input;源碼分析
問題3: components是什麼?components的獲取邏輯是怎樣?測試
components是分詞處理的組件,components的獲取邏輯是有就直接拿來用,沒有就新建一個,後面都用新建的這一個this
問題4:createComponents(fieldName) 方法是個什麼方法?搜索引擎
是建立分詞處理組件的方法
問題5:Analyzer能直接建立對象嗎?
Analyzer是一個抽象類,不能直接建立對象
問題6:爲何它要這樣設計?
使用裝飾器模式方便擴展
問題7:請看一下Analyzer的實現子類有哪些?
問題8:要實現一個本身的Analyzer,必須實現哪一個方法?
必須實現protected abstract TokenStreamComponents createComponents(String fieldName);
是Analizer中惟一的抽象方法,擴展點。經過提供該方法的實現來實現本身的Analyzer。
參數說明:fieldName,若是咱們須要爲不一樣的字段建立不一樣的分詞處理器組件,則可根據這個參數來判斷。不然,就用不到這個參數。
返回值爲 TokenStreamComponents 分詞處理器組件。
咱們須要在createComponents方法中建立咱們想要的分詞處理器組件。
(3)TokenStreamComponents 對應源碼分析:
分詞處理器組件:這個類中封裝有供外部使用的TokenStream分詞處理器。提供了對source(源)和sink(供外部使用分詞處理器)兩個屬性的訪問方法。
問題1:這個類的構造方法有幾個?區別是什麼?從中能發現什麼?
兩個構造方法:
public TokenStreamComponents(final Tokenizer source, final TokenStream result) { this.source = source; this.sink = result; }
public TokenStreamComponents(final Tokenizer source) { this.source = source; this.sink = source; }
區別是參數不同,能夠發現source和sink有繼承關係
問題2:source 和 sink屬性分別是什麼類型?這兩個類型有什麼關係?
Tokenizer source、TokenStream sink,Tokenizer是TokenStream的子類
問題3:在這個類中沒有建立source、sink對象的代碼(而是由構造方法傳入)。也就是說咱們在Analyzer.createComponents方法中建立它的對象前,需先建立什麼?
需先建立source、sink
問題4:在Analyzer中tokenStream() 方法中把輸入流給了誰?獲得的TokenStream對象是誰?TokenStream對象sink中是否必須封裝有source對象?
輸入流給了source.setReader(reader),獲得的TokenStream對象是TokenStream sink
components.getTokenStream()-》
public TokenStream getTokenStream() {
return sink;
}
分詞處理器,負責對輸入文本完成分詞、處理。
問題1:TokenStream對象sink中是否必須封裝有source對象,TokenStream中有沒有對應的給入方法?
沒有
問題2:TokenStream是一個抽象類,有哪些方法,它的抽象方法有哪些?它的構造方法有什麼特色?
抽象方法:
public abstract boolean incrementToken() throws IOException;
構造方法有兩個:
protected TokenStream(AttributeSource input) { super(input); assert assertFinal(); }
protected TokenStream(AttributeFactory factory) { super(factory); assert assertFinal(); }
概念說明:Token: 分項,從字符流中分出一個一個的項
問題3:TokenStream的具體子類分爲哪兩類?有什麼區別?
問題4:TokenStream繼承了誰?它是幹什麼用的?
繼承了AttributeSource,TokenStream進行分詞處理的
概念說明:Token Attribute: 分項屬性(分項的信息):如 包含的詞、位置等
Tokenizer:分詞器,輸入是Reader字符流的TokenStream,完成從流中分出分項
TokenFilter:分項過濾器,它的輸入是另外一個TokenStream,完成對從上一個TokenStream中流出的token的特殊處理。
問題1:請查看Tokenizer類的源碼及註釋,這個類該如何使用?要實現本身的Tokenizer只須要作什麼?
要實現本身的Tokenizer只須要繼承Tokenizer複寫incrementToken()方法
問題2:請查看TokenFilter類的源碼及註釋,如何實現本身的TokenFilter?
要實現本身的TokenFilter只須要繼承TokenFilter複寫incrementToken()方法
問題3:TokenFilter的子類有哪些?
問題4:TokenFilter是否是一個典型的裝飾器模式?若是咱們須要對分詞進行各類處理,只須要按咱們的處理順序一層層包裹便可(每一層完成特定的處理)。不一樣的處理須要,只需不一樣的包裹順序、層數。
是
問題1:咱們在TokenStream及它的兩個子類中是否有看到關於分項信息的存儲,如該分項的詞是什麼、這個詞的位置索引?
概念說明:Attribute 屬性 Token Attribute 分項屬性(分項信息),如 分項的詞、詞的索引位置等等。這些屬性經過不一樣的Tokenizer /TokenFilter處理統計得出。不一樣的Tokenizer/TokenFilter組合,就會有不一樣的分項信息。它是會動態變化的,你不知道有多少,是什麼。那該如何實現分項信息的存儲呢?
答案就是 AttributeSource、Attribute 、AttributeImpl、AttributeFactory
一、AttribureSource 負責存放Attribute對象,它提供對應的存、取方法
二、Attribute對象中則能夠存儲一個或多個屬性信息
三、AttributeFactory 則是負責建立Attributre對象的工廠,在TokenStream中默認使用了AttributeFactory.getStaticImplementation 咱們不須要提供,遵照它的規則便可。
一、某個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 。
咱們在應用中並不直接使用分詞器,只需爲索引引擎和搜索引擎建立咱們想要的分詞器對象。但咱們在選擇分詞器時,會須要測試分詞器的效果,就須要知道如何使用獲得的分詞處理器TokenStream,使用步驟:
一、從tokenStream得到你想要得到分項屬性對象(信息是存放在屬性對象中的)
二、調用 tokenStream 的 reset() 方法,進行重置。由於tokenStream是重複利用的。
三、循環調用tokenStream的incrementToken(),一個一個分詞,直到它返回false
四、在循環中取出每一個分項你想要的屬性值。
五、調用tokenStream的end(),執行任務須要的結束處理。
六、調用tokenStream的close()方法,釋放佔有的資源。
經過前面的源碼分析下面咱們來實現本身的的一個英文分詞器
Tokenizer: 實現對英文按空白字符進行分詞。 須要記錄的屬性信息有: 詞
TokenFilter: 要進行的處理:轉爲小寫
說明:Tokenizer分詞時,是從字符流中一個一個字符讀取,判斷是不是空白字符來進行分詞。
1. 新建一個maven項目CustomizeTokenStreamByLucene
2. 在pom.xml裏面引入lucene 核心模塊
<!-- lucene 核心模塊 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>7.3.0</version> </dependency>
3.創建本身的Attribute接口MyCharAttribute
package com.study.lucene.myattribute; import org.apache.lucene.util.Attribute; /** * 1.創建本身的Attribute接口MyCharAttribute * @author THINKPAD * */ public interface MyCharAttribute extends Attribute { void setChars(char[] buffer, int length); char[] getChars(); int getLength(); String getString(); }
4.創建自定義attribute接口MyCharAttribute的實現類MyCharAttributeImpl
package com.study.lucene.myattribute; import org.apache.lucene.util.AttributeImpl; import org.apache.lucene.util.AttributeReflector; /** * 2.創建自定義attribute接口MyCharAttribute的實現類MyCharAttributeImpl * 注意:MyCharAttributeImpl必定要和MyCharAttribute放在一個包下,不然會出現沒有MyCharAttribute的實現類, * 這是由org.apache.lucene.util.AttributeFactory.DefaultAttributeFactory.findImplClass(Class<? extends Attribute>)這個方法決定的 * @author THINKPAD * */ public 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) { } }
5. 創建分詞器MyWhitespaceTokenizer:實現對英文按空白字符進行分詞
package com.study.lucene.mytokenizer; import java.io.IOException; import org.apache.lucene.analysis.Tokenizer; import com.study.lucene.myattribute.MyCharAttribute; /** * 3. 創建分詞器MyWhitespaceTokenizer:實現對英文按空白字符進行分詞 * @author THINKPAD * */ public 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; } } }
6.創建分項過濾器:把大寫字母轉換爲小寫字母
package com.study.lucene.mytokenfilter; import java.io.IOException; import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import com.study.lucene.myattribute.MyCharAttribute; /** * 4.創建分項過濾器:把大寫字母轉換爲小寫字母 * @author THINKPAD * */ public 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; } }
7. 創建分析器MyWhitespaceAnalyzer
package com.study.lucene.myanalyzer; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.Tokenizer; import com.study.lucene.myattribute.MyCharAttribute; import com.study.lucene.mytokenfilter.MyLowerCaseTokenFilter; import com.study.lucene.mytokenizer.MyWhitespaceTokenizer; /** * 5. 創建分析器 * @author THINKPAD * */ 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); } 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(); ana.close(); System.out.println(); } catch (IOException e) { e.printStackTrace(); } } }
八、運行分析器MyWhitespaceAnalyzer主程序獲得結果
an|attributesource|contains|a|list|of|different|attributeimpls,|and|methods|to|add|and|get|them.|
9. 源碼獲取地址https://github.com/leeSmall/SearchEngineDemo