5、构建索引

Lucene简介

 Lucene是apache软件基金会 jakarta项目组的一个子项目,项目最初是由资深全文索引/检索专家Doug Cutting开发,后来开源贡献出来。 Lucene是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。 

Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。

搜索内容的建模—文档与域

● 文档(Document)与域(Field)是Lucene中的重要概念,事实上任何与索引文档相关的操作,都是在document类与field类的基础上实现的。

● Document在lucene中是一种逻辑文件,Lucene本身无法对物理文件建立索引,只能识别处理Document类型的文件。 Document和物理文件没有关系,是一种数据源的集合,负责向lucene提供原始的要索引的文本内容。

● NOTE:Document是负责收集数据,甚至可以不使用物理文件来构建,一段文本、几个数字甚至是链接都可以作为构建Document的数据源。
在这里插入图片描述

在这里插入图片描述

搜索内容的建模—文档与域

● 在lucene中,数据源是由一个被称为Field(域)的类来表示的,文档是作为包含一个或多个域的容器而存在的,域中存放的就是“真正的”被搜索的内容。每个域有一个标识名称,一般为文本值或者二进制值。
Lucene可以针对域进行如下三种操作
1、域值可以被索引或者不被索引
能被索引的域值要求必须为文本格式,二进制的域值只能被存储而不能被索引。
2、域被索引后可以选择性地存储向量
存储项向量可以被看做这个域的小型反向索引集合,通过该向量可以检索到该域的所有语义单元。
3、单独存储域值
被分析前的域值备份也可以写进索引中,以便后续的检索。

● 在lucene中,这种Document-Field结构类似关系数据库。在关系数据库中,一张表可以看成是Lucene中的索引,表中的每一条记录,就是Lucene中的Document,而表中的每一个字段,相当于Lucene中的Field。表中每个字段有各种属性,可以是字符型也可以是数字型,这和Field一样,具有各种属性。

在这里插入图片描述
● 比较一下Lucene和数据库的异同
在这里插入图片描述
● Field的属性一般分为3类
是否存储
是否索引
是否分词
在实际开发中,会遇到各种各样的数据源,应当根据这些数据源的重要性和功能性,来决定在该数据源的数据在索引中的作用与存储方式。

● Lucene索引创建过程一般分为三个主要步骤:
将原始文档转为文本(Denormalization)
分析文本
将分析好的文本保存到索引中

在这里插入图片描述
使用JAVA编码实现的话是这样的流程:(注意:Java要使用Jre1.7以上的运行环境)
第一步:创建 Directory指定索引文件的存储位置
存放于内存中
Directory directory = new RAMDirectory();
存放在指定目录下
Directory directory = FSDirectory.open(Paths.get(“E:/itxxz/lucene”));
第二步 : 创建 IndexWriterConfig,创建索引所需的配置
这里是配置使用的分词器,使用用的分析器是lucene标准分词类 StandardAnalyzer,也可以更换其它,再后续篇章中会进行讲解,以下代码便是创建IndexWriterConfig 的语句。
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig iwc =
new IndexWriterConfig(analyzer);

第三步 : 创建 Document
如果以数据库为参考,这里的document就相当于一张表,而field便相当于表里的字段
Document doc = new Document();
第四步 :添加 Field Field在lucene的后续版本中进行了细分,比如int、long、string及text类型,根据需要可进行筛选使用
document.add(new LongField(“modified”, f.lastModified(), Field.Store.NO));
document.add(new TextField(“contents”, new FileReader(file)));
document.add(new StringField(“path”, file.toString(), Field.Store.YES));
第五步:使用IndexWriter将文档写入索引
IndexWriter writer = new IndexWriter(directory, iwc);
writer.addDocument(doc);

  1. addDocument(Document)
    使用默认的分词器添加文档,该分析器在创建IndexWriter对象的时候指定,拥有语汇单元化操作。将分析好的文本保存到索引中。
    addDocument(Document,Analyzer)
    使用指定的分析器添加文档和语汇打印操作。
    NOTE: 为了让搜索模块正确工作,需要分词器在搜索是能匹配它在索引时生产的语汇单元。(后续章节会介绍)
    /准备好各种数据源的值/
    //id的值
    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; //定义Directory类型对象用于存放索引

protected void setUp() throws Exception {//每次测试前运行,用于文档与域的初始化
directory = new RAMDirectory(); //索引存入RAM
//创建indexWriter对象
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(directory, iwc);

for (int i = 0; i < ids.length; i++) { 

//根据不同Field的特点构建Field并加到Document中
Document doc = new Document();
doc.add(new Field(“id”, ids[i],
Field.Store.YES,
Field.Index.NOT_ANALYZED));
doc.add(new Field(“country”, unindexed[i],
Field.Store.YES,
Field.Index.NO));
doc.add(new Field(“contents”, unstored[i],
Field.Store.NO,
Field.Index.ANALYZED));
doc.add(new Field(“city”, text[i],
Field.Store.YES,
Field.Index.ANALYZED));
writer.addDocument(doc);
}
writer.close();
}
● 有时一个域的值不止一个,可以使用下面的语句来向这个域写入几个不同的值。

String[] authors = new String[] {“leinhard”, “lana"};
Document doc = new Document();
for (String author: authors) {
doc.add(new Field(“author”, author,
Field.Store.YES,
Field.Index.ANALYZED));
}

基本索引操作——删除索引中的文档

●删除索引中的文档:
deleteDocuments(Term)
删除包含特定项的所有文档
deleteDocuments(Term[ ])
删除包含数组任一元素的所有文档
deleteDocuments(Query)
删除匹配查询语句的所有文档
deleteDocuments(Query[ ])
删除匹配查询语句数组任一元素的所有文档
deleteAll()
删除索引全部文档,与writer先关闭在用参数create
=true重新打开登记,deleteAll()方法好处是不用关闭writer

基本索引操作——删除索引中的文档

public void testDeleteBeforeOptimize() throws IOException {
IndexWriter writer = getWriter();
assertEquals(2, writer.numDocs()); //确认索引中有2个文档
writer.deleteDocuments(new Term(“id”, “1”)); //删除id值是1的文档
writer.commit();
assertTrue(writer.hasDeletions()); //确认被标记为删除的文档
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”));
writer.optimize(); //使用优化方式进行删除文档
writer.commit();
assertFalse(writer.hasDeletions());//确认没有被标记为被删除的文档
assertEquals(1, writer.maxDoc());
assertEquals(1, writer.numDocs());
writer.close();
}
在这里插入图片描述
在这里插入图片描述
使用IndexReader类的UndeleteALL()方法,可以恢复不是优化方式(物理)删除的文档。
样例:
File indexDir = new File(“D:\luceneIndex");
IndexReader ir = IndexReader.open(indexDir);
ir.undeleteAll();
ir.close();

基本索引操作——更新索引中的文档

● 在目前的Lucene版本中,还没提供更新文档中部分域的功能,只支持删除索引中的旧文档,再向索引中添加新文档的方式。
updateDocument(Term,Document)
删除包含Term变量的文档,然后使用writer默认的分析器添加新文档。
2. updateDocument(Term,Document,Analyzer)
删除包含Term变量的文档,然后使用writer指定的分析器添加新文档。
● NOTE:因为updateDocument本质上是先后台调用deleteDocument方法,要确认Term标识的唯一性。
public void testUpdate() throws IOException {

assertEquals(1, getHitCount("city", "Amsterdam"));

IndexWriter writer = getWriter();

Document doc = new Document();               //构建一个新文档            
doc.add(new Field("id", "1",
                  Field.Store.YES,
                  Field.Index.NOT_ANALYZED));    
doc.add(new Field("country", "Netherlands",
                  Field.Store.YES,
                  Field.Index.NO)); 
doc.add(new Field("contents",                    
                  "Den Haag has a lot of museums",
                  Field.Store.NO,
                  Field.Index.ANALYZED));       

//新文档的city域的值是Den Haag
doc.add(new Field(“city”, “Den Haag”,
Field.Store.YES,
Field.Index.ANALYZED)); writer.updateDocument(new Term(“id”, “1”), //更新原来id是1的文档
doc);
writer.close();

assertEquals(0, getHitCount("city", "Amsterdam"));

//验证确认原来的文档已经被删除
assertEquals(1, getHitCount(“city”, “Haag”));
//验证已经更新了索引中的文档
}

域选项简介——域索引选项

● Field类是文档索引期间最重要的类之一:这个类事实上控制着被索引的域值。
域索引选项(Field.Index.*)通过倒排序索引来控制文本是否可以被搜索,选项如下:
Field .Index.ANALYZED: 进行分词和索引,适用于标题、内容等
Field . Index.NOT_ANALYZED: 进行索引,但是不进行分词,如果身份证号,姓名,ID等,适用于精确搜索
Field . Index.ANALYZED_NOT_NORMS: 进行分词但是不存储norms信息,这个norms中包括了创建索引的时间和权值等信息
Field . Index.NOT_ANALYZED_NOT_NORMS: 即不进行分词也不存储norms信息
Field . Index.NO: 不进行索引

域选项简介——域存储选项
● 域存储选项Field.Store.*用来确定是否需要提前存储域的真实值,以便以后搜索时可以及时恢复。

Field.Store.YES或者NO
设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原
设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完全还原(doc.get)

域选项简介——域的对象构造的初始化选项

● Field的值可以构造成很多类型,Field类中定义了4种:String、Reader、byte[]、TokenStream。
Field对象的构造,有一下几种构造方法:
public Field(String name, byte[] value, Store store)
用来存储二进制域,如果不参与索引的域(Index.NO)和没有项向量的域(TermVector.NO)。store参数必须设置为Store.YES。

public Field(String name, Reader reader, TermVector termVector)
使用Reader来表示域值,这种方式域值是不能被存储的(域存储选项强制为Store.NO)而且域会一直被用于分析和索引(Index.ANALYZED)。如果内存中存储String代价高或者不方便,如存入较大值的时候推荐使用。

public Field(String name, Reader reader)
使用Reader来表示域值,但是默认的termVector为NO。

域选项简介——域的对象构造的初始化选项

public Field(String name, String value, Store store, Index index)
使用字符串来表示域值,可以被存储被索引,默认的termVector为NO。

public Field(String name, String value, Store store, Index index, TermVector termVector)
使用字符串来表示域值,可以被存储,并且一直用于分析和索引。

public Field(String name, TokenStream tokenStream)
运行程序对域值进行预分析并生成TokenStream对象,这种域不会被存储并且一直用于分析和索引,但是默认的termVector为NO。

public Field(String name, TokenStream tokenStream, TermVector termVector)
运行程序对域值进行预分析并生成TokenStream对象,这种域不会被存储并且一直用于分析和索引。

文档与域的加权操作——对文档和域加权

● 加权是指对文档和域的重要性通过加权因子进行人为地干预。
加权操作可以在索引期间完成,也可以在搜索期间完成。搜索期间的加权操作会更加动态化, 每次搜索操作都可以根据不通的加权因子独立选择加权或者不加权,但这个策略也可能要稍微多消耗点CPUX效率。
NOTE:无论在什么时候进行加权都需要小心,过多的加权操作,特别是在用户界面没有提示的相应文档已经被加权操作的情况下。这可能会使用户搜索到很多用户不关心的东西(如百度的竞价排名)。
文档的加权操作:doc.setBoots(float)
域的加权操作: Field subjectField =new Field(“author”, author,Field.Store.YES,Field.Index.ANALYZED));
subjectField. setBoots(1.2F);
默认的加权因子是1.0
样例:
Document doc = new Document();
NumericField price = new NumericField(“price”);
price.setDoubleValue(19.99);
doc.add(price);

NumericField timestamp = new NumericField("timestamp");
timestamp.setLongValue(new Date().getTime());
doc.add(timestamp);

Date b = new Date();
NumericField birthday = new NumericField("birthday");
String v = DateTools.dateToString(b, DateTools.Resolution.DAY);
birthday.setIntValue(Integer.parseInt(v));
doc.add(birthday);

域截取——限制每个Field中的词条数目

在一些场景下,应用程序需要对尺寸未知的文档进行索引,域截取作
为一个控制RAM和硬盘空间使用量的安全机制,需要对每个域进行索引时对输入的文档尺寸(词条数)进行限制。

样例:
Directory dir = null;
Analyzer analyzer = null;
IndexWriter writer = new IndexWriter(dir, analyzer,
true, IndexWriter.MaxFieldLength.UNLIMITED);//UNLIMITED表示不使用域截取策略,LIMITED表示只截取域的前1000项
//可以通过setMaxFieldLength方法来设置截取的项
writer.setMaxFieldLength(999);
writer.setInfoStream(System.out);
NOTE:设置不合理的MaxFieldLength值会导致用户搜索不到文档!
索引工具IndexWriter简介
在用户构建完Document、并且为其添加合适的Field后,就需要建立索引
来存放Document了。在lucene中,IndexWriter类的主要作用的是创建索引,加入Document,合并索引段,以及控制索引相关的方方面面。
构造方法如下:
IndexWriter(Directory d, Analyzer a,boolean create, IndexDeletionPolicy deletionPolicy, IndexWriter.MaxFieldLength mfl)

IndexWriter(Directory d, Analyzer a, boolean create, IndexWriter.MaxFieldLength mfl)
IndexWriter(Directory d, Analyzer a, IndexDeletionPolicy deletionPolicy, IndexWriter.MaxFieldLength mfl)
IndexWriter(Directory d, Analyzer a, IndexDeletionPolicy deletionPolicy, IndexWriter.MaxFieldLength mfl, IndexCommit commit)
IndexWriter(Directory d, Analyzer a, IndexWriter.MaxFieldLength mfl)

IndexWriter(directory, new WhitespaceAnalyzer(),
IndexWriter.MaxFieldLength.UNLIMITED);
在这里插入图片描述
Lucene索引index由若干段(segment)组成,每一段由若干的文档(document)组
成,每一个文档由若干的域(field)组成,每一个域由若干的项(term)组成。
项是最小的索引概念单位,它直接代表了一个字符串以及其在文件中的位置、
出现次数等信息。
在这里插入图片描述
Summary
本次课主要讲解了搜索引擎开发中如何建立索引的关键技术,并且介绍了Lucene索引实现的技术细节。
本次课结束

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值