在前面概要的了解了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)。
也就是说,倒排索引并不是回答“一个文档中包含哪些单词、词组”,而是经过优化后回答“哪个文档中包含这个单词、词组”。就是更符合我们的要求和习惯的一种做法。
向索引添加文档
向索引中添加文档的方法主要有:
我们在内存中先建立一下索引,然后用测试方法测试一下添加索引的动作。程序结构如下:
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的数量。
删除索引中的文档
删除索引中的文档主要有下面几个方法:
这两个方法是确定删除文档的程序,程序结构如下:
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,然后更新旧文档,最后确认新文档被索引。
域索引选项
这个主要是控制域文本是否可被搜索,如何搜索,具体的几个选项如下:
域存储选项
用来确定是否需要存储域的真实值,也就是说索引的信息需不需要恢复。两个可选值如下:
多值域
比如你的文档有一个域表示作者名字,但有时该文档的作者数不止一个。这时候就需要我们向域中写入不同的值,就像这样:
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 }
首先要弄清楚一点,优化索引的目的是为了提高搜索速度而不是为了提高索引速度。
如何优化呢,这里简单的做一下整理:
当然这里只是列出了一部分的优化手段,具体的情况还需要根据具体的环境来分析,毕竟满足需求才是最重要的。
1、在lucene中,锁机制是与并发性相关的一个主题,在同一时刻只允许单一进程的所有代码段中,lucene都创建了基于文件的锁,以此来避免误用 lucene的api造成对索引的损坏。每个索引都有自身的锁文件集。锁文件放在计算机的临时目录中,这个目录由java的java.io.tmpdir 中的系统属性所指定。
2、(1)IndexReader的isLocked(Directory)-这个方法可以判断参数中指定的索引是否已经被上锁。
(2)IndexReader的unlock(Directory)-手动解锁,使用它有危险性,因为lucene加锁有其理由。