Lucene對文本解析是作為全文索引及全文檢索的預處理形式出現的,因此在一般的Lucene文檔中,這一部分都不是重點,往往一帶而過,但是對於要建立基於文本的內容推薦引擎來說,卻是相當關鍵的一步,因此有必要認真研究一下Lucene對文解析的過程。
Lucene對文本的解析對使用者的介面是Analyzer的某個子類,Lucene內置了幾個子類,但是對於英文來說StandardAnalyzer是最常用的一個子類,可以處理一般英文的文解析功能。但是對於漢字而言,Lucene提供了兩個擴展包,一個是CJKAnalyzer和SmartChineseAnalyzer,其中SmartAnalyzer對處理中文分詞非常適合,但是遺憾的是,該類將詞典利用隱馬可夫過程演算法,集成在了演算法裡,這樣的優點是減小了體積,並且安裝方便,但是如果想向詞庫中添加單詞就需要重新學習,不太方便。因此我們選擇了MMSeg4j,這個開源的中文分詞模組,這個開源軟體的最大優點就可用戶可擴展中文詞庫,非常方便,缺點是體積大載入慢。
首先通過一個簡單的程式來看中文分詞的使用:
Analyzer analyzer = null;
//analyzer = new StandardAnalyzer(Version.LUCENE_33);
//analyzer = new SimpleAnalyzer(Version.LUCENE_33);
analyzer = new MMSegAnalyzer();
TokenStream tokenStrm = analyzer.tokenStream("content", new StringReader(examples));
OffsetAttribute offsetAttr = tokenStrm.getAttribute(OffsetAttribute.class);
CharTermAttribute charTermAttr = tokenStrm.getAttribute(CharTermAttribute.class);
PositionIncrementAttribute posIncrAttr =
tokenStrm.addAttribute(PositionIncrementAttribute.class);
TypeAttribute typeAttr = tokenStrm.addAttribute(TypeAttribute.class);
String term = null;
int i = 0;
int len = 0;
char[] charBuf = null;
int termPos = 0;
int termIncr = 0;
try {
while (tokenStrm.incrementToken()) {
charBuf = charTermAttr.buffer();
termIncr = posIncrAttr.getPositionIncrement();
if (termIncr > 0) {
termPos += termIncr;
}
for (i=(charBuf.length - 1); i>=0; i--) {
if (charBuf[i] > 0) {
len = i + 1;
break;
}
}
//term = new String(charBuf, offsetAttr.startOffset(), offsetAttr.endOffset());
term = new String(charBuf, 0, offsetAttr.endOffset() - offsetAttr.startOffset());
System.out.print("[" + term + ":" + termPos + "/" + termIncr + ":" +
typeAttr.type() + ";" + offsetAttr.startOffset() + "-" + offsetAttr.endOffset() + "] ");
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
這裡需要注意的是:
TermAttribute已經在Lucene的新版本中被標為過期,所以程式中使用CharTermAttribute來提取每個中文分詞的資訊
MMSegAnalyzer的分詞效果在英文的條件下基本與Lucene內置的StandardAnalyzer相同
可以進行初步的中文分詞之後,我們還需處理停止詞去除,例如的、地、得、了、呀等語氣詞,還有就是添加同義詞:第一種是完全意義上的同義詞,如手機和行動電話,第二種是縮寫與全稱,如中國和中華人民共和國,第三種是中文和英文,如電腦和PC,第四種是各種專業詞彙同義詞,如藥品名和學名,最後可能還有一些網路詞語如神馬和什麼等。
在Lucene架構下,有兩種實現方式,第一種是編寫TokenFilter類來實現轉換和添加,還有一種就是直接集成在相應的Analyzer中實現這些功能。如果像Lucene這樣的開源軟體,講求系統的可擴展性的話,選擇開發獨立的TokenFilter較好,但是對於我們自己的項目,選擇集成在Analyzer中將是更好的選擇,這樣可以提高程式執行效率,因為TokenFilter需要重新逐個過一遍所有的單詞,效率比較低,而集成在Analyzer中可以保證在分解出單詞的過程中就完成了各種分詞操作,效率當然會提高了。
Lucene在文本解析中,首先會在Analyzer中調用Tokenizer,將文本分拆能最基本的單位,英文是單詞,中文是單字或片語,我們的去除停止詞和添加同義詞可以放入Tokenizer中,將每個新拆分的單詞進行處理,具體到我們所選用的MMSeg4j中文分詞模組來說,就是需要在MMSegTokenizer類的incrementToken方法中,添加去除停止詞和添加同義詞:
public boolean incrementToken() throws IOException {
if (0 == synonymCnt) {
clearAttributes();
Word word = mmSeg.next();
currWord = word;
if(word != null) {
// 去除截止詞如的、地、得、了等
String wordStr = word.getString();
if (stopWords.contains(wordStr)) {
return incrementToken();
}
if (synonymKeyDict.get(wordStr) != null) { // 如果具有同義詞則需要先添加本身這個詞,然後依次添加同義詞
synonymCnt = synonymDict.get(synonymKeyDict.get(wordStr)).size(); // 求出同義詞,作為結束條件控制
}
//termAtt.setTermBuffer(word.getSen(), word.getWordOffset(), word.getLength());
offsetAtt.setOffset(word.getStartOffset(), word.getEndOffset());
charTermAttr.copyBuffer(word.getSen(), word.getWordOffset(), word.getLength());
posIncrAttr.setPositionIncrement(1);
typeAtt.setType(word.getType());
return true;
} else {
end();
return false;
}
} else {
char[] charArray = null;
String orgWord = currWord.getString();
int i = 0;
Vector<String> synonyms = (Vector<String>)synonymDict.get(synonymKeyDict.get(orgWord));
if (orgWord.equals(synonyms.elementAt(synonymCnt - 1))) { // 如果是原文中出現的那個詞則不作任何處理
synonymCnt--;
return incrementToken();
}
Lucene對文本的解析對使用者的介面是Analyzer的某個子類,Lucene內置了幾個子類,但是對於英文來說StandardAnalyzer是最常用的一個子類,可以處理一般英文的文解析功能。但是對於漢字而言,Lucene提供了兩個擴展包,一個是CJKAnalyzer和SmartChineseAnalyzer,其中SmartAnalyzer對處理中文分詞非常適合,但是遺憾的是,該類將詞典利用隱馬可夫過程演算法,集成在了演算法裡,這樣的優點是減小了體積,並且安裝方便,但是如果想向詞庫中添加單詞就需要重新學習,不太方便。因此我們選擇了MMSeg4j,這個開源的中文分詞模組,這個開源軟體的最大優點就可用戶可擴展中文詞庫,非常方便,缺點是體積大載入慢。
首先通過一個簡單的程式來看中文分詞的使用:
Analyzer analyzer = null;
//analyzer = new StandardAnalyzer(Version.LUCENE_33);
//analyzer = new SimpleAnalyzer(Version.LUCENE_33);
analyzer = new MMSegAnalyzer();
TokenStream tokenStrm = analyzer.tokenStream("content", new StringReader(examples));
OffsetAttribute offsetAttr = tokenStrm.getAttribute(OffsetAttribute.class);
CharTermAttribute charTermAttr = tokenStrm.getAttribute(CharTermAttribute.class);
PositionIncrementAttribute posIncrAttr =
tokenStrm.addAttribute(PositionIncrementAttribute.class);
TypeAttribute typeAttr = tokenStrm.addAttribute(TypeAttribute.class);
String term = null;
int i = 0;
int len = 0;
char[] charBuf = null;
int termPos = 0;
int termIncr = 0;
try {
while (tokenStrm.incrementToken()) {
charBuf = charTermAttr.buffer();
termIncr = posIncrAttr.getPositionIncrement();
if (termIncr > 0) {
termPos += termIncr;
}
for (i=(charBuf.length - 1); i>=0; i--) {
if (charBuf[i] > 0) {
len = i + 1;
break;
}
}
//term = new String(charBuf, offsetAttr.startOffset(), offsetAttr.endOffset());
term = new String(charBuf, 0, offsetAttr.endOffset() - offsetAttr.startOffset());
System.out.print("[" + term + ":" + termPos + "/" + termIncr + ":" +
typeAttr.type() + ";" + offsetAttr.startOffset() + "-" + offsetAttr.endOffset() + "] ");
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
這裡需要注意的是:
TermAttribute已經在Lucene的新版本中被標為過期,所以程式中使用CharTermAttribute來提取每個中文分詞的資訊
MMSegAnalyzer的分詞效果在英文的條件下基本與Lucene內置的StandardAnalyzer相同
可以進行初步的中文分詞之後,我們還需處理停止詞去除,例如的、地、得、了、呀等語氣詞,還有就是添加同義詞:第一種是完全意義上的同義詞,如手機和行動電話,第二種是縮寫與全稱,如中國和中華人民共和國,第三種是中文和英文,如電腦和PC,第四種是各種專業詞彙同義詞,如藥品名和學名,最後可能還有一些網路詞語如神馬和什麼等。
在Lucene架構下,有兩種實現方式,第一種是編寫TokenFilter類來實現轉換和添加,還有一種就是直接集成在相應的Analyzer中實現這些功能。如果像Lucene這樣的開源軟體,講求系統的可擴展性的話,選擇開發獨立的TokenFilter較好,但是對於我們自己的項目,選擇集成在Analyzer中將是更好的選擇,這樣可以提高程式執行效率,因為TokenFilter需要重新逐個過一遍所有的單詞,效率比較低,而集成在Analyzer中可以保證在分解出單詞的過程中就完成了各種分詞操作,效率當然會提高了。
Lucene在文本解析中,首先會在Analyzer中調用Tokenizer,將文本分拆能最基本的單位,英文是單詞,中文是單字或片語,我們的去除停止詞和添加同義詞可以放入Tokenizer中,將每個新拆分的單詞進行處理,具體到我們所選用的MMSeg4j中文分詞模組來說,就是需要在MMSegTokenizer類的incrementToken方法中,添加去除停止詞和添加同義詞:
public boolean incrementToken() throws IOException {
if (0 == synonymCnt) {
clearAttributes();
Word word = mmSeg.next();
currWord = word;
if(word != null) {
// 去除截止詞如的、地、得、了等
String wordStr = word.getString();
if (stopWords.contains(wordStr)) {
return incrementToken();
}
if (synonymKeyDict.get(wordStr) != null) { // 如果具有同義詞則需要先添加本身這個詞,然後依次添加同義詞
synonymCnt = synonymDict.get(synonymKeyDict.get(wordStr)).size(); // 求出同義詞,作為結束條件控制
}
//termAtt.setTermBuffer(word.getSen(), word.getWordOffset(), word.getLength());
offsetAtt.setOffset(word.getStartOffset(), word.getEndOffset());
charTermAttr.copyBuffer(word.getSen(), word.getWordOffset(), word.getLength());
posIncrAttr.setPositionIncrement(1);
typeAtt.setType(word.getType());
return true;
} else {
end();
return false;
}
} else {
char[] charArray = null;
String orgWord = currWord.getString();
int i = 0;
Vector<String> synonyms = (Vector<String>)synonymDict.get(synonymKeyDict.get(orgWord));
if (orgWord.equals(synonyms.elementAt(synonymCnt - 1))) { // 如果是原文中出現的那個詞則不作任何處理
synonymCnt--;
return incrementToken();
}
// 添加同意詞
charArray = synonyms.elementAt(synonymCnt - 1).toCharArray();//termAtt.setTermBuffer(t1, 0, t1.length);
offsetAtt.setOffset(currWord.getStartOffset(), currWord.getStartOffset() + charArray.length); // currWord.getEndOffset());
typeAtt.setType(currWord.getType());
charTermAttr.copyBuffer(charArray, 0, charArray.length);
posIncrAttr.setPositionIncrement(0);
synonymCnt--;
return true;
}
}
charArray = synonyms.elementAt(synonymCnt - 1).toCharArray();//termAtt.setTermBuffer(t1, 0, t1.length);
offsetAtt.setOffset(currWord.getStartOffset(), currWord.getStartOffset() + charArray.length); // currWord.getEndOffset());
typeAtt.setType(currWord.getType());
charTermAttr.copyBuffer(charArray, 0, charArray.length);
posIncrAttr.setPositionIncrement(0);
synonymCnt--;
return true;
}
}
停止詞實現方式:
private
static String[] stopWordsArray = {"的", "地",
"得",
"了",
"呀",
"嗎",
"啊",
"a", "the", "in", "on"};
"a", "the", "in", "on"};
在構造函數中進行初始化:
if
(null == stopWords) {
int i = 0;
stopWords = new Vector<String>();
for (i=0; i<stopWordsArray.length; i++) {
stopWords.add(stopWordsArray[i]);
}
}
int i = 0;
stopWords = new Vector<String>();
for (i=0; i<stopWordsArray.length; i++) {
stopWords.add(stopWordsArray[i]);
}
}
同義詞的實現方式:
private static Collection<String> stopWords = null;
private static Hashtable<String, String> synonymKeyDict = null;
private static Hashtable<String, Collection<String>> synonymDict = null;
private static Collection<String> stopWords = null;
private static Hashtable<String, String> synonymKeyDict = null;
private static Hashtable<String, Collection<String>> synonymDict = null;
同樣在初始化函數中進行初始化:注意這裡只是簡單的初始化示例
// 先找出一個詞的同義詞片語key值,然後可以通過該key值從
// 最終本部分內容將通過資料庫驅動方式進行初始化
if (null == synonymDict) {
synonymKeyDict = new Hashtable<String, String>();
synonymDict = new Hashtable<String, Collection<String>>();
synonymKeyDict.put("獵人", "0");
synonymKeyDict.put("獵戶", "0");
synonymKeyDict.put("獵手", "0");
synonymKeyDict.put("狩獵者", "0");
Collection<String> syn1 = new Vector<String>();
syn1.add("獵人");
syn1.add("獵戶");
syn1.add("獵手");
syn1.add("狩獵者");
synonymDict.put("0", syn1);
// 添加狗和犬
synonymKeyDict.put("狗", "1");
synonymKeyDict.put("犬", "1");
Collection<String> syn2 = new Vector<String>();
syn2.add("狗");
syn2.add("犬");
synonymDict.put("1", syn2);
}
// 最終本部分內容將通過資料庫驅動方式進行初始化
if (null == synonymDict) {
synonymKeyDict = new Hashtable<String, String>();
synonymDict = new Hashtable<String, Collection<String>>();
synonymKeyDict.put("獵人", "0");
synonymKeyDict.put("獵戶", "0");
synonymKeyDict.put("獵手", "0");
synonymKeyDict.put("狩獵者", "0");
Collection<String> syn1 = new Vector<String>();
syn1.add("獵人");
syn1.add("獵戶");
syn1.add("獵手");
syn1.add("狩獵者");
synonymDict.put("0", syn1);
// 添加狗和犬
synonymKeyDict.put("狗", "1");
synonymKeyDict.put("犬", "1");
Collection<String> syn2 = new Vector<String>();
syn2.add("狗");
syn2.add("犬");
synonymDict.put("1", syn2);
}
在經過上述程式後,再對如下中文進行解析:咬死獵人的狗
解析結果為:
[咬:1/1:word;0-1]
[死:2/1:word;1-2]
[獵人:3/1:word;2-4]
[狩獵者:3/0:word;2-5]
[獵手:3/0:word;2-4]
[獵戶:3/0:word;2-4]
[狗:4/1:word;5-6]
[犬:4/0:word;5-6]
由上面結果可以看出,已經成功將獵人和狗的同義詞加入到分詞的結果中,這個工具就可以作為下面全文內容推薦引擎的實現基礎了。
沒有留言:
張貼留言