摘要

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

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

查询索引

步骤

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

二、入门

2.1 创建索引库

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

如图所示

2.2 查看索引库

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

查看索引

翻页查看文档

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

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

查看分析器的分析效果

使用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字符串或者流自选

这个可以自己参照官网

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

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>