摘要
Lucene是一个基于Java开发的全文检索工具包,将非结构化的数据建立索引库,通过查询索引,查询文档内容。由此将非结构化数据转换成了结构化数据
正文
Lucene8.8.2官方文档
一、概念 1.1 基础 数据分类
结构化数据:格式固定、长度固定、有一定格式、数据类型固定。例如数据库中的数据 非结构化数据:格式不固定、长度不固定、数据类型不固定,例如word文档、pdf文档、邮件、html、txt,格式不固定 数据的查询
结构化数据查询:SQL语句,Structured Query Language 结构化查询语言。查询简单、速度快 非结构化数据查询:从文本文件中找出包含某关键字的文字法一:使用程序将文档读取到内存中,然后匹配字符串,顺序扫描 法二:将非结构化数据转成结构化数据再查询根据空格进行字符串拆分,得到单词列表,基于单词列表创建一个索引(为了提高查询速率,创建某种数据结构的集合) 查询索引,根据单词和文档的对应关系,找到文档列表。 这个过程叫做全文检索 1.2 全文检索 全文检索:先创建索引,然后查询索引的过程。
索引一次创建可以多次使用,表现为每次查询速度都很快。
应用场景
搜索引擎:谷歌、百度 站内搜索:网站内的站内搜索 只要有搜索的地方,就可以使用全文检索技术。
1.3 Lucene Lucene是一个基于Java开发的全文检索工具包
Lucene实现全文检索的流程
展开
创建索引 步骤
获得文档原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档。比如搜索引擎的原始文档就是整个互联网的网页 获取数据方式:搜索引擎使用爬虫获得原始文档,站内搜索直接使用数据库中数据 构建文档对象:对应每个原始文档创建一个Document对象,每个Document对象中包含多个Field(域),每个Field又包含文件名称、文件路径 分析文档(以案例为例)根据空格进行字符串的拆分,得到一个单词列表。 把单词统一换成小写 去掉标点符号 去除停用词:无意义的词,比如and、the 每个关键词都封装到一个Term对象中,Term包含两部分内容关键词所在的域 关键词本身 不同的域中拆分出来的相同的关键词是不同的Term 创建索引基于关键词创建一个索引,保存到索引库 索引库中索引 document对象 关键词和文档对应关系 倒排索引结构在已经建立好的索引的基础上, 通过关键词找文档
展开
查询索引 步骤
用户查询接口,如百度的搜索框 把关键词封装成一个查询对象 执行查询根据要查询的关键词到对应的域上进行搜索 找到关键次,根据关键词找到对应文档 渲染结果 二、入门 2.1 创建索引库
展开
通过lucene查询出以上所有包含spring的文件。
搭建工程
创建一个Java工程 添加Jarlucene-analyzers-common.jar lucene-core.jar commons-io.jar 步骤
创建一个Directory对象,指定索引库保存的位置 基于Directory对象创建一个IndexWriter对象 读取磁盘上的文件,对应每个文件创建一个Document对象 向文档对象中添加域(文件名称、大小、路径、内容) 把文档对象写入索引库 关闭IndexWriter对象 执行下面脚本就能创建索引了。
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
public class LuceneFirst {
public void createIndex () throws Exception {
//1. 创建一个Directory对象,指定索引库保存的位置
//把索引库保存在内存中
//Directory directory = new RAMDirectory();
//把索引盘保存在磁盘中
Directory directory = FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ());
//2. 基于Directory对象创建一个IndexWriter对象
IndexWriter indexWriter = new IndexWriter ( directory , new IndexWriterConfig ());
//3. 读取磁盘上的文件,对应每个文件创建一个Document对象
File dir = new File ( "C:\\Users\\meethigher\\Desktop\\tempStorage" );
File [] files = dir . listFiles ();
for ( File x : files ) {
String fileName = x . getName ();
String filePath = x . getPath ();
String fileContent = FileUtils . readFileToString ( x , "utf-8" );
long fileSize = FileUtils . sizeOf ( x );
//创建field
/**
* 参数1:域名称
* 参数2:域内容
* 参数3:是否存储
*/
Field fieldName = new TextField ( "name" , fileName , Field . Store . YES );
Field fieldPath = new TextField ( "path" , filePath , Field . Store . YES );
Field fieldContent = new TextField ( "content" , fileContent , Field . Store . YES );
Field fieldSize = new TextField ( "size" , fileSize + "" , Field . Store . YES );
//创建文档对象
//4. 向文档对象中添加域(文件名称、大小、路径、内容)
Document document = new Document ();
document . add ( fieldName );
document . add ( fieldPath );
document . add ( fieldContent );
document . add ( fieldSize );
//5. 把文档对象写入索引库
indexWriter . addDocument ( document );
}
//6. 关闭IndexWriter对象
indexWriter . close ();
}
public static void main ( String [] args ) throws Exception {
new LuceneFirst (). createIndex ();
}
}
如图所示
展开
2.2 查看索引库 如果想要查看索引的话,需要用到luke,也就是lucene压缩包里面的一个工具jar包。
展开
查看索引
展开
翻页查看文档
展开
2.3 查询索引库 步骤
创建Directory对象,指定索引库的位置 创建一个IndexReader对象 创建一个IndexSearcher对象,构造方法中的参数就是IndexReader对象 创建一个Query对象,TermQuery,根据关键词查询 执行查询,得到查询结果TopDocs对象。里面包含查询结果总记录数、文档列表 取记录数 取文档列表 打印文档内容 关闭IndexReader对象 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
public class LuceneFirst {
public void searchIndex () throws Exception {
//1. 创建Directory对象,指定索引库的位置
Directory directory = FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ());
//2. 创建一个IndexReader对象
DirectoryReader reader = DirectoryReader . open ( directory );
//3. 创建一个IndexSearcher对象,构造方法中的参数就是IndexReader对象
IndexSearcher indexSearcher = new IndexSearcher ( reader );
//4. 创建一个Query对象,TermQuery,根据关键词查询。term第一个为域名称,第二个关键词
Query query = new TermQuery ( new Term ( "content" , "spring" ));
//5. 执行查询,得到查询结果TopDocs对象。里面包含查询结果总记录数、文档列表
//参数1是查询对象,参数2是查询结果返回的最大记录数
TopDocs topDocs = indexSearcher . search ( query , 10 );
//6. 取记录数
System . out . println ( "查询总记录数:" + topDocs . totalHits );
//7. 取文档列表
ScoreDoc [] scoreDocs = topDocs . scoreDocs ;
//8. 打印文档内容
for ( ScoreDoc scoreDoc : scoreDocs ) {
int id = scoreDoc . doc ;
//根据id取文档对象
Document doc = indexSearcher . doc ( id );
// System.out.println(doc.get("name")+":"+doc.get("content"));
System . out . println ( doc . get ( "name" ));
}
//9. 关闭IndexReader对象
reader . close ();
}
public static void main ( String [] args ) throws Exception {
// new LuceneFirst().createIndex();
new LuceneFirst (). searchIndex ();
}
}
三、分析器 3.1 标准分析器 默认使用的是标准分析器StandardAnalyzer
通过查看源码可知使用的是标准分析器
展开
查看分析器的分析效果
使用Analyzer对象的TokenStream方法返回一个TokenStream对象,词对象中包含了最终的分词结果
实现步骤
创建一个Analyzer对象,StandardAnalyzer对象 使用分析器对象的TokenStream方法获得一个TokenStream对象 向TokenStream对象中设置一个引用,相当于是一个指针 调用TokenStream对象的reset方法,如果不调用,就会抛异常 使用while循环遍历TokenStream对象 关闭TokenStream对象 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LuceneFirst {
public void testTokenStream () throws Exception {
//1. 创建一个Analyzer对象,StandardAnalyzer对象
Analyzer analyzer = new StandardAnalyzer ();
//2. 使用分析器对象的TokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer . tokenStream ( "" , "The Spring Framework provides a comprehensive programming and configuration model." );
//3. 向TokenStream对象中设置一个引用,相当于是一个指针
CharTermAttribute charTermAttribute = tokenStream . addAttribute ( CharTermAttribute . class );
//4. 调用TokenStream对象的reset方法,如果不调用,就会抛异常
tokenStream . reset ();
//5. 使用while循环遍历TokenStream对象
while ( tokenStream . incrementToken ()){
System . out . println ( charTermAttribute . toString ());
}
//6. 关闭TokenStream列表
tokenStream . close ();
}
public static void main ( String [] args ) throws Exception {
// new LuceneFirst().createIndex();
// new LuceneFirst().searchIndex();
new LuceneFirst (). testTokenStream ();
}
}
3.2 第三方分析器 像标准分析器如果用来处理中文内容,就会按照一个汉字来分割,就不能用词语来检索了。
处理中文内容可以使用IKAnalyzer工具包
添加IKAnalyzer的包 把配置文件和扩展词典添加到工程classpath下 注意
扩展词典不能使用windows记事本编辑,保证扩展词典编码格式是UTF-8
windows中记事本的utf-8默认是utf-8+bom格式的,非标准utf-8
扩展词典可以用于添加一些新词
停用词词典:无意义词或者敏感词
引入依赖
1
2
3
4
5
<dependency>
<groupId> com.jianggujin</groupId>
<artifactId> IKAnalyzer-lucene</artifactId>
<version> 8.0.0</version>
</dependency>
测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LuceneFirst {
public void testIKAnalyzer () throws Exception {
//1. 创建一个Analyzer对象,StandardAnalyzer对象
Analyzer analyzer = new IKAnalyzer ();
//2. 使用分析器对象的TokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer . tokenStream ( "" , "向TokenStream对象中政府邸设置一个引用,相当于是一个指针" );
//3. 向TokenStream对象中设置一个引用,相当于是一个指针
CharTermAttribute charTermAttribute = tokenStream . addAttribute ( CharTermAttribute . class );
//4. 调用TokenStream对象的reset方法,如果不调用,就会抛异常
tokenStream . reset ();
//5. 使用while循环遍历TokenStream对象
while ( tokenStream . incrementToken ()){
System . out . println ( charTermAttribute . toString ());
}
//6. 关闭TokenStream列表
tokenStream . close ();
}
public static void main ( String [] args ) throws Exception {
// new LuceneFirst().createIndex();
// new LuceneFirst().searchIndex();
// new LuceneFirst().testTokenStream();
new LuceneFirst (). testIKAnalyzer ();
}
}
四、索引库维护 4.1 常用Field使用 Field域的属性
是否分析:是否对域内容进行分析,也就是分词。比如身份证号,根本不需要分词 是否索引:将分析后的词进行索引,通过索引来查询 是否存储:将域存储在本地 Field类 数据类型 是否分词(分析) 是否索引 是否存储 StringField 字符串 否 是 自选 LongPoint Long 是 是 否 StoredField 支持多种类型 否 否 是 TextField 字符串或者流 是 是 自选
这个可以自己参照官网
展开
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
public class LuceneFirst {
public void createIndex () throws Exception {
Directory directory = FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ());
IndexWriter indexWriter = new IndexWriter ( directory , new IndexWriterConfig ( new IKAnalyzer ()));
File dir = new File ( "C:\\Users\\meethigher\\Desktop\\tempStorage" );
File [] files = dir . listFiles ();
for ( File x : files ) {
String fileName = x . getName ();
String filePath = x . getPath ();
String fileContent = FileUtils . readFileToString ( x , "utf-8" );
long fileSize = FileUtils . sizeOf ( x );
Field fieldName = new TextField ( "name" , fileName , Field . Store . YES );
Field fieldPath = new StoredField ( "path" , filePath );
Field fieldContent = new TextField ( "content" , fileContent , Field . Store . YES );
Field fieldSizeValue = new LongPoint ( "size" , fileSize );
//LongPoint是不存储的,所以如果存储要用StoredField。上面用来计算,下面用来存储
Field fieldSizeStore = new StoredField ( "size" , fileSize );
Document document = new Document ();
document . add ( fieldName );
document . add ( fieldPath );
document . add ( fieldContent );
document . add ( fieldSizeValue );
document . add ( fieldSizeStore );
indexWriter . addDocument ( document );
}
indexWriter . close ();
}
public static void main ( String [] args ) throws Exception {
new LuceneFirst (). createIndex ();
}
}
4.2 添加文档 执行下面的脚本,会将文档追加到索引库里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IndexManager {
public void addDoc () throws Exception {
IndexWriter indexWriter = new IndexWriter ( FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ()),
new IndexWriterConfig ( new IKAnalyzer ()));
Document doc = new Document ();
doc . add ( new TextField ( "name" , "周杰伦" , Field . Store . YES ));
//用string可以不用分词,分词根据词语来查,可能有的查不出来
doc . add ( new TextField ( "content" , "你三婶摸男人?" , Field . Store . NO ));
doc . add ( new StoredField ( "path" , "C:\\Users\\meethigher\\Desktop\\gg" ));
indexWriter . addDocument ( doc );
indexWriter . close ();
}
public static void main ( String [] args ) throws Exception {
new IndexManager (). addDoc ();
}
}
虽然没有保存content,但还是能查询得到的。
展开
4.3 删除文档 两种方式
删除所有 删除查询到的 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IndexManager {
public void deleteIndex () throws Exception {
IndexWriter indexWriter = new IndexWriter ( FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ()),
new IndexWriterConfig ( new IKAnalyzer ()));
indexWriter . deleteAll ();
indexWriter . close ();
}
public void deleteDocumentByQuery () throws Exception {
IndexWriter indexWriter = new IndexWriter ( FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ()),
new IndexWriterConfig ( new IKAnalyzer ()));
indexWriter . deleteDocuments ( new TermQuery ( new Term ( "content" , "三婶" )));
indexWriter . close ();
}
public static void main ( String [] args ) throws Exception {
new IndexManager (). deleteDocumentByQuery ();
}
}
4.4 更新文档 先查询再更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IndexManager {
public void updateDoc () throws Exception {
IndexWriter indexWriter = new IndexWriter ( FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ()),
new IndexWriterConfig ( new IKAnalyzer ()));
Document doc = new Document ();
doc . add ( new TextField ( "name" , "更新" , Field . Store . YES ));
doc . add ( new TextField ( "content" , "更新后内容" , Field . Store . YES ));
indexWriter . updateDocument ( new Term ( "name" , "周杰伦" ), doc );
indexWriter . close ();
}
public static void main ( String [] args ) throws Exception {
// new IndexManager().addDoc();
new IndexManager (). updateDoc ();
}
}
五、索引库的查询 查询方式
使用Query的子类TermQuery:根据关键词进行查询。需要指定要查询的域以及要查询的关键词 RangeQuery:范围查询 使用QueryParser:可以对要查询的内容,先进行分词,然后基于分词结果来进行查询。类似于搜索引擎的搜索 范围查询
比较数据用的LongPoint,读取数据是StoredField,即便没有StoredField也是可以查询出来的,只不过size的大小是null。参照4.1
未存储到索引库也是可以查询的,参照4.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SearchIndex {
public void rangeQuery () throws Exception {
Directory directory = FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ());
DirectoryReader reader = DirectoryReader . open ( directory );
IndexSearcher indexSearcher = new IndexSearcher ( reader );
//这个地方能比较,是因为使用了LongPoint来存储了。即便没有保存,也是可以查询的,只不过没有数值而已
Query query = LongPoint . newRangeQuery ( "size" , 0L , 100L );
TopDocs search = indexSearcher . search ( query , 10 );
System . out . println ( "总条数:" + search . totalHits );
ScoreDoc [] scoreDocs = search . scoreDocs ;
for ( ScoreDoc sd : scoreDocs ) {
int doc = sd . doc ;
Document document = indexSearcher . doc ( doc );
System . out . println ( document . get ( "name" ));
System . out . println ( document . get ( "content" ));
//这个地方能查出来,是因为使用了StoredField存储
System . out . println ( document . get ( "size" ));
}
}
public static void main ( String [] args ) throws Exception {
new SearchIndex (). rangeQuery ();
}
}
QueryParser查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SearchIndex {
public void queryParser () throws Exception {
Directory directory = FSDirectory . open ( new File ( "C:\\Users\\meethigher\\Desktop\\gg" ). toPath ());
DirectoryReader reader = DirectoryReader . open ( directory );
IndexSearcher indexSearcher = new IndexSearcher ( reader );
//参数1:默认搜索域,参数2:分析器对象
//将name域下的内容进行分词,然后与查询内容进行匹配
QueryParser queryParser = new QueryParser ( "name" , new IKAnalyzer ());
Query query = queryParser . parse ( "周杰" );
TopDocs search = indexSearcher . search ( query , 10 );
System . out . println ( "总条数:" + search . totalHits );
ScoreDoc [] scoreDocs = search . scoreDocs ;
for ( ScoreDoc sd : scoreDocs ) {
int doc = sd . doc ;
Document document = indexSearcher . doc ( doc );
System . out . println ( document . get ( "name" ));
System . out . println ( document . get ( "content" ));
System . out . println ( document . get ( "size" ));
}
}
public static void main ( String [] args ) throws Exception {
new SearchIndex (). queryParser ();
}
}
最后放上最终的pom
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns= "http://maven.apache.org/POM/4.0.0"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion> 4.0.0</modelVersion>
<groupId> top.meethigher</groupId>
<artifactId> lucene-notes</artifactId>
<version> 1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
<groupId> org.apache.lucene</groupId>
<artifactId> lucene-core</artifactId>
<version> 8.8.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
<dependency>
<groupId> org.apache.lucene</groupId>
<artifactId> lucene-analyzers-common</artifactId>
<version> 8.8.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId> commons-io</groupId>
<artifactId> commons-io</artifactId>
<version> 2.8.0</version>
</dependency>
<dependency>
<groupId> junit</groupId>
<artifactId> junit</artifactId>
<version> 4.13</version>
<scope> test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.jianggujin/IKAnalyzer-lucene -->
<dependency>
<groupId> com.jianggujin</groupId>
<artifactId> IKAnalyzer-lucene</artifactId>
<version> 8.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
<dependency>
<groupId> org.apache.lucene</groupId>
<artifactId> lucene-queryparser</artifactId>
<version> 8.8.2</version>
</dependency>
</dependencies>
</project>