代码改变世界

Lucene.net 实现全文搜索

2006-07-19 13:26  蓝之风  阅读(4155)  评论(1编辑  收藏  举报
忙了几天终于实现一个简单的全文搜索在此回顾总结一下

本文介绍一下Lucene.Net 是什么?Lucene.Net 能作什么?以及怎么做的问题?最后给出 Lucene.Net 实现全文搜索的一个示例

1、Lucene.Net 是什么?

Lucene.net 起初是一个开源项目然后转向商业化,也在Lucene.net 
2.0已经发布,不过是要money D ,Lucene.net的命运有点类似于FreeTextBox ,它在 1.6.5 版本之后发布的 2.0 开始了商业路线,2.0 提供了 DLL 方式的免费版本,源代码版本则必须购买商业的许可 licence;不过它留下了 1.6.5 版本的源代码,还是可以看到大部分的内部细节,但 2.0 版本中添加的对 Mozilla 浏览器的支持部分只有通过它生成的 HTML 和 JavaScript 脚本去窥测。

Lucene 是 Java 世界中常用的索引 API,使用它提供的方法可以为文本资料创建索引,并提供检索。(参考:NLucene 和 Lucene .NET)NLucene 是第一个的 .net 移植,也是一个有 .net 风格的版本,使用 .net 的命名规范和类库设计。不过 NLucene 项目的 leader 由于精力原因,只发布了 
1.2beta 版本。Lucene.NET 项目出现后,NLucene 就没有新的计划了。

Lucene.NET 当初号称要做 up
-to-date 的 .net Lucene 移植,它只在命名方面采纳了 .net 的建议,主要目标倾向于和 Java Lucene 兼容:一个是索引格式兼容,达到可以共同工作的目的;一个是命名接近(只相差很少,比如大小写等),目的是可以方便开发者使用 Java Lucene 相关的代码和资料。

不知什么时候 Lucene.NET 项目已经放弃了开源计划,转向了商业。它居然把 SourceForge 上已经开源的文件也删除了。与此同时,SourceForge 上又出现了 dotLucene 项目,出于对 Lucene.NET 的抗议,dotLucene 几乎将 Lucene.NET 的代码原封不动放在上面作为他们的起点。(https:
//sourceforge.net/forum/forum.php?thread_id=1153933&forum_id=408004)。

说白了Lucene.Net就是是一个信息检索的函数库(Library),利用它你可以为你的应用加上索引和搜索的功能. 

Lucene的使用者不必深入了解有关全文检索的知识,仅仅学会使用库中的几个类,知道怎么调用Library中的函数,就可以为你的应用实现全文检索的功能. 

不过千万别期望Lucene是一个象google和百度那样的搜索引擎,它仅仅是一个工具,一个Library.你也可以把它理解为一个将索引,搜索功能封装的很好的一套简单易用的API.利用这套API你可以做很多有关搜索的事情,而且很方便,它可以满足你对一个应用做简单的全文搜索,作为应用的开发者(非专业搜索引擎开发者)来说,它的功能足以满足你。

2、Lucene.Net 可以作什么?

Lucene可以对任何的数据做索引和搜索. Lucene不管数据源是什么格式,只要它能被转化为文字的形式,就可以被Lucene所分析利用.也就是说不管是MS word, Html ,pdf还是其他什么形式的文件只要你可以从中抽取出文字形式的内容就可以被Lucene所用.你就可以用Lucene对它们进行索引以及搜索.

3、使用 Lucene.Net 怎么做?

简单的归结为:创建索引,和使用索引,其中创建索引就是将要搜索的数据源的那些信息作为我们的关键信息来存储或者是分析,为搜索留下标记就象Word里面创建目录(个人理解),使用索引就是在搜索的时候根据索引的信息来分析数据源将我们需要的信息提取出来。

具体请看一下示例:

创建索引的类

public class IntranetIndexer
    
{
        
////索引写入器
        private IndexWriter writer;

        
//要写入索引的文件的根目录
        private string docRootDirectory;

        
//要匹配的文件格式
        private string[] pattern;

        
/// <summary>
        
/// 初始化一个索引写入器writer,directory为创建索引的目录,true代表如果不存在索引文件将重新创建索引文件,如果已经存在索引文件将覆写索引文件
        
/// </summary>
        
/// <param name="directory">传入的要创建索引的目录,注意是字符串值,如果目录不存在,他将会被自动创建</param>

        public IntranetIndexer(string directory)
        
{
            writer 
= new IndexWriter(directory, new StandardAnalyzer(), true);
            writer.SetUseCompoundFile(
true);
        }

        
        
public void AddDirectory(DirectoryInfo directory, string [] pattern)
        
{
            
this.docRootDirectory = directory.FullName;
            
this.pattern = pattern;
            addSubDirectory(directory);
        }

        
        
private void addSubDirectory(DirectoryInfo directory)
        
{
            
for(int i=0;i<pattern .Length ;i++)
            
{
                
foreach (FileInfo fi in directory.GetFiles(pattern[i]))
                
{
                    AddHtmlDocument(fi.FullName);
                }

            }

            
foreach (DirectoryInfo di in directory.GetDirectories())
            
{
                addSubDirectory(di);
            }

        }

        
        
public void AddHtmlDocument(string path)
        
{
            
string exname=Path.GetExtension (path);
            Document doc 
= new Document();    
        
            
string html;            
            
if(exname.ToLower ()==".html" ||exname .ToLower ()==".htm"||exname .ToLower ()==".txt")
            
{
                
using(StreamReader sr=new StreamReader (path,System .Text .Encoding .Default ))
                

                     html 
= sr.ReadToEnd();
                }

            }

            
else
            
{
                
using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Unicode  ))
                
{
                    html 
= sr.ReadToEnd();
                }

            }


            
int relativePathStartsAt = this.docRootDirectory.EndsWith("\\"? this.docRootDirectory.Length : this.docRootDirectory.Length + 1;
            
string relativePath = path.Substring(relativePathStartsAt);
            
string title=Path.GetFileName(path);
            
            
//判断若是网页则去标签否则不用
            if(exname.ToLower ()==".html" ||exname .ToLower ()==".htm")
            
{
                doc.Add(Field.UnStored(
"text", parseHtml(html)));
            }

            
else
            
{
                doc.Add (Field .UnStored (
"text",html));
            }

            doc.Add(Field.Keyword(
"path", relativePath));
            
//doc.Add(Field.Text("title", getTitle(html)));
            doc.Add (Field .Text ("title",title));
            writer.AddDocument(doc);
        }
  
        
/// <summary>
        
/// 去除网页中的标签
        
/// </summary>
        
/// <param name="html">网页</param>
        
/// <returns>返回去除后的网页文本</returns>

        private string parseHtml(string html)
        
{
            
string temp = Regex.Replace(html, "<[^>]*>""");
            
return temp.Replace("&nbsp;"" ");
        }

        
/// <summary>
        
/// 获取网页标题
        
/// </summary>
        
/// <param name="html"></param>
        
/// <returns></returns>

        private string getTitle(string html)
        
{
            Match m 
= Regex.Match(html, "<title>(.*)</title>");
            
if (m.Groups.Count == 2)
                
return m.Groups[1].Value;
            
return "文档标题未知";
        }

        
/// <summary>
        
/// 优化索引并关闭写入器
        
/// </summary>

        public void Close()
        
{
            writer.Optimize();
            writer.Close();
        }

    }
 

首先建立Document对象,然后为Document对象添加一些属性Field.你可以把Document对象看成是虚拟文件,将来将从此获取信息.而Field则看成是描述此虚拟文件的元数据(metadata).其中Field包括四个类型: Keywork 

 该类型的数据将不被分析,而会被索引并保存保存在索引中. 
 
UnIndexed 
 该类型的数据不会被分析也不会被索引,但是会保存在索引. 
 
UnStored 
 和UnIndexed刚好相反,被分析被索引,但是不被保存. 
 
Text 
 和UnStrored类似.如果值的类型为string还会被保存.如果值的类型为Reader就不会被保存和UnStored一样. 
 

最后将每一个Document添加到索引当中。

下面是对索引进行搜索 

   
//创建一个索引器
   IndexSearcher searcher = new IndexSearcher(indexDirectory); 
   
//解析索引的text字段以便搜索
   Query query = QueryParser.Parse(this.Q, "text"new StandardAnalyzer());  
   
//将搜索结果放在hits中
   Hits hits = searcher.Search(query);   
   
//统计搜索的总记录数
   this.total = hits.Length();   
   
//高亮显示
   QueryHighlightExtractor highlighter = new QueryHighlightExtractor(query, new StandardAnalyzer(), "<font color=red>""</font>");   
第一步利用IndexSearcher打开索引文件用于后面搜索,其中的参数是索引文件的路径.

第二步使用QueryParser将可读性较好的查询语句(比如查询的词lucene ,以及一些高级方式lucene AND .net)转化为Lucene内部使用的查询对象.

第三步执行搜索.并将结果返回到hits集合.需要注意的是Lucene并不是一次将所有的结果放入hits中而是采取一次放一部分的方式.出于空间考虑. 

然后将搜索的结果进行处理并在页面上显示出来:

for (int i = startAt; i < resultsCount; i++
   
{
    
    Document doc 
= hits.Doc(i);   
   
    
string path = doc.Get("path");   

    
string location =Server.MapPath("documents")+"\\"+path;
                
string exname=Path.GetExtension (path);
    
    
string plainText ;
    
string str=doc.Get ("title");
    
if(exname==".html" || exname ==".htm" || exname ==".txt")
    
{
     
using (StreamReader sr = new StreamReader(location, System.Text.Encoding.Default))
     
{
      plainText 
= parseHtml(sr.ReadToEnd());
     }

    }

    
else
    
{
     
using (StreamReader sr = new StreamReader(location, System.Text.Encoding.Unicode ))
     
{
      plainText 
= sr.ReadToEnd();
     }

    }

    
    
//DataTable 添加行 
    DataRow row = this.Results.NewRow();
    row[
"title"= doc.Get("title");
    
string IP=Request.Url.Host;//获取服务器IP
    
//Request.Url.Port;     
    row["path"]=@"http://"+IP+"/WebUI/Search/documents/"+path;
    row[
"sample"= highlighter.GetBestFragments(plainText, 802"");   
    
this.Results.Rows.Add(row);
   }
 
   searcher.Close();
//关闭搜索器
想对Lucene.Net 进行更高级,更全面,更深层次了解的请参阅一下网站:

http:
//www.alphatom.com/

http:
//blog.tianya.cn/blogger/view_blog.asp?BlogName=aftaft