• Lumia920   2014/7/9 13:56:00
  • Lucene.Net学习之索引的创建和使用
  • 关键字: Lucene.Net 索引 搜索框架
  • Lucene.Net的应用相对比较简单。一段时间以来,我最多只是在项目中写点代码,利用一下它的类库而已,对很多名词术语不是很清晰,甚至理解可能还有偏差。从我过去的博客你也可以看出,语言表达一直不是个人所长,就算”表达“了也有大面积抄书的嫌疑,所以很多概念性的介绍能省则省(除非特别有别要说明),希望有心的初学者注意,理清概念和辨别技术名词非常重要,请参考相关文档。

    Lucene的索引由1或多个segment(片段)构成,一个segment由多个document构成,一个document又由1个或多个field构成,一个field又由一个或多个term构成。下面这张图可以说明一切:


    从图中不难看出,Lucene的索引是一个由点到线,由线到面的组成结构,这一点我们可以通过查看Lucene生成的索引文件看出来。


    创建、优化、删除和更新索引实践
    备注:在解决方案所在文件夹中,有一个测试用的Resource文件夹,内有4个.txt文件。我在本地测试的时候,就使用了Resource下的四个文本文件。

    1、索引保存至文件
    (1)、创建索引
    先初始化一个IndexModifier对象,然后执行创建索引的核心方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    /// <summary> 
    /// 给txt文件创建索引 
    /// </summary> 
    /// <param name="file"></param> 
    /// <param name="modifier"></param> 
    private void IndexFile(FileInfo file, IndexModifier modifier) 
        try
        
            Document doc = new Document();//创建文档,给文档添加字段,并把文档添加到索引书写器里 
            SetOutput("正在建立索引,文件名:" + file.FullName); 
      
            doc.Add(new Field("id", id.ToString(), Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
            id++; 
      
            /* filename begin */
            doc.Add(new Field("filename", file.FullName, Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
            //doc.Add(new Field("filename", file.FullName, Field.Store.YES, Field.Index.UN_TOKENIZED)); 
            //doc.Add(new Field("filename", file.FullName, Field.Store.NO, Field.Index.TOKENIZED)); 
            //doc.Add(new Field("filename", file.FullName, Field.Store.NO, Field.Index.UN_TOKENIZED)); 
            /* filename end */
      
            /* contents begin */
            //doc.Add(new Field("contents", new StreamReader(file.FullName, System.Text.Encoding.Default))); 
      
            string contents = string.Empty; 
            using (TextReader rdr = new StreamReader(file.FullName, System.Text.Encoding.Default)) 
            
                contents = rdr.ReadToEnd();//将文件内容提取出来 
                doc.Add(new Field("contents", contents, Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
                //doc.Add(new Field("contents", contents, Field.Store.NO, Field.Index.TOKENIZED));//不存储索引 
            
            /* contents end */
            modifier.AddDocument(doc); 
        
      
        catch (FileNotFoundException fnfe) 
        
        
    }
    最后,IndexModifier对象执行Close方法。

    几个注意点:

    a、IndexModifier类封装了平时经常使用的IndexWriter和IndexReader,而且不用我们额外考虑多线程;

    b、StandardAnalyzer是经常使用的一个Analyzer,目前对中文分词支持的也还不错(大名鼎鼎的盘古分词请参考牛人eaglet的这几篇);

    c、IndexModifier的Optimize方法的执行可以优化索引文件,但是比较耗时间,根据我的测试,索引文件越大,优化时间线性增加,所以实际的开发中这个方法我们都会按照一定的策略执行;

    d、IndexModifier的Close方法必须执行,否则你所做的一切都是无用功。

    (2)、按照id删除一条索引
    代码相对而言非常简单,直接利用IndexModifier 的DeleteDocuents方法:

    1
    2
    3
    4
    5
    6
    7
    Directory directory = FSDirectory.GetDirectory(INDEX_STORE_PATH, false); 
    IndexModifier modifier = new IndexModifier(directory, new StandardAnalyzer(), false); 
      
    Term term = new Term("id", id); 
    modifier.DeleteDocuments(term);//删除   
    modifier.Close(); 
    directory.Close();
    其中,IndexModifier还有一个方法DeleteDocument,它的参数是整数docNum,通常我们也不知道索引文件的内部docNum是多少,所以非常少用它。

    (3)、按照id更新一条索引
    贴一下主要方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    bool enableCreate = IsEnableCreated();//是否已经创建索引文件 
    Term term = new Term("id", id); 
    Document doc = new Document(); 
    doc = new Document();//创建文档,给文档添加字段,并把文档添加到索引书写器里 
    doc.Add(new Field("id", id, Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
    doc.Add(new Field("filename", filename, Field.Store.YES, Field.Index.TOKENIZED)); 
    doc.Add(new Field("contents", filename, Field.Store.YES, Field.Index.TOKENIZED)); 
    LuceneIO.Directory directory = LuceneIO.FSDirectory.GetDirectory(INDEX_STORE_PATH, enableCreate); 
    IndexWriter writer = new IndexWriter(directory, new StandardAnalyzer(),IndexWriter.MaxFieldLength.LIMITED); 
    writer.UpdateDocument(term, doc); 
    writer.Optimize(); 
    //writer.Commit(); 
    writer.Close(); 
    directory.Close();
    需要注意,这一次,我们使用了IndexWriter对象的UpdateDocument方法,而IndexModifier没有找到现成的UpdateDocument方法。Optimize通常需要执行一下,否则索引文件中会有两个相同id的索引。

    2、索引保存至内存
    如果1你已经理解了,2其实可以不用细究。在IndexModifier的构造函数里有一个重载:

    public IndexModifier(Directory directory, Analyzer analyzer, bool create);
    下面的示例代码中第一个参数RAMDirectory就是一个Directory,我们可以把它定义成静态,创建索引的时候就完成了保存至内存的效果:


    private static RAMDirectory ramDir = null;   
    IndexModifier  modifier = new IndexModifier(ramDir, new StandardAnalyzer(), true);
    经测试,增删改查原理同1。

    3、利用Lucene.Net配合数据库查询
    平时开发中,对于数据库中的海量数据,频繁读库可能不能满足效率和速度的需求。我们也可以利用Lucene.Net配合数据库快速查询结果。至于如何对数据库利用Lucene.Net创建索引,增删改查和同1中的介绍是一模一样的。比如本文demo中创建索引的实现,取前1000个人对他们的Id和姓名进行索引。在编码之前,我先往Person表中插入了一些数据:

    1
    2
    3
    4
    5
    6
    7
    8
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('明','姚',200,223) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('建联','易',180,213) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('德科','诺维斯基',180,211) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('德怀特','霍华德',190,218) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('约什','霍华德',178,197) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('蒂姆','邓肯',183,211) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('凯文','加内特',182,215) 
    INSERT Person(FirstName,LastName,Weight,Height) VALUES('德隆','威廉姆斯',166,197)
    接着先取出1000个人:


    string sql = "SELECT TOP 1000 Id,FirstName,LastName FROM Person(NOLOCK)"; 
    IList<Person> listPersons = EntityConvertor.QueryForList<Person>(sql, strSqlConn, null);
    然后建立索引即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void IndexDB(IndexModifier modifier,IList<Person> listModels) 
       
           SetOutput(string.Format("正在建立数据库索引,共{0}人",listModels.Count)); 
           foreach (Person item in listModels) 
           
               Document doc = new Document();//创建文档,给文档添加字段,并把文档添加到索引书写器里 
               doc.Add(new Field("id", item.Id.ToString(), Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
               doc.Add(new Field("fullname"string.Format("{0} {1}",item.FirstName,item.LastName),
     Field.Store.YES, Field.Index.TOKENIZED));//存储且索引 
               modifier.AddDocument(doc); 
           
       }
    同样的道理,最后我们也执行这两个方法(Optimize方法不是一定要做的):

    modifier.Optimize();//优化索引 
    modifier.Close();//关闭索引读写器
    三、搜索
    本文示例代码中的搜索都是利用Lucene.Net的IndexSearcher默认的比较直接简单的一个搜索方法 Search(Query query, Filter filter, int n),很多重载方法我也没有使用过:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    /// <summary> 
        /// 根据索引搜索 
        /// </summary> 
        /// <param name="keyword"></param> 
        /// <returns></returns> 
        private TopDocs Search(string keyword,string field) 
        
            TopDocs docs = null
            int n = 10;//最多返回多少个结果 
            SetOutput(string.Format("正在检索关键字:{0}", keyword)); 
            try
            
                QueryParser parser = new QueryParser(field, new StandardAnalyzer());//针对内容查询 
                Query query = parser.Parse(keyword);//搜索内容 contents  (用QueryParser.Parse方法实例化一个查询) 
                Stopwatch watch = new Stopwatch(); 
                watch.Start(); 
                docs = searcher.Search(query, (Filter)null, n); //获取搜索结果 
                watch.Stop(); 
                StringBuffer sb = "索引完成,共用时:" + watch.Elapsed.Hours + "时 "
     + watch.Elapsed.Minutes + "分 " + watch.Elapsed.Seconds + "秒 " + watch.Elapsed.Milliseconds + "毫秒"
                SetOutput(sb);