2.4、Lucene索引详解


目录

2.4.1、Lucene 字段类型

2.4.2、索引文档示例

2.4.3、在Luke中查看索引

2.4.4、索引的删除

2.4.5 索引的更新

-———————————————–

上一节 介绍完了 Lucene 分词器,这节介绍 Lucene 是如何索引文档的。

2.4.1、Lucene 字段类型

文档:文档是 Lucene索引的基本单位;

字段:比文档更小的单位,字段是文档的一部分;

​ 每个字段 由 3部分组成:名称(name),类型(type),取值(value);

​ 字段的取值(value)一般为:文本、二进制、数值

Lucene的主要字段类型:

  • TextField:字段内容 -> 索引并词条化 -> 不保存 词向量 -> 整篇文档的body字段,常用TextField进行索引;
  • StringField:只 索引 -> 不词条化 -> 不保存 词向量;
  • IntPoint:适合 int类型 的索引;
  • LongPoint:适合 long类型的 索引;
  • FloatPoint:
  • DoublePoint:
  • SortedDocValuesField:存储值为 文本内容的 DocValue字段,且需要 按值排序;
  • SortedSetDocValuesField:多值域为 DocValue字段,值为文本内容,且 需要 按值分组、聚合;
  • NumbericDocValuesField: DocValue为(int,long,float,double)
  • SortedNumbericDocValuesField:需要排序的 (int,long,float,double)
  • SortedField:索引 保存字段值,不进行其他操作

Lucene 使用 倒排索引 快速搜索 <===> 建立 词项和文档id的关系映射;

搜索过程:

​ 通过 类似hash算法 -> 定位到 搜索关键词 -> 读取文档id集合

上述过程的缺陷:

​ 当我们需要对数据做 聚合操作,排序、分组时,Lucene会提取所有出现在文档集合中的排序字段,再次构建一个排好序的文件集合;这个过程全部在内存中进行,如果数据量巨大,会造成 内存溢出 和 性能缓慢;

Lucene 4.X 之后出现了 DocValues,DocValues是 Lucene 构建索引时,额外建立的一个有序的基于 document => field/value 的映射列表。

2.4.2、索引文档示例

代表新闻的实例类 News.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.learn.lucene.chapter2.index;

/**
* 代表新闻的实例类 News.java
*/
public class News {

private int id;
private String title;
private String content;
private int reply;

public News() {
}

public News(int id, String title, String content, int reply) {
super();
this.id = id;
this.title = title;
this.content = content;
this.reply = reply;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public int getReply() {
return reply;
}

public void setReply(int reply) {
this.reply = reply;
}
}

创建索引文件的CreateIndex.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package com.learn.lucene.chapter2.index;

import com.learn.lucene.chapter2.ik.IKAnalyzer8x;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;

/**
* 创建索引
*/
public class CreateIndex {

public static void main(String[] args) {
// 创建 3 个News对象
News news1 = new News();
news1.setId(1);
news1.setTitle("习近平会见美国总统奥巴马,学习国外经验");
news1.setContent("国家主席习近平9月3日在杭州西湖宾馆会见前来出席二十国集团领导人杭州峰会的美国总统奥巴马");
news1.setReply(672);

News news2 = new News();
news2.setId(2);
news2.setTitle("北大迎4380名新生,农村学生700多人今年最多");
news2.setContent("昨天,北京大学迎来4380名来自全国各地及数十个国家的本科新生。其中,农村学生工700余名,为今年最多...");
news2.setReply(995);

News news3 = new News();
news3.setId(3);
news3.setTitle("特朗普宣誓(Donald Trump)就任美国第45任总统");
news3.setContent("当地时机1月20日,唐纳德·特朗普在美国国会宣誓就职,正式成为美国第45任总统。");
news3.setReply(1872);

// 创建IK分词器
Analyzer analyzer = new IKAnalyzer8x();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
Directory directory = null;
IndexWriter indexWriter = null;
// 索引目录
Path indexPath = Paths.get("indexdir");
// 开始时间
Date start = new Date();
try {
if (!Files.isReadable(indexPath)) {
System.out.println("Document directory '" + indexPath.toAbsolutePath() + "' is not readable! please check");
System.exit(1);
}
directory = FSDirectory.open(indexPath);
indexWriter = new IndexWriter(directory, iwc);
// 设置新闻ID 索引 并存储
FieldType idType = new FieldType();
idType.setIndexOptions(IndexOptions.DOCS);
idType.setStored(true);
// 设置新闻标题索引文档,词项频率,位移信息,和偏移量,存储并词条化
FieldType titleType = new FieldType();
titleType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
titleType.setStored(true);
titleType.setTokenized(true);

FieldType contentType = new FieldType();
contentType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
contentType.setStored(true);
contentType.setTokenized(true);
contentType.setStoreTermVectors(true);
contentType.setStoreTermVectorPositions(true);
contentType.setStoreTermVectorOffsets(true);
contentType.setStoreTermVectorPayloads(true);

Document doc1 = new Document();
doc1.add(new Field("id", String.valueOf(news1.getId()), idType));
doc1.add(new Field("title", news1.getTitle(), titleType));
doc1.add(new Field("content", news1.getContent(), contentType));
doc1.add(new IntPoint("reply", news1.getReply()));
doc1.add(new StoredField("reply_display", news1.getReply()));

Document doc2 = new Document();
doc2.add(new Field("id", String.valueOf(news2.getId()), idType));
doc2.add(new Field("title", news2.getTitle(), titleType));
doc2.add(new Field("content", news2.getContent(), contentType));
doc2.add(new IntPoint("reply", news2.getReply()));
doc2.add(new StoredField("reply_display", news2.getReply()));

Document doc3 = new Document();
doc3.add(new Field("id", String.valueOf(news3.getId()), idType));
doc3.add(new Field("title", news3.getTitle(), titleType));
doc3.add(new Field("content", news3.getContent(), contentType));
doc3.add(new IntPoint("reply", news3.getReply()));
doc3.add(new StoredField("reply_display", news3.getReply()));

indexWriter.addDocument(doc1);
indexWriter.addDocument(doc2);
indexWriter.addDocument(doc3);
indexWriter.commit();
indexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
Date end = new Date();
System.out.println("索引文档用时:" + (end.getTime() - start.getTime()) + " milliseconds.");
}
}

运行结果:

生成对应的索引文件

2.4.3、在Luke中查看索引

打开索引文件目录:D:\java\oschina\lucene-learn\lucene-chapter2\indexdir

2.4.4、索引的删除

索引同样存在 CRUD 操作

本节演示 根据 Term 来删除点单个或多个Document,删除 title 中 包含关键词“美国”的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.learn.lucene.chapter2.index;

import com.learn.lucene.chapter2.ik.IKAnalyzer8x;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* 删除索引
*/
public class DeleteIndex {
public static void main(String[] args) {
// 删除 title 中含有关键字“美国”的文档
deleteDoc("title", "美国");
}

public static void deleteDoc(String field, String key) {
Analyzer analyzer = new IKAnalyzer8x();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
Path indexPath = Paths.get("indexdir");
Directory directory;
try {
directory = FSDirectory.open(indexPath);
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
indexWriter.deleteDocuments(new Term(field, key));
indexWriter.commit();
indexWriter.close();
System.out.println("删除完成");
} catch (IOException e) {
e.printStackTrace();
}
}

}

运行结果:

除此之外,IndexWriter还提供了以下方法:

  • DeleteDocuments(Query query):根据Query条件来删除单个或多个Document。
  • DeleteDocuments(Query[] queries):根据Query条件来删除单个或多个Document。
  • DeleteDocuments(Term term):根据Term条件来删除单个或多个Document。
  • DeleteDocuments(Term[] terms):根据Term条件来删除单个或多个Document。
  • DeleteAll():删除所有的Document。

使用IndexWriter进行Document删除操作时,文档并不会立即被删除,而是把这个删除动作缓存起来,当IndexWriter.Commit() 或 IndexWriter.Close()时,删除操作才会真正执行。

使用Luke 重新打开 索引之后,只剩下了一个索引文档:

2.4.5 索引的更新

本质:先删除索引,再建立新的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.learn.lucene.chapter2.index;

import com.learn.lucene.chapter2.ik.IKAnalyzer8x;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* 更新索引
*/
public class UpdateIndex {

public static void main(String[] args) {
Analyzer analyzer = new IKAnalyzer8x();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
Path indexPath = Paths.get("indexdir");
Directory directory;
try {
directory = FSDirectory.open(indexPath);
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
Document doc = new Document();

// 设置新闻ID 索引 并存储
FieldType idType = new FieldType();
idType.setIndexOptions(IndexOptions.DOCS);
idType.setStored(true);
// 设置新闻标题索引文档,词项频率,位移信息,和偏移量,存储并词条化
FieldType titleType = new FieldType();
titleType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
titleType.setStored(true);
titleType.setTokenized(true);

FieldType contentType = new FieldType();
contentType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
contentType.setStored(true);
contentType.setTokenized(true);
contentType.setStoreTermVectors(true);
contentType.setStoreTermVectorPositions(true);
contentType.setStoreTermVectorOffsets(true);
contentType.setStoreTermVectorPayloads(true);

doc.add(new Field("id", "2", idType));
doc.add(new Field("title", "北大迎4380名新生", titleType));
doc.add(new Field("content", "昨天,北京大学迎来4380名来自全国各地及数十个国家的本科新生。其中,农村学生工700余名,为今年最多...", contentType));
indexWriter.updateDocument(new Term("title", "北大"), doc);
indexWriter.commit();
indexWriter.close();
System.out.println("更新完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}

运行结果:

修改前:

修改后: