Lucene实战-第2章-构建索引

本章要点

  • 执行基本索引操作
  • 在索引过程中对文档和域进行加权操作
  • 对日期、数字和可排序域进行索引
  • 高级索引技术

如果想要搜索存储在硬盘上的文件、电子邮件、网页或是数据库中的数据,Lucene都可帮你完成。但在进行搜索前,你必须对搜索内容进行索引,Lucene同样能帮你完成,这就是本章要讨论的内容。

在第1章我们演示了一个索引示例。本章将更深入讲解索引更新操作,如何通过参数来调整索引过程,以及更高级的索引技巧,从而帮助你更好理解Lucene。本章还将涉及Lucene索引的结构,使用多线程和多进程访问Lucene索引时要关注的重点内容,Lucene索引API的事务语义,通过远程文件系统共享索引,以及防止并发修改索引的锁机制等内容。

在进入大量细节内容之前,别忘了概况:索引操作仅仅是整个搜索程序的一个简单的步骤而已。重要的是搜索程序带给用户的搜索体验;索引操作“只不过”是为了增强用户搜索体验而需要跨越的一道障碍而已。因此,尽管本章有很多有趣的索引操作细节,你最好还是将主要精力花在如何提升用户搜索体验上。对于几乎所有搜索程序来说,搜索功能都比索引操作细节重要得多。话虽这么说,在实现搜索功能时还是要依赖于索引操作期间的相关重要步骤的,正如本章所述。

注意:本章内容非常长。这个长度是必要的,因为Lucene公开了大量索引操作细节。好消息是大多数搜索程序都不比采用Lucene的高级索引选项。事实上,2.1小节、2.2小节、和2.3小节所介绍的内容才是这些搜索程序所需要的。如果你对索引比较感兴趣,同时又不想丢掉其中任何一部分内容,或者你的搜索程序需要使用所有的其他功能,那么可以研读本章余下内容。

下面我们从Lucene有关搜索内容的概念模型开始介绍

2.1 Lucene如何对搜索内容进行建模

我们首先阐述有关内容建模的方法概念。我们从Lucene有关索引和搜索、文档和域的基本单元开始,然后将重点转移到Lucene与当代数据库更为结构化的数据模型之间的区别。

2.1.1 文档和域

文档是Lucene索引和搜索的原子单位。文档为包含一个或多个域的容器,而域则依次包含【真正的】被搜索内容。每个域都有一个标识名称,该名称为一个文本值或二进制值。当你将文档加入到索引中时,可以通过一系列选项来控制Lucene的行为。在对原始数据进行索引操作时,你得首先将数据转换成Lucene所能识别的文档和域。在随手的搜索过程中,被搜索对象则为值域;例如,用户在输入搜索内容“title:lucene”时,搜索结果则为标题值域包含单词【lucene】的所有文档。

进一步的,Lucene可以针对域进行3种操作。

  • 值域可以被索引(或者不被索引)。如果需要搜索一个域,则必须首先对它进行索引。被索引的值域必须是文本格式的(二进制格式的值域只能被存储而不能被索引)。在索引一个域时,需要首先使用分析过程将域值转换成语汇单元,然后将语汇单元加入到索引中。有关索引域值的具体操作选项可以参考2.4.1小结
  • 域被索引后,还可以选择性的存储项向量,后者可以看做该域的一个小型反向索引集合,通过该向量可以检索该域的所有语汇单元。这个机制有助于实现一些高级功能,比如搜索与当前文档相似的文档(更多高级共轭能详见5.7小节)。有关控制索引项向量的具体选项请参考2.4.3小节。
  • 域值可以被单独存储,即是说备份洗钱的域值备份也可以写进索引中,以便后续的检索。这个机制可以使你将原始域值展现给用户,比如文档的标题或摘要。域值的存储选项请参考2.4.2小节。

如何将包含各类信息的原始数据转换成Lucene文档和域呢?这一版需要将搜索程序设计成递归处理方式来完成。Lucene并不知道搜索程序使用哪些域,以及对应的域名称等。文档一般包含多个域,比如标题、作者、日期、只要、正文、URL和关键词等。有时还需要使用杂项域,即包含所有文本的一个独立域以供搜索。一旦建好文档,并将它加入到索引后,就可以在随后的搜索过程中检索哪些匹配查询条件的文档,并将读取到的文档对应域值作为搜索结果展现给用户。

人们通常将Lucene与数据库进行比较,因为二者都会存储数据内容并提供内容检索功能。但两者之间有着重大差别,首先是灵活架构的差别

当搜索程序从通过索引检索文档时,只有被存储的域才会被作为搜索结果展现。例如,被索引但未被存储于文档的域是不会被作为搜索结果展现的。这种机制通常会使得搜索结果具有不确定性。

2.1.2 灵活的架构

与数据库不同的是,Lucene没有一个确定的全局模式。也就是说,加入索引的每个文档都是独立的,它与此前加入的文档完全没有关系;它可以包含任意的域,以及任意的索引、存储和项向量操作选项。它也不必包含于其他文档相同的域。他甚至可以做到于其他文档内容相同,仅是相关操作选项有所区别。

Lucene的这种特性非常实用:这使你能够递归访问文档并建立对应的索引。你可以随时对文档进行索引,而不必提前设计文档的数据结构表。如果随后你想向文档中添加域,那么可以完成添加后重新索引该文档或重建索引即可。

Lucene的灵活架构还意味着单一的索引可以包含不同实体的多个文档,例如,用一个文档的诸如名称和价格等域来表示零售产品,而用另一个文档的诸如姓名、年龄和性别等域来表示人,另外还可以使用一个不可达的【中间态】文档,该文档只包含有关索引或搜索程序的一些中间数据(比如最近一次更新索引的时间,或者被索引的产品目录),同时该【中间态】文档内容不在搜索结果中出现。

Lucene和数据库之间的第二个主要区别是,Lucene要求你在进行索引操作时简单化或者返现规格化原始数据。

2.1.3 反向规格化(Denormalization)

我们面临的一个挑战是解决文档真是结构和Lucene表示能力之间的“不匹配”问题。举例来说,XML文档通过嵌套标记来表示一个递归的文档结构,数据库可能有任意数量的连接点,表之间可以通过主键和次键相互关联起来。微软的Object Linking & Embedding文档可以指向其他嵌入类文档。然而Lucene文档却都是单一文档,因此在创建对应的Lucene文档之前,必须对上述递归文档结构和连接点进行反向规格化操作。建立在Lucene基础之上的开源项目。如Hibernate Search、Compass、LuSQL、DBSight、Browse Engine和Oracle/Lucene integration等,都有各自不同而有趣的方法来解决反向规格化问题。

至此你已经在概念层面上了解了Lucene的文档模型,下面我们将深入阐述Lucene索引步骤。

2.2 理解索引过程

正如第1章所述,索引一个文件只需要调用Lucene公用API的几个方法即可完成。结果是,从表面上看来,用Lucene进行索引操作是一个简单而独立的操作。其实隐藏在这些简单API背后的却是一套巧妙而相对复杂的操作。这些操作从功能上主要分为3个部分,如图2.1所示,下面几节我们将对此进行详述。

图2.1 Lucene索引过程分为3个主要操作步骤:将原始文档转换成文本、分析文本、将分析好的文本保存至索引中
在索引操作期间,文本首先从原始数据中提取出来,并用于创建对应的Document实例,该实例包含多个Field实例,他们都用来保存原始数据信息。随后的分析过程将域文本处理成大量语汇单元。最后将语汇单元加入到段结构中。下面我们从文本提取开始阐述。

2.2.1 提取文本和创建文档

使用Lucene索引数据时,必须先从数据中提取纯文本格式信息,一遍Lucene识别该文本并建立对应的Lucene文档。在第一章我们将搜索和索引的示例文本限制为.txt格式文本,这使得我们能轻易地拆分这些文本内容,并用之建立起Field实例。但事实上文本格式并不都这么简单,图1.4有关“建立文档”步骤其实包含了很多隐藏内容。

假设你需要对一堆PDF格式的手册进行索引,你必须首先想法从这些PDF文件中提取文本格式信息,并用这些文本信息来创建Lucene文档和域。而Java中并没有对应的方法来处理PDF格式文件,该问题同样存在与Microsoft Word文件或者其他非纯文本格式文件中。即使再处理XML或HTML等纯文本格式文件时,也需要灵活考虑到底索引哪些内容,你得索引他们所表达的真正文本内容,而不是XML元素或HTML标签等无意义的文本。

有关提取文本信息的细节将在第7章结合Tika框架详述,使用该框架能使你很轻易地从各种格式的文件中提取文本信息。一旦提取出预想的文本信息并建立起对应的、包含各个域的文档后,下一步就是对这些文本信息进行分析了。

2.2.2 分析文档

一旦建立起Lucene文档和域,就可以调用IndexWriter对象的addDocument方法将数据传递给Lucene进行索引操作了。在索引操作时,Lucene首先分析文本,将文本数据分割成语汇单元串,然后对它们进行一些可选操作。例如,语汇单元在所引起钠需要统一转换为小写,以使搜索不对大小写敏感,这个操作可以通过调用Lucene的LowerCaseFilter类实现。通常还需要调用StopFilter类从输入中去掉一些使用很频繁却没有实际意义的词(如a、an、the、in
等英文文本)同样地,我们也需要分析输入的语汇单元,去掉他们的词干,如调用PorterStemFilter类处理英文文本(对于其他语种,Lucene的contrib分析模块有对应的类调用)。这些将原始数据转换为语汇单元,随后用一系列filter来修正该语汇单元的操作,一起构成了分析器。另外你还可以通过链接Lucene的语汇单元和filter来搭建自己的分析器,或者通过其他自定义方式来搭建分析器。

以上称之为分析的步骤很重要,它涵盖了图1.4中“文档分析“步骤。Lucene对输入的处理方式多种多样,有趣且实用,我们将在第4章对此详细讲解。分析过程会产生大批的语汇单元,随后这些语汇单元将被写入索引文件中。

2.2.3 想索引添加文档

对输入数据分析完毕后,就可以将分析结果写入索引文件中。Lucene将数据以一种倒排索引(inverted index)的数据结构进行存储。在进行关键字快速查找时,这种数据结构能够有效利用磁盘空间。Lucene使用倒排索引数据结构的原因是:把文档中提取出的语汇单元作为查询关键字,而不是将文档作为中心实体,这种思想很像本书索引与页码的对应关系。换句话说,倒排索引并不是回答“这个文档中包含哪些单词”这个问题,而是经过优化后用来快速回答“哪些文档包含单词x”这个问题。

仔细回想一下自己最喜欢的Web搜索引起和最常用的查询方式,你会发现上述查询方式正是你想要的最快的查询方式。现在所有的Web搜索引擎核心都是采用的倒排索引技术。

Lucene的索引文件目录有唯一一个段结构,接下来我们将讨论这个问题。

索引段

Lucene的索引文件格式丰富而详细,而且它随着时间的推移已经被很好地优化过。尽管你在使用Lucene之前并不需要知道该格式细节,但还是建议你在较高层次上对他有一个基本理解。如果你确实对格式细节很感兴趣,可以参考附录B。

Lucene索引都包含一个或多个段,如图2.2所示。每个段都是一个独立的索引,它包含整个文档索引的一个子集。每当writer刷新缓冲区增加的文档,以及挂起目录删除操作时,索引文件都会建立一个新段。在搜索索引时,每个段都是单独访问的,但搜索结果是合并后返回的。

在这里插入图片描述
每个段都包含多个文件,文件格式为_X.<ext>, 这里X代表段名称,<ext>为扩展名,用来标识该文件对应索引的某个部分。各个独立的文件共同组成了索引的不同部分(项向量、存储的域、倒排索引,等等)。如果你是用混合文件格式(这是Lucene默认处理方式,但可以通过IndexWriter.setUseCompoundFile方法进行修改),那么上述索引文件会被压缩成一个单一的文件,_X.cfs。这种方式能在搜索期间减少打开的文件数量。第11章会对该权衡方案有详细介绍。

还有一个特殊文件,名叫段文件,用段_<N>标识,该文件指向所有激活的段。段文件非常重要!Lucene会首先打开改文件,然后打开它所指向的其他文件。值<N>被称为“the generation”,他是一个证书,Lucene每次想索引提交更改时都会将这个数加1。

久而久之,索引会积累很多段,特别是当程序打开和关闭writer较为频繁时。这种情况是没问题的。IndexWriter会周期性地选择一些段,将他们合并到一个新段中,然后删除老的段。被合并段的选取策略由一个独立的MergePolicy类主导。一旦选取好这些段,具体合并操作由MergeScheduler类实现。这些类属于进阶主题范畴,详见2.13.6小节。

下面我们介绍索引时需要进行的基本操作(添加、更新、删除)。

2.3 基本索引操作

前面我们讲到Lucene有关文档建模的相关概念,并随后给出了索引过程的逻辑步骤。现在我们开始通过研究相关代码,了解Lucene有关添加、更新、和删除文档的api。我们的研究从添加索引开始,因为这是使用最为频繁的操作。

2.3.1 向索引添加文档

我们先看看如何创建新的索引并向其添加文档。添加文档的方法有两个:

  • addDocument(Document) - 使用默认分析器添加文档,该分析器在创建IndexWriter对象时指定,用于语汇单元化操作。
  • addDocument(Document, Analyzer) - 使用指定的分析器添加文档和语汇单元化操作。但是要小心!为了让搜索模块正常工作,需要分析器在搜索时能够“匹配”它在索引时生成的语汇单元。具体请参考4.1.2小节。

程序2.1显示了创建新索引和添加两个小文档的必要步骤。在本例中,文档内容包含在源代码字符串中,但实际文档内容是从外部数据源获取的。本例中setUp()方法是在每次单元测试前由JUnit框架调用的。

package com.vava.test.lucene;

import java.io.IOException;

import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;

import junit.framework.TestCase;

/**
 * @author Steve
 * Created on 2020-08
 */
public class IndexingTest extends TestCase {
    protected String[] ids = {"1", "2"};
    protected String[] unindexed = {"Netherlands", "Italy"};
    protected String[] unstored = {"Amsterdam has lots of bridges", "Venice has lots of canals"};
    protected String[] text = {"Amsterdam", "Venice"};

    private Directory directory;

    protected void setUp() throws Exception {
        // 每次测试前运行
        directory = new RAMDirectory();

        // 创建IndexWriter对象
        IndexWriter writer = getWriter();

        // 添加文档
        for (int i = 0; i < ids.length; i++) {
            Document doc = new Document();
            doc.add(new Field("id", ids[i], Store.YES, Index.NOT_ANALYZED));
            doc.add(new Field("country", unindexed[i], Store.YES, Index.NO));
            doc.add(new Field("contents", unstored[i], Store.NO, Index.ANALYZED));
            doc.add(new Field("city", text[i], Store.YES, Index.ANALYZED));
            writer.addDocument(doc);
        }
        writer.close();
    }

    private IndexWriter getWriter() throws IOException {
        // 创建IndexWriter对象
        return new IndexWriter(directory, new WhitespaceAnalyzer(), MaxFieldLength.UNLIMITED);
    }

    protected int getHitCount(String filedName, String searchString) throws IOException {
        // 创建新的IndexSearcher对象
        IndexSearcher searcher = new IndexSearcher(directory);
        // 建立简单的单term查询
        Term t = new Term(filedName, searchString);
        Query query = new TermQuery(t);
        // 获取命中数
        //        int hitCount = TestUtil.hitCount(searcher, query);
        searcher.close();
        return 0;
        //        return hitCount;
    }

    public void testIndexWriter() throws IOException {
        IndexWriter writer = getWriter();
        // 核对写入的文档数
        assertEquals(ids.length, writer.numDocs());
        writer.close();
    }

    public void testIndexReader() throws IOException {
        IndexReader reader = IndexReader.open(directory);
        // 核对读入的文档数
        assertEquals(ids.length, reader.maxDoc());
        assertEquals(ids.length, reader.numDocs());
        reader.clone();
    }

}

1setup()方法首先建立新的RAMDirectory对象用来存放索引。
2接下来在Directory对象上创建IndexWriter对象。这里创建了getWriter方法,因为程序中很多地方都会使用这个方法获取IndexWriter对象。
3最后,setUp()方法递归处理原始内容,并创建Document对象和Field对象,然后将Document对象加入索引。
4 5 6 我们创建IndexSearcher对象,并通过指定字符串来执行基本的单项查询,最后返回与查询内容匹配的文档数。
7 8 我们通过IndexReader对象和IndexWriter对象来确认与查询条件匹配的新增文档数量。索引包含两个文档,分别表示国家及其城市,对应的文本使用WhitespaceAnalyzer类分析的。由于在每项测试之前都要调用setUp()方法,因而每项测试都是针对新创建的索引进行的。

调用getWriter方法时,我们传入3个变量来创建IndexWriter类。

  • Directory类,索引对象存储于该类
  • 分析器,被用来索引语汇单元化的域(分析器详见第4章)
  • MaxFieldLength.UNLIMITED,该变量是必要的,它指示IndexWriter索引文档中所有的语汇单元(具体设置内容详见2.7小节)

IndexWriter类若侦测到Directory类还没索引的话,会自动创建一个新的索引。如果后者已经包含一个索引,前者则只想该索引添加内容。

NOTE IndexWriter类初始化方法并不显式包含索引是否已创建的布尔值,他在初始化时会首先检查传入的Directory类是否已包含索引,如果索引存在,IndexWriter类则在该索引上添加内容,否则后者将向Directory类写入新创建的索引。

IndexWriter类有多个初始化方法。其中一些方法会显示包含创建索引的参数,这允许你强制建立新的索引并覆盖原来的索引。更高级的初始化方法允许你制定自己专用的IndexDeletionPolicy类或IndexCommit类。

一旦建立起索引,就可以使用for循环来初始化文档对象了。过程很简单:首先创建一个新的Document对象,然后根据你的需要想着跟Document对象中逐个添加Field对象。每个文档都有4个域,每个域都有各自不同的选项,最后可以调用writer.addDocument方法来索引文档。for循环结束后,程序将关闭writer,后者会想Directory对象提交所有变化。其实也可以调用commit()方法,这样就能在不关闭writer的情况下提交更改,从而保留writer至下一次提交时刻。

TestUtil:

package com.vava.lucene;

import java.io.IOException;

import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;

/**
 * @author Steve
 * Created on 2020-08
 */
public class TestUtil {
    public static int hitCount(IndexSearcher searcher, Query query) throws IOException {
        return searcher.search(query, 1).totalHits;
    }
}

2.3.2 删除索引中的文档

虽然大多数应用程序更加关注如何将文档加入Lucene索引中,但也有一部分应用程序需要关注如何从索引中删除文档。例如,某家报社想要在索引中只保留上周的有用的新闻,在此之前的新闻全部删除。还有一些程序可能需要删除包含某一项的所有文档,或者在文档内容有所变化时使用新版文档来覆盖旧文档。IndexWriter类提供了各种方法来从索引中删除文档。

  • deleteDocuments(Term)负责删除包含项的所有文档
  • deleteDocuments(Term[]) 负责删除包含项数组任一元素的所有文档
  • deleteDocuments(Query) 负责删除匹配查询语句的所有文档
  • deleteDocuments(Query[]) 负责删除匹配查询语句数组任一元素的所有文档
  • deleteAll() 负责删除索引中所有文档。该功能与关闭writer再用参数create=true重新打开writer等效,但前者不用关闭writer。

如果需要通过Term类删除单个文档,需要确认在每个文档中都已索引过对应的Field类,还需要确认所有域值都是唯一的,这样才能将这个文档单独找出来删除。这个概念跟数据库中主键的概念类似,但Lucene并不一定要这样执行。你可以对这个域进行任意命名,(通常用id命名),该域需要被索引成未被分析的域以保证分析器不会将它分解成语汇单元。然后利用该域来删除对应的文档,操作如下:writer.deleteDocuments(new Term(“ID”, documentID));

调用该方法时一定小心!如果碰巧制定了一个错误的Term对象(例如,由一个普通的被索引的域文本值创建的Term对象,而不是由唯一ID值创建的域),那么Lucene将很容易地快速删除索引中的大量文档。在所有清欠款下,删除操作不会马上执行,而是放入内存缓冲区中,与加入文档的操作类似,最后Lucene会通过周期性刷新文档目录来执行该操作。与加入文档一样,你必须调用writer的commit()或close()方法向索引提交更改。不过即使删除操作已完成,存储该文档的磁盘空间也不会马上释放,Lucene只是将该文档标记为“删除”。

我们看程序2.2中的deleteDocuments方法是如何运行的。这里建立了两个测试用例,用来分别说明deleteDocuments方法的调用以及删除文档后的优化效果。

   public void testDeleteBeforeOptimize() throws IOException {
        IndexWriter writer = getWriter();
        assertEquals(2, writer.numDocs());
        writer.deleteDocuments(new Term("id", "1"));
        writer.commit();
        // 1、测试用例显示hasDeletions()方法用于检查索引中是否包含被标记为已删除的文档
        assertTrue(writer.hasDeletions());
        // 2、代码显示了两个通常容易混淆的方法:maxDoc()方法和numDoc()方法。前者返回索引中被删除和未被删除的文档总数,而后者只返回索引中未被删除的文档总数。
        // 本例中由于被索引包含两个文档其中一个已经被删除,所以numDocs()方法返回1而maxDoc()方法返回2
        assertEquals(2, writer.maxDoc());
        assertEquals(1, writer.numDocs());
        writer.close();
    }
    
    public void testDeleteAfterOptimize() throws IOException {
        IndexWriter writer = getWriter();
        assertEquals(2, writer.numDocs());
        writer.deleteDocuments(new Term("id", "1"));
        // 3、在方法testDeleteAfterOptimize()中,我们调用索引优化来强制Lucene在删除一个文档后合并索引段。所以随后maxDoc()方法返回1而不是2,因为在删除和优化
        // 操作完成后,Lucene实际上已经将该文档删除,最后索引中只包含最后一个文档。
        writer.optimize();
        writer.commit();
        assertFalse(writer.hasDeletions());
        assertEquals(1, writer.maxDoc());
        assertEquals(1, writer.numDocs());
        writer.close();
    }

NOTE 程序员通常会将IndexWriter类和IndexReader类中的maxDoc()方法和numDocs()方法搞混淆。第一个方法maxDoc()返回索引中包括被删除和未删除的文档总数,而numDocs()只返回索引中未被删除的文档数。

讲完添加和删除文档后,下面我们来看看更新文档

2.3.3 更新索引中的文档

很多搜索程序在首次索引完文档后,由于该文档可能被后续修改,而需要对它进行再次索引。例如,如果你的文档是从Web服务器中抓取的,一个检测文档内容是否改变的方法是找到改变后的ETag HTTP文件头。如果该头与你上次索引文档时对应的头不一致,则说明文档内容已发生变化,并且你需要在索引中更新该文档。

在某些情况下,你可能指向更新文档中的部分域,如标题发生改变而正文未发生改变的情况下。遗憾的是,尽管这个需求很普遍,Lucene还是不能做到:Lucene只能删除整个旧文档,然后向索引中添加新文档。这要求新文档必须包含旧文档中所有域,包括内容未发生改变的域。IndexWriter提供了两个简便的方法来更新索引中的文档。

  • updateDocument(Term, Document)首先删除包含Term变量的所有文档,然后使用writer的默认分析器添加新文档
  • updateDocument(Term, Document, Analyzer)功能与上述一致,区别在于他可以指定分析器添加文档。

updateDocument方法可能是最常用的删除文档的方法了。因为更新文档也包含删除操作。注意这两个方法是通过调用deleteDocument(Term)和addDocument两个方法合并实现的。updateDocument方法就像如下使用:

writer.updateDocument(new Term("ID", documentId), newDocument);

由于updateDocument方法要在后台调用deleteDocument方法,我们给出同样的警告:要确认被更新文档的Term标识的唯一性。程序2.3为更新文档示例。

    public void testUpdate() throws IOException {
        assertEquals(1, getHitCount("city", "Amsterdam"));
        IndexWriter writer = getWriter();
        // 为"Haag"建立新文档
        Document doc = new Document();
        doc.add(new Field("id", "1", Store.YES, Index.NOT_ANALYZED));
        doc.add(new Field("country", "Netherlands", Store.YES, Index.NO));
        doc.add(new Field("contents", "Den Haag has a lot of museums", Store.NO, Index.ANALYZED));
        doc.add(new Field("city", "Den Haag", Store.YES, Index.ANALYZED));
        // 更新文档版本
        writer.updateDocument(new Term("id", "1"), doc);
        writer.close();
        // 确认旧文档已删除
        assertEquals(0, getHitCount("city", "Amsterdam"));
        // 确认新文档已被索引
        assertEquals(1, getHitCount("city", "Den Haag"));
    }

在本例中,我们用新文档来替换id为1的旧文档。随后调用updateDocument方法更新文档。这样,我们完成了索引中的文档更新操作。

我们已经讲了添加、删除和更新文档的基本操作。现在,我们将深入研究创建文档时需要指定的域选项。

2.4 域选项

Field类也许是在文档索引期间最重要的类了:该类在事实上控制着被索引的域值。当创建好一个域时,你可以指定多个域选项来控制Lucene在将文档添加进索引后针对该域的行为。本章开头我们曾较为深入地谈到这些选项,现在我们将对此做个回顾,然后列决出有关他们的更多细节。

域选项分为几个独立的类别,我们在接下来的几个小结分别讲解这几类选项:索引选项,存储选项和项向量使用选项。讲完这几个选项,我们还将讲到域的值(包括String的值)。最后我们会提到域选项的常见组合方式。

下面我们从如何控制将域值加入倒排索引开始介绍。

2.4.1 域索引选项

域索引选项(Field.Index.*)通过倒排索引控制域文本是否可被搜索。具体选项如下:

  • Index.ANALYZED: 使用分析器将域值分解成独立的语汇单元流,并使每个语汇单元能被搜索。该选项适用于普通文本域(如正文、标题、摘要等)
  • Index.NOT_ANALYZED:对域进行索引,但不对String值进行分析。该操作实际上将域值作为单一语汇单元并使之能被搜索。该选项适用于索引那些不能被分解的域值,如URL、文件路径、日期、人名、社保号码和电话号码等。该选项尤其适用于“精确匹配”搜索。在程序2.1和程序2.3中我们曾使用这个选项索引ID域。
  • Index.ANALYZED_NO_NORMS: 这是Index.ANALYZED选项的一个变体,他不会在索引中存储norms信息。norms记录了索引中的index-time boost信息,但是当你进行搜索时肯恩会比较耗费内存。
  • Index.NOT_ANALYZED_NO_NORMS:与Index.NOT_ANALYZED选项类似,但也是不存储norms。该选项常用语在搜索器件节省索引空间和减少内存耗费,因为single-token域并不需要norms信息,除非他们已被进行加权操作。
  • Index.NO:使对应的域值不被搜索。

当Lucene建立起倒排索引后,默认情况下他会保存所有必要信息以实施Vector Space Model。该Model需要计算文档中出现的term数,以及它们出现的位置(这是必要的,比如通过词组搜索时用到)。但有时候这些域只是在布尔搜索时用到,他们并不为相关评分做贡献,一个常见的例子是,域只是被用作过滤,如权限过滤和日期过滤。在这种情况下,可以通过调用Field.setOmitTermFreqAndPositions(true)方法让Lucene跳过对该项的出现频率和出现位置的索引。该方法可以节省一些索引在磁盘上的存储空间,还可以加速搜搜和过滤过程,但会悄悄的组织需要位置信息的搜索,如阻止PhraseQuery和SpanQuery类的运行。下面我们转而讨论如何控制Lucene通过域选项来存储域

2.4.2 域存储选项

域存储选项(Field.Store.*)用来确定是否需要存储域的真实值,以便后续搜索时能恢复这个值。

  • Store.YES: 指定存储域值。该情况下,原始的字符串值全部被保存在索引中,并可以由IndexReader类恢复。该选项对于需要展示搜索结果的一些域很有用(如URL、标题或数据库主键)。如果索引的大小在搜索程序考虑之列的话,不要存储太大的域值,因为存储这些域值会消耗掉索引的存储空间。
  • Store.NO: 指定不存储域值。该选项通常跟Index.ANALYZED选项共同用来索引大的文本域值,通常这些域值不用恢复为初始格式,如Web页面的正文,或其他类型的文本文档。

Lucene包含一个很实用的工具类,CompressTools,该类提供静态方法压缩和解压字节数组。该类运行时会在后台调用Java内置的java.util.Zip类。你可以使用CompressTools在存储域值之前对它进行压缩。注意,尽管该方法可以为索引节省一些空间。但节省的幅度跟域值的可压缩程度有关,而且该方法会降低索引和搜索速度。这样其实就是通过消耗更多CPU计算能力来换取更多的磁盘空间,对于很多程序来说,需要仔细权衡一下。如果域值所占空间很小,建议少使用压缩。

下面介绍有关索引项向量的选项。

2.4.3 域的项向量选项

有时索引完文档,你希望在搜索期间该文档所有的唯一项都能完全从文档域中检索。一个常用的方法是在存储的域中加快高亮显示匹配的语汇单元。还有一个方法是使用链接“找到类似的文档”,当运行一个新的点击搜索时,使用原始文档中突出的项。其他解决方法是对文档进行自动分类。

但是项向量到底是什么呢?它是介于索引域和存储域的一个中间结构。

2.4.4 Reader、TokenStream和byte[]域值

Field对象还有其他几个初始化方法,允许传入除String以外的其他参数。

  • Field(String name, Reader value, TermVector termVector)方法使用Reader而不是String对象来表示域值。在这种情况下,域值是不能被存储的(域存储选项被硬编码成Store.NO),并且该域会一直用于分析和索引(Index.ANALYZED)如果在内存中保存String代价较高或者不方便时,如存储的域值较大时,使用这个初始化方法则比较有效。
  • Field(String name, Reader value),与前述方法类似,使用Reader而不是String对象来表示域值,但使用该方法时,默认的termVector为TermVector.NO。
  • Field(String name, TokenStream tokenStream, TermVector termVector)允许程序对域值进行预分析并生成TokenStream对象。此外,这个域不会被存储并将一直用于分析和索引。
  • Field(String name, Token Stream) ,与前一个方法类似,允许程序对域值进行与分析并生成TokenStream对象,但使用该方法时默认的termVector为TermVector.NO。
  • Field(String name, byte[] value, Store store)方法可以用来存储二进制域,乳痈不参与索引的域(Index.NO)和没有项向量的域(TermVector.NO),其中store参数必须设置为Store.YES
  • Field(String name, byte[] value, int offset, int length, Store store),与前一个方法类似,能够对二进制域进行索引,区别在于该方法允许你对这个二进制的部分片段进行引用,该片段的起始位置可以用offset参数表示,处理擦航渡可以用参数length对应的字节数来表示。

现在我们应当清楚,Field类是一个非常复杂的类,他提供了大量的初始化选项,以向Lucene传达精确的域值处理指令。下面我们看看几个有关如何联合使用这些选项的示例。

2.4.5 域选项组合

现在你已经了解所有的3类域选项(索引、排序和项向量)。这些选项可以单独设置,但设置完会形成若干可能的组合。表2.1列出了经常使用的域选项组合以及他们的使用范例,但要注意,这些域选项是可以任意设置的。

索引选项 存储选项 项向量 使用范例
NOT_ANALYZED_NO_NORMS YES NO 标识符(文件名、主键),电话号码和社会安全号码、URL、姓名、日期、用于排序的文本域【不需要分析切词、需要能查到、需要存储原文用于展示】
ANALYZED YES WITH_POSITIONS_OFFSETS 文档标题、摘要【需要分析切词、需要存储原文用于展示、需要偏移量用作高亮】
ANALYZED NO WITH_POSITIONS_OFFSETS 文档正文【需要分析切词、不需要存储原文用于展示、需要偏移量用作高亮】
NO YES NO 文档类型、数据库主键(如果没有用于搜索)【不需要搜索,需要存储原文用于展示】
NOT_ANALYZED NO NO 隐藏的关键词

下面我们来看一看有关域排序的选项。

2.4.6 域排序选项

当Lucene返回撇配搜索条件的文档时,一般是按照默认评分对文档进行排序的。又是你可能需要依照其他标准对结果进行排序,比如在搜索E-mail信息时,你可能会根据发送或接收日期排序,或者根据信息大小或寄件人排序。5.2小节描述了一些排序细节,但为了实现域排序功能,你必须首先正确地完成对域的索引。

如果域是数值类型的,在将它加入文档和进行排序时候,要用NumericField类来表示,具体见2.6.1小节。如果域时文本类型的,如邮件发送者姓名,你得用Field类来表示它和索引它,并且要用Field.Index.NOT_ANALYZED选项避免对它进行分析。如果你的域未进行加权操作,那么在对其索引时就不能带有norm选项,使用Field.Index.NOT_ANALYZED_NO_NORMS,这可以节省磁盘空间和内存空间。

new Field("author", "Arthur C. Clark", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS);

NOTE 用于排序的域时必须进行索引的,而且每个对应文档必须包含一个语汇单元。通常这意味着使用Field.Index.NOT_ANALYZED或Field.Index.NOT_ANALYZED_NO_NORMS(如果你没对文档或域进行加权的话)选项,但若你的分析器只生成一个语汇单元,比如KeyWordAnalyzer,Field.Index.ANALYZED或Field.Index.ANALYZED_NO_NORMS选项也可以使用

这样,我们已经比较详细地介绍了各个域选项,下面还有最后一个有关域的话题,即多值域。

©️2020 CSDN 皮肤主题: 黑客帝国 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值