Lucene学习笔记(1)

搜索引擎是信息检索的工具,一个好的搜索引擎将为用户带来极大的便利。在目下大多数的网站、软件、app中都能看到搜索引擎的存在。有了搜索引擎,用户可以在网站等中快速获取信息。此外,搜索引擎常与网络爬虫配合使用。在Java中,最常使用的搜索引擎框架莫过于Lucene。下面,笔者将根据自己的亲身学习经历,谈谈学习Lucene的心得体会。


开始之前,必须要先介绍几个概念。

1. 基本概念

1.1 信息检索的过程

(1)构建文本库,即文本数据库,保存所有用户可能检索的信息

(2)建立索引

(3)进行搜索

(4)返回结果以前,对结果进行过滤。将信息以一定的规则进行排序或过滤,再返回给用户。

注:以上这些很好理解,假设我们要编纂一部字典,那么首先我们要收集所有需要编入的汉字。然后为它们编好所在位置的页码,建立目录。之后就可以查字了,但通常我们都会遇到同音字,同形字等等,所以需要对查到的内容进行筛选,直到找到我们需要的结果。


1.2 Lucene的索引机制——倒排

一种面向单词的索引机制。通常,由词(关键字)和出现情况两部分组成。对于索引中的每个词(关键字),都跟随一个列表(位置表),用来记录单词在所有文档中出现的位置。

为什么要用倒排索引?

举个例子,假如你要查阅某本书的某个内容,如果根据页数来查找,将会非常麻烦,效率低下。但如果我现在记录下每页中的主要内容,以关键字的形式记下,并对应其页码。这样,当我再查找时,只要首先找到我要看的内容的关键字,再对应上页码就可以查到了。这便是倒排索引的高效之处。


知道了倒排的概念,接下来结合信息检索的过程和倒排的内涵,介绍Lucene检索的核心步骤。

1.3 使用Lucene从文档中检索关键字的核心步骤

(1)对文档预处理

(2)为要处理的文件内容建立索引

(3)构建查询对象

(4)在索引中查找


首先来介绍建立索引。

2. 建立索引

在Lucene中,索引主要依靠Document和Field两个类建立而成。

2.1 Document类

Document和物理的文件没有关系,作为一种数据源的集合,向Lucene提供原始的要索引的文本内容。Lucene会从Document取出相关的数据源内容,并根据属性配置进行相应的处理。


由于Document只是负责收集数据源,因此甚至也可不使用物理文件来构建一个Document,一段文本、几个数字、甚至是一些链接都可以作为数据源。


2.2 Document-Field结构

在Lucene中,数据源是由Field类来表示的。Field类型主要用来标识当前的数据源的各种属性,存储来自数据源的数据内容。

类比Document-Field结构和关系型数据库的联系

数据表——》Lucene索引

表中每一条记录——》Document

表中每一个字段——》Field

索引与数据库表
 Field1:文件名Filed2:文件内容Field3:创建时间Field:修改时间
Document1      神雕侠侣。。。。。。2017-1-12017-1-2
Document2笑傲江湖。。。。。。2017-1-32017-1-4
Document3射雕英雄传。。。。。。2017-1-52017-1-6
Document4天龙八部。。。。。。2017-1-72017-1-8
Document5碧血剑。。。。。。2017-1-92017-1-10
Documentn。。。。。。。。。。。。。。。。。。。。。。。。

2.3 Document的内部实现

Document主要起到对Field的信息进行记录和管理的作用,以便Lucene遍历所有的Field信息。在Document内部,Field是保存在一个Vector类型的对象数组中。

Document可实现对Field的增加、删除、查找等操作。

//为Document加入一个Field
public final void add(Field field)

//删除一个Field
public final void removeField(String name)

//删除多个Field(名字也相同)
public final void removeFields(String name)

//根据一个Field的名称获取其实例
public final Field getField(String name)

//取出Field的值,也就是数据源的值
public final String get(String name)

//得到一个所有Field的枚举
public final Enumeration fields()

//根据名称得到一个Field的数组
public final Field[] getFields(String name)

//根据名称得到一个Field的值的数组
public final String[] getValues(String name)


在Document中添加一个Field

//构建一个Document对象
Document doc = new Document();

//构建一个Field
Field f = new Field("name", "value", Field.Store.YES, Field.Index.TOKENIZED);

//将Field加入Document中
doc.add(f);

这个时候问题来了,在构建Field时,所赋参数name和value都很好理解,但后面那两个是什么鬼?请看下节。


2.4 Field的内部实现

2.4.1 三种属性

Field有三种属性,均为布尔型:

1. 数据源信息是否存储:isStored

该数据源的数据是否要完整地存储于索引中。适合文本较为简短的数据源。

2. 数据源信息是否索引:isIndexed

该数据源的数据是否要在用户检索时被检索。

3. 数据源信息是否分词:isTokenized

该数据源的数据是否要经过分词


什么是分词呢?

即对数据源的文本按照某种规则进行切分,以便进行倒排。

2.4.2 两个静态内部类

Field包含两个静态内部类:

1. Store:表示Field的存储方式

Store.NO:表示该Field不需要存储

Store.YES:表示该Field需要存储

Store.COMPRESS:表示使用压缩方式来保存Field的值

2. Index:表示Field的索引方式

Index.NO:表示该Field不需要索引

Index.TOKENIZED:表示该Field先被分词再索引

Index.UN_TOKENIZED:表示不分词,但索引

Index.NO_NORMS:表示对该Field索引,但不使用Analyzer,同时禁止参加评分(Analyzer为分词所用工具,评分概念见后文)


2.5 Lucene的索引工具IndexWriter

构建Document并添加合适的Field后变需要建立索引,Lucene的索引器为IndexWriter类。

2.5.1 IndexWriter的公有构造函数

public IndexWriter(String path, Analyzer a, boolean create)
public IndexWriter(File path, Analyzer a, boolean create)
public IndexWriter(Directory d, Analyzer a, boolean create)
令人感到奇怪的是,第三个参数,布尔型的create到底是什么?

在指定路径出,删除原目录内的所有内容,重新构建索引;还是在其中已经存在的索引上追加新的Document。通常

第一次构建索引——》true

不断更新中——》false

2.5.2 向索引添加文档

Document bookdoc = new Document();

Field bookNo = new Field("booknumber", "FB309663004", Field.Store.YES, Field.Index.UN_TOKENIZED);

bookdoc.add(bookNo);

//构建一个IndexWriter实例
IndexWriter writer = new IndexWriter(INDEX_STORE_PATH, new StandardAnalyzer(), true);
//向索引中加入Document对象
writer.addDocument(bookdoc);

注意:在使用addDocument方法加入所有的Document后,一定使用IndexWriter的close方法来关闭索引器,使所有在I/O缓存中的数据都写入到磁盘上,关闭各种流。如果没有关闭,就会发现索引目录内除了一个segements文件外一无所有。


2.5.3 限制每个Field中的词条的数量

只需在添加文档前添加一行代码

writer.setMaxFieldLength(100000);


2.6 索引文件格式
2.6.1 segments

Lucene对索引管理的最大单位。每个segments中有许多的Document。每个segments内的所有索引文件都具有相同的前缀。索引文件目录下只会有一个segments文件,记录当前索引内有多少个segments。多个索引文件,文件前缀都相同,后缀不同。每个segment代表Lucene的一个完整索引段,前缀名等于Document数量转成36进制后再前面加上下划线“_”组成。


2.6.2 索引文件格式的意义

1. .fnm

包含了Document中所有Field名称

2. .fdt

存储具有Store.YES属性的Field数据

3. .fdx

本身是一个索引,用于存储Document在.fdt中的位置

4. .tis

存储分词后的词条

5. .tii

.tis的索引文件,标明了每个.tis文件中的词条的位置

6. deletable

在Lucene索引中,文档删除并非立即去除,而是留待下一次合并索引或是对索引优化时才真正删除。

所有的文档在被删除后,会首先在deletable文件中留一个记录,要真正删除时,才将索引除去。

7. .cfs

复合索引格式。解决索引内容巨大而耗费系统资源问题。


2.7 索引过程的调优

1. 合并因子mergeFactor

取值小——》索引使用内存少——》搜索未优化索引更快——》建立索引慢——》适用于间歇性向索引加入文档

取值大——》建索需要更多内存——》搜索未优化索引较慢——》建立索引快——》适用于批量索引建立


2. maxMergeDocs

对mergeFactor中文档数的变化设定上限


3. minMergeDocs

当索引被刷到磁盘中,需要首先保存在内存中,minMerge就是用来限制内存中文档数量


2.8 索引的合并

2.8.1 FSDirectory与RAMDirectory

此二类均为Directory子类

FSDirectory:指的是在文件系统中的一个路径。当Lucene向其中写入索引时会直接将索引写入到磁盘上。

RAMDirectory:是内存中的一个区域。如果将RAMDirectory中的内存写入磁盘,当虚拟机退出后,里面的内容也会随之消失。因此需要将RAMDirectory中的内容转到FSDirectory中。

RAMDirectory ramDir = new RAMDirectory();
FSDirectory fsDir = FSDirectory.getDirectory(INDEX_STORE_PATH, true);

IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);
IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);

2.8.2 使用IndexWriter来合并索引

//初始化一个RAMDirectory对象
RAMDirectory ramDir = new RAMDirectory();
//初始化一个FSDirectory对象
FSDirectory fsDir = FSDirectory.getDirectory(INDEX_STORE_PATH, true);

//构建一个索引器,并以文件系统的目录为其目标路径
IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);
//构建一个索引器,并以内存作为其目标路径
IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);

//创建第一个文档
Document doc1 = new Document();
Field bookNo1 = new Field("booknumber", "BOOKNUM1", Field.Store.YES, Field.Index.UN_TOKENIZED);
doc1.add(bookNo1);
//创建第二个文档
Document doc2 = new Document();
Field bookNo2 = new Field("booknumber", "BOOKNUM2", Field.Store.YES, Field.Index.UN_TOKENIZED);
doc2.add(bookNo2);

//将第一个文档加入内存目录中
ramWriter.addDocument(doc1);
//关闭内存的索引器
ramWriter.close();

//将第二个文档加入磁盘目录中
fsWriter.addDocument(doc2);
//合并索引
fsWriter.addIndexes(new Directory[]{ramDir});
//关闭磁盘索引器
fsWriter.close();


注意:在合并内存中的索引时,一定要先将相应的IndexWriter关闭,以保证滞留在缓存中的文档被“刷”到RAMDirectory中去。

2.9 索引的优化

Lucene索引优化的策略是建立新的segment来取代那些被合并的segment,所以在旧的segment删除前,索引内磁盘空间消耗将会非常大。因此,优化只能在需要的时候进行,而非任意时刻。

IndexWriter的optimize()方法能够对当前IndexWriter所指定的索引目录及其所使用的缓存目录下的所有segment做优化,使所有的segments合并成一个完整的segment,即整个索引目录内只出现一种文件前缀。


2.10 从索引中删除文档

要从索引中删除文档可不是一件容易的事,必须要经过打开索引,取得索引中的某个文档,然后再删除。为此,不得不提Lucene的一个重要类IndexReader

2.10.1 索引读取工具IndexReader

//使用IndexReader来读取索引
IndexReader reader = IndexReader.open(INDEX_STORE_PATH);

//显示索引内所有的Document
for(int i=0; i<reader.numDocs(); i++){
	System.out.println(reader.document());
}

//输出当前索引的版本信息
System.out.println("版本:"+ reader.getVersion());
//输出当前索引的文档数量
System.out.println("索引内的文档数量:"+ reader.numDocs());

//构造一个词条并在索引中查找
Term term1 = new Term("bookname", "女");
TermDocs docs = reader.termDocs(term1);
while(docs.next()){
	System.out.println("--------------------");
	System.out.println("含有所查找的<"+ term1 +">的Document的编号为"+ docs.doc());
	System.out.println("Term在文档中的出现次数"+ docs.freq());
	System.out.println("--------------------");
}


2.10.2 使用文档ID号来删除特定文档

在建立索引过程中,Lucene会为每一个加入索引的Document赋予一个ID号,这个ID号将唯一地表示每个文档。

IndexReader reader = IndexReader.open(INDEX_STORE_PATH);
reader.deleteDocument(0);

原理分析:在Lucene的内部使用类似回收站的机制管理Document的删除。每个Document被从索引中删除时,它只是相当于被扔进了回收站,并未实际删除。一旦进程退出,它们就会又“活”过来。

既然是“回收站”,那么Lucene就应当提供一种反删除的机制。IndexReader的undeleteAll()方法可以帮助实现反删除。

IndexReader reader = IndexReader.open(INDEX_STORE_PATH);
reader.undeleteAll();
reader.close();


如何真正地从一个索引中删除文档?

IndexWriter writer = new IndexWriter(......);
writer.optimize();
writer.close();


执行后,Lucene会重新为每个文档分配ID值,那些被标记为已删除的Document就真正地被物理删除了。

2.10.3 使用Field信息来删除批量文档

Term类是用于表示词条的工具,能够将词条表示为<field, value>对。<bookname, 女>表示在bookname中含有关键字女。当将Term对象传入IndexReader的deleteDocument()方法时,IndexReader就会删除掉所有在bookname这个Field中含有“女”这个Term的Document。

IndexReader reader = InderReader.open(INDEX_STORE_PATH);
Term term = new Term("bookname", "女");
reader.deleteDocuments(term);
reader.close();

IndexReader reader2 = IndexReader.open(INDEX_STORE_PATH); 
System.out.println(reader2.numDocs());


2.11 Lucene同步法则及结论

2.11.1 同步法则

1. 在同一时刻,Lucene的索引只允许有一个进程对其进行加入文档、删除文档、更新索引等操作。

2. 在同一时刻,Lucene的索引允许多个线程同时对其进行检索

2.11.2 结论

1. 任一时刻,在系统中只能有一个IndexWriter的实例对索引进行操作,不允许有多个IndexWriter向索引添加Document,或是优化、索引、合并segement

2. 任一时刻,不能有多个IndexReader在执行文档的删除操作。下一个IndexReader的操作应当在上一个IndexReader执行close方法之后运行

3. 在使用IndexWriter向索引加入文档之前,必须先关闭执行删除操作的IndexReader实例

4. 在使用IndexReader删除前,必须先关闭执行删除操作的IndexWriter的实例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值