本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名原文链接~~~

全文检索工具包Lucene

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:


什么是全文检索

数据的分类

结构化数据:指的是格式固定、长度固定、数据类型固定的数据,例如数据库中的数据。

非结构化数据:指的是格式不固定、长度不固定、数据类型不固定的数据,例如 word 文档、pdf 文档、邮件、html。

数据的查询

结构化数据的查询:像数据库中的数据我们可以通过 SQL 语句来进行查询,简单且速度快。

非结构化数据的查询:以“从多个文本文件中查询出包含 spring 单词的文件”为例,我们可以通过一个一个打开通过目测浏览文本内容进行查找,也可以通过将文档读到内存中,依次匹配字符串进行查找,再或者可以将非结构化数据转换为结构化数据,如将其保存到数据库中。

全文检索

先创建索引,然后通过索引查找的过程就叫做全文检索。

全文检索的应用场景

  • 搜索引擎,如百度、360 搜索、google、搜狗等。
  • 站内搜索,如论坛搜索年、微博、文章搜索等。
  • 电商搜索,如淘宝商品搜索、京东商品搜索。

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

Lucene 介绍

简述

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

实现全文检索的流程

image.png

  • 左方框中表示创建索引的过程,对要搜索的原始内容进行索引构建一个索引库。
  • 右边框中表示搜索的过程,从索引库中搜索内容。

创建索引

  1. 获得文档。
    原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档。
    搜索引擎:使用爬虫获得原始文档。
    站内搜索:数据库中的数据。
    案例:直接使用 IO 流读取磁盘上的文件。

  2. 构建文档对象。
    对应的每个原始文档就是一个 Document 对象,每个 Document 对象包含多个域(Field),域中保存的就是原始文档数据(域的名称及域的值),每个文档对象都有一个唯一的编号,就是文档 id。

  3. 分析文档。
    就是分词的过程。如:
    a) 根据空格对字符串进行拆分,得到一个单词列表。
    b) 把单词统一转换成小写。
    c) 去除标点符号。
    d) 去除停用词(无意义的词)。
    e) 最后将拆分出来的每一个关键词封装成一个 Term 对象(Term 中包含两部分内容,一是关键词所在的域,二是关键词本身,不同的域中拆分出来的相同的关键词是不同的 Term)。

  4. 创建索引。
    基于关键词列表创建一个索引保存到索引库中,索引库中包含了索引、Document 对象、关键词和文档的对应关系。

查询索引

  1. 用户查询接口。
    其实就是用户输入查询条件的地方,如百度的搜索框。

  2. 把关键词封装为一个对象,对象中包含了要查询的域和要搜索的关键词。

  3. 查询索引。
    根据查询的关键词对象到对应的域上搜索,找到关键词,即也能通过关键词与文档的对应关系找到对应的文档。

  4. 渲染结果。
    根据文档的 id 找到文档对象,对关键词进行高亮显示、分页处理等操作,最终显示给用户。

Lucene 的使用

准备

1、官网下载 Lucene:http://lucene.apache.org。

2、最低要求 jdk1.8。

3、依赖 jar 如下:

image.png

4、在 E:/temp/searchsouce 有如下测试文件:

# document1.txt
do you like spring
# show.txt
do you like show you
# test2.txt
do you like spring  mybatis
# wna.txt
do you like spring  mybatisdeawdffeef  show eyour resource

入门程序

创建索引库

import org.apache.commons.io.FileUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
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 org.junit.Test;

import java.io.File;

public class LuceneFirst {
    @Test
    public void createIndex() throws Exception{
        //1、创建一个 Directory 对象,指定索引库保存的位置。
        // 把索引库保存在内存中
        // Directory directory = new RAMDirectory();
        // 把索引库保存在磁盘中
        Directory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        //2、基于 Directory 对象创建一个 IndexWriter 对象。
        IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
        //3、读取磁盘上的文件,对应每个文件来创建一个文档(Document)对象。
        File dir = new File("E:/temp/searchsouce");
        File[] files = dir.listFiles();
        for (File file : files) {
            // 取文件名
            String fileName = file.getName();
            // 文件路径
            String filePath = file.getPath();
            // 文件内容
            String fileContent = FileUtils.readFileToString(file, "utf-8");
            // 文件大小
            long fileSize = FileUtils.sizeOf(file);
            // 创建 field
            // 参数1:域名称 参数2:域的内容 参数3:是否保存
            Field nameField = new TextField("name", fileName, Field.Store.YES);
            Field pathField = new TextField("path", filePath, Field.Store.YES);
            Field contentField = new TextField("content", fileContent, Field.Store.YES);
            Field sizeField = new TextField("size", fileSize + "", Field.Store.YES);
            // 创建文档对象
            Document document = new Document();
            //4、向文档对象中添加域(Field)。
            document.add(nameField);
            document.add(pathField);
            document.add(contentField);
            document.add(sizeField);
            //5、把文档对象写入索引库。
            indexWriter.addDocument(document);
        }
        //6、关闭 IndexWriter 对象。
        indexWriter.close();
    }
}

执行完上述单元测试后,在 E:/temp/index 下就会创建如下索引文件:

image.png

查询索引库

import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;

import java.io.File;

public class LuceneFirst {
    @Test
    public void searchIndex() throws Exception{
        // 1、创建一个 Directory 对象,指定索引库位置。
        FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        // 2、创建一个 IndexReader 对象。
        IndexReader indexReader = DirectoryReader.open(directory);
        // 3、创建一个 IndexSearcher 对象,构造参数为 IndexReader 对象。
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        // 4、创建一个 Query 对象(TermQuery),在 content 域中查询 spring 关键词。
        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;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文档 id
            int id = scoreDoc.doc;
            // 根据 id 取文档对象
            Document document = indexSearcher.doc(id);
            // 8、打印文档的内容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 9、关闭 IndexReader 对象
        indexReader.close();
    }
}

控制台输出:

image.png

查看索引库内容

如果我们想要查看创建好的索引库内容,可以通过一个工具——luke 查看,下载地址 https://github.com/DmitryKey/luke/releases。

下载解压后通过 luke.bat 批处理文件打开如下可视化界面:

image.png

分析器

在创建索引的代码中创建 IndexWriter 对象时传入的了 IndexWriterConfig 对象:

new IndexWriter(directory, new IndexWriterConfig());

在该对象中其实就创建了默认的分析器,看它的构造方法:

public IndexWriterConfig() {
    this(new StandardAnalyzer());
}

即默认的分析器就是 StandardAnalyzer

默认标准分析器

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

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.junit.Test;

public class AnalyzerTest {
    @Test
    public void test() throws Exception {
        // 1、创建一个 Analyzer 对象(StandardAnalyzer)。
        Analyzer analyzer = new StandardAnalyzer();
        // 2、使用分析器对象的 tokenStream() 方法得到一个 TokenStream 对象。
        TokenStream tokenStream = analyzer.tokenStream("", "do you like spring mybatis");
        // 3、向 TokenStream 对象中设置一个引用,相当于一个指针。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 4、调用 TokenStream 对象的 reset() ,如果不调用则会抛异常。
        tokenStream.reset();
        // 5、遍历 TokenStream 对象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 6、关闭 TokenStream 对象。
        tokenStream.close();
    }
}

执行上述单元测试控制台输出如下:

image.png

即默认的 StandardAnalyzer 分析器是按空格分词,最终得到的关键词列表就是分析结果。(它是不支持中文的,当要分析的内容包含中文时,会将每一个中文字符当做一个关键词。)

IK 中文分析器

我们已经知道默认的分析器是不支持中文的,所以我们可以使用第三方提供的支持中文分析的分析器工具——IK中文分析器,点击下载(提取码:t9yg)。

在工程中加入下载下来的 jar 包,在 classpath 下添加相应配置文件:

<!-- IKAnalyzer.cfg.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
<properties>  
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <!--<entry key="ext_dict">ext.dic;</entry>-->

    <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">stopword.dic;</entry> 
</properties>
# stopword.dic
a
an
and
are
as
at
be
but
by
for
if
in
into
is
it
no
not
of
on
or
such
that
the
their
then
there
these
they
this
to
was
will
with

然后只需要将原来的分析器实例( StandardAnalyzer )替换为用 IK 分析器( IKAnalyzer )的实例即可,如下:

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

public class IKAnalyzerTest {
    @Test
    public void test() throws Exception{
        // 1、创建一个 Analyzer 对象(StandardAnalyzer)。
        Analyzer analyzer = new IKAnalyzer();
        // 2、使用分析器对象的 tokenStream() 方法得到一个 TokenStream 对象。
        TokenStream tokenStream = analyzer.tokenStream("", "小张今天很开心");
        // 3、向 TokenStream 对象中设置一个引用,相当于一个指针。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 4、调用 TokenStream 对象的 reset() ,如果不调用则会抛异常。
        tokenStream.reset();
        // 5、遍历 TokenStream 对象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 6、关闭 TokenStream 对象。
        tokenStream.close();
    }
}

执行上述单元测试,控制台输出结果如下:

image.png

回头看一下 IKAnalyzer.cfg.xml 配置文件,在其中配置了两个 entry 节点,它们分别有不同的 key 属性值。

key 名称为 ext_dict 对应的节点用来指定一个扩展词文件路径,该扩展词文件中可以定义一些我们自己希望识别为关键词的词语,以换行隔开即可;

key 名称为 ext_stopwords 对应的节点用来指定一个停用词文件路径,该停用词文件中可以定义一些我们指定的无意义词语。

如果想要在创建索引库的时候使用 IKAnalyzer 进行分词,那么只需要在创建 IndexWriterConfig 实例时指定其构造参数为 IKAnalyzer 的实例即可,如:

IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new IKAnalyzer()));

索引库的维护

Field 域的属性

Field 域一般有三个属性:

  • 是否分析:是否对该域的内容进行分词处理,前提是我们要对该域的内容进行查询。
  • 是否索引:将 Field 分析后的词或整个 Field 值进行索引,只有索引后的 Filed 内容才能被搜索到。
    如:商品名称、商品简介需分析后进行索引,而订单号和身份证号不用分析但也需要索引,因为它们都有可能作为查询条件。
  • 是否存储:将 Field 值存储在文档中,存储在文档中的 Field 才可以从 Document 中获取。
    如商品名称、订单号等凡是将来要从 Document 中获取的 Field 都要存储。

Field 域的常用实现有如下几种:

Field存储数据类型是否分析(Analyzed)是否索引(Indexed)是否存储(Stored)说明
StringField(FieldName,FiledValue,Store)字符串类型NYY或N这个 Field 用来构建一个字符串 Field,但是不会进行分析,会将整个串直接索引,通常用来存储如身份证号、姓名等字段内容,是否存储通过第三个参数指定 Store.YES 或 Store.NO 决定。
LongPoint(FieldName,FieldValue)Long 类型YYN可以使用 LongPoint、IntPoint 等类型存储数值类型的数据来让数值类型进行索引,但仅使用它是不能存储数据的,要同时存储数据需要搭配 StoredField 一起使用。
StoredField(FieldName,FieldValue)重载构造方法,支持多种类型。NNY这个 Field 用来构建不同类型的 Field,不分析、不索引,但会让 Field 存储在文档中。
TextField(FieldName,FieldValue,Store)字符串或流YYY或N如果是一个 Reader,Lucene 会猜测内容比较多,采用 UnStored 即不存储的策略。

文档管理

文档管理指的就是针对索引库中文档的 CRUD 操作,基本的操作方式大致如下:

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.File;

public class DocumentManagerTest {
    IndexWriter indexWriter;

    @Before
    public void init() throws Exception {
        // 创建一个 IndexWriter 对象,使用 IKAnalyzer 作为分析器
        indexWriter = new IndexWriter(FSDirectory.open(new File("E:/temp/index").toPath()), new IndexWriterConfig(new IKAnalyzer()));
    }

    /**
     * 添加文档到索引库
     */
    @Test
    public void addDocument() throws Exception {
        // 创建一个 Document 对象
        Document document = new Document();
        // 向 Document 对象中添加域
        document.add(new TextField("name", "新添加的文件名", Field.Store.YES));
        document.add(new TextField("content", "新添加的文件内容", Field.Store.YES));
        document.add(new StoredField("path", "E:/temp/searchsource/test1.txt"));
        // 把文档写入索引库
        indexWriter.addDocument(document);
        // 释放 IndexWriter
        indexWriter.close();
    }

    /**
     * 删除全部文档
     */
    @Test
    public void deleteAllDocument() throws Exception {
        // 删除全部文档
        indexWriter.deleteAll();
        // 释放 IndexWriter
        indexWriter.close();
    }

    /**
     * 根据查询对象删除指定文档
     */
    @Test
    public void deleteDocumentByQuery() throws Exception {
        // 删除 content 域中包含关键词 Spring 的文档
        indexWriter.deleteDocuments(new Term("content", "Spring"));
        // 释放 IndexWriter
        indexWriter.close();
    }

    /**
     * 更新文档 实际上就是先删除再新增
     */
    @Test
    public void updateDocument() throws Exception{
        // 创建一个新的文档
        Document document = new Document();
        // 向 Document 对象中添加域
        document.add(new TextField("name", "要更新的文件名", Field.Store.YES));
        document.add(new TextField("content", "要更新的文件内容", Field.Store.YES));
        document.add(new StoredField("path", "E:/temp/searchsource/test2.txt"));
        // 更新操作 实际上是先删除 name 域中包含关键词 Spring 的文档,再添加 document 文档到索引库
        indexWriter.updateDocument(new Term("name","Spring"),document);
        // 释放 IndexWriter
        indexWriter.close();
    }
}

索引库的查询

索引库的常用查询方式有如下几种:

1、使用 Query 的子类:

  • TermQuery ,根据关键词进行查询,需指定要查询的域及要查询的关键词,在上面入门程序的查询索引库中已经使用过了就不再赘述。
  • RangeQuery ,针对数值类型的域进行范围查询。

例:

import org.apache.lucene.document.Document;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;

import java.io.File;

public class RangeQueryTest {

    IndexSearcher indexSearcher;
    IndexReader indexReader;

    @Before
    public void init() throws Exception {
        // 1、创建一个 Directory 对象,指定索引库位置。
        FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        // 2、创建一个 IndexReader 对象。
        indexReader = DirectoryReader.open(directory);
        // 3、创建一个 IndexSearcher 对象,构造参数为 IndexReader 对象。
        indexSearcher = new IndexSearcher(indexReader);
    }

    @Test
    public void test() throws Exception {
        // 4、创建查询对象
        // 参数1:要查询的域名称 参数 2:查询内容值的最小值 3:查询内容值的最大值
        Query query = LongPoint.newRangeQuery("size", 1, 1000);
        // 5、执行查询,得到一个 TopDocs 对象。
        // 参数1:查询对象 参数2:返回的记录最大条数
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 6、取查询结果的总记录数。
        System.out.println(topDocs.totalHits);
        // 7、取文档列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文档 id
            int id = scoreDoc.doc;
            // 根据 id 取文档对象
            Document document = indexSearcher.doc(id);
            // 8、打印文档的内容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 9、关闭 IndexReader 对象
        indexReader.close();
    }
}

2、使用 QueryParser 进行查询:

使用 QueryParser 实际上是对要查询的内容先分词,然后基于分词的结果进行查询。要使用它需要再添加如下 jar 包:

lucene-queryparser-8.1.1.jar
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.File;

public class QueryParserTest {
    IndexReader indexReader;
    IndexSearcher indexSearcher;

    @Before
    public void init() throws Exception {
        // 1、创建一个 Directory 对象,指定索引库位置。
        FSDirectory directory = FSDirectory.open(new File("E:\\temp\\index").toPath());
        // 2、创建一个 IndexReader 对象。
        indexReader = DirectoryReader.open(directory);
        // 3、创建一个 IndexSearcher 对象,构造参数为 IndexReader 对象。
        indexSearcher = new IndexSearcher(indexReader);
    }

    @Test
    public void test() throws Exception {
        // 创建一个 QueryParser 对象,参数1:要查询的域名称 参数2:要使用的分析器对象
        QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
        // 使用 QueryParser 对象创建一个 Query 对象
        Query query = queryParser.parse("spring is the season after winter and before summer");
        // 执行查询
        // 5、执行查询,得到一个 TopDocs 对象。
        // 参数1:查询对象 参数2:返回的记录最大条数
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 6、取查询结果的总记录数。
        System.out.println(topDocs.totalHits);
        // 7、取文档列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文档 id
            int id = scoreDoc.doc;
            // 根据 id 取文档对象
            Document document = indexSearcher.doc(id);
            // 8、打印文档的内容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 9、关闭 IndexReader 对象
        indexReader.close();
    }
}
# Java 杂项  

如果这篇文章对您有帮助,可点击下方链接分享给你的朋友们😋,如果遇到问题欢迎评论、留言~~~😇

评论

公众号:zze_coding

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×