一步一步学lucene——(第三步:索引篇)
在前面概要的了解了lucene的内容下面就深入一下lucene的各个模块。这里我们主要深入一下lucene的索引,就是如何构建索引的过程及概念。
lucene与关系型数据库
从两个角度比较一下吧,一个是从索引方面,一个是模糊查询,其实归为一类的化就是全文检索的对比。
1、索引的对比
对比项 | 全文检索库(Lucene) | 关系型数据库 |
核心功能 | 以文本检索为主,插入、删除、修改比较麻烦,适合于大文本块的查询。 | 插入、删除、修改十分方便,有专门的SQL命令,但对于大文本块类型的检索效率较低。 |
库 | 与数据库类似,都可以建多个库,而且各个库的存储位置可以不同。 | 可以建多个库。一般每个库都有控制文件和数据文件等,比较复杂。 |
表 | 没有严格的表的概念,Lucene的表只是由入库时的定义字段松散构成 | 有严格的表结构,有主键,有字段类型等 |
记录 | 由于没有严格的表的概念,所以记录体现为一个对象,记录对应的类是Document。 | Record,与表结构对应。 |
字段 | 字段类型只有文本和日期两种,字段一般不支持运算,更无函数功能,字段对应的类是Field类。 | 字段类型丰富,功能强大。 |
查询结果集 | 在Lucene里表示查询结果集的类是Hits,如hits(doc1,doc2,doc3……) | 在JDBC中使用Resultset |
2、模糊查询的对比
对比项 | Lucene全文检索 | 数据库模糊查询 |
索引 | 将数据源中的数据——建立倒排索引,速度较快 | 无法使用数据库索引,需要遍历所有记录进行模糊匹配,所以查询速度有多个数量级的下降 |
匹配效果 | 通过词元匹配,通过语言分析接口进行关键诩拆分,能够实现对中文的支持 | 由于是模糊查询,匹配不精确,可能查出无关信息或漏查信息 |
匹配度 | 有匹配度算法,将匹配度比较高的结果排在前面 | 没有匹配度算法,一个关键词在记录中出现多少次结果都是一样的 |
结果输出 | 通过特别的算法,将匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的,系统开销较小 | 返回所有的结果集,在匹配条目非常多的时候需要大量的内存存放这些临时结果集,系统开销大 |
可定制性 | 通过API接口可定制出符合检索排序需要的排序规则 | 不可定制 |
适用情况 | 高负载的模糊查询应用,索引资料量比较大,速度要求比较快,匹配度要求比较高的情况 | 使用率低,模糊匹配规则的简单或者需要模糊查询的资料量少的情况 |
索引创建的过程
索引创建的过程可以分为将原始文档转换成文本、分析文本、将分析好的文本保存至索引中这么几个过程。
图:lucene构建索引过程
1、提取文本的过程可以使用我们自己的处理方式也可以使用开源框架Tika来处理。
2、分析文档这个过程很重要,当我们建立起文档和域之后,就可以使用IndexWriter对象的addDocument方法将数据传递给Lucene进行索引操作了。
3、当输入数据分析完毕后,就可以将分析的结果写入到索引文件中了。Lucene将输入数据以一种倒排索引的数据结构进行存储。
什么是倒排索引
倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值, 而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。
也就是说,倒排索引并不是回答“一个文档中包含哪些单词、词组”,而是经过优化后回答“哪个文档中包含这个单词、词组”。就是更符合我们的要求和习惯的一种做法。
基本索引操作
向索引添加文档
向索引中添加文档的方法主要有:
- addDocument(Document)——使用默认的分析器添加文档
- addDocument(Document,Analyzer)——使用指定的分析器添加文档和语汇单元化操作
我们在内存中先建立一下索引,然后用测试方法测试一下添加索引的动作。程序结构如下:
1 protected String[] ids = { "1", "2" }; 2 protected String[] unindexed = { "Netherlands", "Italy" }; 3 protected String[] unstored = { "Amsterdam has lots of bridges", 4 "Venice has lots of canals" }; 5 protected String[] text = { "Amsterdam", "Venice" }; 6 7 private Directory directory; 8 9 protected void setUp() throws Exception { // 1 10 directory = new RAMDirectory(); 11 12 IndexWriter writer = getWriter(); // 2 13 14 for (int i = 0; i < ids.length; i++) { // 3 15 Document doc = new Document(); 16 doc.add(new Field("id", ids[i], Field.Store.YES, 17 Field.Index.NOT_ANALYZED)); 18 doc.add(new Field("country", unindexed[i], Field.Store.YES, 19 Field.Index.NO)); 20 doc.add(new Field("contents", unstored[i], Field.Store.NO, 21 Field.Index.ANALYZED)); 22 doc.add(new Field("city", text[i], Field.Store.YES, 23 Field.Index.ANALYZED)); 24 writer.addDocument(doc); 25 } 26 writer.close(); 27 } 28 29 private IndexWriter getWriter() throws IOException { // 2 30 return new IndexWriter(directory, new WhitespaceAnalyzer(), // 2 31 IndexWriter.MaxFieldLength.UNLIMITED); // 2 32 } 33 34 protected int getHitCount(String fieldName, String searchString) 35 throws IOException { 36 IndexSearcher searcher = new IndexSearcher(directory); // 4 37 Term t = new Term(fieldName, searchString); 38 Query query = new TermQuery(t); // 5 39 int hitCount = TestUtil.hitCount(searcher, query); // 6 40 searcher.close(); 41 return hitCount; 42 } 43 44 public void testIndexWriter() throws IOException { 45 IndexWriter writer = getWriter(); 46 assertEquals(ids.length, writer.numDocs()); // 7 47 writer.close(); 48 } 49 50 public void testIndexReader() throws IOException { 51 IndexReader reader = IndexReader.open(directory); 52 assertEquals(ids.length, reader.maxDoc()); // 8 53 assertEquals(ids.length, reader.numDocs()); // 8 54 reader.close(); 55 }
其中testIndexWriter()方法用来核对写入的文档数,也就是说我们向索引中加入的Document的数量。
上面程序中ids的数量是2,所以这里assertEquals()得出的结果也应该是2,两个结果相同,程序正常执行。
然后我们可以看测试程序testIndexReader()方法是用来得到索引对象并且读出Document的数量。
删除索引中的文档
删除索引中的文档主要有下面几个方法:
- deleteDocuments(Term)——删除指定包含项的文档
- deleteDocuments(Term[ ])——删除包含项数组中的所有文档
- deleteDocuments(Query)——删除匹配查询语句的所有文档
- deleteDocuments(Query[ ])——删除匹配查询数组中的所有文档
- deleteAll()——删除索引中的所有文档
这两个方法是确定删除文档的程序,程序结构如下:
1 public void testDeleteBeforeOptimize() throws IOException { 2 IndexWriter writer = getWriter(); 3 assertEquals(2, writer.numDocs()); // A 4 writer.deleteDocuments(new Term("id", "1")); // B 5 writer.commit(); 6 assertTrue(writer.hasDeletions()); // 1 7 assertEquals(2, writer.maxDoc()); // 2 8 assertEquals(1, writer.numDocs()); // 2 9 writer.close(); 10 } 11 12 public void testDeleteAfterOptimize() throws IOException { 13 IndexWriter writer = getWriter(); 14 assertEquals(2, writer.numDocs()); 15 writer.deleteDocuments(new Term("id", "1")); 16 writer.optimize(); // 3 17 writer.commit(); 18 assertFalse(writer.hasDeletions()); 19 assertEquals(1, writer.maxDoc()); // C 20 assertEquals(1, writer.numDocs()); // C 21 writer.close(); 22 }
这两个测试程序都是删除已经构建好的索引并且测试得到的结果。
更新索引中的文档
其实在lucene中的更新操作就是先删除原来的旧的文档然后加入新的文档,也就是如果我们想更新某个文档中的域的变化,那么就需要先删除原来的Document,然后再新加入新的Document。
程序结构如下:
1 public void testUpdate() throws IOException { 2 3 assertEquals(1, getHitCount("city", "Amsterdam")); 4 5 IndexWriter writer = getWriter(); 6 7 Document doc = new Document(); //A 8 doc.add(new Field("id", "1", 9 Field.Store.YES, 10 Field.Index.NOT_ANALYZED)); //A 11 doc.add(new Field("country", "Netherlands", 12 Field.Store.YES, 13 Field.Index.NO)); //A 14 doc.add(new Field("contents", 15 "Den Haag has a lot of museums", 16 Field.Store.NO, 17 Field.Index.ANALYZED)); //A 18 doc.add(new Field("city", "Den Haag", 19 Field.Store.YES, 20 Field.Index.ANALYZED)); //A 21 22 writer.updateDocument(new Term("id", "1"), //B 23 doc); //B 24 writer.close(); 25 26 assertEquals(0, getHitCount("city", "Amsterdam"));//C 27 assertEquals(1, getHitCount("city", "Haag")); //D 28 }
在这个程序里就是先建立新的Document,然后更新旧文档,最后确认新文档被索引。
Field(域)
域索引选项
这个主要是控制域文本是否可被搜索,如何搜索,具体的几个选项如下:
- Index.ANALYZED——分析指定的文本,就是我们在域中指定的选项,比如文章的标题、正文、摘要等。
- Index.NOT_ANALYZED——这个比较适合于精确匹配,比如我们要搜索的是一个固定的电话号码,有点类似于SQL中的select * from 表 where phoneNum='指定值'。
- Index.NO——对应的域值不被索引。
域存储选项
用来确定是否需要存储域的真实值,也就是说索引的信息需不需要恢复。两个可选值如下:
- Store.YES——存储的值是原始值,也就是说根据索引能够得到原始的值,适合不太大的域值,太大的话会很消耗内存。
- Store.NO——不存储原始值,也就是不能恢复,通常用来索引大块的域。
多值域
比如你的文档有一个域表示作者名字,但有时该文档的作者数不止一个。这时候就需要我们向域中写入不同的值,就像这样:
1 Document doc = new Document(); 2 for (String author : authors) { 3 doc.add(new Field("author", author, Field.Store.YES, 4 Field.Index.ANALYZED)); 5 }
这种方式的处理是被鼓励和接受的。
加权
如果我们有这样一个需求,就是对索引的文档分出主次或者区分出权限比重,那么使用加权操作就会非常容易的实现这个功能。
给文档加权
如果我们为公司设计搜索程序来索引和搜索公司的E-Mail情况,该程序要求在进行搜索结果排序时,公司员工的E-Mail比其它E-Mail有更重要的位置,那么就会用到加权操作。
设置不同的加权因子,程序结构如下:
1 public void docBoostMethod() throws IOException { 2 3 Directory dir = new RAMDirectory(); 4 IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED); 5 6 // START 7 Document doc = new Document(); 8 String senderEmail = getSenderEmail(); 9 String senderName = getSenderName(); 10 String subject = getSubject(); 11 String body = getBody(); 12 doc.add(new Field("senderEmail", senderEmail, 13 Field.Store.YES, 14 Field.Index.NOT_ANALYZED)); 15 doc.add(new Field("senderName", senderName, 16 Field.Store.YES, 17 Field.Index.ANALYZED)); 18 doc.add(new Field("subject", subject, 19 Field.Store.YES, 20 Field.Index.ANALYZED)); 21 doc.add(new Field("body", body, 22 Field.Store.NO, 23 Field.Index.ANALYZED)); 24 String lowerDomain = getSenderDomain().toLowerCase(); 25 if (isImportant(lowerDomain)) { 26 doc.setBoost(1.5F); //1 27 } else if (isUnimportant(lowerDomain)) { 28 doc.setBoost(0.1F); //2 29 } 30 writer.addDocument(doc); 31 // END 32 writer.close(); 33 34 /* 35 #1 Good domain boost factor: 1.5 36 #2 Bad domain boost factor: 0.1 37 */ 38 }
对公司内部的人员邮件加索引时,默认加权因子设置为1.5,其它的设置为0.1,好了,在搜索的期间,这些权值高的就会被先搜索出来。
给域加权
还是上面的例子,如何能使邮件的主题比作者更重要呢,那么就会用到域加权操作。给文档加权会默认给文档中的所有域都进行加权,如果想给域加权,我们需要使用Field的setBoost(float)方法,程序结构如下:
1 public void fieldBoostMethod() throws IOException { 2 3 String senderName = getSenderName(); 4 String subject = getSubject(); 5 6 // START 7 Field subjectField = new Field("subject", subject, 8 Field.Store.YES, 9 Field.Index.ANALYZED); 10 subjectField.setBoost(1.2F); 11 // END 12 }
索引数字、日期和时间
为什么要单独出来说这个呢,因为有的时候你可能有这样的需求,比如你要搜索的是价格信息,需要的是一个精度的搜索,有时候你要搜索一个长度的范围或者接收信息的日期等信息,这些信息通常都是默认被索引成数字,也就是说你可能不能找到你想要匹配的结果,这时候就需要做一些单独的的处理,在我们加入Field的时候。
索引数字的程序结构:
1 public void numberField() { 2 Document doc = new Document(); 3 // START 4 doc.add(new NumericField("price").setDoubleValue(19.99)); 5 // END 6 }
索引日期和时间的程序结构:
1 public void numberTimestamp() { 2 Document doc = new Document(); 3 // START 4 doc.add(new NumericField("timestamp") 5 .setLongValue(new Date().getTime())); 6 // END 7 8 // START 9 doc.add(new NumericField("day") 10 .setIntValue((int) (new Date().getTime()/24/3600))); 11 // END 12 13 Date date = new Date(); 14 // START 15 Calendar cal = Calendar.getInstance(); 16 cal.setTime(date); 17 doc.add(new NumericField("dayOfMonth") 18 .setIntValue(cal.get(Calendar.DAY_OF_MONTH))); 19 // END 20 }
优化索引
首先要弄清楚一点,优化索引的目的是为了提高搜索速度而不是为了提高索引速度。
如何优化呢,这里简单的做一下整理:
- 确认你在使用Lucene的最新版本
- 尽量使用本地文件系统
- 使用更快的硬件设备,特别是更快的IO设备
- 加大你的机器内存容量,给Java虚拟机分配更多的内存
- 在程序中使用一个唯一的IndexSearch实例
- 当测试搜索速度时,忽略第一次查询时间
- 在搜索之前调用optimize优化你的索引
- 考虑使用filters
当然这里只是列出了一部分的优化手段,具体的情况还需要根据具体的环境来分析,毕竟满足需求才是最重要的。
索引的锁机制
1、在lucene中,锁机制是与并发性相关的一个主题,在同一时刻只允许单一进程的所有代码段中,lucene都创建了基于文件的锁,以此来避免误用 lucene的api造成对索引的损坏。每个索引都有自身的锁文件集。锁文件放在计算机的临时目录中,这个目录由java的java.io.tmpdir 中的系统属性所指定。
2、(1)IndexReader的isLocked(Directory)-这个方法可以判断参数中指定的索引是否已经被上锁。
(2)IndexReader的unlock(Directory)-手动解锁,使用它有危险性,因为lucene加锁有其理由。