全文检索工具包Lucene

什么是全文检索

数据的分类

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

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

数据的查询

结构化数据的查询:像数据库中的数据咱们能够经过 SQL 语句来进行查询,简单且速度快。git

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

全文检索

先建立索引,而后经过索引查找的过程就叫作全文检索。spring

全文检索的应用场景

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

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

Lucene介绍

简述

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

实现全文检索的流程

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

建立索引

  1. 得到文档。

    原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档。mybatis

    搜索引擎:使用爬虫得到原始文档。ide

    站内搜索:数据库中的数据。工具

    案例:直接使用 IO 流读取磁盘上的文件。

  2. 构建文档对象。

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

  3. 分析文档。

    就是分词的过程。如:

    a) 根据空格对字符串进行拆分,获得一个单词列表。

    b) 把单词统一转换成小写。

    c) 去除标点符号。

    d) 去除停用词(无心义的词)。

    e) 最后将拆分出来的每个关键词封装成一个 Term 对象(Term 中包含两部份内容,一是关键词所在的域,二是关键词自己,不一样的域中拆分出来的相同的关键词是不一样的 Term)。

  4. 建立索引。

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

查询索引

  1. 用户查询接口。

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

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

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

  4. 渲染结果。

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

Lucene的使用

准备

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

二、最低要求 jdk1.8。

三、依赖 jar 以下:

四、在 E:/temp/searchsouce 有以下测试文件:

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

入门程序

建立索引库

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{
        //一、建立一个 Directory 对象,指定索引库保存的位置。
        // 把索引库保存在内存中
        // Directory directory = new RAMDirectory();
        // 把索引库保存在磁盘中
        Directory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        //二、基于 Directory 对象建立一个 IndexWriter 对象。
        IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
        //三、读取磁盘上的文件,对应每一个文件来建立一个文档(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();
            //四、向文档对象中添加域(Field)。
            document.add(nameField);
            document.add(pathField);
            document.add(contentField);
            document.add(sizeField);
            //五、把文档对象写入索引库。
            indexWriter.addDocument(document);
        }
        //六、关闭 IndexWriter 对象。
        indexWriter.close();
    }
}

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

查询索引库

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{
        // 一、建立一个 Directory 对象,指定索引库位置。
        FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        // 二、建立一个 IndexReader 对象。
        IndexReader indexReader = DirectoryReader.open(directory);
        // 三、建立一个 IndexSearcher 对象,构造参数为 IndexReader 对象。
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        // 四、建立一个 Query 对象(TermQuery),在 content 域中查询 spring 关键词。
        Query query = new TermQuery(new Term("content", "spring"));
        // 五、执行查询,获得一个 TopDocs 对象。
        // 参数1:查询对象 参数2:返回的记录最大条数
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 六、取查询结果的总记录数。
        System.out.println(topDocs.totalHits);
        // 七、取文档列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文档 id
            int id = scoreDoc.doc;
            // 根据 id 取文档对象
            Document document = indexSearcher.doc(id);
            // 八、打印文档的内容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 九、关闭 IndexReader 对象
        indexReader.close();
    }
}

控制台输出:

查看索引库内容

若是咱们想要查看建立好的索引库内容,能够经过一个工具——luke 查看,下载地址 https://github.com/DmitryKey/luke/releases

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

分析器

在建立索引的代码中建立 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 {
        // 一、建立一个 Analyzer 对象(StandardAnalyzer)。
        Analyzer analyzer = new StandardAnalyzer();
        // 二、使用分析器对象的 tokenStream() 方法获得一个 TokenStream 对象。
        TokenStream tokenStream = analyzer.tokenStream("", "do you like spring mybatis");
        // 三、向 TokenStream 对象中设置一个引用,至关于一个指针。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 四、调用 TokenStream 对象的 reset() ,若是不调用则会抛异常。
        tokenStream.reset();
        // 五、遍历 TokenStream 对象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 六、关闭 TokenStream 对象。
        tokenStream.close();
    }
}

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

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

IK中文分析器

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

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

<?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>
IKAnalyzer.cfg.xml
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
stopword.dic

而后只须要将原来的分析器实例( 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{
        // 一、建立一个 Analyzer 对象(StandardAnalyzer)。
        Analyzer analyzer = new IKAnalyzer();
        // 二、使用分析器对象的 tokenStream() 方法获得一个 TokenStream 对象。
        TokenStream tokenStream = analyzer.tokenStream("", "小张今天很开心");
        // 三、向 TokenStream 对象中设置一个引用,至关于一个指针。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 四、调用 TokenStream 对象的 reset() ,若是不调用则会抛异常。
        tokenStream.reset();
        // 五、遍历 TokenStream 对象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 六、关闭 TokenStream 对象。
        tokenStream.close();
    }
}

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

回头看一下 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) 字符串类型 N Y Y或N 这个 Field 用来构建一个字符串 Field,可是不会进行分析,会将整个串直接索引,一般用来存储如身份证号、姓名等字段内容,是否存储经过第三个参数指定 Store.YES 或 Store.NO 决定。
LongPoint(FieldName,FieldValue) Long 类型 Y Y N 可使用 LongPoint、IntPoint 等类型存储数值类型的数据来让数值类型进行索引,但仅使用它是不能存储数据的,要同时存储数据须要搭配 StoredField 一块儿使用。
StoredField(FieldName,FieldValue) 重载构造方法,支持多种类型。 N N Y 这个 Field 用来构建不一样类型的 Field,不分析、不索引,但会让 Field 存储在文档中。
TextField(FieldName,FieldValue,Store) 字符串或流 Y Y Y或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();
    }
}

索引库的查询

索引库的经常使用查询方式有以下几种:

一、使用 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 {
            // 一、建立一个 Directory 对象,指定索引库位置。
            FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
            // 二、建立一个 IndexReader 对象。
            indexReader = DirectoryReader.open(directory);
            // 三、建立一个 IndexSearcher 对象,构造参数为 IndexReader 对象。
            indexSearcher = new IndexSearcher(indexReader);
        }
    
        @Test
        public void test() throws Exception {
            // 四、建立查询对象
            // 参数1:要查询的域名称 参数 2:查询内容值的最小值 3:查询内容值的最大值
            Query query = LongPoint.newRangeQuery("size", 1, 1000);
            // 五、执行查询,获得一个 TopDocs 对象。
            // 参数1:查询对象 参数2:返回的记录最大条数
            TopDocs topDocs = indexSearcher.search(query, 10);
            // 六、取查询结果的总记录数。
            System.out.println(topDocs.totalHits);
            // 七、取文档列表。
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
                // 取文档 id
                int id = scoreDoc.doc;
                // 根据 id 取文档对象
                Document document = indexSearcher.doc(id);
                // 八、打印文档的内容。
                System.out.println(document.get("name"));
                System.out.println(document.get("path"));
                System.out.println(document.get("content"));
                System.out.println(document.get("size"));
            }
            // 九、关闭 IndexReader 对象
            indexReader.close();
        }
    }
    例:

二、使用 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 {
        // 一、建立一个 Directory 对象,指定索引库位置。
        FSDirectory directory = FSDirectory.open(new File("E:\\temp\\index").toPath());
        // 二、建立一个 IndexReader 对象。
        indexReader = DirectoryReader.open(directory);
        // 三、建立一个 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");
        // 执行查询
        // 五、执行查询,获得一个 TopDocs 对象。
        // 参数1:查询对象 参数2:返回的记录最大条数
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 六、取查询结果的总记录数。
        System.out.println(topDocs.totalHits);
        // 七、取文档列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文档 id
            int id = scoreDoc.doc;
            // 根据 id 取文档对象
            Document document = indexSearcher.doc(id);
            // 八、打印文档的内容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 九、关闭 IndexReader 对象
        indexReader.close();
    }
}
例:
相关文章
相关标签/搜索