Lucene 实现全文检索

前言:本文是衔接上一篇文章来写的,详情看上一篇 【Lucene】全文检索技术介绍

2. Lucene 实现全文检索的流程

2.1.索引和搜索流程图

索引适合加在长文本上吗 索引内容_Lucene

  • 绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容 -> 采集文档 -> 创建文档 -> 分析文档 ->索引文档
  • 红色表示搜索过程,从索引库中搜索内容,搜索过程包括:用户通过搜索界面 -> 创建查询 -> 执行搜索,从索引库搜索 -> 渲染搜索结果

2.2.创建索引

步骤:

  • 获得文档
  • 原始文档:要基于那些数据来进行搜索,那么这些数据就是原始文档。
  • 搜索引擎:使用爬虫获得原始文档
  • 站内搜索:数据库中的数据。
  • 案例:直接使用 io 流读取磁盘上的文件。
  • 构建文档对象
  • 对应每个原始文档创建一个 Document 对象
  • 每个 document 对象中包含多个域(field)
  • 域中保存的就是原始文档的数据
  • 域的名称、域的值
  • 每个文档都有一个唯一的编号,就是文档id。

注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)

  • 分析文档
  • 就是分词的过程
  • 根据空格进行字符串拆分,得到一个单词列表
  • 把单词统一转换成小写。
  • 去除标点符号。
  • 去除停用词(无意义的词)
  • 每个关键词都封装成一个 Term 对象
  • Term 中包含两部分内容:
  • 关键词所在的域
  • 关键词本身
  • 不同的域中拆分出来的相同的关键词是不同的 Term 。
  • 创建索引
  • 基于关键词列表创建一个索引。保存到索引库中。
  • 索引库中:
  • 索引
  • document 对象
  • 关键词和文档的对应关系
  • 通过词语找文档,这种索引的结构叫倒排索引结构。如下图:

    倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。

2.3.查询索引

  • 用户查询接口
    用户输入查询条件的地方
    例如:百度的搜索框
  • 把关键词封装成一个查询对象(创建查询
  • 要查询的域
  • 要搜索的关键词
  • 执行查询
  • 根据要查询的关键词到对应的域上进行搜索。
  • 找到关键词,根据关键词找到 对应的文档
  • 渲染结果
  • 根据文档的 id 找到文档的对象
  • 对关键词进行高亮显示
  • 分页处理
  • 最终展示给用户看。

3.入门程序

3.1.配置开发环境

Lucene 下载

Lucene是开发全文检索功能的工具包,从官方网站下载lucene-7.4.0,并解压。

索引适合加在长文本上吗 索引内容_lucene_02

使用的jar包

lucene-core-7.4.0.jar

lucene-analyzers-common-7.4.0.jar

3.2.需求

实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字的文件都需要找出来。还可以根据中文词语进行查询,并且需要支持多个条件查询。

本案例中的原始内容就是磁盘上的文件,如下图:

索引适合加在长文本上吗 索引内容_索引适合加在长文本上吗_03

3.3.创建索引

实现步骤:

第一步:创建一个java工程,并导入jar包。
第二步:创建一个indexwriter对象
1)指定索引库的存放位置Directory对象
2)指定一个IndexWriterConfig对象
第二步:创建document对象
第三步:创建field对象,将field添加到document对象中。
第四步:使用indexwriter对象将document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
第五步:关闭IndexWriter对象。

代码实现:

/**
 * @Auther: lss
 * @Date: 2019/5/7 17:27
 * @Description:
 */
public class LuceneFirst {

    @Test
    public void createIndex() throws IOException {

        // 创建一个 Directory 对象,指定索引库保存的位置
        // 把索引库保存在内存中
        // Directory directory = new RAMDirectory();
        // 把索引库保存在磁盘中
        Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
        // 基于 Directory 对象创建一个 IndexWriter 对象
        IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig());
        // 读取磁盘上的文件,对应每个文件创建一个文档对象。
        File dir = new File("D:\\searchsource");
        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

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

            // 创建文档对象
            Document document = new Document();
            // 向文档对象中添加域
            document.add(fieldName);
            document.add(fieldPath);
            document.add(fieldContent);
            document.add(fieldSize);
            // 把文档对象写入索引库
            writer.addDocument(document);
        }
        // 关闭 indexWriter 对象
        writer.close();
    }
}

执行测试: 生成的索引库

索引适合加在长文本上吗 索引内容_lucene_04


这里生成的了一堆看不懂的文件,这些文件没办法用普通的文本编辑器查看,这该怎么办,有个小软件 luck 可以查看索引库。我会把文件上传到下载上。下载使用 luke 查看索引库中的内容

索引适合加在长文本上吗 索引内容_Lucene_05


我们使用的luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以(PS:jdk 1.8貌似也是可以的)。

3.4.查询索引

实现步骤:

第一步:创建一个Directory对象,也就是索引库存放的位置。
第二步:创建一个indexReader对象,需要指定Directory对象。
第三步:创建一个indexsearcher对象,需要指定IndexReader对象
第四步:创建一个TermQuery对象,指定查询的域和查询的关键词。
第五步:执行查询。
第六步:返回查询结果。遍历查询结果并输出。
第七步:关闭IndexReader对象

代码实现:

@Test
public void searchIndex() throws Exception {
    // 创建一个 Directory 对象,指定索引库的位置
    Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
    // 创建一个 IndexReader 对象
    IndexReader indexReader = DirectoryReader.open(directory);
    // 创建一个Indexsearcher 对象,构造方法中的参数 indexReader 对象。
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    // 创建一个 Query 对象,TermQuery
    Query query = new TermQuery(new Term("name", "spring"));
    // 执行查询,得到一个 TopDocs 对象
    // 参数1:查询对象 参数2:查询结果返回的最大记录数
    TopDocs topDocs = indexSearcher.search(query, 10);
    // 取查询结果的总记录数
    System.out.println("查询总记录数:" + topDocs.totalHits);
    // 取文档列表
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    // 打印文档中的内容
    for (ScoreDoc doc : scoreDocs) {
        // 取文档 id
        int docId = doc.doc;
        // 根据 id 取文档对象
        Document document = indexSearcher.doc(docId);
        System.out.println(document.get("name"));
        System.out.println(document.get("path"));
        // System.out.println(document.get("content"));
        System.out.println(document.get("size"));
        System.out.println("------------------华丽的分割线");
    }
    // 关闭 IndexReader 对象
    indexReader.close();
}

执行测试:

索引适合加在长文本上吗 索引内容_Lucene_06


这里没有打印内容,太多了不好截屏,同学们可以自行测试。

我们使用 luck 查看分词的结果,发现对于中文是不友好的,英文以按照单词分,而中文只能是一个字一个字的,

搜索英文是没有问题的,中文就不行了,单个字搜索还可以。这就有了问题,这就引入了一个分析器。

索引适合加在长文本上吗 索引内容_索引适合加在长文本上吗_07

4.分析器

4.1.分析器的分词效果

代码实现:

@Test
public void testTokenStream() throws Exception {
    // 创建一个标准分析器对象
    Analyzer analyzer = new StandardAnalyzer();
    // 获得tokenStream对象
    // 第一个参数:域名,可以随便给一个
    // 第二个参数:要分析的文本内容
    TokenStream tokenStream = analyzer.tokenStream("", "The Spring Framework provides a comprehensive programming and configuration model.");
    // TokenStream tokenStream = analyzer.tokenStream("", "单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”");
    // 添加一个引用,可以获得每个关键词
    CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
    // 添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
    OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
    // 将指针调整到列表的头部
    tokenStream.reset();
    // 遍历关键词列表,通过incrementToken方法判断列表是否结束
    while (tokenStream.incrementToken()) {
        // 关键词的起始位置
        //            System.out.println("start->" + offsetAttribute.startOffset());
        // 取关键词
        System.out.println(charTermAttribute);
        // 结束位置
        //            System.out.println("end->" + offsetAttribute.endOffset());
    }
    // 关闭 tokenStream 对象
    tokenStream.close();
}

**执行测试:**英文

索引适合加在长文本上吗 索引内容_索引适合加在长文本上吗_08


中文:

索引适合加在长文本上吗 索引内容_索引适合加在长文本上吗_09


对中文不友好,这不行啊,我们查看源码发现 创建索引库我们没有指定分词器,使用的是默认的分词器 StandardAnalyzer

索引适合加在长文本上吗 索引内容_System_10


下面介绍中文分析器

4.2.中文分析器

4.2.1.Lucene 自带中文分词器
  • StandardAnalyzer:
    单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
    效果:“我”、“爱”、“中”、“国”。
  • SmartChineseAnalyzer
    对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理。
4.2.2.IKAnalyzer

索引适合加在长文本上吗 索引内容_lucene_11


使用方法:

第一步:把jar包添加到工程中

第二步:把配置文件和扩展词典和停用词词典添加到classpath下(hotword.dic 和 stopword.dic,配文件 IKAnalyzer.cfg.xml)

注意:hotword.dic和ext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码也就是说禁止使用windows记事本编辑扩展词典文件

使用EditPlus.exe保存为无BOM 的UTF-8 编码格式,如下图:

索引适合加在长文本上吗 索引内容_搜索_12


-**扩展词典:**添加一些新词

**停用词词典:**无意义的词或者是敏感词汇

4.3.使用自定义分析器

代码实现:

@Test
public void addDocument() throws Exception {
    // 创建一个 IndexWriter 对象,需要使用 IKAnalyzer 作为分析器
    Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
    // 创建一个 IndexWriter 对象,需要使用 IKAnalyzer 作为分析器
    IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new IKAnalyzer());
    IndexWriter writer = new IndexWriter(directory, indexWriterConfig);
    // 创建一个 document 对象
    Document document = new Document();

    // 向 document 对象添加域
    document.add(new TextField("name", "新添加的文件", Field.Store.YES));
    document.add(new TextField("content", "新添加的文件内容", Field.Store.NO));
    document.add(new StoredField("path", "C:/temp/hello"));
    // 把文档添写入索引库
    writer.addDocument(document);
    // 关闭索引库
    writer.close();
}

索引适合加在长文本上吗 索引内容_lucene_13


使用上面分词效果的代码测试:

索引适合加在长文本上吗 索引内容_索引适合加在长文本上吗_14


以后我们在创建索引库时,使用 IKAnalyzer 就可以了。

先到这里了,下一节介绍索引库的维护。
【Lucene】系列文章
【Lucene】全文检索技术介绍【Lucene】实现全文检索【Lucene】索引库的维护【Lucene】索引库的查询