基於內容的推薦引擎有兩種實現途徑,一種是根據條目的中繼資料(可以將中繼資料理解為屬性),另一種是根據條目的文本描述資訊。本系列中將先描述基於條目描述資訊的全文檢索實現方式,然後描述基於中繼資料的內容推薦引擎實現方式。
對於基於條目文本描述資訊的內容推薦引擎,目前有很多資料可以參考,基本步聚是先對文本內容進行分詞,包括提取出單詞、去掉常用詞如的地得、加入同意詞、對英語還有去掉複數形式和過去分詞形式等;第二步是計算各個詞在每篇文章中的出現頻率,以及在所有文章中的出現頻率,即TF/IDF;第三步計算文章向量;最後是利用自動聚類演算法,對條目進行聚類,這樣就可以實現向使用者推薦同類產品的需求了。
但是在這裡有一個非常重要的問題沒有解決,就是中文分詞的問題,這些文章中絕大部分都是以英文為背景的,而英文分詞方面,分出單詞很簡單,只需要空格作為分隔符號就可以了,而中文中詞與詞之間沒有空格,其次是英文中單複數、過去分詞等比較多,需要還原成單數現在式,但是中文中這個問題基本不存在,再有就是英文需要在分詞後識別長的片語,而中文這一步也不需進行。
針對以上這些難題,在我的專案中,採用了MMSeg4j中文分詞模組,這個專案集成了據說是搜狗輸入法的10萬多詞庫(大家知道中文分詞的關鍵是中文詞庫)。
另外,我還希望中文分詞可以在全文檢索引擎和全文內容推薦引擎共用,由於全文檢索引擎採用了Apache Lucene 3.x版本,需要中文分詞模組符合Lucene的體系架構,幸運的是MMSeg4j提供了Lucene所需的Tokenizer實現類,同時還需要重點解決如下問題:
- 由於打開索引檔比較慢,所以整個程式共用一個indexer和searcher
- 考慮到准即時性需求,採用了Lucene新版本中reopen機制,每次查詢前讀入索引增量
- 採用Lucene默鎖機制
在專案中我定義了全文檢索引擎類:
public class FteEngine {
public static void
initFteEngine(String _indexPathname) {
indexPathname = _indexPathname;
}
indexPathname = _indexPathname;
}
public static FteEngine
getInstance()
{ // Singleton模式
if (null == engine) {
engine = new FteEngine();
}
return engine;
}
if (null == engine) {
engine = new FteEngine();
}
return engine;
}
public IndexWriter
getIndexWriter() {
return writer;
}
return writer;
}
public
IndexSearcher getIndexSearcher() {
try {
IndexReader newReader = reader.reopen(); // 讀入新增加的增量索引內容,滿足即時索引需求
if (!reader.equals(newReader)) {
reader.close();
reader = newReader;
}
searcher = new IndexSearcher(reader);
} catch (CorruptIndexException e) { ....
try {
IndexReader newReader = reader.reopen(); // 讀入新增加的增量索引內容,滿足即時索引需求
if (!reader.equals(newReader)) {
reader.close();
reader = newReader;
}
searcher = new IndexSearcher(reader);
} catch (CorruptIndexException e) { ....
} catch (IOException e) {....
}
return searcher;
}
}
return searcher;
}
public Analyzer
getAnalyzer() {
return analyzer;
}
return analyzer;
}
public void stop()
{
try {
if (searcher != null) {
searcher.close();
}
reader.close();
writer.close();
indexDir.close();
} catch (IOException e) {....
}
}
try {
if (searcher != null) {
searcher.close();
}
reader.close();
writer.close();
indexDir.close();
} catch (IOException e) {....
}
}
private FteEngine()
{
analyzer = new MMSegAnalyzer(); // 初始化中文分詞模組,會讀入中文字典
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_31, analyzer);
iwc.setOpenMode(OpenMode.CREATE_OR_APPEND);
try {
indexDir = FSDirectory.open(new File(indexPathname));
writer = new IndexWriter(indexDir, iwc); // writer和reader整個程式共用
reader = IndexReader.open(writer, true);
} catch (CorruptIndexException e) {......
} catch (LockObtainFailedException e) {......
} catch (IOException e) {.....
}
}
private static FteEngine engine = null;
private static String indexPathname = null;
private Directory indexDir = null;
private IndexWriter writer = null;
private IndexSearcher searcher = null;
private Analyzer analyzer = null;
private IndexReader reader = null;
}
analyzer = new MMSegAnalyzer(); // 初始化中文分詞模組,會讀入中文字典
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_31, analyzer);
iwc.setOpenMode(OpenMode.CREATE_OR_APPEND);
try {
indexDir = FSDirectory.open(new File(indexPathname));
writer = new IndexWriter(indexDir, iwc); // writer和reader整個程式共用
reader = IndexReader.open(writer, true);
} catch (CorruptIndexException e) {......
} catch (LockObtainFailedException e) {......
} catch (IOException e) {.....
}
}
private static FteEngine engine = null;
private static String indexPathname = null;
private Directory indexDir = null;
private IndexWriter writer = null;
private IndexSearcher searcher = null;
private Analyzer analyzer = null;
private IndexReader reader = null;
}
具體中文分詞可以使用如下代碼:
FteEngine fteEngine = FteEngine.getInstance();
Analyzer analyzer = fteEngine.getAnalyzer();
String text = "測試2011年如java有意見 分岐其中華人民共合國,oracle咬死獵人的狗!";
TokenStream tokenStrm = analyzer.tokenStream("contents", new StringReader(text));
OffsetAttribute offsetAttr = tokenStrm.getAttribute(OffsetAttribute.class);
CharTermAttribute charTermAttr = tokenStrm.getAttribute(CharTermAttribute.class);
String term = null;
int i = 0;
int len = 0;
char[] charBuf = null;
try {
while (tokenStrm.incrementToken()) {
charBuf = charTermAttr.buffer();
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.println(term);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Analyzer analyzer = fteEngine.getAnalyzer();
String text = "測試2011年如java有意見 分岐其中華人民共合國,oracle咬死獵人的狗!";
TokenStream tokenStrm = analyzer.tokenStream("contents", new StringReader(text));
OffsetAttribute offsetAttr = tokenStrm.getAttribute(OffsetAttribute.class);
CharTermAttribute charTermAttr = tokenStrm.getAttribute(CharTermAttribute.class);
String term = null;
int i = 0;
int len = 0;
char[] charBuf = null;
try {
while (tokenStrm.incrementToken()) {
charBuf = charTermAttr.buffer();
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.println(term);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
列印的內容如下:
測試 2011 年 如 java 有 意見 分 岐 其中 華 人民 共 合 國 oracle 咬 死 獵人 的 狗
當我們在缺省詞庫中加入單詞:分岐 中華人民共合國後,那麼分詞結果可以變為:
測試 2011 年 如 java 有 意見 分岐 其 中華人民共合國 oracle 咬 死 獵人 的 狗
由此可見,可以通過完善中文詞庫,得到越來越好的中文分詞效果。
沒有留言:
張貼留言