Lucene读书笔记——2. 构建索引

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

文档和域

文档是Lucene索引和搜索的原子单位。文档为包含一个或多个域的容器,而域则依次包含"真正的"被搜索内容。每个域都有一个标识名称,该名称为一个文本值或二进制值。

lucene可以针对域进行3种操作:

域值可以被索引
域被索引后,还可以选择性地存储项向量,后者可以看做该域的一个小型反向索引集合,通过该向量能够检索该域的所有语汇单元。
域值可以被单独存储,即是说被分析前的域值备份也可以写进行索引中,以便后续的检索。这个机制可以使将原始域值展现给用户,比如文档的标题或摘要。


灵活的架构

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

Lucene要求你在进行索引操作时简单化或反向规格化原始数据。

反向规格化

理解索引的过程

将原始文档转换为成文本 分析文本将分析好的文本保存至索引中

提取文本和创建文档
7.Tika

分析文档
4.Analyzer

向索引添加文档
Lucene将输入数据以一种倒排索引的数据结构进行存储。每个段都包含整个文档索引的一个子集。当writer刷新缓冲区增加的文档,以及挂起目录删除操作时,索引文件都会建立一个新段。在搜索索引时,每个段都是单独访问的,但搜索结果是合并后返回的。

索引段:Lucene索引都包含一个或多个段,如果2.2所示。每个段都是一个独立的索引,它包含整个文档索引的一个子集。
IndexWriter类会周期性地选择一些段,然后将它们合并到一个新段中,然后删除老的段。被合并段的选取策略由一个独立的MergePolicy类主导。一旦选取好这些段,具体合并操作由MergeScheduler类实现。

基本索引操作
向索引添加文档
IndexWriter.addDocument(document),使用默认分析器添加文档,该分析器在创建IndexWriter时指定,用于语汇单元化操作。

addDocument(document, analyzer), 使用指定的分析器添加文档和语汇单元化操作。但是要小心!为了让搜索模块正确工作,需要分析器在搜索时能够“匹配”它在索引时生成的与汇单元。

调用getWriter方法时,我们传入3个变量来创建IndexWriter类。
Directory类,索引对象存储于该类
分析器,被用来索引语汇单元化的域
MaxFieldLength.UNLIMITED,该变量是必要的,它指示IndexWriter索引文档中所有的语汇单元。


删除索引中的文档

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

使用Term删除单个文档时,需要确认在每个文档中都已索引过对应的Field类,还需要确认所有域值都是唯一的,这样才能将这个文档单独找出来删除。 如果碰巧指定了一个错误的Term对象,(例如,由一个普通的被索引的域文本值创建的Term对象,而不是由唯一ID值创建的域),那么Lucene将很容易地快速删除索引中的大量文档。

在所有情况下,删除操作不会马上执行,而是放入内存缓冲区,与加入文档一样,你必须调用writer的 commit()close()方法向索引提交更改。不过即使删除操作已完成,存储该文挡的磁盘空间也不会马上释放,Lucene只是将文档标记为“删除”。

writer.hasDeletions() 用于检索索引中是否包含被标记为已删除的文档。
maxDocs()方法和numDocs()方法。前者返回索引中被删除和未被删除的文档总数,而后者只返回索引中未被删除的文档总数。

writer.optimize() 调用索引优化来强制Lucene在删除一个文档后合并索引段。 (是否真正删除?)


更新索引中的文档

Lucene只能删除整个旧文档,然后向索引中添加新文档。这要求新文档必须包含旧文档中的所有域,包括内容未发生改变的域。

updateDocument(Term, Document)
updateDocument(Term, Document, Analyzer)

这两个方法会调用deleteDocuments

域选项
域索引选项 
Index.ANALYZED                使用分析器将域值分解成独立的语汇单元流,并使每个语汇单元能被搜索。该选项适用于普通文本域。
Index.NOT_ANALYZED        对域进行索引,但不对String值进行分析。该操作实际上将域值作为单一语汇单元并使之能被搜索
Index.ANALYZED_NO_NORMS    分析不加权
Index.NOT_ANALYZED_NO_NORMS 不分析不加权
Index.NO            对应索引不被搜索

Field.setOmitTermFreqAndPositions(true) 方法让Lucene跳过对该项的出现频率和出现位置的索引。可以节省一些索引在磁盘上的磁盘存储空间,还可以加速搜索和过滤过程,但会悄悄地阻止需要位置信息的搜索,如阻止PhraseQuery和SpanQuery类的运行。

域存储选项
Store.YES
指定存储域值。该情况下,原始的字符串值全部被保存在索引中,并可以由IndexReader类恢复。该选项对于需要展示搜索结果的一些域很有用。如果索引的大小在搜索程序考虑之列的话,不要存储太大的域值,因为存储这些域值会消耗掉索引的存储空间。

Store.NO
指定不存储域值。该选项通常跟Index.ANALYZED选项共同用来索引大的文本域值,通常这些域值不用恢复为初始格式,如web页面的正文或其他类型的文本文档。

域的项向量选项
Reader、TokenStream和byte[]域值
域选项组合

域排序选项
一般是按照默认评分对文档进行排序的。
new Field("author", "Arthur C. Clark", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS);
用于排序的域是必须进行索引的,而且每个对应文档必须包含一个语汇单元。

多值域
Document doc = new Document();
for(String author : authors) {
    doc.add(new Field("author', author, Field.Store.YES, Field.Index.ANALYZED));
}


对文档和域进行加权操作
文档加权操作
document.setBoost(float) 默认是1f

在搜索期间,Lucene将自动根据加权情况来加大或减小文档的评分。有时我们还需要更精细的适合域粒度的加权操作,这也可以通过Lucene实现。

域加权操作
当加权一个文档时,Lucene在内部采用同一个加权因子来对该文档中的域进行加权。
field.setBoost(1.2f);

较短的域会有一个隐含的加权

加权基准

索引数字、日期和时间
索引数字
WhitespaceAnalyzer和StandardAnalyzer两个类可以做为候选
SimpleAnalyzer和StopAnalyzer两个类会将语汇单元流中的数字剔除
使用Luke来核实数字是否由分析器保留下来并写入索引

NumericField类

特里结构

索引时间和日期
按照数字来处理日期和时间
doc.add(new NumberField("timestamp").setLongValue(new Date().getTime()))

Calendar cal = new Calendar.getInstance();
cal.setTime(date);
doc.add(new NumberField("dayOfMonth").setIntValue(cal.get(Calendar.DAY_OF_MONTH)))

域截取

IndexWriter允许你对域进行截取后再索引它们,这样一来,被分析的域就只有前面N个项会被编入索引。

MaxFieldLength.UNLIMITED/LIMITED
setMaxFieldLength

近实时搜索

文档的即时索引和即时搜索:
IndexWriter.getReader(),该方法能实时刷新缓冲区中新增或删除的文档,然后创建新的包含这些文档的只读型
IndexReader实例。调用getReader方法会降低索引效率,因为这会使得IndexWriter马上刷新段内容,而不是等到内存缓冲填满再刷新。

优化索引

优化索引就是将索引的多个段合并成一个或者少量段。
优化索引提高的是搜索速度,而不是索引速度。

optimize(),将索引压缩至一个段,操作完成再返回
optimize(int maxNumSegments)也称作部分优化(Partial Optimize),将索引压缩为最多maxNumSegments个段。
由于将多个段合并到一个段的开销最大,建议优化至5个段,它能比优化至一个段更快完成。
optimize(boolean dowait) ,跟optimize() 类似,若dowait参数传入false值,这样的话调用会立即执行,但合并工作是在后台运行的。dowait=false只适合后台线程调用合并程序,如果默认ConcurrentMergeScheduler。
optimize(int maxNumSegments, boolean doWait) 也是部分优化方法,如果dowait参数传入false的话也在后台运行。

优化操作会消耗大量的cpu和i/o资源

IndexWriter.commit或关闭IndexWriter进行提交前,旧段并不能被删除。这意味着你必须为程序预留3倍于优化用量的临时磁盘空间。

其他Directory子类

并发、线程安全及锁机制

线程安全和多虚拟机安全
Lucene并发规则:任意数量的只读属性的IndexReader可以同时打开一个索引。在单个JVM内,最好办法是使用多个线程共享单个的IndexWriter实例。
对于一个索引来说,一次只能打开一个writer。Lucene采用文件锁来提供保障。一旦建立起IndexWriter对象,系统会分配一个锁给它。该锁只有当IndexWriter对象被关闭时才会被释放。
当使用IndexReader对象来改变索引的话——比如修改norms或者删除文档,——这时IndexReader对象会作为Writer使用:它必须在修改上述内容之前成功地获取Write锁,并在被关闭时释放该锁。

IndexReader对象甚至可以在IndexWriter正在修改索引时打开。每个IndexReader对象将向索引展示自己被打开的时间点。该对象只有在IndexWriter对象提交修改或自己被重新打开后才能获知索引的修改情况。所以一个更好的选择是,在已经有IndexReader对象被打开的情况下,打开IndexReader时采用参数create=true:这样,新的IndexReader会持续检查索引的情况。

任意多个线程都可以共享同一个IndexReader类或IndeWriter类。这些类不仅是线程安全的,而且是线程友好的,即是说它们能够很好的扩展到新增线程。

通过远程文件系统访问索引


索引锁机制

如果锁文件write.lock存在于你的索引所在目录内,writer会马上打开该索引。此时若企图针对同一索引创建其他writer的话,将产生一个LockObtainFailedException异常。这是个很重要的保护机制,
因为若针对同一索引打开两个writer的话,会导致索引的损坏。

Directory.setLockFactory将任何LockFactory的子类设置为你自己的锁实现。
Lucene提供的锁实现:
NativeFSLockFactory FSLockFactory默认的锁,使用java.nio本地操作系统锁
SimpleFSLockFactory 
SingleInstanceLockFactory 在内存中创建一个完全的锁
NoLockFactory 完全关闭锁机制


调试索引
writer.setInfoStream(System.out)

高级索引概念
用IndexReader删除文档
IndexReader能够根据文档号删除文档。Writer不能这样操作,是因为文档号可能因为段合并操作而立即发生变化。


IndexReader能够根据Term对象删除文档,这与IndexWriter类似。但IndexReader会返回被删除的文档号,而IndexWriter则不能。IndexReader可以立即决定删除哪个文档,因此就能够对这些文档数量
进行计算;而IndexWriter仅仅是将被删除的Term进行缓存,后续再进行实际的删除操作。

如果程序使用相同的reader进行搜索的话,IndexReader的删除操作会即时生效。这意味着你可以在删除操作后马上进行搜索操作,并发现被删除文档已经不会出现在搜索结果中了。

IndexWriter可以通过Query对象执行删除操作,但IndexReader则不行。

IndexReader提供了一个有时非常有用的方法undeleteAll,该方法能反向操作索引中所有挂起的删除。

注意:使用IndexReader进行删除操作前,必须关闭已打开的任何IndexWriter,反之亦然。 最好是只用IndexWriter完成所有删除操作。

回收被删除文档所使用的磁盘空间

合并操作时和调用optimize时,这些磁盘空间才能被回收。

缓冲和刷新
当一个新文档被添加至Lucene时,或者当挂起一个删除操作时,这些操作首先被缓存至内存,而不是立即在磁盘中进行。
这种缓冲技术主要出于降低磁盘I/O操作等性能原因而使用的。这些操作会以新段的形式周期性写入索引的Directory目录。

IndexWriter根据3个可能的标准来触发实际上的刷新操作,这3个标准是程序控制的:

1.当缓存所占用的空间超过预设的RAM比例时进行实施刷新,预设方法为setRAMBufferSizeMB。RAM缓存的尺寸不能视为最大内存用量。
2.还可以在指定文档号对应的文档被添加进索引之后通过调用setMaxBufferedDocs来完成刷新操作。
3.在删除项和查询语句等操作所占用的缓存总量超过预设值时可以通过调用setMaxBufferdDeleteTerm方法来触发刷新操作。

刷新操作是用来释放缓存的更改的。而提交操作是用来让所有的更改(被缓存的更改或者已经刷新的更改)在索引中保持可视。

tip: 当IndexReader向索引提交更改时,一个刚打开的IndexReader不会看见其中的任何更改的,直到程序调用commit()或close()方法并且重新打开reader为止。
这个机制可以采用create=true来打开新的 IndexWriter(还是IndexReader)。但新打开的近实时Reader却能在不调用commit()或close()方法的情况下看到这些更改。

索引提交
程序每次调用IndexWriter的commit方法之一时都会创建一个新的索引提交。commit方法有两个:commit()创建一个新的索引提交,而commit(Map<String, String> commitUserData)则将提供
String映射图以不透明元数据的形式记录至提交,以用于随后的检索。 关闭writer时也会调用commit()方法。

提交步骤:
  1. 刷新所有缓存的文档和文档删除操作
  2. 对所有新创建的文件进行同步,这包括新刷新的文件,还包括上一次调用commit()方法或者从打开IndexWriter后已完成的段合并操作所生成的所有文件。IndexWriter调用Directory.sync方法来实现这一目标,
  3. 写入和同步下一个segments_N文件。一旦完成操作,IndexWriter会立即看到上一次提交后的所有变化。
  4. 通过调用IndexDeletionPolicy删除旧的提交。

两阶段提交
索引删除策略
IndexDeletionPolicy类负责通知IndexWriter何时能安全删除旧的提交。默认策略是KeepOnlyLastCommitDeletionPolicy,该策略会在每次创建完新的提交后删除先前的提交。


ACID事务和索引连续性
Atomic 所有针对writer的变更要么全部提交至索引,要么全不提交,没有中间状态
Consistency 索引必须是连续的;举例来说,你看到的删除会保存与来自于updateDocument的addDocument方法对应;你也将一直看到索引将全部或全不由addIndexes调用添加
Isolation 当使用IndexWriter进行索引变更时,只有进行后续提交时,新打开的IndexReader才能看到上一次提交的索引变化
Durability (持久性) 如果你的应用程序遇到无法处理的异常。

合并段

段合并策略:MergePolicy

expungeDelete

Lucene提供了两个核心的合并策略,它们都是LogMergePolicy的子类。
第一:LogByteSizeMergePolicy,测量段所包含的所有文件总字节数
第二:LogDocMergePolicy,它完成与第一个子类相同的段合并策略,区别在于它对段尺寸的测量是用段中文档数量来表示的。

通过MergeScheduler来实施合并工作
1.ConcurrentMergeScheduler,该类利用后台线程完成段的合并。
2.SerialMergeScheduler,可以由调用它的线程来完成段的合并。

最后,如果你需要等待所有的段合并操作完成再进行下一步操作,那么可以调用IndexWriter的waitForMerges方法。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值