Lucene

·  阅读 147

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

什么是全文检索

数据分类

我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件

结构化数据搜索

常见的结构化数据也就是数据库中的数据

在数据库中搜索很容易实现,通常都是使用sql语句进行查询,而且能很快的得到查询结果。

image.png

为什么数据库搜索很容易?
因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的。

非结构化数据查询方法

顺序扫描法(Serial Scanning)
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容,只是相当的慢。

全文检索(Full-text Search)
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方法。这个过程类似于通过字典的目录查字的过程。

将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引

例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。

这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-Text Search)。虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的。

如何实现全文检索

可以使用Lucene实现全文检索。Lucene是apache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。

Lucene适用场景:

  • 在应用中为数据库中的数据提供全文检索实现。
  • 开发独立的搜索引擎服务、系统

Lucene的特性:

1.稳定、索引性能高

  • 每小时能够索引150GB以上的数据
  • 对内存的要求小,只需要1MB的堆内存
  • 增量索引和批量索引一样快
  • 索引的大小约为索引文本大小的20%~30%

2.高效、准确、高性能的搜索算法

  • 良好的搜索排序
  • 强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等
  • 支持字段搜索(如标题、作者、内容)
  • 可根据任意字段排序
  • 支持多个索引查询结果合并
  • 支持更新操作和查询操作同时进行
  • 支持高亮、join、分组结果功能
  • 速度快
  • 可扩展排序模块,内置包含向量空间模型、BM25模型可选
  • 可配置存储引擎

3.跨平台

  • 纯java编写
  • 作为Apache开源许可下的开源项目,你可以在商业或开源项目中使用
  • Lucene有多种语言实现版(如C,C++、Python等),不仅仅是JAVA

Lucene架构:

image.png

全文检索的应用场景

对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,

  • 单机软件的搜索:word、markdown
  • 站内搜索:京东、淘宝、拉勾,索引源是数据库
  • 搜索引擎:百度、Google,索引源是爬虫程序抓取的数据

Lucene 实现全文检索的流程说明

索引和搜索流程图

image.png

1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
确定原始内容即要搜索的内容-->采集文档-->创建文档-->分析文档-->索引文档

2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
用户通过搜索界面-->创建查询-->执行搜索,从索引库搜索-->渲染搜索结果

创建索引

核心概念:

Document
用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。

Field
一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在Document中存储的。
Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。
如果对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。

Term
Term是搜索的最小单位,它表示文档的一个词语,Term由两部分组成:它表示的词语和这个词语所出现的Field的名称。

我们以拉勾招聘网站的搜索为例,在网站上输入关键字搜索显示的内容不是直接从数据库中来的,而是从索引库中获取的,网站的索引数据需要提前创建的。以下是创建的过程:

第一步: 获得原始文档:就是从mysql数据库中通过sql语句查询需要创建索引的数据

第二步: 创建文档对象(Document),把查询的内容构建成lucene能识别的Document对象,获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档,文档中包括一个一个的域(Field),这个域对应就是表中的列。

注意:每个 Document 可以有多个 Field,不同的 Document 可以有不同的 Field,同一个Document可以有相同的 Field(域名和域值都相同)。每个文档都有一个唯一的编号,就是文档 id。

第三步: 分析文档
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。

分好的词会组成索引库中最小的单元:term,一个term由域名和词组成

第四步: 创建索引

对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到 Document(文档)。

注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫 倒排索引结构。

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

倒排索引

倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的文档及出现的位置。

文档:索引库中的每一条原始数据,例如一个商品信息、一个职位信息

词条:原始数据按照分词算法进行分词,得到的每一个词

创建倒排索引,分为以下几步:

1)创建文档列表:
lucene首先对原始文档数据进行编号(DocID),形成列表,就是一个文档列表

2)创建倒排索引列表
对文档中数据进行分词,得到词条(分词后的一个又一个词)。对词条进行编号,以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。

搜索的过程:
当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。然后根据这些编号去文档列表中找到文档

查询索引

查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档

第一步:创建用户接口:用户输入关键字的地方
第二步:创建查询 指定查询的域名和关键字
第三步:执行查询
第四步:渲染结果(结果内容显示到页面上关键字需要高亮)

Lucene实战案例

需求说明

生成职位信息索引库,从索引库检索数据

创建数据库es,将sql脚本导入数据库执行。

准备开发环境

第一步:创建一个maven工程,已经学过Spring Boot,我们就创建一个SpringBoot项目

第二步:导入依赖

<!--spring boot 父启动器依赖-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
</parent>

<dependencies>
    <!--web依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--测试依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--lombok工具-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.4</version>
        <scope>provided</scope>
    </dependency>
    <!--热部署-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.2</version>
    </dependency>
    <!--pojo持久化使用-->
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>javax.persistence-api</artifactId>
        <version>2.2</version>
    </dependency>
    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!--引入Lucene核心包及分词器包-->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-core</artifactId>
        <version>4.10.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-analyzers-common</artifactId>
        <version>4.10.3</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!--编译插件-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>11</source>
                <target>11</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
        <!--打包插件-->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
复制代码

第三步:创建引导类


@SpringBootApplication
@MapperScan("com.ran.mapper")
public class FullTextSearchDempApplication {
    public static void main(String[] args) {
        SpringApplication.run(FullTextSearchDempApplication.class,args);
    }
}
复制代码

第四步:配置properties文件

server:
    port: 9000
Spring:
    application:
        name: ran-lucene
    datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/es?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: Ran@123456

#开启驼峰命名匹配映射
mybatis:
    configuration:
        map-underscore-to-camel-case: true
复制代码

第五步:创建实体类、mapper、service

创建索引

@RunWith(SpringRunner.class)
@SpringBootTest
public class LuceneIndexTest {

    @Autowired
    private JobInfoService jobInfoService;

    /**
     * 创建索引
     */
    @Test
    public void create()throws Exception{
        //1.指定索引文件的存储位置,索引具体的表现形式就是一组有规则的文件
        Directory directory = FSDirectory.open(new File("/Users/RG/Documents/class/index"));
        //2.配置版本及其分词器
        Analyzer analyzer = new IKAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST,analyzer);
        //3.创建IndexWriter对象,作用就是创建索引
        IndexWriter indexWriter = new IndexWriter(directory,config);
        //先删除已经存在的索引库
        indexWriter.deleteAll();
        //4.获得索引源/原始数据
        List<JobInfo> jobInfoList = jobInfoService.selectAll();
        //5. 遍历jobInfoList,每次遍历创建一个Document对象
        for (JobInfo jobInfo: jobInfoList) {
            //创建Document对象
            Document document = new Document();
            //创建Field对象,添加到document中
            document.add(new LongField("id",jobInfo.getId(), Field.Store.YES));
            //切分词、索引、存储
            document.add(new TextField("companyName",jobInfo.getCompanyName(), Field.Store.YES));
            document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(), Field.Store.YES));
            document.add(new TextField("companyInfo",jobInfo.getCompanyInfo(), Field.Store.YES));
            document.add(new TextField("jobName",jobInfo.getJobName(), Field.Store.YES));
            document.add(new TextField("jobAddr",jobInfo.getJobAddr(), Field.Store.YES));
            document.add(new TextField("jobInfo",jobInfo.getJobInfo(), Field.Store.YES));
            document.add(new IntField("salaryMin",jobInfo.getSalaryMin(), Field.Store.YES));
            document.add(new IntField("salaryMax",jobInfo.getSalaryMax(), Field.Store.YES));
            document.add(new StringField("url",jobInfo.getUrl(), Field.Store.YES));
            //将文档追加到索引库中
            indexWriter.addDocument(document);
        }
        //关闭资源
        indexWriter.close();
        System.out.println("create index success!");
    }
}
复制代码

查询索引

@Test
public void query()throws Exception{
    //1.指定索引文件的存储位置,索引具体的表现形式就是一组有规则的文件
    Directory directory = FSDirectory.open(new File("/Users/RG/Documents/class/index"));
    //2.IndexReader对象
    IndexReader indexReader = DirectoryReader.open(directory);
    //3.创建查询对象,IndexSearcher
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    //使用term,查询公司名称中包含"北京"的所有的文档对象
    Query query = new TermQuery(new Term("companyName","北京"));
    TopDocs topDocs = indexSearcher.search(query, 100);
    //获得符合查询条件的文档数
    int totalHits = topDocs.totalHits;
    System.out.println("符合条件的文档数:"+totalHits);
    //获得命中的文档  ScoreDoc封装了文档id信息
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for(ScoreDoc scoreDoc : scoreDocs){
        //文档id
        int docId = scoreDoc.doc;
        //通过文档id获得文档对象
        Document doc = indexSearcher.doc(docId);
        System.out.println("id:"+doc.get("id"));
        System.out.println("companyName:"+doc.get("companyName"));
        System.out.println("companyAddr:"+doc.get("companyAddr"));
        System.out.println("companyInfo:"+doc.get("companyInfo"));
        System.out.println("jobName:"+doc.get("jobName"));
        System.out.println("jobInfo:"+doc.get("jobInfo"));
        System.out.println("*******************************************");
    }
    //资源释放
    indexReader.close();
}
复制代码

中文分词器的使用

第一步:导依赖

<!--IK中文分词器-->
<dependency>
    <groupId>com.janeluo</groupId>
    <artifactId>ikanalyzer</artifactId>
    <version>2012_u6</version>
</dependency>
复制代码

第二步:可以添加配置文件

image.png

第三步 创建索引时使用IKanalyzer

image.png

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改