搜索技术
10/02
4
09/12
28
CSphSource 数据源
CSphSource_XMLPipe2-XML文件获取数据
CSphSource_SQL-SQL(MySQL)获取数据
CSphIndex 索引器
派生类CSphIndex_VLN
// 索引过程
virtual int Build ( CSphDict * pDict,
const CSphVector & dSources, // 所有数据源
int iMemoryLimit, // 内存设置
ESphDocinfo eDocinfo );
/// available docinfo storage strategies
enum ESphDocinfo
{
SPH_DOCINFO_NONE = 0, ///< no docinfo available
SPH_DOCINFO_INLINE = 1, ///< inline docinfo into index (specifically, into doclists)
SPH_DOCINFO_EXTERN = 2 ///< store docinfo separately
};
BYTE ** CSphSource_SQL::NextDocument ( CSphString & sError )
{
m_tDocInfo.m_iDocID = sphToDocid ( SqlColumn(0) ); // 取得文档ID值
}
bool CSphSource_Document::IterateHitsNext ( CSphString & sError )
{
while ( ( sWord = m_pTokenizer->GetToken() )!=NULL ) //分词
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
CSphAutofile fdTmpDocinfos ("tmp2") // 存储文档信息
DOCINFO2ID(pDocinfo) = pSource->m_tDocInfo.m_iDocID; // 自定义ID
memcpy ( DOCINFO2ATTRS(pDocinfo), pSource->m_tDocInfo.m_pRowitems, sizeof(CSphRowitem)*m_tSchema.GetRowSize() );
pDocinfo += iDocinfoStride;
Sphinx使用的文件包括 "sph", "spa", "spi", "spd", "spp", "spm" ,还有锁文件。其中sph是系统的配置文件。其它则为索引文件。
.Spi 文件:保存WordId及指向此WordId对应的文档信息在spd文件的指针。Spi文件在检索程序启动时完全加载入内存。Spi文件是分块的,块内排序,块之间也排序。分块的目的应该是为了快速检索到WordId,因为Spi中的WordId是变长压缩的,索引需要先在块级别做二分定位,再在快内解压缩查找。
文件结构,每块中结构,wordId实际存储的是差值
.Spd文件:文件结构
.Spp文件: 文件结构
.Spa文件:存储DocInfo的文件,检索程序启动时会把此文件加载如内存,sphinx可以指定DocInfo的存储方式,
① 存储到spd文件中(InLine)
②. 另外单独存储。指定此,就会生成spa文件
文件结构:
.Spm文件:在DocInfo中,有一种特殊的属性,叫MVA,多值属性。Sphinx对此属性特殊处理,需要存储在spm文件中。检索程序启动时会把此文件加载如内存。此(MVA)属性在DocInfo对应位置存储其在此文件中的字节偏移量。
文件结构:
由于在第一趟扫描过程中会出现WordID相同的不同Hits(不同文档或者不同位置不同字段),二趟前会根据WordID排序,WordID相同的Hits会连续出现并合并(合并到第一次出现的相同WordID中)
/////////////////////////////////////////////
Spi文件
// 遍历所有的数据源
// 遍历所有的文档
struct CSphWordHit
{
SphDocID_t m_iDocID; ///< document ID
SphWordID_t m_iWordID; ///< word ID in current dictionary
DWORD m_iWordPos; ///< word position in current document
};
CSphWordHit.. CSphWordHit…CSphWordHit…CSphWordHit串接
dHitBlocks.Add( cidxWriteRawVLB ( fdTmpHits.GetFD(), dHits, iHits, NULL, 0, 0 ) );//
GetIndexFileName("tmp1") 将Hits流写入tmp1文件。临时文件
不论重复,不论字段
dBins[i]->ReadHit(&tHit,iRowitems,dInlineAttrs+i*iRowitems )// 重新读入内存
cidxHit(tQueue.m_pData,iRowitems?dInlineAttrs+iBin*iRowitems : NULL );
// 统计后刷入m_wrWordlist,形如以下字节流(有缓冲,缓冲满则写入磁盘)
此时写入的是最终的索引文件,格式如上
Spd文件
文件结构
//tQuery.m_eMode = SPH_MATCH_ANY;
tQuery.m_eMode = SPH_MATCH_BOOLEAN;
//tQuery.m_eMode = SPH_MATCH_PHRASE;
//tQuery.m_eMode = SPH_MATCH_EXTENDED;
//tQuery.m_eMode = SPH_MATCH_EXTENDED2;
//SPH_SORT_RELEVANCE 模式, 按相关度降序排列(最好的匹配排在最前面)
//SPH_SORT_ATTR_DESC 模式, 按属性降序排列(属性值越大的越是排在前面)
//SPH_SORT_ATTR_ASC模式, 按属性升序排列(属性值越小的越是排在前面)
//SPH_SORT_TIME_SEGMENTS 模式, 先按时间段(最近一小时/天/周/月)降序,再按
// 相关度降序
//SPH_SORT_EXTENDED 模式, 按一种类似SQL的方式将列组合起来,升序或降序排
// 列。
//SPH_SORT_EXPR 模式,按某个算术表达式排序。
switch ( pQuery->m_eMode )
{
case SPH_MATCH_ALL: bMatch = MatchAll ( pQuery, pResult, iSorters, ppSorters ); break;// 与查询
case SPH_MATCH_PHRASE: bMatch = MatchAll ( pQuery, pResult, iSorters, ppSorters ); break;//短语查询
case SPH_MATCH_ANY: bMatch = MatchAny ( pQuery, pResult, iSorters, ppSorters ); break; //或查询
case SPH_MATCH_BOOLEAN: bMatch = MatchBoolean ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;//布尔查询
case SPH_MATCH_EXTENDED: bMatch = MatchExtendedV1 ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;//扩展查询
case SPH_MATCH_EXTENDED2: bMatch = MatchExtended ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;
case SPH_MATCH_FULLSCAN: bMatch = MatchFullScan ( pQuery, iSorters, ppSorters, tTermSetup ); break;
default: sphDie ( "INTERNAL ERROR: unknown matching mode (mode=%d)", pQuery->m_eMode );
}
ISphTokenizer * pTokenizer
// 用作解析的配置信息,例如字符集,切词规则(n-gram)
// 包括同义词,过滤词
1. pResult = pIndex->Query ( pTokenizer, pDict, &tQuery );
{
ISphMatchSorter * pTop = sphCreateQueue ( pQuery, m_tSchema, sError );(
pTop=newCSphMatchQueue> ( pQuery->m_iMaxMatches, bUsesAttrs ); //选择排序规则
)
2. if ( QueryEx ( pTokenizer, pDict, pQuery, pResult, pTop ) )
3. bool bRes = MultiQuery ( pTokenizer, pDict, pQuery, pResult, 1, &pTop );
4. case SPH_MATCH_BOOLEAN: bMatch = MatchBoolean ( pQuery, pResult, iSorters, ppSorters, tTermSetup );
CSphSource_XMLPipe2-XML文件获取数据
CSphSource_SQL-SQL(MySQL)获取数据
CSphIndex 索引器
派生类CSphIndex_VLN
// 索引过程
virtual int Build ( CSphDict * pDict,
const CSphVector
int iMemoryLimit, // 内存设置
ESphDocinfo eDocinfo );
/// available docinfo storage strategies
enum ESphDocinfo
{
SPH_DOCINFO_NONE = 0, ///< no docinfo available
SPH_DOCINFO_INLINE = 1, ///< inline docinfo into index (specifically, into doclists)
SPH_DOCINFO_EXTERN = 2 ///< store docinfo separately
};
BYTE ** CSphSource_SQL::NextDocument ( CSphString & sError )
{
m_tDocInfo.m_iDocID = sphToDocid ( SqlColumn(0) ); // 取得文档ID值
}
bool CSphSource_Document::IterateHitsNext ( CSphString & sError )
{
while ( ( sWord = m_pTokenizer->GetToken() )!=NULL ) //分词
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
CSphAutofile fdTmpDocinfos ("tmp2") // 存储文档信息
DOCINFO2ID(pDocinfo) = pSource->m_tDocInfo.m_iDocID; // 自定义ID
memcpy ( DOCINFO2ATTRS(pDocinfo), pSource->m_tDocInfo.m_pRowitems, sizeof(CSphRowitem)*m_tSchema.GetRowSize() );
pDocinfo += iDocinfoStride;
Sphinx使用的文件包括 "sph", "spa", "spi", "spd", "spp", "spm" ,还有锁文件。其中sph是系统的配置文件。其它则为索引文件。
.Spi 文件:保存WordId及指向此WordId对应的文档信息在spd文件的指针。Spi文件在检索程序启动时完全加载入内存。Spi文件是分块的,块内排序,块之间也排序。分块的目的应该是为了快速检索到WordId,因为Spi中的WordId是变长压缩的,索引需要先在块级别做二分定位,再在快内解压缩查找。
文件结构,每块中结构,wordId实际存储的是差值
WordId | SpdFilePointer | DocNum | HitNum
.Spd文件:文件结构
DocID | [DocInfo] | HitFilePointer | FieldNum | HitNum
.Spp文件: 文件结构
HitPos
.Spa文件:存储DocInfo的文件,检索程序启动时会把此文件加载如内存,sphinx可以指定DocInfo的存储方式,
① 存储到spd文件中(InLine)
②. 另外单独存储。指定此,就会生成spa文件
文件结构:
DocId | DocInfo
.Spm文件:在DocInfo中,有一种特殊的属性,叫MVA,多值属性。Sphinx对此属性特殊处理,需要存储在spm文件中。检索程序启动时会把此文件加载如内存。此(MVA)属性在DocInfo对应位置存储其在此文件中的字节偏移量。
文件结构:
DocId | Anum,A1,A2,…,An | Bnum,B1,B2,…,Bn | …
由于在第一趟扫描过程中会出现WordID相同的不同Hits(不同文档或者不同位置不同字段),二趟前会根据WordID排序,WordID相同的Hits会连续出现并合并(合并到第一次出现的相同WordID中)
/////////////////////////////////////////////
Spi文件
// 遍历所有的数据源
// 遍历所有的文档
struct CSphWordHit
{
SphDocID_t m_iDocID; ///< document ID
SphWordID_t m_iWordID; ///< word ID in current dictionary
DWORD m_iWordPos; ///< word position in current document
};
CSphWordHit.. CSphWordHit…CSphWordHit…CSphWordHit串接
dHitBlocks.Add( cidxWriteRawVLB ( fdTmpHits.GetFD(), dHits, iHits, NULL, 0, 0 ) );//
GetIndexFileName("tmp1") 将Hits流写入tmp1文件。临时文件
不论重复,不论字段
dBins[i]->ReadHit(&tHit,iRowitems,dInlineAttrs+i*iRowitems )// 重新读入内存
cidxHit(tQueue.m_pData,iRowitems?dInlineAttrs+iBin*iRowitems : NULL );
// 统计后刷入m_wrWordlist,形如以下字节流(有缓冲,缓冲满则写入磁盘)
WordId | SpdFilePointer | DocNum | HitNum
此时写入的是最终的索引文件,格式如上
Spd文件
文件结构
DocID | [DocInfo] | HitFilePointer | FieldNum | HitNum
//tQuery.m_eMode = SPH_MATCH_ANY;
tQuery.m_eMode = SPH_MATCH_BOOLEAN;
//tQuery.m_eMode = SPH_MATCH_PHRASE;
//tQuery.m_eMode = SPH_MATCH_EXTENDED;
//tQuery.m_eMode = SPH_MATCH_EXTENDED2;
//SPH_SORT_RELEVANCE 模式, 按相关度降序排列(最好的匹配排在最前面)
//SPH_SORT_ATTR_DESC 模式, 按属性降序排列(属性值越大的越是排在前面)
//SPH_SORT_ATTR_ASC模式, 按属性升序排列(属性值越小的越是排在前面)
//SPH_SORT_TIME_SEGMENTS 模式, 先按时间段(最近一小时/天/周/月)降序,再按
// 相关度降序
//SPH_SORT_EXTENDED 模式, 按一种类似SQL的方式将列组合起来,升序或降序排
// 列。
//SPH_SORT_EXPR 模式,按某个算术表达式排序。
switch ( pQuery->m_eMode )
{
case SPH_MATCH_ALL: bMatch = MatchAll ( pQuery, pResult, iSorters, ppSorters ); break;// 与查询
case SPH_MATCH_PHRASE: bMatch = MatchAll ( pQuery, pResult, iSorters, ppSorters ); break;//短语查询
case SPH_MATCH_ANY: bMatch = MatchAny ( pQuery, pResult, iSorters, ppSorters ); break; //或查询
case SPH_MATCH_BOOLEAN: bMatch = MatchBoolean ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;//布尔查询
case SPH_MATCH_EXTENDED: bMatch = MatchExtendedV1 ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;//扩展查询
case SPH_MATCH_EXTENDED2: bMatch = MatchExtended ( pQuery, pResult, iSorters, ppSorters, tTermSetup ); break;
case SPH_MATCH_FULLSCAN: bMatch = MatchFullScan ( pQuery, iSorters, ppSorters, tTermSetup ); break;
default: sphDie ( "INTERNAL ERROR: unknown matching mode (mode=%d)", pQuery->m_eMode );
}
ISphTokenizer * pTokenizer
// 用作解析的配置信息,例如字符集,切词规则(n-gram)
// 包括同义词,过滤词
1. pResult = pIndex->Query ( pTokenizer, pDict, &tQuery );
{
ISphMatchSorter * pTop = sphCreateQueue ( pQuery, m_tSchema, sError );(
pTop=newCSphMatchQueue
)
2. if ( QueryEx ( pTokenizer, pDict, pQuery, pResult, pTop ) )
3. bool bRes = MultiQuery ( pTokenizer, pDict, pQuery, pResult, 1, &pTop );
4. case SPH_MATCH_BOOLEAN: bMatch = MatchBoolean ( pQuery, pResult, iSorters, ppSorters, tTermSetup );
09/08
21
1、问题的来源
增加分词以后结果的准确度提高了,但是用户反映返回结果的速度很慢。原因是,Lucene做每一篇文档的相关关键词的高亮显示时,在运行时执行了很多遍的分词操作。这样降低了性能。
2、解决方法
在 Lucene1.4.3版本中的一个新功能可以解决这个问题。Term Vector现在支持保存Token.getPositionIncrement() 和Token.startOffset() 以及Token.endOffset() 信息。利用Lucene中新增加的Token信息的保存结果以后,就不需要为了高亮显示而在运行时解析每篇文档。通过Field方法控制是否保存该信息。修改HighlighterTest.java的代码如下:
//增加文档时保存Term位置信息。
private void addDoc(IndexWriter writer, String text) throws IOException
{
Document d = new Document();
//Field f = new Field(FIELD_NAME, text, true, true, true);
Field f = new Field(FIELD_NAME, text ,
Field.Store.YES, Field.Index.TOKENIZED,
Field.TermVector.WITH_POSITIONS_OFFSETS);
d.add(f);
writer.addDocument(d);
}
//利用Term位置信息节省Highlight时间。
void doStandardHighlights() throws Exception
{
Highlighter highlighter =new Highlighter(this,new QueryScorer(query));
highlighter.setTextFragmenter(new SimpleFragmenter(20));
for (int i = 0; i < hits.length(); i++)
{
String text = hits.doc(i).get(FIELD_NAME);
int maxNumFragmentsRequired = 2;
String fragmentSeparator = "...";
TermPositionVector tpv = (TermPositionVector)reader.getTermFreqVector(hits.id(i),FIELD_NAME);
//如果没有stop words去除还可以改成 TokenSources.getTokenStream(tpv,true); 进一步提速。
TokenStream tokenStream=TokenSources.getTokenStream(tpv);
//analyzer.tokenStream(FIELD_NAME,new StringReader(text));
String result =
highlighter.getBestFragments(
tokenStream,
text,
maxNumFragmentsRequired,
fragmentSeparator);
System.out.println("\t" + result);
}
}
最后把highlight包中的一个额外的判断去掉。对于中文来说没有明显的单词界限,所以下面这个判断是错误的:
tokenGroup.isDistinct(token)
这样中文分词就不会影响到查询速度了。
给Lucene.NET增加中文分词
一、Lucene的.NET版本介绍
到目前为止,Lucene的C#移植有三个版本,最开始是NLucene,然后是Lucene.NET,当Lucene.NET转向商业化之后,SourceForge上又出现了dotLucene项目。
猎兔推出完全使用C#开发的,支持Lucene.NET的中文分词模块。
二、调用接口
seg.result.CnTokenizer,该类继承Lucene.Net.Analysis.TokenStream。
其中环境变量dic.dir指定数据文件路径,如:
"-Ddic.dir=d:/lg/work/SSeg/dic"
一个简单的使用例子是:
using System;
using System.Runtime.InteropServices;
using seg.result;
using Lucene.Net.Analysis;
namespace ConsoleApplication1
{
///
/// Class1 的摘要说明。
///
class Class1
{
///
/// 应用程序的主入口点。
///
[DllImport("Kernel32.DLL", SetLastError=true)]
public static extern bool SetEnvironmentVariable(string lpName, string lpValue);
[STAThread]
static void Main(string[] args)
{
SetEnvironmentVariable( "dic.dir", "d:/lg/work/SSeg/dic");
//
// TODO: 在此处添加代码以启动应用程序
//
testCnAnalyzer();
System.Console.Read();
}
public static void testCnAnalyzer()
{
System.IO.TextReader input;
CnTokenizer.makeTag= true;
string sentence = "邀请王振国今年9月参加在洛杉矶举行的30届美国治癌成就大奖会";
input = new System.IO.StringReader(sentence);
TokenStream tokenizer = new seg.result.CnTokenizer(input);
for (Token t = tokenizer.Next(); t != null; t = tokenizer.Next())
{
System.Console.WriteLine(t.TermText() + " " + t.StartOffset() + " "
+ t.EndOffset() + " "+t.Type());
}
}
}
}
三、输出结果介绍
输出结果中的词性标注代码和分词效果与当前Java版的一样,可以参考向Lucene增加中文分词功能。
dotLucene中文分词的highlight显示
1、准备的原料
lucene.net的1.4.3版本比java版的Lucene 1.4.3功能要少,所以需要lucene.net-1.9的版本。highlighter.net也用当前最新的版本1.4.0,但是这个版本的功能也比java当前版的功能要少,缺少一个实现快速显示highlight的类TokenSources。
2、TokenSources.cs的代码
using System;
using IComparer = System.Collections.IComparer;
using ArrayList = System.Collections.ArrayList;
using Analyzer = Lucene.Net.Analysis.Analyzer;
using Token = Lucene.Net.Analysis.Token;
using TokenStream = Lucene.Net.Analysis.TokenStream;
using IndexReader = Lucene.Net.Index.IndexReader;
using TermFreqVector = Lucene.Net.Index.TermFreqVector;
using TermPositionVector = Lucene.Net.Index.TermPositionVector;
using TermVectorOffsetInfo = Lucene.Net.Index.TermVectorOffsetInfo;
using Document = Lucene.Net.Documents.Document;
namespace Lucene.Net.Search.Highlight
{
///
/// TokenSources used for fast highlight,it's a must for chinese word segment.
///
public class TokenSources
{
/**
* A convenience method that tries a number of approaches to getting a token stream.
* The cost of finding there are no termVectors in the index is minimal (1000 invocations still
* registers 0 ms). So this "lazy" (flexible?) approach to coding is probably acceptable
* @param reader
* @param docId
* @param field
* @param analyzer
* @return null if field not stored correctly
* @throws IOException
*/
public static TokenStream GetAnyTokenStream(IndexReader reader,int docId, String field,Analyzer analyzer)
{
TokenStream ts=null;
TermFreqVector tfv=(TermFreqVector) reader.GetTermFreqVector(docId,field);
if(tfv!=null)
{
if(tfv is TermPositionVector)
{
ts=GetTokenStream((TermPositionVector) tfv);
}
}
//No token info stored so fall back to analyzing raw content
if(ts==null)
{
ts=GetTokenStream(reader,docId,field,analyzer);
}
return ts;
}
/**
*
* */
public static TokenStream GetTokenStream(TermPositionVector tpv)
{
//assumes the worst and makes no assumptions about token position sequences.
return GetTokenStream(tpv,false);
}
/**
* an object used to iterate across an array of tokens
* */
public class StoredTokenStream : TokenStream
{
Token[] tokens;
int currentToken=0;
/**
* */
public StoredTokenStream(Token[] tokens)
{
this.tokens=tokens;
}
/**
* */
public override Token Next()
{
if(currentToken>=tokens.Length)
{
return null;
}
return tokens[currentToken++];
}
}
class CompareClass : IComparer
{
public Int32 Compare(Object o1, Object o2)
{
Token t1=(Token) o1;
Token t2=(Token) o2;
if(t1.StartOffset()>t2.StartOffset())
return 1;
if(t1.StartOffset()
return -1;
return 0;
}
}
/**
* Low level api.
* Returns a token stream or null if no offset info available in index.
* This can be used to feed the highlighter with a pre-parsed token stream
*
* In my tests the speeds to recreate 1000 token streams using this method are:
* - with TermVector offset only data stored - 420 milliseconds
* - with TermVector offset AND position data stored - 271 milliseconds
* (nb timings for TermVector with position data are based on a tokenizer with contiguous
* positions - no overlaps or gaps)
* The cost of not using TermPositionVector to store
* pre-parsed content and using an analyzer to re-parse the original content:
* - reanalyzing the original content - 980 milliseconds
*
* The re-analyze timings will typically vary depending on -
* 1) The complexity of the analyzer code (timings above were using a
* stemmer/lowercaser/stopword combo)
* 2) The number of other fields (Lucene reads ALL fields off the disk
* when accessing just one document field - can cost dear!)
* 3) Use of compression on field storage - could be faster cos of compression (less disk IO)
* or slower (more CPU burn) depending on the content.
*
* @param tpv
* @param tokenPositionsGuaranteedContiguous true if the token position numbers have no overlaps or gaps. If looking
* to eek out the last drops of performance, set to true. If in doubt, set to false.
*/
public static TokenStream GetTokenStream(TermPositionVector tpv, bool tokenPositionsGuaranteedContiguous)
{
//System.out.println("fastfastfast");
//code to reconstruct the original sequence of Tokens
String[] terms=tpv.GetTerms();
int[] freq=tpv.GetTermFrequencies();
int totalTokens=0;
for (int t = 0; t < freq.Length; t++)
{
totalTokens+=freq[t];
}
Token[] tokensInOriginalOrder=new Token[totalTokens];
ArrayList unsortedTokens = null;
for (int t = 0; t < freq.Length; t++)
{
TermVectorOffsetInfo[] offsets=tpv.GetOffsets(t);
if(offsets==null)
{
return null;
}
int[] pos=null;
if(tokenPositionsGuaranteedContiguous)
{
//try get the token position info to speed up assembly of tokens into sorted sequence
pos=tpv.GetTermPositions(t);
}
if(pos==null)
{
//tokens NOT stored with positions or not guaranteed contiguous - must add to list and sort later
if(unsortedTokens==null)
{
unsortedTokens=new ArrayList();
}
for (int tp = 0; tp < offsets.Length; tp++)
{
unsortedTokens.Add(new Token(terms[t],
offsets[tp].GetStartOffset(),
offsets[tp].GetEndOffset()));
}
}
else
{
//We have positions stored and a guarantee that the token position information is contiguous
// This may be fast BUT wont work if Tokenizers used which create >1 token in same position or
// creates jumps in position numbers - this code would fail under those circumstances
//tokens stored with positions - can use this to index straight into sorted array
for (int tp = 0; tp < pos.Length; tp++)
{
tokensInOriginalOrder[pos[tp]]=new Token(terms[t],
offsets[tp].GetStartOffset(),
offsets[tp].GetEndOffset());
}
}
}
//If the field has been stored without position data we must perform a sort
if(unsortedTokens!=null)
{
tokensInOriginalOrder=(Token[]) unsortedTokens.ToArray(typeof( Token) );
System.Array.Sort(tokensInOriginalOrder, new CompareClass() );
}
return new StoredTokenStream(tokensInOriginalOrder);
}
/**
* */
public static TokenStream GetTokenStream(IndexReader reader,int docId, String field)
{
TermFreqVector tfv=(TermFreqVector) reader.GetTermFreqVector(docId,field);
if(tfv==null)
{
throw new Exception(field+" in doc #"+docId
+"does not have any term position data stored");
}
if(tfv is TermPositionVector)
{
TermPositionVector tpv=(TermPositionVector) reader.GetTermFreqVector(docId,field);
return GetTokenStream(tpv);
}
throw new Exception(field+" in doc #"+docId
+"does not have any term position data stored");
}
//convenience method
public static TokenStream GetTokenStream(IndexReader reader,int docId, String field,Analyzer analyzer)
{
Document doc=reader.Document(docId);
String contents=doc.Get(field);
if(contents==null)
{
throw new Exception("Field "+field +" in document #"+docId+ " is not stored and cannot be analyzed");
}
return analyzer.TokenStream(field,new System.IO.StringReader(contents));
}
}
}
3、 附加工作
去掉highlight包中的单词界限判断:
tokenGroup.isDistinct(token)
增加分词以后结果的准确度提高了,但是用户反映返回结果的速度很慢。原因是,Lucene做每一篇文档的相关关键词的高亮显示时,在运行时执行了很多遍的分词操作。这样降低了性能。
2、解决方法
在 Lucene1.4.3版本中的一个新功能可以解决这个问题。Term Vector现在支持保存Token.getPositionIncrement() 和Token.startOffset() 以及Token.endOffset() 信息。利用Lucene中新增加的Token信息的保存结果以后,就不需要为了高亮显示而在运行时解析每篇文档。通过Field方法控制是否保存该信息。修改HighlighterTest.java的代码如下:
//增加文档时保存Term位置信息。
private void addDoc(IndexWriter writer, String text) throws IOException
{
Document d = new Document();
//Field f = new Field(FIELD_NAME, text, true, true, true);
Field f = new Field(FIELD_NAME, text ,
Field.Store.YES, Field.Index.TOKENIZED,
Field.TermVector.WITH_POSITIONS_OFFSETS);
d.add(f);
writer.addDocument(d);
}
//利用Term位置信息节省Highlight时间。
void doStandardHighlights() throws Exception
{
Highlighter highlighter =new Highlighter(this,new QueryScorer(query));
highlighter.setTextFragmenter(new SimpleFragmenter(20));
for (int i = 0; i < hits.length(); i++)
{
String text = hits.doc(i).get(FIELD_NAME);
int maxNumFragmentsRequired = 2;
String fragmentSeparator = "...";
TermPositionVector tpv = (TermPositionVector)reader.getTermFreqVector(hits.id(i),FIELD_NAME);
//如果没有stop words去除还可以改成 TokenSources.getTokenStream(tpv,true); 进一步提速。
TokenStream tokenStream=TokenSources.getTokenStream(tpv);
//analyzer.tokenStream(FIELD_NAME,new StringReader(text));
String result =
highlighter.getBestFragments(
tokenStream,
text,
maxNumFragmentsRequired,
fragmentSeparator);
System.out.println("\t" + result);
}
}
最后把highlight包中的一个额外的判断去掉。对于中文来说没有明显的单词界限,所以下面这个判断是错误的:
tokenGroup.isDistinct(token)
这样中文分词就不会影响到查询速度了。
给Lucene.NET增加中文分词
一、Lucene的.NET版本介绍
到目前为止,Lucene的C#移植有三个版本,最开始是NLucene,然后是Lucene.NET,当Lucene.NET转向商业化之后,SourceForge上又出现了dotLucene项目。
猎兔推出完全使用C#开发的,支持Lucene.NET的中文分词模块。
二、调用接口
seg.result.CnTokenizer,该类继承Lucene.Net.Analysis.TokenStream。
其中环境变量dic.dir指定数据文件路径,如:
"-Ddic.dir=d:/lg/work/SSeg/dic"
一个简单的使用例子是:
using System;
using System.Runtime.InteropServices;
using seg.result;
using Lucene.Net.Analysis;
namespace ConsoleApplication1
{
///
/// Class1 的摘要说明。
///
class Class1
{
///
/// 应用程序的主入口点。
///
[DllImport("Kernel32.DLL", SetLastError=true)]
public static extern bool SetEnvironmentVariable(string lpName, string lpValue);
[STAThread]
static void Main(string[] args)
{
SetEnvironmentVariable( "dic.dir", "d:/lg/work/SSeg/dic");
//
// TODO: 在此处添加代码以启动应用程序
//
testCnAnalyzer();
System.Console.Read();
}
public static void testCnAnalyzer()
{
System.IO.TextReader input;
CnTokenizer.makeTag= true;
string sentence = "邀请王振国今年9月参加在洛杉矶举行的30届美国治癌成就大奖会";
input = new System.IO.StringReader(sentence);
TokenStream tokenizer = new seg.result.CnTokenizer(input);
for (Token t = tokenizer.Next(); t != null; t = tokenizer.Next())
{
System.Console.WriteLine(t.TermText() + " " + t.StartOffset() + " "
+ t.EndOffset() + " "+t.Type());
}
}
}
}
三、输出结果介绍
输出结果中的词性标注代码和分词效果与当前Java版的一样,可以参考向Lucene增加中文分词功能。
dotLucene中文分词的highlight显示
1、准备的原料
lucene.net的1.4.3版本比java版的Lucene 1.4.3功能要少,所以需要lucene.net-1.9的版本。highlighter.net也用当前最新的版本1.4.0,但是这个版本的功能也比java当前版的功能要少,缺少一个实现快速显示highlight的类TokenSources。
2、TokenSources.cs的代码
using System;
using IComparer = System.Collections.IComparer;
using ArrayList = System.Collections.ArrayList;
using Analyzer = Lucene.Net.Analysis.Analyzer;
using Token = Lucene.Net.Analysis.Token;
using TokenStream = Lucene.Net.Analysis.TokenStream;
using IndexReader = Lucene.Net.Index.IndexReader;
using TermFreqVector = Lucene.Net.Index.TermFreqVector;
using TermPositionVector = Lucene.Net.Index.TermPositionVector;
using TermVectorOffsetInfo = Lucene.Net.Index.TermVectorOffsetInfo;
using Document = Lucene.Net.Documents.Document;
namespace Lucene.Net.Search.Highlight
{
///
/// TokenSources used for fast highlight,it's a must for chinese word segment.
///
public class TokenSources
{
/**
* A convenience method that tries a number of approaches to getting a token stream.
* The cost of finding there are no termVectors in the index is minimal (1000 invocations still
* registers 0 ms). So this "lazy" (flexible?) approach to coding is probably acceptable
* @param reader
* @param docId
* @param field
* @param analyzer
* @return null if field not stored correctly
* @throws IOException
*/
public static TokenStream GetAnyTokenStream(IndexReader reader,int docId, String field,Analyzer analyzer)
{
TokenStream ts=null;
TermFreqVector tfv=(TermFreqVector) reader.GetTermFreqVector(docId,field);
if(tfv!=null)
{
if(tfv is TermPositionVector)
{
ts=GetTokenStream((TermPositionVector) tfv);
}
}
//No token info stored so fall back to analyzing raw content
if(ts==null)
{
ts=GetTokenStream(reader,docId,field,analyzer);
}
return ts;
}
/**
*
* */
public static TokenStream GetTokenStream(TermPositionVector tpv)
{
//assumes the worst and makes no assumptions about token position sequences.
return GetTokenStream(tpv,false);
}
/**
* an object used to iterate across an array of tokens
* */
public class StoredTokenStream : TokenStream
{
Token[] tokens;
int currentToken=0;
/**
* */
public StoredTokenStream(Token[] tokens)
{
this.tokens=tokens;
}
/**
* */
public override Token Next()
{
if(currentToken>=tokens.Length)
{
return null;
}
return tokens[currentToken++];
}
}
class CompareClass : IComparer
{
public Int32 Compare(Object o1, Object o2)
{
Token t1=(Token) o1;
Token t2=(Token) o2;
if(t1.StartOffset()>t2.StartOffset())
return 1;
if(t1.StartOffset()
return -1;
return 0;
}
}
/**
* Low level api.
* Returns a token stream or null if no offset info available in index.
* This can be used to feed the highlighter with a pre-parsed token stream
*
* In my tests the speeds to recreate 1000 token streams using this method are:
* - with TermVector offset only data stored - 420 milliseconds
* - with TermVector offset AND position data stored - 271 milliseconds
* (nb timings for TermVector with position data are based on a tokenizer with contiguous
* positions - no overlaps or gaps)
* The cost of not using TermPositionVector to store
* pre-parsed content and using an analyzer to re-parse the original content:
* - reanalyzing the original content - 980 milliseconds
*
* The re-analyze timings will typically vary depending on -
* 1) The complexity of the analyzer code (timings above were using a
* stemmer/lowercaser/stopword combo)
* 2) The number of other fields (Lucene reads ALL fields off the disk
* when accessing just one document field - can cost dear!)
* 3) Use of compression on field storage - could be faster cos of compression (less disk IO)
* or slower (more CPU burn) depending on the content.
*
* @param tpv
* @param tokenPositionsGuaranteedContiguous true if the token position numbers have no overlaps or gaps. If looking
* to eek out the last drops of performance, set to true. If in doubt, set to false.
*/
public static TokenStream GetTokenStream(TermPositionVector tpv, bool tokenPositionsGuaranteedContiguous)
{
//System.out.println("fastfastfast");
//code to reconstruct the original sequence of Tokens
String[] terms=tpv.GetTerms();
int[] freq=tpv.GetTermFrequencies();
int totalTokens=0;
for (int t = 0; t < freq.Length; t++)
{
totalTokens+=freq[t];
}
Token[] tokensInOriginalOrder=new Token[totalTokens];
ArrayList unsortedTokens = null;
for (int t = 0; t < freq.Length; t++)
{
TermVectorOffsetInfo[] offsets=tpv.GetOffsets(t);
if(offsets==null)
{
return null;
}
int[] pos=null;
if(tokenPositionsGuaranteedContiguous)
{
//try get the token position info to speed up assembly of tokens into sorted sequence
pos=tpv.GetTermPositions(t);
}
if(pos==null)
{
//tokens NOT stored with positions or not guaranteed contiguous - must add to list and sort later
if(unsortedTokens==null)
{
unsortedTokens=new ArrayList();
}
for (int tp = 0; tp < offsets.Length; tp++)
{
unsortedTokens.Add(new Token(terms[t],
offsets[tp].GetStartOffset(),
offsets[tp].GetEndOffset()));
}
}
else
{
//We have positions stored and a guarantee that the token position information is contiguous
// This may be fast BUT wont work if Tokenizers used which create >1 token in same position or
// creates jumps in position numbers - this code would fail under those circumstances
//tokens stored with positions - can use this to index straight into sorted array
for (int tp = 0; tp < pos.Length; tp++)
{
tokensInOriginalOrder[pos[tp]]=new Token(terms[t],
offsets[tp].GetStartOffset(),
offsets[tp].GetEndOffset());
}
}
}
//If the field has been stored without position data we must perform a sort
if(unsortedTokens!=null)
{
tokensInOriginalOrder=(Token[]) unsortedTokens.ToArray(typeof( Token) );
System.Array.Sort(tokensInOriginalOrder, new CompareClass() );
}
return new StoredTokenStream(tokensInOriginalOrder);
}
/**
* */
public static TokenStream GetTokenStream(IndexReader reader,int docId, String field)
{
TermFreqVector tfv=(TermFreqVector) reader.GetTermFreqVector(docId,field);
if(tfv==null)
{
throw new Exception(field+" in doc #"+docId
+"does not have any term position data stored");
}
if(tfv is TermPositionVector)
{
TermPositionVector tpv=(TermPositionVector) reader.GetTermFreqVector(docId,field);
return GetTokenStream(tpv);
}
throw new Exception(field+" in doc #"+docId
+"does not have any term position data stored");
}
//convenience method
public static TokenStream GetTokenStream(IndexReader reader,int docId, String field,Analyzer analyzer)
{
Document doc=reader.Document(docId);
String contents=doc.Get(field);
if(contents==null)
{
throw new Exception("Field "+field +" in document #"+docId+ " is not stored and cannot be analyzed");
}
return analyzer.TokenStream(field,new System.IO.StringReader(contents));
}
}
}
3、 附加工作
去掉highlight包中的单词界限判断:
tokenGroup.isDistinct(token)
09/08
21
CLucene - a C++ search engine http://sourceforge.net/projects/clucene/
传统的全文检索都是基于数据库的,Sql Server Oracle mysql 都提供全文检索,但这些比较大,不适合单机或小应用程序(Mysql4.0以上可以作为整合开发),Mysql也不支持中文。
后来得知Apache有一个开源的全文检索引擎,而且应用比较广,Lucene是Apache旗下的JAVA版的全文检索引擎,性能相当出色,可惜是 java版的,我一直在想有没有C或C++版的,终于有一天在http://sourceforge.net 淘到一个好东东,Clucene!CLucene是C++版的全文检索引擎,完全移植于Lucene,不过对中文支持不好,而且有很多的内存泄露,
Cluene 不支持中文的分词,我就写了一个简单的中文分词,大概思路就是传统的二分词法,因为中文的分词不像英文这类的语言,一遇到空格或标点就认为是一个词的结束,所以就采用二分词法,二分词法就是例如:北京市,就切成 北京 ,京市。这样一来词库就会很大,不过是一种简单的分词方法(过段时间我再介绍我对中文分词的一些思路) ,当然了,在检索时就不能输入“北京市”了,这样就检索不到,只要输入:“+北京 +京市”,就可以检索到北京市了,虽然精度不是很高,但适合简单的分词,而且不怕会漏掉某些单词。
我照着Clucene的分词模块,做了一个ChineseTokenizer,这个模块就负责分词工作了,我把主要的函数写出来
ChineseTokenizer.cpp:
Token* ChineseTokenizer::next() {
while(!rd.Eos())
{
char_t ch = rd.GetNext();
if( isSpace((char_t)ch)!=0 )
{
continue;
}
// Read for Alpha-Nums and Chinese
if( isAlNum((char_t)ch)!=0 )
{
start = rd.Column();
return ReadChinese(ch);
}
}
return NULL;
}
Token* ChineseTokenizer::ReadChinese(const char_t prev)
{
bool isChinese = false;
StringBuffer str;
str.append(prev);
char_t ch = prev;
if(((char_t)ch>>&&(char_t)ch>=0xa0)
isChinese = true;
while(!rd.Eos() && isSpace((char_t)ch)==0 )
{
ch = rd.GetNext();
if(isAlNum((char_t)ch)!=0)
{
//是数学或英语就读到下一个空格.或下一个汉字
//是汉字.就读下一个汉字组成词组,或读到空格或英文结束
if(isChinese)
{
//汉字,并且ch是汉字
if(((char_t)ch>>&&(char_t)ch>=0xa0)
{
// 返回上一个汉字
str.append(ch);
rd.UnGet();
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
else
{
//是字母或数字或空格
rd.UnGet();
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
}
else
{
//非汉字
// ch是汉字
if(((char_t)ch>>&&(char_t)ch>=0xa0)
{
// wprintf(_T("[%s]"),str);
rd.UnGet();
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
str.append( ch );
}
}
}
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::ALPHANUM] );
}
同时,这个中文分词不支持文件,只能支持内存流的形式,因为我用到了rd.UnGet();如果是文件的话,嘿嘿,只能回退半个字节哦
传统的全文检索都是基于数据库的,Sql Server Oracle mysql 都提供全文检索,但这些比较大,不适合单机或小应用程序(Mysql4.0以上可以作为整合开发),Mysql也不支持中文。
后来得知Apache有一个开源的全文检索引擎,而且应用比较广,Lucene是Apache旗下的JAVA版的全文检索引擎,性能相当出色,可惜是 java版的,我一直在想有没有C或C++版的,终于有一天在http://sourceforge.net 淘到一个好东东,Clucene!CLucene是C++版的全文检索引擎,完全移植于Lucene,不过对中文支持不好,而且有很多的内存泄露,
Cluene 不支持中文的分词,我就写了一个简单的中文分词,大概思路就是传统的二分词法,因为中文的分词不像英文这类的语言,一遇到空格或标点就认为是一个词的结束,所以就采用二分词法,二分词法就是例如:北京市,就切成 北京 ,京市。这样一来词库就会很大,不过是一种简单的分词方法(过段时间我再介绍我对中文分词的一些思路) ,当然了,在检索时就不能输入“北京市”了,这样就检索不到,只要输入:“+北京 +京市”,就可以检索到北京市了,虽然精度不是很高,但适合简单的分词,而且不怕会漏掉某些单词。
我照着Clucene的分词模块,做了一个ChineseTokenizer,这个模块就负责分词工作了,我把主要的函数写出来
ChineseTokenizer.cpp:
Token* ChineseTokenizer::next() {
while(!rd.Eos())
{
char_t ch = rd.GetNext();
if( isSpace((char_t)ch)!=0 )
{
continue;
}
// Read for Alpha-Nums and Chinese
if( isAlNum((char_t)ch)!=0 )
{
start = rd.Column();
return ReadChinese(ch);
}
}
return NULL;
}
Token* ChineseTokenizer::ReadChinese(const char_t prev)
{
bool isChinese = false;
StringBuffer str;
str.append(prev);
char_t ch = prev;
if(((char_t)ch>>&&(char_t)ch>=0xa0)
isChinese = true;
while(!rd.Eos() && isSpace((char_t)ch)==0 )
{
ch = rd.GetNext();
if(isAlNum((char_t)ch)!=0)
{
//是数学或英语就读到下一个空格.或下一个汉字
//是汉字.就读下一个汉字组成词组,或读到空格或英文结束
if(isChinese)
{
//汉字,并且ch是汉字
if(((char_t)ch>>&&(char_t)ch>=0xa0)
{
// 返回上一个汉字
str.append(ch);
rd.UnGet();
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
else
{
//是字母或数字或空格
rd.UnGet();
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
}
else
{
//非汉字
// ch是汉字
if(((char_t)ch>>&&(char_t)ch>=0xa0)
{
// wprintf(_T("[%s]"),str);
rd.UnGet();
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::CHINESE] );
}
str.append( ch );
}
}
}
// wprintf(_T("[%s]"),str);
return new Token(str.getBuffer(), start, rd.Column(), tokenImage[lucene::analysis::chinese::ALPHANUM] );
}
同时,这个中文分词不支持文件,只能支持内存流的形式,因为我用到了rd.UnGet();如果是文件的话,嘿嘿,只能回退半个字节哦
09/08
21
信息的飞速增长,使搜索引擎成为人们查找信息的首选工具,Google、百度、中国搜索等大型搜索引擎一直是人们讨论的话题。随着搜索市场价值的不断增加,越来越多的公司开发出自己的搜索引擎,阿里巴巴的商机搜索、8848的购物搜索等也陆续面世,自然,搜索引擎技术也成为技术人员关注的热点。
搜索引擎技术的研究,国外比中国要早近十年,从最早的Archie,到后来的Excite,以及altvista、overture、google等搜索引擎面世,搜索引擎发展至今,已经有十几年的历史,而国内开始研究搜索引擎是在上世纪末本世纪初。在许多领域,都是国外的产品和技术一统天下,特别是当某种技术在国外研究多年而国内才开始的情况下。例如操作系统、字处理软件、浏览器等等,但搜索引擎却是个例外。虽然在国外搜索引擎技术早就开始研究,但在国内还是陆续涌现出优秀的搜索引擎,像百度(http://www.baidu.com)、中搜(http://www.zhongsou.com)等。目前在中文搜索引擎领域,国内的搜索引擎已经和国外的搜索引擎效果上相差不远。之所以能形成这样的局面,有一个重要的原因就在于中文和英文两种语言自身的书写方式不同,这其中对于计算机涉及的技术就是中文分词。
什么是中文分词
众所周知,英文是以词为单位的,词和词之间是靠空格隔开,而中文是以字为单位,句子中所有的字连起来才能描述一个意思。例如,英文句子I am a student,用中文则为:“我是一个学生”。计算机可以很简单通过空格知道student是一个单词,但是不能很容易明白“学”、“生”两个字合起来才表示一个词。把中文的汉字序列切分成有意义的词,就是中文分词,有些人也称为切词。我是一个学生,分词的结果是:我 是 一个 学生。
中文分词和搜索引擎
中文分词到底对搜索引擎有多大影响?对于搜索引擎来说,最重要的并不是找到所有结果,因为在上百亿的网页中找到所有结果没有太多的意义,没有人能看得完,最重要的是把最相关的结果排在最前面,这也称为相关度排序。中文分词的准确与否,常常直接影响到对搜索结果的相关度排序。笔者最近替朋友找一些关于日本和服的资料,在搜索引擎上输入“和服”,得到的结果就发现了很多问题。下面就以这个例子来说明分词对搜索结果的影响,在现有三个中文搜索引擎上做测试,测试方法是直接在Google(http://www.google.com)、百度(http://www.baidu.com)、中搜(http: //www.zhongsou.com)上以“和服”为关键词进行搜索:
在Google上输入“和服”搜索所有中文简体网页,总共结果507,000条,前20条结果中有14条与和服一点关系都没有。在第一页就有以下错误:
“通信信息报:瑞星以技术和服务开拓网络安全市场”
“使用纯HTML的通用数据管理和服务- 开发者- ZDNet ...”
“陈慧琳《心口不一》化妆和服装自己包办”
“::外交部:中国境外领事保护和服务指南(2003年版) ...”
“产品和服务”
等等。第一页只有三篇是真正在讲“和服”的结果。
在百度上输入“和服”搜索网页,总共结果为287,000条,前20条结果中有6条与和服一点关系都没有。在第一页有以下错误:
“福建省晋江市恒和服装有限公司系独资企业”
“关于商品和服务实行明码标价的规定”
“青岛东和服装设备”
在中搜上输入“和服”搜索网页,总共结果为26,917条,前20条结果都是与和服相关的网页。
这次搜索引擎结果中的错误,就是由于分词的不准确所造成的。通过笔者的了解,Google的中文分词技术采用的是美国一家名叫Basis Technology(http://www.basistech.com)的公司提供的中文分词技术,百度使用的是自己公司开发的分词技术,中搜使用的是国内海量科技(http://www.hylanda.com)提供的分词技术。由此可见,中文分词的准确度,对搜索引擎结果相关性和准确性有相当大的关系。
中文分词技术
中文分词技术属于自然语言处理技术范畴,对于一句话,人可以通过自己的知识来明白哪些是词,哪些不是词,但如何让计算机也能理解?其处理过程就是分词算法。
现有的分词算法可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。
1、基于字符串匹配的分词方法
这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与标注相结合的一体化方法。常用的几种机械分词方法如下:
1)正向最大匹配法(由左到右的方向);
2)逆向最大匹配法(由右到左的方向);
3)最少切分(使每一句中切出的词数最小)。
还可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各种其它的语言信息来进一步提高切分的准确率。
一种方法是改进扫描方式,称为特征扫描或标志切分,优先在待分析字符串中识别和切分出一些带有明显特征的词,以这些词作为断点,可将原字符串分为较小的串再来进机械分词,从而减少匹配的错误率。另一种方法是将分词和词类标注结合起来,利用丰富的词类信息对分词决策提供帮助,并且在标注过程中又反过来对分词结果进行检验、调整,从而极大地提高切分的准确率。
对于机械分词方法,可以建立一个一般的模型,在这方面有专业的学术论文,这里不做详细论述。
2、基于理解的分词方法
这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。
3、基于统计的分词方法
从形式上看,词是稳定的字的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻共现的频率或概率能够较好的反映成词的可信度。可以对语料中相邻共现的各个字的组合的频度进行统计,计算它们的互现信息。定义两个字的互现信息,计算两个汉字X、Y的相邻共现概率。互现信息体现了汉字之间结合关系的紧密程度。当紧密程度高于某一个阈值时,便可认为此字组可能构成了一个词。这种方法只需对语料中的字组频度进行统计,不需要切分词典,因而又叫做无词典分词法或统计取词方法。但这种方法也有一定的局限性,会经常抽出一些共现频度高、但并不是词的常用字组,例如“这一”、“之一”、“有的”、“我的”、“许多的”等,并且对常用词的识别精度差,时空开销大。实际应用的统计分词系统都要使用一部基本的分词词典(常用词词典)进行串匹配分词,同时使用统计方法识别一些新的词,即将串频统计和串匹配结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。
到底哪种分词算法的准确度更高,目前并无定论。对于任何一个成熟的分词系统来说,不可能单独依靠某一种算法来实现,都需要综合不同的算法。笔者了解,海量科技的分词算法就采用“复方分词法”,所谓复方,相当于用中药中的复方概念,即用不同的药才综合起来去医治疾病,同样,对于中文词的识别,需要多种算法来处理不同的问题。
分词中的难题
有了成熟的分词算法,是否就能容易的解决中文分词的问题呢?事实远非如此。中文是一种十分复杂的语言,让计算机理解中文语言更是困难。在中文分词过程中,有两大难题一直没有完全突破。
1、歧义识别
歧义是指同样的一句话,可能有两种或者更多的切分方法。例如:表面的,因为“表面”和“面的”都是词,那么这个短语就可以分成“表面 的”和“表面的”。这种称为交叉歧义。像这种交叉歧义十分常见,前面举的“和服”的例子,其实就是因为交叉歧义引起的错误。“化妆和服装”可以分成“化妆 和服装”或者“化妆 和服 装”。由于没有人的知识去理解,计算机很难知道到底哪个方案正确。
交叉歧义相对组合歧义来说是还算比较容易处理,组合歧义就必需根据整个句子来判断了。例如,在句子“这个门把手坏了”中,“把手”是个词,但在句子“请把手拿开”中,“把手”就不是一个词;在句子“将军任命了一名中将”中,“中将”是个词,但在句子“产量三年中将增长两倍”中,“中将”就不再是词。这些词计算机又如何去识别?
如果交叉歧义和组合歧义计算机都能解决的话,在歧义中还有一个难题,是真歧义。真歧义意思是给出一句话,由人去判断也不知道哪个应该是词,哪个应该不是词。例如:“乒乓球拍卖完了”,可以切分成“乒乓 球拍 卖 完 了”、也可切分成“乒乓球 拍卖 完了”,如果没有上下文其他的句子,恐怕谁也不知道“拍卖”在这里算不算一个词。
2、新词识别
新词,专业术语称为未登录词。也就是那些在字典中都没有收录过,但又确实能称为词的那些词。最典型的是人名,人可以很容易理解句子“王军虎去广州了”中,“王军虎”是个词,因为是一个人的名字,但要是让计算机去识别就困难了。如果把“王军虎”做为一个词收录到字典中去,全世界有那么多名字,而且每时每刻都有新增的人名,收录这些人名本身就是一项巨大的工程。即使这项工作可以完成,还是会存在问题,例如:在句子“王军虎头虎脑的”中,“王军虎”还能不能算词?
新词中除了人名以外,还有机构名、地名、产品名、商标名、简称、省略语等都是很难处理的问题,而且这些又正好是人们经常使用的词,因此对于搜索引擎来说,分词系统中的新词识别十分重要。目前新词识别准确率已经成为评价一个分词系统好坏的重要标志之一。
中文分词的应用
目前在自然语言处理技术中,中文处理技术比西文处理技术要落后很大一段距离,许多西文的处理方法中文不能直接采用,就是因为中文必需有分词这道工序。中文分词是其他中文信息处理的基础,搜索引擎只是中文分词的一个应用。其他的比如机器翻译(MT)、语音合成、自动分类、自动摘要、自动校对等等,都需要用到分词。因为中文需要分词,可能会影响一些研究,但同时也为一些企业带来机会,因为国外的计算机处理技术要想进入中国市场,首先也是要解决中文分词问题。在中文研究方面,相比外国人来说,中国人有十分明显的优势。
分词准确性对搜索引擎来说十分重要,但如果分词速度太慢,即使准确性再高,对于搜索引擎来说也是不可用的,因为搜索引擎需要处理数以亿计的网页,如果分词耗用的时间过长,会严重影响搜索引擎内容更新的速度。因此对于搜索引擎来说,分词的准确性和速度,二者都需要达到很高的要求。目前研究中文分词的大多是科研院校,清华、北大、中科院、北京语言学院、东北大学、IBM研究院、微软中国研究院等都有自己的研究队伍,而真正专业研究中文分词的商业公司除了海量科技以外,几乎没有了。科研院校研究的技术,大部分不能很快产品化,而一个专业公司的力量毕竟有限,看来中文分词技术要想更好的服务于更多的产品,还有很长一段路。
搜索引擎技术的研究,国外比中国要早近十年,从最早的Archie,到后来的Excite,以及altvista、overture、google等搜索引擎面世,搜索引擎发展至今,已经有十几年的历史,而国内开始研究搜索引擎是在上世纪末本世纪初。在许多领域,都是国外的产品和技术一统天下,特别是当某种技术在国外研究多年而国内才开始的情况下。例如操作系统、字处理软件、浏览器等等,但搜索引擎却是个例外。虽然在国外搜索引擎技术早就开始研究,但在国内还是陆续涌现出优秀的搜索引擎,像百度(http://www.baidu.com)、中搜(http://www.zhongsou.com)等。目前在中文搜索引擎领域,国内的搜索引擎已经和国外的搜索引擎效果上相差不远。之所以能形成这样的局面,有一个重要的原因就在于中文和英文两种语言自身的书写方式不同,这其中对于计算机涉及的技术就是中文分词。
什么是中文分词
众所周知,英文是以词为单位的,词和词之间是靠空格隔开,而中文是以字为单位,句子中所有的字连起来才能描述一个意思。例如,英文句子I am a student,用中文则为:“我是一个学生”。计算机可以很简单通过空格知道student是一个单词,但是不能很容易明白“学”、“生”两个字合起来才表示一个词。把中文的汉字序列切分成有意义的词,就是中文分词,有些人也称为切词。我是一个学生,分词的结果是:我 是 一个 学生。
中文分词和搜索引擎
中文分词到底对搜索引擎有多大影响?对于搜索引擎来说,最重要的并不是找到所有结果,因为在上百亿的网页中找到所有结果没有太多的意义,没有人能看得完,最重要的是把最相关的结果排在最前面,这也称为相关度排序。中文分词的准确与否,常常直接影响到对搜索结果的相关度排序。笔者最近替朋友找一些关于日本和服的资料,在搜索引擎上输入“和服”,得到的结果就发现了很多问题。下面就以这个例子来说明分词对搜索结果的影响,在现有三个中文搜索引擎上做测试,测试方法是直接在Google(http://www.google.com)、百度(http://www.baidu.com)、中搜(http: //www.zhongsou.com)上以“和服”为关键词进行搜索:
在Google上输入“和服”搜索所有中文简体网页,总共结果507,000条,前20条结果中有14条与和服一点关系都没有。在第一页就有以下错误:
“通信信息报:瑞星以技术和服务开拓网络安全市场”
“使用纯HTML的通用数据管理和服务- 开发者- ZDNet ...”
“陈慧琳《心口不一》化妆和服装自己包办”
“::外交部:中国境外领事保护和服务指南(2003年版) ...”
“产品和服务”
等等。第一页只有三篇是真正在讲“和服”的结果。
在百度上输入“和服”搜索网页,总共结果为287,000条,前20条结果中有6条与和服一点关系都没有。在第一页有以下错误:
“福建省晋江市恒和服装有限公司系独资企业”
“关于商品和服务实行明码标价的规定”
“青岛东和服装设备”
在中搜上输入“和服”搜索网页,总共结果为26,917条,前20条结果都是与和服相关的网页。
这次搜索引擎结果中的错误,就是由于分词的不准确所造成的。通过笔者的了解,Google的中文分词技术采用的是美国一家名叫Basis Technology(http://www.basistech.com)的公司提供的中文分词技术,百度使用的是自己公司开发的分词技术,中搜使用的是国内海量科技(http://www.hylanda.com)提供的分词技术。由此可见,中文分词的准确度,对搜索引擎结果相关性和准确性有相当大的关系。
中文分词技术
中文分词技术属于自然语言处理技术范畴,对于一句话,人可以通过自己的知识来明白哪些是词,哪些不是词,但如何让计算机也能理解?其处理过程就是分词算法。
现有的分词算法可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。
1、基于字符串匹配的分词方法
这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与标注相结合的一体化方法。常用的几种机械分词方法如下:
1)正向最大匹配法(由左到右的方向);
2)逆向最大匹配法(由右到左的方向);
3)最少切分(使每一句中切出的词数最小)。
还可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各种其它的语言信息来进一步提高切分的准确率。
一种方法是改进扫描方式,称为特征扫描或标志切分,优先在待分析字符串中识别和切分出一些带有明显特征的词,以这些词作为断点,可将原字符串分为较小的串再来进机械分词,从而减少匹配的错误率。另一种方法是将分词和词类标注结合起来,利用丰富的词类信息对分词决策提供帮助,并且在标注过程中又反过来对分词结果进行检验、调整,从而极大地提高切分的准确率。
对于机械分词方法,可以建立一个一般的模型,在这方面有专业的学术论文,这里不做详细论述。
2、基于理解的分词方法
这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。
3、基于统计的分词方法
从形式上看,词是稳定的字的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻共现的频率或概率能够较好的反映成词的可信度。可以对语料中相邻共现的各个字的组合的频度进行统计,计算它们的互现信息。定义两个字的互现信息,计算两个汉字X、Y的相邻共现概率。互现信息体现了汉字之间结合关系的紧密程度。当紧密程度高于某一个阈值时,便可认为此字组可能构成了一个词。这种方法只需对语料中的字组频度进行统计,不需要切分词典,因而又叫做无词典分词法或统计取词方法。但这种方法也有一定的局限性,会经常抽出一些共现频度高、但并不是词的常用字组,例如“这一”、“之一”、“有的”、“我的”、“许多的”等,并且对常用词的识别精度差,时空开销大。实际应用的统计分词系统都要使用一部基本的分词词典(常用词词典)进行串匹配分词,同时使用统计方法识别一些新的词,即将串频统计和串匹配结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。
到底哪种分词算法的准确度更高,目前并无定论。对于任何一个成熟的分词系统来说,不可能单独依靠某一种算法来实现,都需要综合不同的算法。笔者了解,海量科技的分词算法就采用“复方分词法”,所谓复方,相当于用中药中的复方概念,即用不同的药才综合起来去医治疾病,同样,对于中文词的识别,需要多种算法来处理不同的问题。
分词中的难题
有了成熟的分词算法,是否就能容易的解决中文分词的问题呢?事实远非如此。中文是一种十分复杂的语言,让计算机理解中文语言更是困难。在中文分词过程中,有两大难题一直没有完全突破。
1、歧义识别
歧义是指同样的一句话,可能有两种或者更多的切分方法。例如:表面的,因为“表面”和“面的”都是词,那么这个短语就可以分成“表面 的”和“表面的”。这种称为交叉歧义。像这种交叉歧义十分常见,前面举的“和服”的例子,其实就是因为交叉歧义引起的错误。“化妆和服装”可以分成“化妆 和服装”或者“化妆 和服 装”。由于没有人的知识去理解,计算机很难知道到底哪个方案正确。
交叉歧义相对组合歧义来说是还算比较容易处理,组合歧义就必需根据整个句子来判断了。例如,在句子“这个门把手坏了”中,“把手”是个词,但在句子“请把手拿开”中,“把手”就不是一个词;在句子“将军任命了一名中将”中,“中将”是个词,但在句子“产量三年中将增长两倍”中,“中将”就不再是词。这些词计算机又如何去识别?
如果交叉歧义和组合歧义计算机都能解决的话,在歧义中还有一个难题,是真歧义。真歧义意思是给出一句话,由人去判断也不知道哪个应该是词,哪个应该不是词。例如:“乒乓球拍卖完了”,可以切分成“乒乓 球拍 卖 完 了”、也可切分成“乒乓球 拍卖 完了”,如果没有上下文其他的句子,恐怕谁也不知道“拍卖”在这里算不算一个词。
2、新词识别
新词,专业术语称为未登录词。也就是那些在字典中都没有收录过,但又确实能称为词的那些词。最典型的是人名,人可以很容易理解句子“王军虎去广州了”中,“王军虎”是个词,因为是一个人的名字,但要是让计算机去识别就困难了。如果把“王军虎”做为一个词收录到字典中去,全世界有那么多名字,而且每时每刻都有新增的人名,收录这些人名本身就是一项巨大的工程。即使这项工作可以完成,还是会存在问题,例如:在句子“王军虎头虎脑的”中,“王军虎”还能不能算词?
新词中除了人名以外,还有机构名、地名、产品名、商标名、简称、省略语等都是很难处理的问题,而且这些又正好是人们经常使用的词,因此对于搜索引擎来说,分词系统中的新词识别十分重要。目前新词识别准确率已经成为评价一个分词系统好坏的重要标志之一。
中文分词的应用
目前在自然语言处理技术中,中文处理技术比西文处理技术要落后很大一段距离,许多西文的处理方法中文不能直接采用,就是因为中文必需有分词这道工序。中文分词是其他中文信息处理的基础,搜索引擎只是中文分词的一个应用。其他的比如机器翻译(MT)、语音合成、自动分类、自动摘要、自动校对等等,都需要用到分词。因为中文需要分词,可能会影响一些研究,但同时也为一些企业带来机会,因为国外的计算机处理技术要想进入中国市场,首先也是要解决中文分词问题。在中文研究方面,相比外国人来说,中国人有十分明显的优势。
分词准确性对搜索引擎来说十分重要,但如果分词速度太慢,即使准确性再高,对于搜索引擎来说也是不可用的,因为搜索引擎需要处理数以亿计的网页,如果分词耗用的时间过长,会严重影响搜索引擎内容更新的速度。因此对于搜索引擎来说,分词的准确性和速度,二者都需要达到很高的要求。目前研究中文分词的大多是科研院校,清华、北大、中科院、北京语言学院、东北大学、IBM研究院、微软中国研究院等都有自己的研究队伍,而真正专业研究中文分词的商业公司除了海量科技以外,几乎没有了。科研院校研究的技术,大部分不能很快产品化,而一个专业公司的力量毕竟有限,看来中文分词技术要想更好的服务于更多的产品,还有很长一段路。
09/08
21
搜索引擎主要核心技术为:
(1)中英文分词语言处理;
(2)排序算法;
(3)网络爬虫;
(4)查询/存储技术
开发搜索引擎系统主要涉及到的具体技术为:
(1)http网络协议.
(2)多线程技术.
(3)socket通信.
(4)高效服务端程序开发.
(1)中英文分词语言处理;
(2)排序算法;
(3)网络爬虫;
(4)查询/存储技术
开发搜索引擎系统主要涉及到的具体技术为:
(1)http网络协议.
(2)多线程技术.
(3)socket通信.
(4)高效服务端程序开发.
09/04
8
Examples:
索引重建
./indexer --config ../conf/sphinx.conf main
./indexer --config ../conf/sphinx.conf delta
关闭搜索
./searchd -c ../conf/sphinx.conf --stop
合并索引
./indexer --merge main delta --config ../conf/sphinx.conf
启动搜索
./searchd -c ../conf/sphinx.conf
索引重建
./indexer --config ../conf/sphinx.conf main
./indexer --config ../conf/sphinx.conf delta
关闭搜索
./searchd -c ../conf/sphinx.conf --stop
合并索引
./indexer --merge main delta --config ../conf/sphinx.conf
启动搜索
./searchd -c ../conf/sphinx.conf
09/03
26
20款开源搜索引擎系统





