Lucene使用小结

Lucene基本概念:

Lucene中最基础的概念是索引(index)、段(Segement)、文档(document)、字段(field)、词条(term)和Tocken。

    索引(Index)包含了一个文档的序列。

    段(Segment):可以理解为一个子索引,添加索引时并不是每个document都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个segment

    文档(Document)是一些域(Field)的序列,用来描述文档(文档可以是一个 HTML 页面,一封电子邮件、一个文本文件、字符串或者数据库表的一条记录等)。一个 Document 对象由多个 Field 对象组成的。用户提供的一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。

    字段(Field)是一些项的序列, 用来描述一个文档的某个属性,一个document通常被分成几个field,用于保存不同的信息。Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。

    词条(Term)就是一个字串,是搜索的基本单位,表示文档的一个词语。一般每个field由多个term组成, term由两部分组成:它表示的词语(及本身的值)和这个词语所出现的field(及属于哪一field)。所以两个不同的field里的相同的词语并不是一个Term.

    tocken:tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。

这些概念间的关系及Lucene索引文件格式在”开放源代码的全文检索引擎Lucene”一文中作了一个说明:

首先在Lucene的文件格式中,以字节为基础,定义了如下的数据类型:

 

    以上的数据类型就是Lucene索引文件格式中用到的全部数据类型,由于它们都以字节为基础定义而来,因此保证了是平台无关,这也是Lucene索引文件格式平台无关的主要原因。接下来我们看看Lucene索引文件的概念组成和结构组成。

 

  以上就是Lucene的索引文件的概念结构。Lucene索引index由若干段(segment)组成,每一段由若干的文档(document)组成,每一个文档由若干的域(field)组成,每一个域由若干的项(term)组成。项是最小的索引概念单位,它直接代表了一个字符串以及其在文件中的位置、出现次数等信息。域是一个关联的元组,由一个域名和一个域值组成,域名是一 个字串,域值是一个项,比如将“标题”和实际标题的项组成的域。文档是提取了某个文件中的所有信息之后的结果,这些组成了段,或者称为一个子索引。子索引可以组合为索引,也可以合并为一个新的包含了所有合并项内部元素的子索引。我们可以清楚的看出,Lucene的索引结构在概念上即为传统的倒排索引结构。

从概念上映射到结构中,索引被处理为一个目录(文件夹),其中含有的所有文件即为其内容,这些文件按照所属的段不同分组存放,同组的文件拥有相同的文件名,不同的扩展名。此外还有三个文件,分别用来保存所有的段的记录、保存已删除文件的记录和控制读写的同步,它们分别是segments,deletable和lock文件,都没有扩展名。每个段包含一组文件,它们的文件扩展名不同,但是文件名均为记录在文件segments中段的名字。让我们看如下的结构图3.2。

 

接下来我们从宏观关系上说明一下这些文件组成。

 每个段的文件中,主要记录了两大类的信息:域集合与项集合。这两个集合中所含有的文件在图3.2中均有表明。由于索引信息是静态存储的,域集合与项集合中的文件组采用了一种类似的存储办法:一个小型的索引文件,运行时载入内存;一个对应于索引文件的实际信息文件,可以按照索引中指示的偏移量随机访问;索引文件与信息文件在记录的排列顺序上存在隐式的对应关系,即索引文件中按照“索引项1、索引项2…”排列,则信息文件则也按照“信息项1、信息项2…”排列。比如在图3.2所示文件中,segment1.fdx与segment1.fdt之间,segment1.tii与segment1.tis、segment1.prx、segment1.frq之间,都存在这样的组织关系。而域集合与项集合之间则通过域的在域记录文件(比如segment1.fnm)中所记录的域记录号维持对应关系,在图3.2中segment1.fdx与segment1.tii中就是通过这种方式保持联系。这样,域集合和项集合不仅仅联系起来,而且其中的文件之间也相互联系起来。此外,标准化因子文件和被删除文档文件则提供了一些程序内部的辅助设施(标准化因子用在评分排序机制中,被删除文档是一种伪删除手段)。这样,整个段的索引信息就通过这些文档有机的组成。

以上所阐述的,就是Lucene所采用的索引文件格式。基本上而言,它是一个倒排索引,但是Lucene在文件的安排上做了一些努力,比如使用索引/信息文件的方式,从文件安排的形式上提高查找的效率。

关于Index文件格式在Lucene官文网站上有详细说明。

 

使用Lucene构建搜索功能,首先要用 Lucene 对记录或目录建立索引,其次才能在建立好的索引中搜索我们所要查找的文档。

 

建立索引

对记录进行索引,需要用到这么几个类:Document, Field, Directory,Analyzer,IndexWriter。

Document:用于描述文档,如前面所述;

Field:描述文档的属性,如前面所述;

Directory:表示索引存储位置;它有两个基本实现:FSDirectory一个存储在文件系统中的索引的位置;RAMDirectory表示一个存储在内存中的索引的位置。

Analyzer:分析器实现分词功能,Lucene中已有一些内建的分析器。

IndexWriter: Lucene创建索引的核心类,作用是把Document对象添加到索引中。

 

建立索引的过程一般如下:
 1.首先创建了一个IndexWriter,并指定存放索引的目录、使用的分析器、是否覆盖已有的索引文件。
2.然后新建一个document。
3.向document添加一个field,对它进行存储并索引。
4.然后将document添加到IndexWriter中,如果有多个文档,可以重复上面的操作,创建document并添加。
5.随后将writer关闭,这点很重要。

 

示例:

public class TxtIndexer {
  public static void main(String[] args) throws Exception {

      Analyzer analyzer = new IKAnalyzer();
      IndexWriterConfig config = new IndexWriterConfig(org.apache.lucene.util.Version.LUCENE_35, analyzer);
      Directory directory = FSDirectory.open(new File("D:\\temp\\test"));

      IndexWriter writer = new IndexWriter(directory, config);
      Document doc = new Document();

      doc.add(new Field("title", "Lucene使用小结", Field.Store.YES, Field.Index.ANALYZED));
      doc.add(new Field("content", "本文对Lucene的简单使用作一个简单的介绍", Field.Store.YES, Field.Index.ANALYZED));

      //将文档写入索引
      writer.addDocument(doc);
      writer.optimize();
      //关闭写索引器
      writer.close();

    }
}

 

对Filed中相关参数的解释:

    Field.Store参数指示是否对这个Field进行存储。

    Field.Store.YES:存储字段值(未分词前的字段值)

    Field.Store.NO:不存储,存储与索引没有关系

    Field.Store.COMPRESS:压缩存储,用于长文本或二进制,但性能受损

   

    Field.Index参数指示记录是否进行索引

    Field.Index.ANALYZED:分词建索引

    Field.Index.ANALYZED_NO_NORMS:分词建索引,但是Field的值不像通常那样被保存,而是只取一个byte,这样节约存储空间(ANALYZED存储了index time,boost information等norms,而ANALYZED_NO_NORMS不存储)

    Field.Index.NOT_ANALYZED:不分词且索引,即不使用 analyzer分析,整体作为一个token,常用语精确匹配,例如文件名,ID号等就用这个

    Field.Index.NOT_ANALYZED_NO_NORMS:不分词建索引,Field的值去一个byte保存

 

    term 就是analyzer分词后的词组。 每一个document都含有一个term vector,TermVector表示文档的条目(由一个Document和Field定位)和它们在当前文档中所出现的次数, 存储了这个document含有的term(unique,如果某个term出现多次也只存一个),以及这个term出现在field 中的position,以及offset。这些信息可以用来以后高亮一个选中的term等等。Field.TermVector参数指示index是否存储term vector。

    Field.TermVector.YES:为每个文档(Document)存储该字段的TermVector

    Field.TermVector.NO:不存储TermVector

    Field.TermVector.WITH_POSITIONS:存储位置

    Field.TermVector.WITH_OFFSETS:存储偏移量

    Field.TermVector.WITH_POSITIONS_OFFSETS:存储位置和偏移量

 

存储和索引的组合如下:

Field.Index                     Field.Store                    说明

TOKENIZED(分词)               YES                    被分词索引且存储

TOKENIZED                       NO                     被分词索引但不存储

NO                                    YES                   将不能被搜索,但可作用被搜索内容的附属内容

UN_TOKENIZED                YES/NO               不分词,将被作为一个整体被搜索,不能进行部分搜索

NO                                    NO                    无此种用法

 

故:

1.如果某字段需要进行搜索,则要使用Field.Index.TOKENIZED或Field.Index.UN_TOKENIZED。通常进行模糊搜索的字段就用TOKENIZED,TOKENIZED会对Field的内容进行分词;需要精心精确搜索的字段就用UN_TOKENIZED,UN_TOKENIZED不会进行分词,只有全词匹配,该Field才会被选中。

2.对于只需要随搜索结果显示而不需要按照其内容进行搜索的字段,使用Field.Index.NO。

3.如果Field.Store是No,则无法在搜索结果中从索引数据直接提取该域的值,会返回null。

lucene3.5中使用writer.forceMerge(1)来替换optimize()方法来优化索引操作。

搜索文档

为一条记录建立好了索引后,现在就可以在这个索引上进行搜索找到包含某个关键词或短语的文档。进行记录查找,需要用到这么几个类:IndexSearcher, Term, Query, TermQuery, Hits.。

Query:将用户查询输入的字符串进行封装

Term:搜索的基本单位,如前所述

IndexSearch:以只读方式打开一个索引进行搜索。

Hits:保存搜索结果

使用Lucene执行搜索,首先要创建IndexSearcher对象,然后要通过Term和Query对象来封装用户输入的搜索条件,最后将结果封装在Hits对象中,返回给用户。

示例:

public class TxtFileSearcher {
    public static void main(String[] args) throws Exception {
        String queryString = "介绍";
        IndexSearcher searcher = new IndexSearcher(IndexReader.open(FSDirectory.open(new File("D:\\temp\\test"))));
        //查询解析器
        Query query = IKQueryParser.parse("title", queryString);
        BooleanQuery blQuery = new BooleanQuery();
        blQuery.add(query, BooleanClause.Occur.MUST);
        blQuery.add(new TermQuery(new Term("content", queryString)), BooleanClause.Occur.SHOULD);

        //搜索结果使用Hits存储
        TopDocs results = searcher.search(blQuery, 1000);

        ScoreDoc[] hits = results.scoreDocs;
        for (ScoreDoc sd : hits) {
            int docID = sd.doc;
            Document doc = searcher.doc(docID);
            System.err.println("title = " + doc.get("title"));
            System.err.println("content = " + doc.get("content"));
        }

    }
}

参数BooleanClause.Occur中的常量SHOULD、MUST、MUST_NOT分别表示:

    BooleanClause.Occur.SHOULD               or  关系
    BooleanClause.Occur.MUST                    and关系
    BooleanClause.Occur.MUST_NOT          not 关系

它们的组合如下:

SHOULD     与  MUST:               SHOULD不起作用,查询结果为MUST子句的检索结果

SHOULD     与  MUST_NOT:       类似MUST查询

SHOULD     与  SHOULD:           表示一个并集,查询结果为所有检索子句的并集

MUST         与   MUST:              表示一个交集,查询结果为所有检索子句的交集

MUST         与  MUST_NOT:       表示一个差集,查询结果为不包含NUST_NOT子句结果的集合

MUST_NOT 与  MUST_NOT:       检索无结果,无意义

 

其他相关说明:

1.查询表达式

Lucene有很多种 Query类,它们都继承自Query,执行各种特殊的查询。

  •  词条搜索 TermQuery:基本的查询

  •  组合搜索 BooleanQuery:用BooleanQuery可以连接多个TermQuery,多个TermQuery间的关联缺省为或。

  •  通配符搜索 WildcardQuery:对某单词进行通配符查询。通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符

  •  短语搜索 PhraseQuery:可以设置两个关键字之间的字符数量对某单词进行查询。

  •  前缀搜索 PrefixQuery:查询以某字母开头的单词。

  •  多短语搜索 MultiPhraseQuery:MultiPhraseQuery is a generalized version of PhraseQuery, with an added method add(Term[]). To use this class, to search for the phrase "Microsoft app*" first use add(Term) on the term "Microsoft", then find all terms that have "app" as prefix using IndexReader.terms(Term), and use MultiPhraseQuery.add(Term[] terms) to add them to the query.

  •  模糊搜索 FuzzyQuery:搜索相似的term,使用Levenshtein算法。this query is not very scalable with its default prefix length of 0 - in this case, *every* term will be enumerated and cause an edit score calculation.

  •  文本范围搜索 TermRangeQuery:matches documents within an range of terms

  •  数字范围搜索 NumericRangeQuery:matches numeric values within a specified range

 

除了以下的多种Query类外,Lucene还提供了一种类似于SQL语句的查询语法,你可以任意组合query string,完成复杂操作,如:

        TermQuery可以用“field:key”方式,例如“content:Lucene”。
        BooleanQuery中‘与’用‘+’,‘或’用‘ ’,例如“content:java contenterl”。
        WildcardQuery仍然用‘?’和‘*’,例如“content:use*”。
        PhraseQuery用‘~’,例如“content:"中日"~5”。
        PrefixQuery用‘*’,例如“中*”。
        FuzzyQuery用‘~’,例如“content: wuzza ~”。
        RangeQuery用‘[]’或‘{}’,前者表示闭区间,后者表示开区间,例如“time:[20060101 TO 20060130]”,注意TO区分大小写。

 

2.Filter过滤

Filter实现对数据源进行预处理,限制查询索引的子集。

 

3.排序

Lucene的搜索结果默认按相关度排序,这个相关度排序是基于内部的Score和DocID,Score又基于关键词的内部评分和做索引时的boost。默认Score高的排前面,如果Score一样,再按索引顺序,先索引的排前面。

排序有几种方式:

1)  在查询的时候,将一个Sort作为搜索参数。例如:

               IndexSearcher.search(query,sort);

    public Sort()

    public Sort(SortField field)  //通过构造某个域(field)的SortField对象根据一个域进行排序

    public Sort(SortField[] fields) //通过构造一组域(field)的SortField对象组实现根据多个域排序

    public Sort(String field) //根据某个域(field)的名称构造Sort进行排序

    public Sort(String field, boolean reverse) //根据某个域(field)的名称构造SortField进行排序,reverse为true为升序

    public Sort(String[] fields) //根据一组域(field)的名称构造一组Sort进行排序

 

2) 使用SortField实现排序

     public SortField(String field,int type) //type表示当前Field值的类型

     public SortField(String field,int type,boolean reverse)  //默认为false,升序

     SortField sf1=new SortField(“bookNumber”,SortField.INT,false);

     SortField sf2=new SortField(“bookname”,SortField.STRING,false);

 

type对应的值分别为:

     SortField. SCORE 按积分排序

     SortField. DOC 按文档排序

     SortField. AUTO 域的值为int、long、float都有效

     SortField.STRING 域按STRING排序

     SortField.FLOAT

     SortField.LONG

     SortField.DOUBLE

     SortField.SHORT

     SortField.CUSTOM 通过比较器排序

     SortField.BYTE

 

3)  在构造Sort的时候,指定排序规则。

按照文档的得分降序排序

       Hits hits=searcher.search(query,Sort.RELEVANCE);

 

按文档的内部ID升序排序

       Hits hits=searcher.search(query, Sort.INDEXORDER);

 

按照一个Field来排序

       Sort sort=new Sort();

       SortField sf=new SortField(“bookNumber”,SortField.INT,false);

       sort.setSort(sf);

       Hits hits=searcher.search(query,sort);

 

按照多个Field来排序

       Sort sort=new Sort();

       SortField sf1=new SortField(“bookNumber”,SortField.INT,false);//升序

       SortField sf2=new SortField(“publishdate”,SortField.STRING,true);//降序

       sort.setSort(new SortField[]{sf1,sf2});

       Hits hits=searcher.search(query,sort);

 

改变SortField中的Locale信息

       String str1=”我”; String str2=”你”;

       Collator co1=Collator.getInstance(Locale.CHINA);

       Collator co2=Collator.getInstance(Locale.JAPAN);

       System.out.println(Locale.CHINA+”:”+co1.compare(str1,str2));

       System.out.println(Locale.JAPAN+”:”+co2.compare(str1,str2));

 

4)自定义排序

 

5)  改变Document的boost(激励因子)

改变boost的大小,会导致Document的得分的改变,从而按照Lucene默认的对检索结果集的排序方式,改变检索结果中Document的排序的提前或者靠后。这些值是在索引阶段就写入索引文件的,存储在标准化因子(.nrm)文件中,一旦设定,除非删除此文档,否则无法改变。

在计算得分的时候,使用到了boost的值,默认boost的值为1.0,也就说默认情况下Document的得分与boost的无关的。一旦改变了默认的boost的值,也就从Document的得分与boost无关,变为相关了:boost值越大,Document的得分越高。

如:Document doc = new Document();

      doc.add(new Field("title", "Lucene使用介绍", Field.Store.YES, Field.Index.ANALYZED));

      doc.add(new Field("content", "本文对Lucene的简单使用作一个简单的介绍", Field.Store.YES, Field.Index.ANALYZED));

      doc.setBoost(100);//

 

6)改变Field的boost(激励因子)

改变Field的boost值,和改变Document的boost值是一样的。因为Document的boost是通过添加到Docuemnt中Field体现的,所以改变Field的boost值,可以改变Document的boost值。

 

4.分页

通过TopScoreDocCollector来实现搜索,如下:

    TopScoreDocCollector results =TopScoreDocCollector.creat(200,false);

    search.search(query,results);

    //0是开始的记录,5是搜索几条记录数

   TopDocs topDocs = results.topDocs(0, 5);

   ScoreDoc[] scoreDocs = topDocs.ScoreDocs;

再依次遍历,就可以获取Document,获取对象了。

 

索引的增加/修改/删除

前面的例子中只是简单地提到了Index的增加,增加的索引还可能被修改或删除。引自API中的一段来说明索引的增删改操作:

documents are added with addDocument and removed with deleteDocuments(Term) or deleteDocuments(Query). A document can be updated with updateDocument (which just deletes and then adds the entire document). When finished adding, deleting and updating documents, close should be called.

索引删除的一个代码示例片段:

    Analyzer analyzer = new IKAnalyzer();

    IndexWriterConfig config = new IndexWriterConfig(org.apache.lucene.util.Version.LUCENE_35, analyzer);

    Directory directory = FSDirectory.open(new File("D:\\temp\\test"));

    writer = new IndexWriter(directory,config);

    //参数可以是一个Query或是一个term,term用于精确查找

    //调用deleteDocuments删除的文档不会被完全删除,可以恢复

    //执行完操作后,存储位置(文件夹)下会多出一个名叫_0_1.del的文件,即删除的文件在这个文件中被记录下

    writer.deleteDocuments(new Term("title","Lucene使用介绍"));

    writer.commit();

 

在FQA中提到,Deleted documents do not get removed from the index immediately, until they are merged away. 调用forceMergeDeletes可以强制删除索引,但不推荐使用这个方法,比较消耗内存,lucene会自动根据容量大小删除所标记删除的文件

关于删除,参考Lucene FQA中的一段:

How do I delete documents from the index?

IndexWriter allows you to delete by Term or by Query. The deletes are buffered and then periodically flushed to the index, and made visible once commit() or close() is called.

IndexReader can also delete documents, by Term or document number, but you must close any open IndexWriter before using IndexReader to make changes (and, vice/versa). IndexReader also buffers the deletions and does not write changes to the index until close() is called, but if you use that same IndexReader for searching, the buffered deletions will immediately take effect. Unlike IndexWriter's delete methods, IndexReader's methods return the number of documents that were deleted.

Generally it's best to use IndexWriter for deletions, unless 1) you must delete by document number, 2) you need your searches to immediately reflect the deletions or 3) you must know how many documents were deleted for a given deleteDocuments invocation.

If you must delete by document number but would otherwise like to use IndexWriter, one common approach is to make a primary key field, that holds a unique ID string for each document. Then you can delete a single document by creating the Term containing the ID, and passing that to IndexWriter's deleteDocuments(Term) method.

Once a document is deleted it will not appear in TermDocs nor TermPositions enumerations, nor any search results. Attempts to load the document will result in an exception. The presence of this document may still be reflected in the docFreq statistics, and thus alter search scores, though this will be corrected eventually as segments containing deletions are merged.

 

 

索引更新的一个代码示例片段:

    Analyzer analyzer = new IKAnalyzer();

    IndexWriterConfig config = new IndexWriterConfig(org.apache.lucene.util.Version.LUCENE_35, analyzer);

    Directory directory = FSDirectory.open(new File("D:\\temp\\test"));

    writer = new IndexWriter(directory,config);

    Document doc = new Document();

    doc.add(new Field("title","Lucene使用介绍",Field.Store.YES,Field.Index.ANALYZED));

    doc.add(new Field("content","本文对Lucene的简单使用作一个简单的介绍",Field.Store.NO,Field.Index.ANALYZED));

    doc.add(new Field("comments","关于Lucene使用的说明",Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));

    writer.updateDocument(new Term("title","Lucene使用介绍"), doc);

 

对于updateDocument操作的机制,可以从API说明了解:Updates a document by first deleting the document(s) containing term and then adding the new document.

同时,Any updates to the index, either added or deleted documents, will not be visible until the IndexReader is re-opened. So your application must periodically re-open its IndexReaders to see the latest updates.

 

posted @ 2013-04-24 22:32  Jevo  阅读(2076)  评论(1编辑  收藏  举报