lucene搜索实现过程详解_第1页
lucene搜索实现过程详解_第2页
lucene搜索实现过程详解_第3页
已阅读5页,还剩9页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、lucene 搜索实现过程详解郭玉璞2008-05-05一文档说明1关于 lucene 索引生成过程和生成文件结构,在关于lucene建立索引的详细过程及相关文件结构一文中已经说明,本文涉及到的文件结构,请参考该文档,这里不再累赘, 并且我们假设您已经对索引文件格式有了一定的了解。2为了对搜索的实现过程有一个清晰的认识,本文采用分级的形势, 层层深入。3For personal use only in study and research; not for commercial use45对于分词这一块,将有专题讲解,本文在不影响理解的情况下,没有详解。二搜索过程简介索引的过程主要分为以下几个

2、步骤:1For personal use only in study and research; not for commercial use23 打开索引文件,并将索引文件的相关信息读入。代码如下:IndexReader reader = IndexReader.Open("index");其中,index 为索引文件所在的文件夹名称。该句执行完以后,索引文件的相关信息将存放在reader中。4实例化 IndexSearcher,以便于后面的搜索操作。代码如下:IndexSearcher searcher = new IndexSearcher(reader);该句执行完

3、后,索引文件信息将存放在searcher 中,后面的的搜索将运用searcher,而不是运用 reader。当然, 初始化方式有很多种,这里就以代码所示为例。5声明查询分析器QueryPaser。 代码如下:QueryParser parser = new QueryParser(field, analyzer);其中, field 为要查询的字段, analyzer 为所选择的分析器。6设置 Query 间的逻辑关系。 代码如下:parser.SetDefaultOperator(;该句的功能是,如果用户以空格隔开两个字符串,设置这两个字符串之间的关系,代码所示的是与的关系,根据需要也可以设

4、置成OR 等关系。此句可有可无, Lucene 默认的是 OR 的关系。7生成 Query 子对象。 代码如下:Query query = parser.Parse(strQuery);其中, strQuery 为用户输入的待查询的字符串。该句的主要功能是将用户输入的字符串进行分词,并记录每个Token 之间的位置关系,以便于后面的查询。当然,Lucene允许用户直接创建 Query,也允许用户采用多种方法构建 Query。例如:按词条搜索 TermQuery、 “与或 ”搜索 BooleanQuery、在某一范围内搜索 RangeQuery、使用前缀搜索 PrefixQuery、多关键字的搜

5、索 PhraseQuery、使用短语缀搜索 PhrasePrefixQuery、相近词语的搜索 FuzzyQuery、使用通配符搜索 WildcardQuery。各种方法原理相同,都是将各单个 Query 搜索,然后再将各自结果按照“与”或者“或”的关系得出最终结果。在本文中,我们将以代码所示的简单方式为例。8搜索,返回处理结果。 代码如下:Hits hits = searcher.Search(query);在该句中,不仅涉及搜索的过程,而且还有排序、过滤等过程。7根据搜索生成的内部编号,返回真正的结果。代码如下:Document doc = hits.Doc(i);三搜索过程详解。1Ind

6、exReader reader = IndexReader.Open("index"); 该句调用 IndexReader的Open 方法:publicstaticIndexReader Open(System.Stringpath)returnOpen(FSDirectory.GetDirectory(path,false ),true );11 FSDirectory.GetDirectory(path,false),该方法获取索引文件夹的完全路径和创建临时文件路径。其中,path为索引文件所在文件夹的名称,false表示不要创建新的文件夹。该方法调用FSDirecto

7、ry 的GetDirectory 方法:publicstaticFSDirectory GetDirectory(System.Stringpath,bool create)returnGetDirectory(new , create);111 GetDirectory(new , create);该方法是获取文夹完整路径的核心方法,其他方法都调用此方法。首先,通过语句 file = new ; 获得索引文件所在文件夹的完整路径到file 。其次,根据 file 创建临时文件将要存放的路径:FSDirectory dir;dir = (FSDirectory) DIRECTORIESfile

8、;dir = (FSDirectory) ;然后,进行初始化工作: dir.Init(file, create); 由于现在是搜索过程,并非创建索引的过程,初始化工作只是将两个完整路径传给dir 。最后,返回 dir。12 Open(FSDirectory.GetDirectory(path, false), true);该方法调用 IndexReader的 Open(Directory directory, bool closeDirectory)方法。其中, directroy 就是上面返回的 dir。在该方法中,有三个主要方法 MakeLock 、AnonymousClassWith、R

9、un()。1 2 1directory.MakeLock(IndexWriter.COMMIT_LOCK_NAME); 其 中COMMIT_LOCK_NAME为锁文件名,在 Lucene 中为 commit.lock 。此刻该文件表示有进程在读“ segment”文件和打开某些段的文件。在该方法中,首先获取索引文件目录的前缀, 并以此来命名锁文件名称; 然后将临时文件夹的完整路径和 锁 文 件 名 称 组 成 完 整 了 锁 文 件 名 称LockFile ; 最 后 初 始 化 该 锁 :AnonymousClassLock(lockFile, this);其中 this 为当前索引文件目录

10、路径。122AnonymousClassWith(directory,closeDirectory,directory.MakeLock(IndexWriter.COMMIT_LOCK_NAME),IndexWriter.COMMIT_LOCK_TIMEOUT);该方法主要做一些初始化工作。 其中,COMMIT_LOCK_NAME为获取锁文件的限定时间,Lucene 默认时间为10000毫秒。123 Run()这是读取索引文件的核心方法。该方法代码如下:publicvirtualSystem. Object Run()bool locked =tryfalse ;locked = lock_R

11、enamed.Obtain(lockWaitTimeout);returnDoBody();finallyif(locked)lock_Renamed.Release();1231 根据限定时间lockWaitTimeOut 获取锁文件。在获取过程中,每秒钟试一次,直到获得锁文件为止。如果重试超过十次,则被阻塞。1 2 3 2 DoBody() ;该方法首先声明一个SegmentInfos 对象,用以管理SegmentInfo:SegmentInfos infos = new SegmentInfos();12321 infos.Read(directory);其中 directory 为索引

12、文件目录。该方法读取Segments文件信息。1)IndexInput input = directory.OpenInput(IndexFileNames.SEGMENTS);创建一个Segments文件的输入流,用以读取Segments文件信息。2)int format = input.ReadInt();读取索引文件格式信息。Lucene2.0默认为 1。3)version = input.ReadLong();读取版本信息。4)counter = input.ReadInt();读取 counter, counter 用于给新生成的索引起名字。5)在 for 循环中, int i =

13、input.ReadInt(); 读取 segment的个数 count。6)SegmentInfo si = new SegmentInfo(input.ReadString(), input.ReadInt(), directory);依次读入每个 segment的名字和包含文档的个数,并用Add(si); 将每个 si 加入到ArrayList 。当然,在旧版本的lucene 当中,可能读取的顺序不太一样,但道理是一样的,都是根据索引的结构依次读出。12322 对于生成的索引, segment的个数可能不止一个,本文就以一个segment为例,多个 segment与之类似。return

14、SegmentReader.Get(infos, infos.Info(0), closeDirectory);读取其他文件或为其他文件创建输入流,以供以后读取。Get 方法层层调用,现在只分析最后的核心方法:1)instance = (SegmentReader)创;建 SegmentReader实例 instance。2)instance.Init(dir, sis, closeDir, ownDir);初始化工作。3)instance.Initialize(si);读取的核心部分。首先要根据 .cfs 文件是否存在来判断是否是复合索引结构,本文采用非复合索引结构来说明,复合索引结构与之类

15、似。a)fieldInfos = new FieldInfos(cfsDir, segment + ".fnm"); 读取 .fnm 文件信息。该方法实现如下:IndexInput input = d.OpenInput(name);name为 .fnm 文件名。为.fnm 文件创建输入流 input,以供读取。Read(input);读取信息:int size = input.ReadVInt();读取字段( filed )的个数。System.String name = String.Intern(input.ReadString();读取各个字段的名称。byte bi

16、ts = input.ReadByte();读取标志位。下面五句是根据 bits 的值来判断该域是否被索引等信息。AddInternal(name, isIndexed, storeTermVector, storePositionsWithTermVector,storeOffsetWithTermVector, omitNorms); 将该域的这些信息存储,存储的方法有两种,一种是byName,根据名字来存储;一种是byNumber,根据编号来存储。Input.Close();关闭 .fnm 文件的输入流。b)fieldsReader = new FieldsReader(cfsDir,

17、segment, fieldInfos);为.fdt 和 .fdx 文件创建输入流 fieldStream、 indexStream,并记录 document的数量 size。c)tis = new TermInfosReader(cfsDir, segment, fieldInfos);读取 .tis 和 .tii 文件的相关信息,实现如下:origEnum = new SegmentTermEnum(directory.OpenInput(segment + ".tis"), fieldInfos, false);读取 .tis 文件信息:directory.OpenI

18、nput(segment + ".tis");创建 .tis 文件的输入流。int firstInt = input.ReadInt(); 获取版本号。 Lucene2.0当中为 2。size = input.ReadLong();读取 term 的个数。indexInterval = input.ReadInt();读取索引间隔,默认值为128。skipInterval = input.ReadInt();读取跳跃间隔,默认值为16。indexEnum = new SegmentTermEnum(directory.OpenInput(segment+ ".ti

19、i"),fieldInfos, true); 读取 .tis 文件信息。由于 .tis 文件结构与 .tii 文件结构相似,这里就不详细展开叙述。d)if (HasDeletions(si)deletedDocs = new BitVector(Directory(), segment + ".del");如果之前有删除文档的记录,那么这里将删除。关于document 的删除工作,将有专题讲述,这里就不再叙述。e) freqStream = cfsDir.OpenInput(segment + ".frq"); 创建 .frq 文件的输入流fr

20、qStream。f) proxStream = cfsDir.OpenInput(segment + ".prx"); 创建 .prx 文件的输入流proxStream。g) OpenNorms(cfsDir); 读取 .f 文件的相关信息。4)return instance;返回该实例。该实例记载了各个文件的信息。也就是说,关于索引各个文件的信息,记录在searcher当中,后面的查询工作将用到searcher。关于 searcher的结构,请参看Lucene 的源代码。2IndexSearcher searcher = new IndexSearcher(reader)

21、;该方法主要是进行一些初始化工作。 需要说明的是 similarity ,它是用于后面排序的积分计算的。这里调用 similarity = Similarity.GetDefault(); 即采用 Lucene 默认的值。3QueryParser parser = new QueryParser("describ", analyzer);声明一个查询分析器,并进行一些初始化的处理。该方法调用CharStream 类型为参数的重载构造函数 public QueryParser(CharStream stream);然后初始化。3 1 publicQueryParser(Cha

22、rStream stream); 该方 法主 要是 利用一个空的CharStream类型来进行初始化工作, 为下面的分词工作做好准备。 初始化过程中,涉及很多参数,比如:fuzzyMinSim 最小相似度, fuzzyPrefixLength前缀长度,用于模糊查询;一些以jj 开头的参数,是由javaCC 生成的,用于分词的工作,这点在分析器的文档中将详细介绍,这里不再累赘。32 将传入的分析器 analyzer、字段 field 传给对象: analyzer = a; ield = f;至此,完成查询分析器的初始化工作。4Query query = parser.Parse(strQuery

23、);这是对用户输入字符串处理分词和记录位置关系的最核心部分。在该方法中,主要只有两句。41 ReInit(new FastCharStream(new ;411 该方法首先用StringReader对象构造一个FastCharStream对象作为重新初始化(因为在声明QueryPaser对象的时候已经初始化过)的参数。即:new FastCharStream(new;412 调用 QueryPaser的方法 ReInit(CharStream stream);其中 stream 就是上面生成的 FastCharStream对象,FastCharStream类是 CharStream类的子类。该

24、方法和声明查询分析器时的构造方法相似,这里就不再说明。42 return Query(field); 其中 field 为要查询的字段名。该方法比较复杂,但我们只要把握一点,就是它的主要功能是对用户输入内容进行分词,然后构建Query对象用于查询,也就很好理解了。421 mods = Modifiers(); 该方法主要是判断用户是否使用复合查询,即是否使用 BooleanQuery 等查询。本文为了简单起见,就以简单查询为例,复合查询的方法是类似的,这点我们在前面已经讲过了。4211 Jj_ntk();这个是 javaCC 生成的方法,用于判断得出mods 的值。 mods的默认值是 0,即

25、简单查询。return (jj_ntk = (token.next = token_source.GetNextToken().kind);或者 return (jj_ntk = jj_nt.kind);由此可知,该方法返回的是字符串的类型。4212 switch (jj_ntk = - 1) ? Jj_ntk() : jj_ntk) ,这个 switch 语句就是根据jj_ntk的值,来判定查询方法。由代码我们可以看出, 主要由三种方法: PLUS、MINUS 、NOT。由于我们以简单方法为例,ret 的值并没有改变,返回默认值0。422 q = Clause(field);该方法实现分词功

26、能。4221 Jj_2_1(2); javaCC 生成的方法,判断是否为术语或者冒号。4222 这里同样有一个 switch (jj_ntk = - 1) ? Jj_ntk() : jj_ntk)语句,不过它是用来判断字符类型的, 根据不同的类型, 进行不同的处理。 分词涉及的类型很多,您可以从QueryParserConstants清楚地看出来。查询的分词和索引时的分词是类似的。关于分词,我们将在分析器的专题中详细介绍,这里就不再累赘。最终返回的 q 便为分词的结果。 它是 Query 类型,记录了每个 Token 的位置position、所属字段 field 、乘积因子 boost、跨度

27、slop 和内容 content 等详细信息。423 AddClause(clauses, CONJ_NONE, mods, q);把每个 Token 加入到 ArrayList的对象当中。在这个方法中,涉及到几个判断。4231 if (clauses.Count > 0 && conj = CONJ_AND) ,这是对于复合查询而言的,如果之前已有 Token 加入 ArrayList 对象中,并且之前与现在是与的关系,那么将现在的 Token 加入其中,并设置与的关系。 由于这里是简单查询, 不作详细说明。4232 if (clauses.Count > 0 &

28、amp;& operator_Renamed = AND_OPERATOR &&conj = CONJ_OR),这也是对复合查询而言的。4233 if (operator_Renamed = OR_OPERATOR),用于判断当前各个Token之间的逻辑关系。下面将根据 required和 prohibited 两个参数的值,决定每个 Token的与、或、非,然后将各个Token 添加到 ArrayList 的对象 clauses。4234 接下来的一个 while 循环,根据之前判定的查询方式、 token 间逻辑关系等参数,返回 Query 对象。在简单查询中,直接

29、返回对象 q。5Hits hits = searcher.Search(query);这是整个搜索过程的核心部分,前面的部分都是为该部分作准备。现在,索引文件信息存储在searcher中,用户输入字符串信息存储在 query 中,两个条件都具备了,可以开始查询了。查询的结果存放在 Hits 的对象 hits 当中。需要说明的是, Search 有很多个重载方法,但最终都要调用Hits 的构造函数实现查询工作。 本文以构造函数Hits(Searcher s, Query q, Filter f)为例。其中 s、q 分别为上面传入的参数searcher、query, f 为过滤器,这里默认为nul

30、l 。Hits(Searcher s, Query q, Filter f)的功能实现主要通过两个关键语句。51 weight = q.Weight(s);该方法主要计算权重, 是查询结果的排序工作的依据之一。Weight 是一个接口类,它存在的目的是使检索不改变一个Query,使得 Query可以重用,即实现Query 对象的重复使用。511 Query query = searcher.Rewrite(this);实现 query 的重写。重写的目的是似的最终的 query 存放的是本次查询用户输入的信息,即第4 部分返回的对象q。它的实现方法很简单,如果是第一次查询,则直接返回;如果不是

31、第一次查询,则将本次查询对象赋值给query 然后再返回。512 Weight weight = query.CreateWeight(searcher);该方法中,有一个判断语句if (terms.Count = 1),即如果只有一个 token,那么由于不存在两个token 之间的位 置关 系, 可 以直 接计 算权 重, 如果 不 止一 个token , 那 么 调用 方法PhraseWeight(this, searcher);this就是本次 query。5121 PhraseWeight(this, searcher)该方法首先进行一些初始化工作,相似度等参数都采用默认的值。接着调

32、用idf = similarity.Idf(Enclosing_Instance.terms,searcher)计算每个短语的得分因子idf 。5122 float sum = weight.SumOfSquaredWeights();计算得分。5123 float norm = GetSimilarity(searcher).QueryNorm(sum);计算标准化因子。5124 weight.Normalize(norm); 对权重进行标准化处理。关于各种因子的计算方法,请参考附件一。52 GetMoreDocs(50);返回查询结果得分最高的前100 条。在 lucene2.0 当中,查

33、询结果如果足够多, 并不是一次性的全部返回,lucene 默认先返回得分最高的100 条,一般来讲,这 100 条已经能够满足用户的需求了;如果还不能满足用户的需求,根据用户需要, 再返回得分次高的200 条,接着 400 条 依次类推,直到满足用户需求为止。该方法的实现如下:privatevoidGetMoreDocs( intmin)if(hitDocs.Count > min)min = hitDocs.Count;intn = min * 2;/ double # retrievedTopDocs topDocs = (sort =null ) ? searcher.Search

34、(weight, filter, n) :searcher.Search(weight, filter, n, sort);length = topDocs.totalHits;ScoreDoc scoreDocs = topDocs.scoreDocs;floatscoreNorm = 1.0f;if(length > 0 && topDocs.GetMaxScore() > 1.0f)scoreNorm = 1.0f / topDocs.GetMaxScore();intend = scoreDocs.Length < length?scoreDocs.L

35、ength:length;for( inti = hitDocs.Count; i < end; i+)hitDocs.Add(new HitDoc(scoreDocsi.score * scoreNorm, scoreDocsi.doc);521 TopDocs topDocs = (sort = null) ? searcher.Search(weight,filter, n) :searcher.Search(weight,filter, n, sort); 由于之前调用的Hits 构造函数没有为sort赋值,因此这里sortnull 。所以执行 searcher.Search(w

36、eight, filter, n);这里 n 为默认值 100。5211 TopDocCollector collector = new TopDocCollector(nDocs); 声明一个大小为 nDocs 的 TopDocCollector 对象 collector,用于存放查询的结果。5212 Search(weight, filter, collector);这句是查询的核心语句,实现真正的查询功能。这里的filter 为默认的 null 。52121 Scorer scorer = weight.Scorer(reader);1)TermPositions tps = new T

37、ermPositionsEnclosing_;声明一个 TermPostions类型的数组,用于存放每个term 在索引文件中的位置等信息。2)一个for 循环当中,执行TermPositions p = reader.TermPositions(Term)Enclosing_Instance.termsi);查询每个 term 的位置:a ) TermPositions termPositions = TermPositions(); 该 方 法 最 终 调 用SegmentTermDocs方法 .frq 文件输入流 frqStream、删除文档信息 deletedDocs 和跳跃间隔因子

38、skipInterval ,然后获取 .prx 文件输入流 proxStream。b) termPositions.Seek(term); 查找 term 在索引文件中的位置。TermInfo ti = ;由于此时 termPositions 记录了相关索引文件的输入流, 在该方法中,核心是调用 GetIndexOffset(Term term)方法,采用折半查找的方式,查询每个 term 在.tis 当中的位置。该句执行完后, ti 记录了该 term 出现的频率 docFreq、在 .frq 文件中的指针 frepPointer、在 .prx 文件中的指针proxPointer、跳跃因子

39、skipOffset。Seek(ti);有了以上的位置信息,就可以寻找索引中的该term 了。3 ) ExactPhraseScorer(this, tps,Enclosing_Instance.GetPositions(), similarity,reader.Norms(Enclosing_Instance.field);其中, this 就是上面的 weight,tps 为各个term 的位置、频率等记录信息。该句的作用是优化的作用,让各个term 形成一个链表。52122 scorer.Score(collector); 该句的作用是把查询符合结果的doc 与其对应的得分形成一对然后存放在collector 当中。5213 return collector.TopDocs(); 该句返回查询结果。522 hitDocs.Add(new HitDoc(scoreDocsi.score * scoreNorm, scoreDocsi.doc);将查询结果放入 Array

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论