7、自定義分詞和中文分詞(lucene筆記)

一、自定義分詞器

這里我們自定義一個停用分詞器,也就是在進行分詞的時候將某些詞過濾掉。
MyStopAnalyzer.java

package cn.itcast.util;
import java.io.Reader;
import java.util.Set;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.LetterTokenizer;
import org.apache.lucene.analysis.LowerCaseFilter;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.analysis.StopFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.util.Version;

public class MyStopAnalyzer extends Analyzer {
    
    @SuppressWarnings("rawtypes")
    private Set stops;//用于存放分詞信息
    
    public MyStopAnalyzer() {
        stops = StopAnalyzer.ENGLISH_STOP_WORDS_SET;//默認停用的語匯信息
    }
    
    //這里可以將通過數組產生分詞對象
    public MyStopAnalyzer(String[] sws) {
        //System.out.println(StopAnalyzer.ENGLISH_STOP_WORDS_SET);
        stops = StopFilter.makeStopSet(Version.LUCENE_35, sws, true);//最后的參數表示忽略大小寫
        stops.addAll(StopAnalyzer.ENGLISH_STOP_WORDS_SET);
    }

    @Override
    public TokenStream tokenStream(String fieldName, Reader reader) {
        //注意:在分詞過程中會有一個過濾器鏈,最開始的過濾器接收一個Tokenizer,而最后一個接收一個Reader流
        //這里我們看到我們可以在過濾器StopFilter中接收LowerCaseFilter,而LowerCaseFilter接收一個Tokenizer
        //當然如果要添加更多的過濾器還可以繼續添加
        return new StopFilter(Version.LUCENE_35, new LowerCaseFilter(Version.LUCENE_35, 
                new LetterTokenizer(Version.LUCENE_35, reader)), stops);
    }
}

說明:

  • 這里我們定義一個Set集合用來存放分詞信息,其中在無參構造器我們將默認停用分詞器中停用的語匯單元賦給stops,這樣我們就可以使用默認停用分詞器中停用的語匯。而我們通過一個字符串數組將我們自己想要停用的詞傳遞進來,同時stops不接受泛型,也就是說不能直接將字符串數組賦值給stops,而需要使用makeStopSet方法將需要停用的詞轉換為相應的語匯單元,然后再添加給stops進行存儲。
  • 自定義的分詞器需要繼承Analyzer接口,實現tokenStream方法,此方法接收三個參數,第一個是版本,最后一個是停用的語匯單元,這里是stops,而第二個參數是別的分詞器,因為分詞過程中是一個分詞器鏈。

測試:
TestAnalyzer.java

@Test
public void test04(){
    //對中文分詞不適用
    Analyzer analyzer = new MyStopAnalyzer(new String[]{"I","you"});
    Analyzer analyzer2 = new StopAnalyzer(Version.LUCENE_35);//停用分詞器
    
    String text = "how are you thank you I hate you";
    System.out.println("************自定義分詞器***************");
    AnalyzerUtils.displayAllTokenInfo(text, analyzer);
    System.out.println("************停用分詞器***************");
    AnalyzerUtils.displayAllTokenInfo(text, analyzer2);
}

說明:從測試結果中我們可以很容易看出自定義分詞器和默認分詞器之間的區別,自定義分詞相比默認分詞器多了我們自定義的詞語。

1

二、中文分詞器

這里我們使用MMSEG中文分詞器,其分詞信息使用的是搜狗詞庫。我們使用的是版本1.8.5.這個版本的包中有兩個可用的jar包:

mmseg4j-all-1.8.5.jar
mmseg4j-all-1.8.5-with-dic.jar

其中第二個相比第一個多了相關的語匯信息,便于我們進行分詞,當然我們可以使用第一個,但是這樣便和默認分詞器沒有多大差別,我們在方法中直接測試:

@Test
public void test02(){
    //對中文分詞不適用
    Analyzer analyzer1 = new StandardAnalyzer(Version.LUCENE_35);//標準分詞器
    Analyzer analyzer2 = new StopAnalyzer(Version.LUCENE_35);//停用分詞器
    Analyzer analyzer3 = new SimpleAnalyzer(Version.LUCENE_35);//簡單分詞器
    Analyzer analyzer4 = new WhitespaceAnalyzer(Version.LUCENE_35);//空格分詞器
    Analyzer analyzer5 = new MMSegAnalyzer();
    
    
    String text = "西安市雁塔區";
    AnalyzerUtils.displayToken(text, analyzer1);
    AnalyzerUtils.displayToken(text, analyzer2);
    AnalyzerUtils.displayToken(text, analyzer3);
    AnalyzerUtils.displayToken(text, analyzer4);
    AnalyzerUtils.displayToken(text, analyzer5);
}

說明:此時我們直接使用MMSEG中文分詞器,測試結果為:

2

我們看到和默認的分詞器并無多大差別,當然我們也可以在方法中指定相關語匯信息存放的目錄:

Analyzer analyzer5 = new MMSegAnalyzer(new File("E:/API/Lucene/mmseg/data"));

此時的測試結果為:

3

在目錄E:/API/Lucene/mmseg/data中存在四個文件:

chars.dic
units.dic
words.dic
words-my.dic

這寫文件便存放了相關的語匯單元,當然如果我們想停用某些詞,可以在最后一個文件中直接進行添加。

三、同義詞索引(1)

3.1思路

4

說明:首先我們需要使用MMSEG進行分詞,之后我們自定義的分詞器從同義詞容器中取得相關的同義詞,然后將同義詞存儲在同一個位置,我們在之前講過,就是同一個偏移量可以有多個語匯單元。

3.2 自定義分詞器

MySameAnalyzer.java

package cn.itcast.util;
import java.io.Reader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import com.chenlb.mmseg4j.Dictionary;
import com.chenlb.mmseg4j.MaxWordSeg;
import com.chenlb.mmseg4j.analysis.MMSegTokenizer;

public class MySameAnalyzer extends Analyzer {

    @Override
    public TokenStream tokenStream(String fieldName, Reader reader) {
        
        Dictionary dic = Dictionary.getInstance("E:/API/Lucene/mmseg/data");
        
        //我們首先使用MMSEG進行分詞,將相關內容分成一個一個語匯單元
        return new MySameTokenFilter(new MMSegTokenizer(new MaxWordSeg(dic), reader));
    }
}

說明:和之前一樣還是需要實現Analyzer接口。這里我們實例化Dictionary對象,此對象是單例的,用于保存相關的語匯信息。可以看到,首先是經過MMSEG分詞器,將相關內容分成一個一個的語匯單元。

自定義同義詞過濾器MySameTokenFilter.java

package cn.itcast.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;

public class MySameTokenFilter extends TokenFilter {
    
    private CharTermAttribute cta = null;

    protected MySameTokenFilter(TokenStream input) {
        super(input);
        cta = this.addAttribute(CharTermAttribute.class);
    }

    @Override
    public boolean incrementToken() throws IOException {
        if(!this.input.incrementToken()){//如果輸入進來的內容中沒有元素
            return false;
        }
        //如果有,則需要進行相應的處理,進行同義詞的判斷處理
        String[] sws = getSameWords(cta.toString());
        if(sws != null){
            //處理
            for(String s : sws){
                cta.setEmpty();
                cta.append(s);
            }
        }
        return true;
    }
    
    private String[] getSameWords(String name){
        Map<String, String[]> maps = new HashMap<String, String[]>();
        maps.put("中國", new String[]{"天朝", "大陸"});
        maps.put("我", new String[]{"咱", "俺"});
        return maps.get(name);
    }
}

說明:這里我們需要定義一個CharTermAttribute 屬性,在之前說過,這個類相當于在分詞流中的一個標記。

相關方法AnalyzerUtils.java

public static void displayAllTokenInfo(String str, Analyzer analyzer){
    try {
        TokenStream stream = analyzer.tokenStream("content", new StringReader(str));
        PositionIncrementAttribute pia = stream.addAttribute(PositionIncrementAttribute.class);
        OffsetAttribute oa = stream.addAttribute(OffsetAttribute.class);
        CharTermAttribute cta = stream.addAttribute(CharTermAttribute.class);
        TypeAttribute ta = stream.addAttribute(TypeAttribute.class);

        while (stream.incrementToken()) {
            System.out.print("位置增量: " + pia.getPositionIncrement());//詞與詞之間的空格
            System.out.print(",單詞: " + cta + "[" + oa.startOffset() + "," + oa.endOffset() + "]");
            System.out.print(",類型: " + ta.type()) ;
            System.out.println();
        }
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}

測試:

@Test
public void test05(){
    //對中文分詞不適用
    Analyzer analyzer = new MySameAnalyzer();
    
    String text = "我來自中國西安市雁塔區";
    System.out.println("************自定義分詞器***************");
    AnalyzerUtils.displayAllTokenInfo(text, analyzer);
}

說明:整個執行流程就是:

  • 1.首先實例化一個自定義的分詞器MySameAnalyzer,在此分詞器中實例化一個MySameTokenFilter過濾器,而從過濾器中的參數中可以看到接收MMSEG分詞器,而MySameTokenFilter的構造方法中接收一個分詞流,然后將CharTermAttribute加入到此流中。
  • 2.在displayAllTokenInfo方法中我們調用incrementToken方法時先是調用getSameWords方法查看分詞流中有沒有同義詞,如果沒有則直接返回,否則進行相關的處理。
  • 3.在這里的處理方式中,先是使用方法setEmpty將原來的語匯單元清除,然后將此語匯單元同義詞添加進去,但是這樣就將原來的語匯單元刪除了,這顯然不符合要求。測試結果為:
    5

    可以看到將“我”換成了“俺”,將“中國”換成了“大陸”。也就是說我們使用同義詞將原來的詞語替換掉了。

解決方法
我們之前說過,每個語匯單元都有一個位置,這個位置由PositionIncrTerm屬性保存,如果兩個語匯單元的位置相同,或者說距離為0,那么就表示是同義詞了。而我們看到上面的測試結果中每個語匯單元的距離都為1,顯然不是同義詞。而對于上面例子中的問題,我們可以這樣解決:
MySameTokenFilter.java

package cn.itcast.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
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 org.apache.lucene.util.AttributeSource;

public class MySameTokenFilter extends TokenFilter {
    
    private CharTermAttribute cta = null;
    private PositionIncrementAttribute pia = null;
    private AttributeSource.State current ;
    private Stack<String> sames = null;

    protected MySameTokenFilter(TokenStream input) {
        super(input);
        cta = this.addAttribute(CharTermAttribute.class);
        pia = this.addAttribute(PositionIncrementAttribute.class);
        sames = new Stack<String>();
    }

    @Override
    public boolean incrementToken() throws IOException {
        while(sames.size() > 0){
            //將元素出棧,并且獲取這個同義詞
            String str = sames.pop();
            restoreState(current);//還原到原來的狀態
            cta.setEmpty();
            cta.append(str);
            //設置位置為0
            pia.setPositionIncrement(0);
            return true;
        }
        
        if(!this.input.incrementToken()){//如果輸入進來的內容中沒有元素
            return false;
        }
        if(getSameWords(cta.toString())){
            //如果有同義詞,捕獲當前的狀態
            current = captureState();
        }
        return true;
    }
    
    private boolean getSameWords(String name){
        Map<String, String[]> maps = new HashMap<String, String[]>();
        maps.put("中國", new String[]{"天朝", "大陸"});
        maps.put("我", new String[]{"咱", "俺"});
        String[] sws = maps.get(name);
        if(sws != null){
            for(String s : sws){
                sames.push(s);
            }
            return true;
        }
        return false;
    }
}

說明:

  • 1.首先我們添加了三個屬性PositionIncrementAttribute 、AttributeSource.State、Stack,分別是位置屬性、當前狀態、棧。其中棧用來保存同義詞單元。在構造函數中初始化相關屬性。
  • 2.在調用incrementToken方法開始時我們先使用方法incrementToken,讓標記CharTermAttribute 向后移動一個位置,同時將本位置(current )保留下來。而此時第一個語匯單元“我”已經寫入到分詞流中了,然后我們利用current在讀取到同義詞之后回到前一個位置進行添加同義詞,其實就是將同義詞的位置設置為0(同義詞之間的位置為0),這樣就將原始單元和同義詞單元都寫入到了分詞流中了。這就將第一個單元的同義詞設置好了,立即返回,進入到下一個語匯單元進行處理。
  • 測試結果為:


    6

下面我們編寫一個測試方法進行同義詞查詢操作:

@Test
public void test06() throws CorruptIndexException, LockObtainFailedException, IOException{
    //對中文分詞不適用
    Analyzer analyzer = new MySameAnalyzer();
    
    String text = "我來自中國西安市雁塔區";
    Directory dir = new RAMDirectory();
    IndexWriter write = new IndexWriter(dir, new IndexWriterConfig(Version.LUCENE_35, analyzer));
    Document doc = new Document();
    doc.add(new Field("content", text, Field.Store.YES, Field.Index.ANALYZED));
    write.addDocument(doc);
    write.close();
    IndexSearcher searcher = new IndexSearcher(IndexReader.open(dir));
    //TopDocs tds = searcher.search(new TermQuery(new Term("content", "中國")), 10);
    TopDocs tds = searcher.search(new TermQuery(new Term("content", "大陸")), 10);
    Document d = searcher.doc(tds.scoreDocs[0].doc);
    System.out.println(d.get("content"));
    System.out.println("************自定義分詞器***************");
    AnalyzerUtils.displayAllTokenInfo(text, analyzer);
}

說明:我們在查詢的時候可以使用“中國”的同義詞“大陸”進行查詢。但是這種方式并不好,因為將將同義詞等信息都寫死了,不便于管理。

四、同義詞索引(2)

(工程lucene_analyzer02
這里我們專門創建一個類用來存放同義詞:
SamewordContext.java

package cn.itcast.util;
public interface SamewordContext {
    public String[] getSamewords(String name);
}

實現SimpleSamewordContext.java

package cn.itcast.util;
import java.util.HashMap;
import java.util.Map;

public class SimpleSamewordContext implements SamewordContext {
    
    private Map<String, String[]> maps = new HashMap<String, String[]>();
    
    public SimpleSamewordContext() {
        maps.put("中國", new String[]{"天朝", "大陸"});
        maps.put("我", new String[]{"咱", "俺"});
    }
    
    @Override
    public String[] getSamewords(String name) {
        return  maps.get(name);
    }
}

說明:這里我們只是簡單的實現了接口,封裝了一些同義詞,之后我們在使用的時候便可以使用此類來獲取同義詞。測試我們需要改進相關的類:
MySameTokenFilter.java

package cn.itcast.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
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 org.apache.lucene.util.AttributeSource;

public class MySameTokenFilter extends TokenFilter {
    
    private CharTermAttribute cta = null;
    private PositionIncrementAttribute pia = null;
    private AttributeSource.State current ;
    private Stack<String> sames = null;
    private SamewordContext samewordContext ;//用來存儲同義詞

    protected MySameTokenFilter(TokenStream input, SamewordContext samewordContext) {
        super(input);
        cta = this.addAttribute(CharTermAttribute.class);
        pia = this.addAttribute(PositionIncrementAttribute.class);
        sames = new Stack<String>();
        this.samewordContext = samewordContext;
    }

    @Override
    public boolean incrementToken() throws IOException {
        
        while(sames.size() > 0){
            //將元素出棧,并且獲取這個同義詞
            String str = sames.pop();
            restoreState(current);//還原到原來的狀態
            cta.setEmpty();
            cta.append(str);
            //設置位置為0
            pia.setPositionIncrement(0);
            return true;
        }
        
        if(!this.input.incrementToken()){//如果輸入進來的內容中沒有元素
            return false;
        }
        if(addSames(cta.toString())){
            //如果有同義詞,捕獲當前的狀態
            current = captureState();
        }
        return true;
    }
    
    private boolean addSames(String name){
        String[] sws = samewordContext.getSamewords(name);
        if(sws != null){
            for(String s : sws){
                sames.push(s);
            }
            return true;
        }
        return false;
    }
}

說明:在此類中我們太添加了一個屬性SamewordContext,用來保存相關的同義詞,在方法addSames中使用此類來獲取相關的同義詞。于是我們在后面使用MySameTokenFilter類的時候需要通過構造函數將此類傳遞進去。注意:這里需要面向接口編程,在后面我們需要想更換同義詞存儲類,只需要重現實現接口即可。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,915評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,242評論 25 708
  • 一、概述 1.1 分詞的基本過程 首先是TokenStream通過接收一個StringReader流將需要進行分詞...
    yjaal閱讀 833評論 0 1
  • 上午牛牛(6歲)跟爺爺去醫院打乙肝育苗第三針,爺爺給了醫生100元,醫生問牛牛:“這一針是50元,我該找你多少錢?...
    玉如藍閱讀 319評論 0 3
  • 承接上次的文章,另有三本分享給大家。 1、《紅與黑》 幸福,不就近在咫尺嗎?過這樣的生活,無需多少花費。我可以隨自...
    李澤賢閱讀 736評論 0 12