摘要

Lucene是一个基于Java开发的全文检索工具包,将非结构化的数据建立索引库,通过查询索引,查询文档内容。由此将非结构化数据转换成了结构化数据

正文

Lucene8.8.2官方文档

一、概念

1.1 基础

数据分类

  1. 结构化数据:格式固定、长度固定、有一定格式、数据类型固定。例如数据库中的数据
  2. 非结构化数据:格式不固定、长度不固定、数据类型不固定,例如word文档、pdf文档、邮件、html、txt,格式不固定

数据的查询

  1. 结构化数据查询:SQL语句,Structured Query Language 结构化查询语言。查询简单、速度快
  2. 非结构化数据查询:从文本文件中找出包含某关键字的文字
    • 法一:使用程序将文档读取到内存中,然后匹配字符串,顺序扫描
    • 法二:将非结构化数据转成结构化数据再查询
      1. 根据空格进行字符串拆分,得到单词列表,基于单词列表创建一个索引(为了提高查询速率,创建某种数据结构的集合)
      2. 查询索引,根据单词和文档的对应关系,找到文档列表。
      3. 这个过程叫做全文检索

1.2 全文检索

全文检索:先创建索引,然后查询索引的过程。

索引一次创建可以多次使用,表现为每次查询速度都很快。

应用场景

  1. 搜索引擎:谷歌、百度
  2. 站内搜索:网站内的站内搜索

只要有搜索的地方,就可以使用全文检索技术。

1.3 Lucene

Lucene是一个基于Java开发的全文检索工具包

Lucene实现全文检索的流程

1.png

创建索引

步骤

  1. 获得文档
    • 原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档。比如搜索引擎的原始文档就是整个互联网的网页
    • 获取数据方式:搜索引擎使用爬虫获得原始文档,站内搜索直接使用数据库中数据
  2. 构建文档对象:对应每个原始文档创建一个Document对象,每个Document对象中包含多个Field(域),每个Field又包含文件名称、文件路径
  3. 分析文档(以案例为例)
    • 根据空格进行字符串的拆分,得到一个单词列表。
    • 把单词统一换成小写
    • 去掉标点符号
    • 去除停用词:无意义的词,比如and、the
    • 每个关键词都封装到一个Term对象中,Term包含两部分内容
      1. 关键词所在的域
      2. 关键词本身
    • 不同的域中拆分出来的相同的关键词是不同的Term
  4. 创建索引
    • 基于关键词创建一个索引,保存到索引库
    • 索引库中
      1. 索引
      2. document对象
      3. 关键词和文档对应关系

倒排索引结构在已经建立好的索引的基础上, 通过关键词找文档

2.png

查询索引

步骤

  1. 用户查询接口,如百度的搜索框
  2. 把关键词封装成一个查询对象
    • 要查询的域
    • 要搜索的关键词
  3. 执行查询
    • 根据要查询的关键词到对应的域上进行搜索
    • 找到关键次,根据关键词找到对应文档
  4. 渲染结果
    • 关键词高亮显示
    • 分页显示

二、入门

2.1 创建索引库

3.png

通过lucene查询出以上所有包含spring的文件。

搭建工程

  1. 创建一个Java工程
  2. 添加Jar
    • lucene-analyzers-common.jar
    • lucene-core.jar
    • commons-io.jar

步骤

  1. 创建一个Directory对象,指定索引库保存的位置
  2. 基于Directory对象创建一个IndexWriter对象
  3. 读取磁盘上的文件,对应每个文件创建一个Document对象
  4. 向文档对象中添加域(文件名称、大小、路径、内容)
  5. 把文档对象写入索引库
  6. 关闭IndexWriter对象

执行下面脚本就能创建索引了。

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
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();
    }
}

如图所示

4.png

2.2 查看索引库

如果想要查看索引的话,需要用到luke,也就是lucene压缩包里面的一个工具jar包。

5.png

查看索引

7.png

翻页查看文档

6.png

2.3 查询索引库

步骤

  1. 创建Directory对象,指定索引库的位置
  2. 创建一个IndexReader对象
  3. 创建一个IndexSearcher对象,构造方法中的参数就是IndexReader对象
  4. 创建一个Query对象,TermQuery,根据关键词查询
  5. 执行查询,得到查询结果TopDocs对象。里面包含查询结果总记录数、文档列表
  6. 取记录数
  7. 取文档列表
  8. 打印文档内容
  9. 关闭IndexReader对象
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
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

通过查看源码可知使用的是标准分析器

8.png

查看分析器的分析效果

使用Analyzer对象的TokenStream方法返回一个TokenStream对象,词对象中包含了最终的分词结果

实现步骤

  1. 创建一个Analyzer对象,StandardAnalyzer对象
  2. 使用分析器对象的TokenStream方法获得一个TokenStream对象
  3. 向TokenStream对象中设置一个引用,相当于是一个指针
  4. 调用TokenStream对象的reset方法,如果不调用,就会抛异常
  5. 使用while循环遍历TokenStream对象
  6. 关闭TokenStream对象
java
 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工具包

  1. 添加IKAnalyzer的包
  2. 把配置文件和扩展词典添加到工程classpath下

注意

扩展词典不能使用windows记事本编辑,保证扩展词典编码格式是UTF-8

windows中记事本的utf-8默认是utf-8+bom格式的,非标准utf-8

扩展词典可以用于添加一些新词

停用词词典:无意义词或者敏感词

引入依赖

xml
1
2
3
4
5
<dependency>
    <groupId>com.jianggujin</groupId>
    <artifactId>IKAnalyzer-lucene</artifactId>
    <version>8.0.0</version>
</dependency>

测试类

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
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域的属性

  1. 是否分析:是否对域内容进行分析,也就是分词。比如身份证号,根本不需要分词
  2. 是否索引:将分析后的词进行索引,通过索引来查询
  3. 是否存储:将域存储在本地
Field类数据类型是否分词(分析)是否索引是否存储
StringField字符串自选
LongPointLong
StoredField支持多种类型
TextField字符串或者流自选

这个可以自己参照官网

9.png

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
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 添加文档

执行下面的脚本,会将文档追加到索引库里

java
 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,但还是能查询得到的。

10.png

4.3 删除文档

两种方式

  1. 删除所有
  2. 删除查询到的
java
 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 更新文档

先查询再更新

java
 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();
    }
}

五、索引库的查询

查询方式

  1. 使用Query的子类
    • TermQuery:根据关键词进行查询。需要指定要查询的域以及要查询的关键词
    • RangeQuery:范围查询
  2. 使用QueryParser:可以对要查询的内容,先进行分词,然后基于分词结果来进行查询。类似于搜索引擎的搜索
    • 需要添加Jar包:queryparser

范围查询

比较数据用的LongPoint,读取数据是StoredField,即便没有StoredField也是可以查询出来的,只不过size的大小是null。参照4.1

未存储到索引库也是可以查询的,参照4.2

java
 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查询

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
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

xml
 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>