Spring Boot中集成Lucence并实现全文检索

时间:2022-10-25 23:14:19 类型:JAVA
字号:    

1. 什么是Lucence

Lucene提供了一个简单却强大的应用程式接口,能够做全文索引[把非结构化的文件信息形成结构化的数据(就像数据库信息)]和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。

1.1 全文检索

何为全文检索?举个例子,比如要在一个文件中查找某个字符串,最直接的想法就是从头开始检索,查到了就OK,这种对小数据量的文件来说,很简单实用,但是对于大数据量的文件来说,就比较吃力了。或者说反过来查找包含某个字符串的文件(比如哪个文件中包含springboot),也是这样,如果在一个拥有几十个 G 的硬盘中找那效率可想而知,是非常低的。 


文件中的数据是属于非结构化数据,也就是说它没有什么结构可言(不像我们数据库中的信息,可以一行一行的去匹配查询),要解决上面提到的效率问题,首先我们得将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构(说白了,就是变成关系数据库型一行一行的数据),然后对这些有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这就叫全文搜索。即先建立索引(表结构,把文件中的关键词提取出来),再对索引进行搜索的过程。


1.2 Lucene 建立索引的方式

    那么 Lucene 中是如何建立索引的呢?假设现在有两篇文章,内容如下:

            文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too. 

            文章2的内容为:He once lived in Shanghai.

    首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符

    号和停词。所谓的停词指的是没有特别意义的词,比如英文中的 a,the,too 等。经过分词后,得到词

    元(Token) 。如下:

    

        文章1经过分词后的结果: [Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]

        文章2经过分词后的结果: [He] [lives] [Shanghai]

        

然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小写,将单词缩减为词根形式,如 ”lives” 到 ”live” 等,将单词转变为词根形式,如 ”drove” 到 ”drive”等。然后得到词(Term)。如下:

        文章1经过处理后的结果: [tom] [live] [guangzhou] [i] [live] [guangzhou]

        文章2经过处理后的结果: [he] [live] [shanghai]

最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:

      

关键词  文章号[出现频率]出现位置
guangzhou 1[2]

3,6

he2[1] 

1

1[1] 4
live1[2],2[1]2,5,2
shanghai 2[1]3
tom1[1] 1

  

以上就是Lucene 索引结构中最核心的部分。它的关键字是按字符顺序排列的,因此 Lucene 可以用二元搜索算法快速定位关键词。实现时 Lucene 将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。

搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果,然后就可以在具体的文章中根据出现位置找到该词了。所以 Lucene 在第一次建立索引的时候可能会比较慢,但是以后就不需要每次都建立索引了,就快了


1.png



知道了Lucene的分词及创建索引的原理,接下来通过Spring Boot中集成Lucene并实现 创建索引(可以理解为把各个文件中的信息通过分词然后有序的存储的数据库表中)和搜索功能


2. Spring Boot 中集成 Lucence

    首先需要导入 Lucene 的依赖,它的依赖有好几个,如下:

<!-- Lucence核心包 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- Lucene查询解析包 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- 常规的分词(英文) -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>5.3.1</version>
</dependency>
<!--支持分词高亮 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-highlighter</artifactId>
    <version>5.3.1</version>
</dependency>
<!--支持中文分词 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-smartcn</artifactId>
    <version>5.3.1</version>
</dependency>

最后一个依赖是用来支持中文分词的,因为默认是支持英文的。

2.2 快速入门

    根据上文的分析,全文检索有两个步骤,先建立索引,再检索。所以为了测试这个过程,我们这里创建两个java 类,一个用来建立索引,另一个用来检索。

    

2.2.1 建立索引

        我们自己弄几个文件,放到 F:\lucene\datas 目录下,新建一个 Indexer 类来实现建立索引功能。首

先在构造方法中初始化标准分词器并生成索引实例。

import org.apache.lucene.analysis.standard.StandardAnalyzer;
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 java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;

public class Indexer {
    /*writer : 索引对象, 能够建立索引(即能够把文件中的词提取出来,并标注出现的次数及出现的位置及哪个文件)*/
    private IndexWriter writer;
    /*
     * 构造方法,实例化IndexWriter
     * @param indexDir   //索引目录(要搜索信息的目录)
     * @throws Exception
     */
    public Indexer(String indexDir) throws IOException {
        // 构造方法传递一个存储建立索引的目录(文件夹的路径), 即要放建立的索引存储在哪里
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        //打开索引文件夹
        StandardAnalyzer analyzer = new StandardAnalyzer();
        //标准分词器,会自动去掉空格, is a the等单词

        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //将标准分词器配置到写索引的配置中, 索引时将会 去掉空格, is, a, the等

        writer = new IndexWriter(dir, config);
        //创建实例化索引对象
    }

    /**
     * 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
     * @param file
     * @return
     * @throws Exception
     */
    private Document getDocument(File file) throws Exception{
        Document doc = new Document();
        //开始添加字段
        // 把doc当成数据库中的表的一行记录信息, 三个字段及对应的值
        //字段一: contents:值(表中的内容)
        //字段二: fileName:值(文件名)
        //字段三: fullPath:值(文件的路径)
        //添加内容
        doc.add(new TextField("contents", new FileReader(file)));
        //添加文件名,并把这个字段存到索引文件里
        doc.add(new TextField("fileName", file.getName(), Field.Store.YES));
        //添加文件路径
        doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES));
        return doc;
        //doc: 文档对象,有三个属性contents,fileName,fullPath
    }

    /*索引指定的文件
    @param file
    @throws Exception
    */
    private void indexFile(File file) throws Exception{
        System.out.println("索引文件的路径:" + file.getCanonicalPath());
        Document doc = getDocument(file);
        //调用上面的getDocument方法, 获取该文件的document对象
        writer.addDocument(doc);
        //将doc添加到索引实例对象中
    }
   /* 索引指定目录下的所有文件
    @param dataDir
    @return
    @throws Exception*/
    public int indexAll(String dataDir) throws Exception{
        File[] files = new File(dataDir).listFiles();
        //获取dataDir目录下的所有文件
                   int numDocs = 0;
                    if(null != files){
                        for(File file:files){
                            //调用上面的indexFile方法,对每个文件进行索引
                            indexFile(file);
                            //理解为: 有多少个文件,在writer中就有多少行信息
                            //每行信息含有文件名,文件路径,及文件内容
                        }
                        numDocs = writer.numDocs();
                        writer.close();
                    }
                    return numDocs;
        //返回索引的文件数
    }

}

生成索引:

public class MakeIndexer {
    public static void main(String[] args) {
        String indexDir = "F:\\java\\lucence";
        //索引保存到的路径
        String dataDir = "F:\\java\\lucence\\data";

        Indexer indexer = null;
        int indexedNum = 0;
        //记录索引开始时间
        long startTime = System.currentTimeMillis();
        try{
            //开始构建索引
            indexer = new Indexer(indexDir);
            indexedNum = indexer.indexAll(dataDir);
        }
        catch (Exception e){
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("索引耗时" + (endTime - startTime) + "毫秒");
        System.out.println("共索引了" + indexedNum + "个文件");
    }
}

建立搜索索引类:

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
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.Directory;
import org.apache.lucene.store.FSDirectory;

import java.nio.file.Paths;

public class Searcher {
    public static void search(String indexDir,String q) throws Exception{
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        //获取要查询的路径, 也就是索引所在的位置
        IndexReader reader = DirectoryReader.open(dir);
        //构建IndexSearcher
        IndexSearcher searcher = new IndexSearcher(reader);
        //标准分词器, 会自动去掉空格, is a the等单词
        Analyzer analyzer = new StandardAnalyzer();
        //查询解析器   查询的字段为contents(建立索引时生成的表字段)
        QueryParser parser = new QueryParser("contents",analyzer);
        //通过解析要查询的String, 获取查询对象, q为传赤来的待查的字符串
        Query query = parser.parse(q);
        //记录索引开始时间
        long startTime = System.currentTimeMillis();
        //开始查询,查询前10条数据, 将记录保存在docs中
        TopDocs docs = searcher.search(query,10);
        //记录索引结束时间
        long endTime = System.currentTimeMillis();
        System.out.println("匹配" + q + "共耗时" + (endTime - startTime) + "毫秒");
        System.out.println("查询到" + docs.totalHits + "条记录");

        //取出每条查询结果
        for(ScoreDoc scoreDoc : docs.scoreDocs){
            //scoreDoc.doc相当于docId, 根据这个docID来获取文档
            Document doc = searcher.doc(scoreDoc.doc);
            //fullPath是刚刚建立索引时候我们定义的一个字段,表示路径。也可以取其它的内容,只要我们在建立索引时有定义即可.
            System.out.println(doc.get("fullPath"));
        }
        reader.close();
    }
}

搜索测试操作:

public class SerchIndexer {
    public static void main(String[] args) {
        String indexDir = "F:\\java\\lucence";
        //查询这个字符串
        String q = "thank";
        try{
            Searcher.search(indexDir,q);
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
}
执行搜索结果如下:
匹配thank共耗时35毫秒
查询到1条记录
F:\java\lucence\data\2.txt


<