Lucene系列三:Lucene分詞器詳解、實現自己的一個分詞器


一、Lucene分詞器詳解

1. 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方法,不能被覆蓋的,在這兩個方法中是如何構建分詞處理器的呢?

對應源碼分析:

  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?

  從components.getTokenStream()得到了TokenStream

問題2:方法傳入的字符流Reader 給了誰?

  方法傳入的字符流Reader 最終給了Tokenizer的inputPending(類型:Reader):initReader(fieldName, reader)-components.setReader(r)-source.setReader(reader)-this.inputPending = input;

問題3: components是什么?components的獲取邏輯是怎樣?

  components是分詞處理的組件,components的獲取邏輯是有就直接拿來用,沒有就新建一個,后面都用新建的這一個

問題4:createComponents(fieldName) 方法是個什么方法?

  是創建分詞處理組件的方法

問題5:Analyzer能直接創建對象嗎?

  Analyzer是一個抽象類,不能直接創建對象

問題6:為什么它要這樣設計?

  使用裝飾器模式方便擴展

問題7:請看一下Analyzer的實現子類有哪些?

問題8:要實現一個自己的Analyzer,必須實現哪個方法?

   必須實現protected abstract TokenStreamComponents createComponents(String fieldName);

(2)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;
}

(4)org.apache.lucene.analysis.TokenStream

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

問題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: 分項屬性(分項的信息):如 包含的詞、位置等

(5)TokenStream 的兩類子類

Tokenizer:分詞器,輸入是Reader字符流的TokenStream,完成從流中分出分項

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

問題1:請查看Tokenizer類的源碼及注釋,這個類該如何使用?要實現自己的Tokenizer只需要做什么?

要實現自己的Tokenizer只需要繼承Tokenizer復寫incrementToken()方法

問題2:請查看TokenFilter類的源碼及注釋,如何實現自己的TokenFilter?

   要實現自己的TokenFilter只需要繼承TokenFilter復寫incrementToken()方法

問題3:TokenFilter的子類有哪些?

 

問題4:TokenFilter是不是一個典型的裝飾器模式?如果我們需要對分詞進行各種處理,只需要按我們的處理順序一層層包裹即可(每一層完成特定的處理)。不同的處理需要,只需不同的包裹順序、層數。

 是

(6)TokenStream 繼承了 AttributeSource

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

概念說明:Attribute  屬性     Token Attribute  分項屬性(分項信息),如 分項的詞、詞的索引位置等等。這些屬性通過不同的Tokenizer /TokenFilter處理統計得出。不同的Tokenizer/TokenFilter組合,就會有不同的分項信息。它是會動態變化的,你不知道有多少,是什么。那該如何實現分項信息的存儲呢?

答案就是 AttributeSource、Attribute 、AttributeImpl、AttributeFactory

1、AttribureSource 負責存放Attribute對象,它提供對應的存、取方法

2、Attribute對象中則可以存儲一個或多個屬性信息

3、AttributeFactory 則是負責創建Attributre對象的工廠,在TokenStream中默認使用了AttributeFactory.getStaticImplementation 我們不需要提供,遵守它的規則即可。

(7)AttributeSource使用規則說明

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

<T extends Attribute> T addAttribute(Class<T> attClass) 該方法要求傳人你需要添加的屬性的接口類(繼承Attribute),返回對應的實現類實例給你。從接口到實例,這就是為什么需要AttributeFactory的原因。這個方法是我們常用的方法

void addAttributeImpl(AttributeImpl att)

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

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

4、在TokenStream中,我們用自己實現的Attribute,默認的工廠。當我們調用這個add方法時,它怎么知道實現類是哪個?這里有一定規則要遵守:

1、自定義的屬性接口 MyAttribute 繼承 Attribute

2、自定義的屬性實現類必須繼承 Attribute,實現自定義的接口MyAttribute

3、自定義的屬性實現類必須提供無參構造方法

4、為了讓默認工廠能根據自定義接口找到實現類,實現類名需為:接口名+Impl 。

(8)TokenStream 的使用步驟。

我們在應用中並不直接使用分詞器,只需為索引引擎和搜索引擎創建我們想要的分詞器對象。但我們在選擇分詞器時,會需要測試分詞器的效果,就需要知道如何使用得到的分詞處理器TokenStream,使用步驟:

1、從tokenStream獲得你想要獲得分項屬性對象(信息是存放在屬性對象中的)

2、調用 tokenStream 的 reset() 方法,進行重置。因為tokenStream是重復利用的。

3、循環調用tokenStream的incrementToken(),一個一個分詞,直到它返回false

4、在循環中取出每個分項你想要的屬性值。

5、調用tokenStream的end(),執行任務需要的結束處理。

6、調用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();
        }

    }
}

 8、運行分析器MyWhitespaceAnalyzer主程序得到結果

an|attributesource|contains|a|list|of|different|attributeimpls,|and|methods|to|add|and|get|them.|

 9. 源碼獲取地址https://github.com/leeSmall/SearchEngineDemo


免責聲明!

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



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