1. 程式人生 > >Lucene筆記19-Lucene的分詞-實現自定義同義詞分詞器-實現分詞器

Lucene筆記19-Lucene的分詞-實現自定義同義詞分詞器-實現分詞器

一、同義詞分詞器的程式碼實現

package com.wsy;

import com.chenlb.mmseg4j.Dictionary;
import com.chenlb.mmseg4j.MaxWordSeg;
import com.chenlb.mmseg4j.analysis.MMSegTokenizer;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

public class MySameAnalyzer extends Analyzer {
    @Override
    public TokenStream tokenStream(String string, Reader reader) {
        // 指定分詞字典
        Dictionary dictionary = Dictionary.getInstance("E:\\Lucene\\mmseg4j-1.8.5\\data");
        return new MySameTokenFilter(new MMSegTokenizer(new MaxWordSeg(dictionary), reader));
    }

    public static void displayAllToken(String string, Analyzer analyzer) {
        try {
            TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(string));
            // 放入屬性資訊,為了檢視流中的資訊
            // 位置增量資訊,語彙單元之間的距離
            PositionIncrementAttribute positionIncrementAttribute = tokenStream.addAttribute(PositionIncrementAttribute.class);
            // 每個語彙單元的位置偏移量資訊
            OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
            // 每一個語彙單元的分詞資訊
            CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
            // 使用的分詞器的型別資訊
            TypeAttribute typeAttribute = tokenStream.addAttribute(TypeAttribute.class);
            while (tokenStream.incrementToken()) {
                System.out.println(positionIncrementAttribute.getPositionIncrement() + ":" + charTermAttribute + "[" + offsetAttribute.startOffset() + "-" + offsetAttribute.endOffset() + "]-->" + typeAttribute.type());
            }
            System.out.println("----------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        String string = "我來自中國山東聊城。";
        MySameAnalyzer analyzer = new MySameAnalyzer();
        Directory directory = new RAMDirectory();
        IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35, analyzer));
        Document document = new Document();
        document.add(new Field("content", string, Field.Store.YES, Field.Index.ANALYZED));
        indexWriter.addDocument(document);
        indexWriter.close();
        IndexSearcher indexSearcher = new IndexSearcher(IndexReader.open(directory));
        TopDocs topDocs = indexSearcher.search(new TermQuery(new Term("content", "天朝")), 10);
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        if (scoreDocs.length > 0) {
            document = indexSearcher.doc(scoreDocs[0].doc);
            System.out.println(document.get("content"));
        }
        MySameAnalyzer.displayAllToken(string, analyzer);
    }
}
package com.wsy;

import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

public class MySameTokenFilter extends TokenFilter {
    private CharTermAttribute charTermAttribute;
    private PositionIncrementAttribute positionIncrementAttribute;
    private State state;
    private Stack<String> stack;

    protected MySameTokenFilter(TokenStream input) {
        super(input);
        charTermAttribute = this.addAttribute(CharTermAttribute.class);
        positionIncrementAttribute = this.addAttribute(PositionIncrementAttribute.class);
        stack = new Stack();
    }

    // 這裡的incrementToken()方法有點像iterator.hasnext()
    // 如果後面還有待處理的元素,那麼返回true
    // 如果後面沒有待處理的元素,那麼返回false
    @Override
    public boolean incrementToken() throws IOException {
        // 如果棧中有同義詞
        if (stack.size() > 0) {
            // 出棧並拿到這個元素
            String string = stack.pop();
            // 還原狀態即獲取到之前狀態的一個副本
            restoreState(state);
            // 將當前token的內容清空並新增上同義詞
            charTermAttribute.setEmpty();
            charTermAttribute.append(string);
            // 設定當前token和前一個token的間隔是0,也就是和前一個的位置一樣
            positionIncrementAttribute.setPositionIncrement(0);
            return true;
        }
        if (input.incrementToken() == false) {
            return false;
        }
        if (getSameWords(charTermAttribute.toString())) {
            // 如果有同義詞就捕獲當前狀態
            state = captureState();
        }
        return true;
    }

    private boolean getSameWords(String key) {
        Map<String, String[]> map = new HashMap();
        map.put("中國", new String[]{"天朝", "大陸"});
        map.put("我", new String[]{"俺", "咱"});
        String[] sameWords = map.get(key);
        if (sameWords != null) {
            for (String sameWord : sameWords) {
                stack.push(sameWord);
            }
            return true;
        }
        return false;
    }
}

參與搜尋的字串是“我來自中國山東聊城。”,我們使用了同義詞分詞器處理這個字串,將“我”和“中國”做了同義詞處理。當搜尋“中國”的同義詞“天朝”的時候,我們發現,同樣可以把結果搜尋出來,但是原字串裡面並沒有“天朝”,這就是同義詞分詞器的作用效果了。

二、程式碼詳細解釋

單純的看上面的程式碼,去理解這個過程可能有些吃力,那麼下面,我就帶著大家一步一步的來分析下,上面這段程式是怎麼執行的,更好的理解一下同義詞分詞器的執行原理和實現原理,這裡主要看一下MySameTokenFilter類中的incrementToken()方法,理解了這個方法,就知道了怎麼實現的同義詞的添加了。

既然說到同義詞,那麼就要用一個map去儲存,map的key是一個詞語,map的value是這個詞語的所有同義詞,所以泛型寫法是<String, String[]>,這裡用到一個數據結構:棧。為什麼要用到棧呢?因為我們在處理同義詞的時候,當把一個同義詞新增到tokenStream中之後,這個同義詞就用不到了,於是就需要從集合中移除,一是為了處理剩下的同義詞,二是為了防止干擾,自然而然就想到了需要使用棧來解決這個問題。這裡我們先模擬建立一個map,自己定義兩個同義詞,即“中國”和{“天朝”與“大陸”},“我”和{“俺”與“咱”}。其中getSameWords()方法就是根據key來獲取同義詞,如果獲取到了同義詞,就將同義詞放到棧中備用,並返回true表示有同義詞需要處理,否則返回false即可。

如果待分詞的字串是“我來自中國山東聊城”,不做任何處理的情況下,會被分成{"我","來自","中國","山東","聊","城"},下面來看incrementToken()方法。

第一個字分詞“我”的時候,stack是空的,因為還沒有執行到後面的getSameWords()方法,第一個if沒有進去,因為後面還有元素,第二個if沒有進去,第三個if進去了,因為getSameWords("我")返回true,將“俺”與“咱”壓入了棧中,並儲存了當前分詞“我”的狀態,因為走到“來自”的時候,“我”的狀態就被覆蓋了,所以,對於需要做同義詞的詞語,需要儲存狀態,防止被後面的覆蓋,最後方法返回true。incrementToken()方法的返回值有點像iterator.hasnext(),如果有下一個元素,返回true,沒有下一個元素返回false。因為還有元素,incrementToken()方法會再次執行,第一個if進去了,因為此時棧中有兩個元素,彈出棧頂元素並獲取,當前遍歷到的元素是“來自”,所以需要還原回“我”的狀態,將charTermAttribute進行清空並賦新值,設定和前一個元素的間隔是0,也就意味著“咱”和“我”在同一個位置,此時方法體返回true。繼續執行,第一個if又進去了,因為還有“俺”在棧中,同樣的操作,方法體返回true。

當前遍歷的元素是“來自”,檢視棧中已經是空的,第一個if沒進去,第二個if沒進去,第三個if沒進去,方法體返回true。

當前遍歷的元素是“中國”,類比“我”的情景來思考一下即可。

我突然發奇想了,當我把待分詞的字串改為“我來自中國”後,“中國”後面沒有東西了,還會處理“中國”的同義詞嗎?執行之後,發現“中國”的同義詞也做了處理,單步除錯看看為什麼,當前元素走到“中國”的時候,incrementToken()並不是false,Lucene自動在最後一個分詞後加了一個空字串,所以處理完了“中國”兩個字,當前元素是空串,才返回false。

最後看一下執行結果吧。可以看出來“俺”和“咱”的positionIncrement的值是0,意味著它們和“我”在同一個位置的。

我來自中國
1:我[0-1]-->word
0:咱[0-1]-->word
0:俺[0-1]-->word
1:來自[1-3]-->word
1:中國[3-5]-->word
0:大陸[3-5]-->word
0:天朝[3-5]-->word

在getSameWords()方法中,還有待優化的地方,具體優化內容請看下一篇筆記