天天看点

Lucene&Elasticsearch

1. 全文索引

  • 数据分类
    • 结构化数据:具有固定格式或有限长度的数据,如数据库,元数据等
    • 非结构化数据:指不定长或无固定合适的数据,如word,邮件,磁盘文件等
  • 结构化数搜索
    • 通常用sql语句进行查询,数据库存储有规律,有行有列存储成表,数据格式长度固定
  • 非结构化数据搜索
    • 顺序扫描法(Serial Scanning):从头到尾顺序查找,直到扫描完所有的文件,速度慢
    • 全文检索(Full-text Search): 建立索引库生成索引进行文件查找,计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找。相当于字典查字
  • Lucene 实现全文检索
    • Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,方便在目标系统中实现全文检索
    • 适用场景:在应用中为数据库中的数据提供全文检索实现,开发独立的搜索引擎服务、系统
    • 特性:
      •  稳定、索引性能高:每小时能够索引150GB以上的数据;对内存的要求小,只需要1MB的堆内存;增量索引和批量索引一样快;索引的大小约为索引文本大小的20%~30%
      •  高效、准确、高性能的搜索算法:良好的搜索排序,强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等;支持字段搜索(如标题、作者、内容);可根据任意字段排序;支持多个索引查询结果合并;支持更新操作和查询操作同时进行;支持高亮、join、分组结果功能;速度快,可扩展排序模块,内置包含向量空间模型、BM25模型可选;可配置存储引擎
      • 跨平台:纯java编写,开源,有多种语言实现版(如C,C++、Python等)
      • 架构
      Lucene&Elasticsearch
  • 全文检索应用场景:对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,如单机软件的搜索:word、markdown;站内搜索:京东、淘宝、拉勾,索引源是数据库;搜索引擎:百度、Google,索引源是爬虫程序抓取的数据

2. Lucene 实现全文检索

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

2.1 创建索引

  • Document:用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。
  • Field:一个Document可以包含多个信息域,有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。
  • Term:搜索的最小单位,它表示文档的一个词语,Term由两部分组成:它表示的词语和这个词语所出现的Field的名称。
  • 创建过程:
    • 获得原始文档:就是从mysql数据库中通过sql语句查询需要创建索引的数据
    • 创建文档对象(Document),把查询的内容构建成lucene能识别的Document对象,获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档,文档中包括一个一个的域(Field),这个域对应就是表中的列。
    • 每个 Document 可以有多个 Field,不同的 Document 可以有不同的 Field,同一个Document可以有相同的 Field(域名和域值都相同)。每个文档都有一个唯一的编号,就是文档 id。
    • 分析文档:将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词,分好的词会组成索引库中最小的单元:term,一个term由域名和词组成
    • 创建索引,对所有文档分析得出的语汇单元进行索引,创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构
    Lucene&Elasticsearch
    • 倒排索引结构是根据内容(词语)找文档,也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大
  • 倒排索引:倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的文档及出现的位置
    • 首先创建文档列表:lucene首先对原始文档数据进行编号(DocID),形成列表,就是一个文档列表
    Lucene&Elasticsearch
    • 创建倒排索引列表:对文档中数据进行分词,得到词条(分词后的一个又一个词)。对词条进行编号,以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)
    Lucene&Elasticsearch
    • 搜索:当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号,然后根据这些编号去文档列表中找到文档

2.2 查询索引

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

2.3 Lucene实战

  • 准备开发环境sql脚本
  • 创建SpringBoot项目,导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>lucene</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--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>


</project>      
  • 创建引导类
package com.rf;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.rf.mapper")
public class LuceneApplication {
    public static void main(String[] args) {
        SpringApplication.run(LuceneApplication.class,args);
    }
}      
  • 配置yml文件
server:
  port: 9000
spring:
  application:
    name: lagou-lucene
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/es?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456

#开启驼峰命名匹配映射
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true      
  • 创建实体类、mapper、service
package com.rf.pojo;


import lombok.Data;

import javax.persistence.Id;
import javax.persistence.Table;

@Data
@Table(name = "job_info")
public class JobInfo {
@Id
  private long id;
  private String companyName;
  private String companyAddr;
  private String companyInfo;
  private String jobName;
  private String jobAddr;
  private String jobInfo;
  private int salaryMin;
  private int salaryMax;
  private String url;
  private String time;


}      
package com.rf.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rf.pojo.JobInfo;

public interface JobInfoMapper extends BaseMapper<JobInfo> {
}      
package com.rf.service.iml;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rf.mapper.JobInfoMapper;
import com.rf.pojo.JobInfo;
import com.rf.service.JobInfoService;
package com.rf.service;

import com.rf.pojo.JobInfo;

import java.util.List;


public interface JobInfoService {

   public JobInfo selectById(Long id);
    public List<JobInfo> selectAll();

}



import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class JobInfoServiceImpl implements JobInfoService {
    
    @Autowired
    private JobInfoMapper jobInfoMapper;
    @Override
    public JobInfo selectById(Long id) {
        return jobInfoMapper.selectById(id);
    }

    @Override
    public List<JobInfo> selectAll() {
        QueryWrapper<JobInfo> wrapper = new QueryWrapper<>();
        return jobInfoMapper.selectList(wrapper);
    }
}      
package com.rf.controller;

import com.rf.pojo.JobInfo;
import com.rf.service.JobInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/job")
public class JobInfoController {
    @Autowired
    private JobInfoService jobInfoService;
    @RequestMapping("/query/{id}")
    public JobInfo selectById(@PathVariable Long id){
        return jobInfoService.selectById(id);
    }
    @RequestMapping("/query")
    public List<JobInfo> selectAll(){
        return jobInfoService.selectAll();
    }
}      
  • 创建索引和索引目录
package com.rf;

import com.rf.pojo.JobInfo;
import com.rf.service.JobInfoService;
import com.rf.service.iml.JobInfoServiceImpl;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
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.apache.lucene.util.Version;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.File;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LuceneManager {
    @Autowired
    private JobInfoService jobInfoService;
    @Test
    public void testCreateIndex() throws Exception{
        // 1、指定索引文件存储的位置 D:\class\index
        Directory directory= FSDirectory.open(new File("D:\\index"));
        // 2、配置版本和分词器
        Analyzer standardAnalyzer = new StandardAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, standardAnalyzer);
        // 3、创建一个用来创建索引的对象 IndexWriter
        IndexWriter indexWriter = new IndexWriter(directory, config);
        // 4、获取原始数据
        List<JobInfo> jobInfos = jobInfoService.selectAll();
        for (JobInfo jobInfo : jobInfos) {
            Document document = new 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("jobName",jobInfo.getJobName(),
                    Field.Store.YES));
            document.add(new TextField("jobAddr",jobInfo.getJobAddr(),
                    Field.Store.YES));
            document.add(new IntField("salaryMin",jobInfo.getSalaryMin(),
                    Field.Store.YES));
            document.add(new IntField("salaryMax",jobInfo.getSalaryMax(),
                    Field.Store.YES));
            // StringField 不需要分词时使用 举例:url 、电话号码、身份证号
            document.add(new StringField("url",jobInfo.getUrl(),
                    Field.Store.YES));
            indexWriter.addDocument(document);

        }
        // 关闭资源
        indexWriter.close();
    }
}      

测试类要和启动类都在二级包下,结构要相同

  

Lucene&amp;Elasticsearch
    • 索引(Index):在Lucene中一个索引是放在一个文件夹中的,同一文件夹中的所有的文件构成一个Lucene索引。
    • 段(Segment):按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term),记录了此索引包含的段,文档,域,词
    • 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并,具有相同前缀文件的属同一个段,图中共一个段 "_0"
    • segments.gen和segments_1是段的元数据文件,也即它们保存了段的属性信息
    • Field的特性:Document(文档)是Field(域)的承载体, 一个Document由多个Field组成. Field由名称和值两部分组成,Field的值是要索引的内容, 也是要搜索的内容.
      • 是否分词(tokenized)
      • 是否索引(indexed):是否建立索引, 存储到索引域. 索引的目的是为了搜索, 只要可能作为用户查询条件的词, 都需要索引.
      • 是否存储(stored):凡是将来在搜索结果页面展现给用户的内容, 都需要存储
    • 常见的Field类型
    Lucene&amp;Elasticsearch
  • 查询索引:
public void testQueryIndex() throws Exception{
        // 1、指定索引文件存储的位置 D:\class\index
        Directory directory = FSDirectory.open(new File("D:\\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 = " + totalHits);//查询的总数量
        //获取命中的文档 存储的是文档的id
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 根据id查询文档
            int docId = scoreDoc.doc;
            Document document = indexSearcher.doc(docId);
            System.out.println( "id:"+ document.get("id"));
            System.out.println( "companyName:"+ document.get("companyName"));
            System.out.println( "companyAddr:"+ document.get("companyAddr"));
            System.out.println( "salaryMax:"+ document.get("salaryMax"));
            System.out.println( "salaryMin:"+ document.get("salaryMin"));
            System.out.println( "url:"+ document.get("url"));
            System.out.println("**************************************************************");
        }
    }      
  • 中文分词器的使用
    • 导依赖
    <!--IK中文分词器-->
            <dependency>
                <groupId>com.janeluo</groupId>
                <artifactId>ikanalyzer</artifactId>
                <version>2012_u6</version>
            </dependency>      
    • 可以添加配置文件,放入到resources文件夹中
    Lucene&amp;Elasticsearch
    • 创建索引时使用IKanalyzer :把原来的索引数据删除,再重新生成索引文件,再使用关键字“北京”就可以查询到结果了
    package com.rf;
    
    import com.rf.pojo.JobInfo;
    import com.rf.service.JobInfoService;
    import org.apache.lucene.analysis.Analyzer;
    import org.apache.lucene.analysis.standard.StandardAnalyzer;
    import org.apache.lucene.document.*;
    import org.apache.lucene.index.*;
    import org.apache.lucene.search.*;
    import org.apache.lucene.store.Directory;
    import org.apache.lucene.store.FSDirectory;
    import org.apache.lucene.util.Version;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.wltea.analyzer.lucene.IKAnalyzer;
    
    import java.io.File;
    import java.util.List;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class LuceneManager {
        @Autowired
        private JobInfoService jobInfoService;
        @Test
        public void testCreateIndex() throws Exception{
            // 1、指定索引文件存储的位置 D:\class\index
            Directory directory= FSDirectory.open(new File("D:\\index"));
            // 2、配置版本和分词器
            Analyzer standardAnalyzer = new IKAnalyzer();
            IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, standardAnalyzer);
            // 3、创建一个用来创建索引的对象 IndexWriter
            IndexWriter indexWriter = new IndexWriter(directory, config);
            // 4、获取原始数据
            List<JobInfo> jobInfos = jobInfoService.selectAll();
            indexWriter.deleteAll();
            for (JobInfo jobInfo : jobInfos) {
                Document document = new 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("jobName",jobInfo.getJobName(),
                        Field.Store.YES));
                document.add(new TextField("jobAddr",jobInfo.getJobAddr(),
                        Field.Store.YES));
                document.add(new IntField("salaryMin",jobInfo.getSalaryMin(),
                        Field.Store.YES));
                document.add(new IntField("salaryMax",jobInfo.getSalaryMax(),
                        Field.Store.YES));
                // StringField 不需要分词时使用 举例:url 、电话号码、身份证号
                document.add(new StringField("url",jobInfo.getUrl(),
                        Field.Store.YES));
                indexWriter.addDocument(document);
    
            }
            // 关闭资源
            indexWriter.close();
        }
        @Test
        public void testQueryIndex() throws Exception{
            // 1、指定索引文件存储的位置 D:\class\index
            Directory directory = FSDirectory.open(new File("D:\\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 = " + totalHits);//查询的总数量
            //获取命中的文档 存储的是文档的id
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
                // 根据id查询文档
                int docId = scoreDoc.doc;
                Document document = indexSearcher.doc(docId);
                System.out.println( "id:"+ document.get("id"));
                System.out.println( "companyName:"+ document.get("companyName"));
                System.out.println( "companyAddr:"+ document.get("companyAddr"));
                System.out.println( "salaryMax:"+ document.get("salaryMax"));
                System.out.println( "salaryMin:"+ document.get("salaryMin"));
                System.out.println( "url:"+ document.get("url"));
                System.out.println("**************************************************************");
            }
        }
    
    }