91-Lucene+ElasticSeach核心技术

Lucene+ElasticSeach

什么是全文检索:
数据分类:
我们生活中的数据总体分为两种:结构化数据和非结构化数据
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等
非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件
结构化数据搜索:
常见的结构化数据也就是数据库中的数据
在数据库中搜索很容易实现,通常都是使用 sql语句进行查询,而且能很快的得到查询结果

91-Lucene+ElasticSeach核心技术_第1张图片

为什么数据库搜索很容易:
因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的
非结构化数据查询方法:
顺序扫描法(Serial Scanning):
用户搜索----->文件
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看
对于每一个文 档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件
接着看下一个文件,直到扫描完所有的文件,如利用 windows 的搜索也可以搜索文件内容,只是相当的慢
比如下面搜索A(不区分大小写的,即a和A一样)

在这里插入图片描述

全文检索(Full-text Search):
该操作一般也可以用来操作结构化的数据,但一般是自己实现的,而不是使用技术,如数据库的全文索引
但我们一般不操作数据库,因为他是需要连接以及操作语句的,即中间操作多
就算是操作他的全文,速度一般还是比单纯的全文要慢
所以我们一般将他的查询结果进行全文索引,虽然可能会更新
因为你操作的全文索引信息可能并不是新的
一般是在磁盘,而不是内存,一般只会读取一次,所以可能不是新的,但数据库信息可能是新的
文档(文件或者数据库)---->生成索引
用户通过查询索引库---->生成的索引----->文档(文件或者数据库)
全文检索是指计算机索引程序(Lucene差不多就是这样)通过扫描文章中的每一个词,对每一个词建立一个索引
指明该词在 文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找
并将查找的结果 反馈给用户的检索方法,这个过程类似于通过字典的目录查字的过程
注意:一般英文之间是有空格的(因为不分开就算一组英文了),也就是一个英文组合代表一个词,但对于中文来说
比如"我是中国人",那么每个中文算一个词,而不是总体的"我是中国人"
即相当于默认加上空格
之所以这样是因为分词算法(一般的分词算法都是如此,即默认的分词算法,这里的Lucene也是这样)的缘故
所以我们也需要第三方(可以操作中文的)的分词算法来操作中文,这里注意即可,后面会进行说明
索引:
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构 的数据进行搜索
从而达到搜索相对较快的目的,这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引
注意:此索引也可以是说是数据库的索引,他是直接的指定位置
而不是以数据库的普通索引信息(不是全文索引的)为主的查询(即其他不用看了,提高了效率这里注意一下)
例如:字典,字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的
如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描,然而字的某些信息可以提取出来进行结构化处理
比如读音,就比较结构化,分声母和韵母,分别只有几种可以依次列举,于是将 读音拿出来按一定的顺序排列
每一项读音都指向此字的详细解释的页数,比如我们搜索时按结构化的拼音 搜到读音,然后按其指向的页数
便可找到我们的非结构化数据,也即对字的解释,当然这是比喻而已,一般字典有很多的索引对照
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-Text Search)
虽然创建索引的过 程也是非常耗时的(一般由我们来操作索引的创建,对应的可以先不上线,所以不是用户来创建)
但是索引一旦创建就可以多次使用
全文检索主要处理的是查询,所以耗时间创建 索引是值得的,即我们浪费时间不要紧,但用户节省了时间,所以这里是好的操作
如何实现全文检索 :
可以使用 Lucene 实现全文检索,Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包
提 供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言),也可以将Lucene创建的索引称为索引库
或者将Lucene他称为索引库也可以
Lucene 的目的是 为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能
Lucene适用场景:
在应用中为数据库中的数据提供全文检索实现,开发独立的搜索引擎服务、系统
Lucene的特性:
1:稳定、索引性能高
每小时能够索引150GB以上的数据
对内存的要求小,只需要1MB的堆内存
增量索引和批量索引一样快
索引的大小约为索引文本大小的20%~30%
2:高效、准确、高性能的搜索算法
良好的搜索排序
强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等
支持字段搜索(如标题、作者、内容)
可根据任意字段排序
支持多个索引查询结果合并
支持更新操作和查询操作同时进行
支持高亮、join、分组结果功能
速度快
可扩展排序模块,内置包含向量空间模型、BM25模型可选
可配置存储引擎
3:跨平台
纯java编写
作为Apache开源许可下的开源项目,你可以在商业或开源项目中使用
Lucene有多种语言实现版(如C,C++、Python等),不仅仅是JAVA
Lucene架构:

91-Lucene+ElasticSeach核心技术_第2张图片

91-Lucene+ElasticSeach核心技术_第3张图片

全文检索的应用场景:
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索
单机软件的搜索:word、markdown
站内搜索:京东、淘宝、拉勾,索引源是数据库
搜索引擎:百度、Google,索引源是爬虫程序抓取的数据
最后:主要与mysql的区别就在于模糊的查询,mysql模糊查询基本不能使用索引,所以在这方面比lucene要差很多
Lucene 实现全文检索的流程说明:
索引和搜索流程图:

91-Lucene+ElasticSeach核心技术_第4张图片

1:绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库
索引过程包括:确定原始内容即要搜索的内容–>采集文档–>创建文档对象–>分析文档–>索引文档
2:红色表示搜索过程,从索引库中搜索内容
搜索过程包括:用户通过搜索界面–>创建查询–>执行搜索,从索引库搜索–>渲染搜索结果
实际上索引库可以是看成对应的直接查询的语句优化存放的地方,所以说,全文索引可以看成一个使得结构化的操作
只是比普通的结构化,更加的高效而已,但是数据库,他的结构一般是操作少字段
当然,若操作数据库的自己的全文索引那么差不多高效,但还是要慢一点,即中间操作多
而正是如此,一般我们只会操作非结构化的操作全文索引,因为结构化的一般有全文索引的操作
当然,若将对应的结构化的查询结果,进行操作全文索引,那么也是可以的
只是一般数据库的数据会变化而已
因为你操作的全文索引信息可能并不是新的
一般是磁盘,而不是内存,一般只会读取一次,所以可能不是新的,但数据库信息可能是新的
创建索引:
核心概念:
Document:
用户提供的源是一条条记录,这些记录中,某条记录可以是文本文件、字符串或者数据库表的一条记录等等
一条记录 经过索引之后,就是以一个Document的形式存储在索引文件中的,用户进行搜索,也是以Document列表的形式返回
即我们也说,一条记录(不是全部),也就是一个Document对象,里面的信息一般以Field来存储的
也就相当于文件的信息是字节来存储的,即是存放字节的地方,所以Document对象是存放Field域信息的地方
所以操作分词时,实际上是操作Field域
Field:
一个Document可以包含多个信息域,例如一篇文章可以包含"标题"、“正文”、"最后修改时间"等信息域
这些信息域就是通过Field在Document中存储的
Field有两个属性可选:存储和索引(一般索引也是不选的,而只选择存储),通过存储属性你可以控制是否对这个Field进行存储
通过索引 属性你可以控制是否对该Field进行索引
如果对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果 中提取文章标题
所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引 文件大小,将正文域的存储属性设置为假
当需要时再直接读取文件,若我们只是希望能从搜索解果中提 取最后修改时间,不需要对它进行搜索
所以我们把最后修改时间域的存储属性设置为真,索引属性设 置为假
上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你 那么设置
因为既不存储又不索引的域是没有意义的
我们可以将真的代表是否操作,如索引为真,那么可以搜索,否则搜索不到,若是存储为真
那么搜索到的结果可以显示,否则不显示,以搜索为主,所以这里能够显示最后修改时间
即Field可以说是由名称(域名)和值(域值)组成,名称操作索引,值操作存储
Term:
Term是搜索的最小单位,它表示文档的一个词语,Term由两部分组成:它表示的词语和这个词语所出现的Field的名称
我们以拉勾招聘网站的搜索为例,在网站上输入关键字搜索显示的内容不是直接从数据库中来的
而是 从索引库中获取的,网站的索引数据需要提前创建的,以下是创建的过程
第一步:获得原始文档,比如从mysql数据库中通过sql语句查询需要创建索引的数据
第二步:创建文档对象(Document),把查询的内容构建成lucene能识别的Document对象,获取原 始内容的目的是为了索引
在索引前需要将原始内容创建成文档,文档中包括一个一个的域(Field), 这个域对应就是表中的列
注意:每个 Document 可以有多个 Field
不同的 Document 可以有不同的 Field,这是自然的,有相同的也是自然的
主要是同一个Document 可以有相同的 Field(域名和域值都相同)
也就相当于合并了(从分词来说,所以也就相当于只有一个,但实际上还是存储了两个)
域值不同的话,也是合并,当然也有不同的Field,那也是自然的
每个文档都有一个唯一的编号,就是文档 id
第三步:分析文档,将原始内容创建为包含域(Field)的文档(document)之后,需要再对域中的内容进行分析
分析的过程 是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单 元
可以将语汇单元理解为一个一个的单词,比如:

91-Lucene+ElasticSeach核心技术_第5张图片

分好的词会组成索引库中最小的单元:term,一个term由域名和词组成
第四步:创建索引
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索
最终要实现只搜索被索引的语汇单 元从而找到 Document(文档)
注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫 倒排索引结构
倒排索引结构是根据内容(词语)找文档,如下图:

91-Lucene+ElasticSeach核心技术_第6张图片

倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合 较大
倒排索引:
倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的 文档及出现的位置
文档:索引库中的每一条原始数据,例如一个商品信息、一个职位信息
词条:原始数据按照分词算法进行分词,得到的每一个词 创建倒排索引,分为以下几步:
创建文档列表:
lucene首先对原始文档数据进行编号(DocID),形成列表,就是一个文档列表

91-Lucene+ElasticSeach核心技术_第7张图片

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

91-Lucene+ElasticSeach核心技术_第8张图片

91-Lucene+ElasticSeach核心技术_第9张图片

搜索的过程:
当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条
然后拿着这 些词条去倒排索引列表中进行匹配,找到这些词条就能找到包含这些词条的所有文档的编号
然后根据这些编号去文档列表中找到文档
查询索引 :
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的 过程
根据关键字搜索索引,根据索引找到对应的文档
第一步:创建用户接口:用户输入关键字的地方

91-Lucene+ElasticSeach核心技术_第10张图片

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

91-Lucene+ElasticSeach核心技术_第11张图片

Lucene实战:
需求说明:
生成职位信息索引库,从索引库检索数据

91-Lucene+ElasticSeach核心技术_第12张图片

分词算法也称为分词器
创建数据库es,将sql脚本导入数据库执行
数据库地址:
链接:https://pan.baidu.com/s/1I1zyzDwaWMnHVSWKuoYvfg
提取码:alsk
建议直接的执行他,而不是打开赋值执行或者查看,因为文件很大
准备开发环境 :
第一步:创建一个maven工程,已经学过Spring Boot,我们就创建一个SpringBoot项目
目录如下:

91-Lucene+ElasticSeach核心技术_第13张图片

参照这个目录创建并编写如下:
第二步:导入依赖

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.6.RELEASEversion>
    parent>
 <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
     
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.4version>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.3.2version>
        dependency>
        
        <dependency>
            <groupId>javax.persistencegroupId>
            <artifactId>javax.persistence-apiartifactId>
            <version>2.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
     
        <dependency>
            
            <groupId>org.apache.lucenegroupId>
            <artifactId>lucene-coreartifactId>
            <version>4.10.3version>
        dependency>
        <dependency>
            
            <groupId>org.apache.lucenegroupId>
            <artifactId>lucene-analyzers-commonartifactId>
            <version>4.10.3version>
        dependency>
    dependencies>
    <build>
        <plugins>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <configuration>
                    <source>11source>
                    <target>11target>
                    <encoding>utf-8encoding>
                configuration>
            plugin>
            
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackagegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>
第三步:创建启动类
package com.lagou; //这个包自己创建

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LuceneApplication {

    public static void main(String[] args) {
        SpringApplication.run(LuceneApplication.class, args);
    }

}

第四步:配置properties文件
server:
  port: 9000
Spring:
  application:
   name: lagou-lucene
  datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/es?
  useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
   username: root
   password: 123456
#开启驼峰命名匹配映射
mybatis:
  configuration:
   map-underscore-to-camel-case: true


第五步:创建实体类
package com.lagou.pojo; //自己创建该包


import lombok.Data;

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

@Data
@Table(name = "job_info") //声明此对象映射到哪个表,也表示对应的内容为job_info,不操作sql语句,可以删除
public class JobInfo {

  @Id //声明属性为主键,不操作sql语句,可以删除
    //由于这两个可以删除,所以对应的依赖也可以删除,一般是操作持久化的
    //这里并没有用到,可以删除,具体可以百度看看作用和实现的方式
  private long id;
  private String companyName;
  private String companyAddr;
  private String companyInfo;
  private String jobName;
  private String jobAddr;
  private String jobInfo;
  private long salaryMin;
  private long salaryMax;
  private String url;
  private String time;



}

对应的mapper接口:
package com.lagou.mapper; //这个包自己创建

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

/**
 *
 */
public interface JobInfoMapper extends BaseMapper<JobInfo> {
}

对应的service包下的类和实现类:
package com.lagou.service; //这个包自己创建

import com.lagou.pojo.JobInfo;

import java.util.List;

/**
 *
 */
public interface JobInfoService {
    JobInfo selectById(Long id);

    List<JobInfo> selectAll();
}

package com.lagou.service.impl; //这个包自己创建

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lagou.mapper.JobInfoMapper;
import com.lagou.pojo.JobInfo;
import com.lagou.service.JobInfoService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 *
 */
public class JobInfoServiceImpl implements JobInfoService {

    @Autowired
    private JobInfoMapper jobInfoMapper; //虽然编译期检查会报错,但是运行期可以操作
    //若不要看爆红,可以alt+回车,点击Ins开头的,然后点击Dis开头的即可,即不检查了


    @Override
    public JobInfo selectById(Long id) {
        return jobInfoMapper.selectById(id);

    }

    @Override
    public List<JobInfo> selectAll() {
        QueryWrapper<JobInfo> queryWrapper = new QueryWrapper<>();
        return jobInfoMapper.selectList(queryWrapper);

    }
}

测试类:
package com.lagou; //这个包自己创建

import com.lagou.pojo.JobInfo;
import com.lagou.service.JobInfoService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
class LuceneApplicationTests {

    @Autowired
    private JobInfoService jobInfoService;
    @Test
    void contextLoads() {
        JobInfo jobInfo = jobInfoService.selectById(1403l);
        System.out.println(jobInfo);
    }

}

对应的controller包下的类:
package com.lagou.controller; //这个包自己创建

import com.lagou.pojo.JobInfo;
import com.lagou.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("/jobInfo")
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.lagou;

import com.lagou.pojo.JobInfo;
import com.lagou.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.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.util.Version;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;

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


@SpringBootTest
public class LuceneApplicationTests {

    @Autowired
    private JobInfoService jobInfoService;
    @Test
    void contextLoads() {
        JobInfo jobInfo = jobInfoService.selectById(1403l);
        System.out.println(jobInfo);
    }

     /*
    创建索引
     */

    @Test
    public void create() throws IOException {
        //指定索引文件的存储位置,索引具体的表现形式就是一组有规划的文件
        Directory directory = FSDirectory.open(new File("E:/class/index"));

        //创建配置版本对象及其分词器对象
        //包是这个import org.apache.lucene.analysis.Analyzer;
        Analyzer analyzer = new StandardAnalyzer(); 
        //分词器创建,标准的编写,一般是默认这样操作,即默认的分词器

        //参数列表:
        //参数1:传递了最新的版本
        //参数2:传递分词器,初始化对应的赋值
        //现在准备使用该配置,后面的使用
        IndexWriterConfig Config = new IndexWriterConfig(Version.LATEST,analyzer);

        //创建IndexWriter对象,作用就是创建索引
        //参数列表:
        //参数1:传递了目录
        //参数2:传递了对应的赋值好的配置版本对象
        //这样我们来操作创建索引,即我们创建的索引会到指定的文件里面
        //在创建的过程中,会根据该版本来操作分词器,从而可以得到索引

        //文件不存在,会进行创建的,在这里也只有这一步会创建文件(因为写入操作)
        //其他步骤不会,所以他也就需要关闭资源
        IndexWriter indexWriter = new IndexWriter(directory,Config);

        //删除已存在的索引库,对应的规定数据不会删除,比如write.lock文件
        indexWriter.deleteAll();;

        //获得索引源即原始数据
        List<JobInfo> jobInfos = jobInfoService.selectAll();

        //我们知道,在前面说过,每一条数据,则代表一个Document对象
        //所以这里遍历jobInfos,每次遍历创建一个Document对象
        for(JobInfo jobInfo : jobInfos){
            //创建Document对象
            //导入的包是import org.apache.lucene.document.Document;
            Document document = new Document();

            //他里面存放的是一个又一个的Field,所以我们创建Field对象,添加到document里面去
            //参数列表:
            //参数1:传递域名
            //参数2:传递域名值
            //参数3:是否操作存储
            //一般对应的方法中,也只有存储可以手动指定设置,虽然有些方法也不可以
            //而索引的设置,基本不能手动设置,只能是固定的了
            //所以后面的方法中,基本只有存储来进行的参数设置,而没有是否手动的操作索引的操作设置
            //即他们的方法已经决定了

            //切分词,可以索引,然后存储
            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));
            //薪资范围,最小
            //实际上也有IntField,以及DoubleField等等,自然还有很多,他们决定了第二个参数的类型
            //一般他们的默认以及是否可以设置存储可行性的,与Long是相同的
            //但由于Long基本上是最大的,所以这里以Long为主
            document.add(new LongField("salaryMin",jobInfo.getSalaryMin(), Field.Store.YES));
            //薪资范围,最大
            document.add(new LongField("salaryMax",jobInfo.getSalaryMax(), Field.Store.YES));
            //招聘信息详情页
            document.add(new StringField("url",jobInfo.getUrl(), Field.Store.YES));

            //将文档追加到索引库中
            indexWriter.addDocument(document); 
            //追加后,一般默认会将该文档进行编号,一般从0开始递增(包括0)



        }

        //关闭资源(主要是对文件的连接)
        indexWriter.close();

        System.out.println("创建成功");;;;;;;;;;;;;;;; //多加";"没有事,不会影响运行,但是会影响查看
    }
    
}

所以可以看出,Field的确是Document里面的信息,且Document里面的信息以Field存储
一般我们分词时,分词的对象是Field的域值,然后分好后,多出的term,代表分好的词和从那个分词对象的Field的名称的组合
即域名和词组成,该域名由于是Field的名称,那么自然是会对应文档,多个文档的域名可能有相同的
所以我们也说,一个词可以找到很多文档,大多数情况下,就是这样的说明
当然,有时候我们只会根据词来查找,也就是说,查询对应的文档中的所有Field域名
当然,我们也可以指定域名查找,这样其他的Field域名就不会查找了
具体的实际情况,就比如说,公司名称或者公司职位等信息,域名就代表他们的意思
上面的代码总体介绍是:
在生成的索引目录E:\class\index中创建索引
索引(Index): 在Lucene中一个索引是放在一个文件夹中的,如下图,同一文件夹中的所有的文件构成一个Lucene索引

91-Lucene+ElasticSeach核心技术_第14张图片

我们也可以发现,对应的索引库信息还是挺大的,因为存放了对应的索引对应的信息,或者说,查询的文档信息,分词信息等
且在硬盘(或者磁盘,硬盘只是磁盘的一种,即硬磁盘,以前一般是使用软磁盘)里面
段(Segment):
按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)
即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了 那些词。
一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可 以合并
如上图,具有相同前缀文件的属同一个段,图中共一个段 “_ 0” ,当我们创建索引时,就会变成"_ 1"
如果不操作删除,那么"_ 0"和"_ 1"自然是一起的,而不是覆盖
segments.gen和segments_3是段的元数据文件,也即它们保存了段的属性信息
这个"_ 3"代表该文件第几次创建,只要我们操作了创建,那么就会变成"_ 4"
无论是否操作了删除,因为不操作删除就是覆盖,所以他也只有一个,segments.gen也同样如此
而正是因为写操作,我们一般需要在可以操作写的目录下,而不是不能写的目录下
但通常来说,除了C盘(可能也可以,但他的文件一般是有设置的),其他的盘基本都可以写,所以我们也最好不要在C盘写
除非你确认可以写,且容量大,那么就行
Field的特性:
Document(文档)是Field(域)的承载体,一个Document由多个Field组成
Field由名称和值两部分组成,Field的值是要索引的内容,也是要搜索的内容
是否分词(tokenized):
是:将Field的值进行分词处理,分词的目的是为了索引,如:商品名称,商品描述
这些内容用户会通过输入关键词进行查询,由于内容多样,需要进行分词处理建立索引
否:不做分词处理,如:订单编号,身份证号,是一个整体,分词以后就失去了意义,故不需要分词
是否索引(indexed):
是:将Field内容进行分词处理后得到的词(或整体Field内容)建立索引,存储到索引域,索引的目的是为了搜索
如:商品名称,商品描述需要分词建立索引,订单编号,身份证号作为整体建立索引
只要可能作为用户查询条件的词,都需要索引
否:不索引,如:商品图片路径,不会作为查询条件,不需要建立索引
是否存储(stored):
是:将Field值保存到Document中,如:商品名称,商品价格,凡是将来在搜索结果页面展现给用户的内容,都需要存储
否:不存储,如:商品描述,内容多格式大,不需要直接在搜索结果页面展现,不做存储,需要的时候可以从关系数据库取
常用的Field类型:

91-Lucene+ElasticSeach核心技术_第15张图片

查询索引 :
回到测试类,添加如下方法:
 @Test
    public void query() throws IOException {
        //指定索引文件的存储位置,索引具体的表现形式就是一组有规划的文件
        Directory directory = FSDirectory.open(new File("E:/class/index")); 
        //单纯的指定文件位置,并不需要释放资源,而使用文件需要,如读和写

        //之前创建时,使用的是IndexWriter,现在我们使用IndexReader
        //一般得到了对应的信息,即对应的文档对象信息
        IndexReader indexReader = DirectoryReader.open(directory);

        //创建查询对象,上面的对象一般是存放得到的信息,这里我们传递这个信息,准备操作
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        //使用term,我们知道,他是由域名和词组成的,所以我们要查询
        //一般需要指定域名和词,来确认操作对应的Field
        //当然,这里有单独的参数也就是域名的参数,那么默认值是空的,所以可以是单独的域名参数
        //导入的包是import org.apache.lucene.search.Query;

        //查询公司名称包含"北京"的所有文档对象,注意:这里只是定义条件,我们需要使用该条件
        //所以一般操作时,我们前端传递的数据,就是在这里进行操作的
        Query query = new TermQuery(new Term("companyName","北"));  
        //注意:这里没有操作切分词(无论是否是默认的),即基本上没有分词操作,可能现在有了,具体可以百度
        //所以是"北"去匹配,假设是"北放",那么只会匹配"北放",而不是"北"和"放"
        //一般的切分词不会有"北放",但在es(后面会说明)里面可以进行切分词(使用默认和自己设置的)
        //即会将查询条件进行切分词
        //这里自然是对应的域名里面的"北",这是肯定的,其他的域名不算

        //根据条件操作文档对象信息,返回对应的信息,一般只是查询文档id,而不是具体信息
        //所以还是需要上面的查询对象操作这个id来得到具体的文档对象
        //参数列表:
        //参数1:传递条件
        //参数2:展示多少条数据
        //当然参数2他只是指定上限,自然少的还是会出现,并不是必须要刚好满足,即最大显示100条
        TopDocs topDocs = indexSearcher.search(query, 100);

        //获得符合条件(也就是上面的topDocs)的文档数量
        int totalHits = topDocs.totalHits;

        System.out.println("符合条件的文档数:" + totalHits);

        //获得符合条件的文档信息
        //里面是存放的对象,有查询出来的文档id信息,即文档编号信息
        ScoreDoc[] scoreDoc = topDocs.scoreDocs;
        for(ScoreDoc s : scoreDoc){
            //得到文档id
            int doc = s.doc;
            //至此,我们通过条件得到了对应的文档id,那么现在我们通过这个id回到查询的所有信息里面进行查询
            //通过文档id获得文档对象
            Document doc1 = indexSearcher.doc(doc);
            //所以说,条件得到的并不是对应的值,因为假设可以得到值,那么就是又操作了对应的值的赋值
            //在数据量大的情况下,肯定比单纯的得到文档id要差


            //得到该文档中域名为id的域值
            System.out.println("id"+doc1.get("id"));
            System.out.println("companyName"+doc1.get("companyName"));
            System.out.println("companyAddr"+doc1.get("companyAddr"));
            System.out.println("companyInfo"+doc1.get("companyInfo"));
            System.out.println("jobName"+doc1.get("jobName"));
            System.out.println("jobInfo"+doc1.get("jobInfo"));
            System.out.println("---------------------");


        }

        //释放资源,因为他操作了读取信息
        indexReader.close();



    }

如果将"北"修改成"北京",查看结果你会发现,居然没有数据,修改成"京"或者其他的单独的词,一般都会有(只要存在)
原因是因为前面说的中文会一个字一个字的分词,显然这个分词器是自带的(默认的),操作中文不方便,即是不合适的
所以我们需要使用可以合理分词的分词器,其中最有名 的是IKAnalyzer分词器(分词器也可以叫做分词算法)
中文分词器的使用:
使用方式:
第一步:导依赖

<dependency>
    
   <groupId>com.janeluogroupId>
   <artifactId>ikanalyzerartifactId>
   <version>2012_u6version>
 dependency>
第二步:可以添加配置文件进行操作扩展(不是覆盖默认的配置),当然,不添加那么对应的操作一般使用默认的
所以这里可以操作扩展,如果默认的没有,你可以添加这样的文件,来进行扩展,当然,默认的也是可以使用的,因为不是覆盖
添加如下文件到资源文件夹(resources):
IKAnalyzer.cfg.xml文件:

DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
<properties>  
	<comment>IK Analyzer 扩展配置comment>
	
	
	<entry key="ext_stopwords">stopword.dic;entry> 
    
	
properties>
stopword.dic文件(相当于一个普通文本文件,但是一般是一个单词占一行的格式)
如果以dic来操作,那么一行其他多余的基本不会识别,如果是单纯的操作写入或者读取,如操作File类
那么也就是一个文件(无论是否是不同的后缀)而已,只是有后缀的区别:
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
第三步:创建索引时使用IKanalyzer
//将Analyzer analyzer = new StandardAnalyzer(); 修改成
Analyzer analyzer = new IKAnalyzer(); //这样就使用可以操作的中文分词器了
把原来的索引数据删除,再重新生成索引文件,再使用关键字"北京"进行测试,发现可以查询到结果了
考虑一个问题:一个大型网站中的索引数据会很庞大的,所以使用lucene这种原生的写代码的方式就不合适了
比如说添加时,添加域名和域值等信息的代码,和获取时,得到对应的域名对应的域值信息的代码,编写超级麻烦
或者我们需要看看索引库的文档信息总共有多少个(条),一般没有这样的API
通常需要我们来操作编写,比如慢慢的将参数从0开始,一直加1得到文档,并操作try来解决没有编号的错误等等
或者我们要看看对应的根据条件查询的信息占总信息的多少,即命中率等等信息,即或多或少有些操作是没有的或者很麻烦的
所以需要借助一个成熟的项目或软件来实现,目前比较有名是solr和elasticSearch,所以接下来我们学习elasticSearch的使用
Elastic search介绍和安装:
Elasticsearch是一个需要安装配置的软件,可以说是封装了lucene的框架
并在他的基础上进行了一系列的扩展,比如上面的查询总文档有多少个(条),所以他Elasticsearch也可以称为索引库
虽然lucene和Elasticsearch都可以称为索引库,但实际上只是他们的创建索引的位置的地方(文件目录),才是真正的索引库
这里对他们的称呼是一个整体操作,所以也可以将索引库称为是整体操作的集合
ELK技术栈说明:
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等
前面说的三个就是大家常说的ELK技术栈(开源实时日志分析平台)

91-Lucene+ElasticSeach核心技术_第16张图片

Logstash 的作用就是一个数据收集器,将各种格式各种渠道的数据通过它收集解析之后格式化输出到 Elasticsearch
最后再由Kibana 提供的比较友好的 Web 界面进行汇总、分析、搜索。
ELK 内部实际就是个管道结构,数据从 Logstash 到 Elasticsearch 再到 Kibana 做可视化展示
这三个 组件各自也可以单独使用,比如 Logstash 不仅可以将数据输出到Elasticsearch ,也可以到数据库、缓 存等
在安装之前,首先说明一下Elastic
简介:
Elastic官网:https://www.elastic.co/cn/

91-Lucene+ElasticSeach核心技术_第17张图片

91-Lucene+ElasticSeach核心技术_第18张图片

Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈
所以对应的资源可以说是Elastic里面的,即基本可以说Elastic是他们的父辈,或者说Elastic是一系列框架的集合体,就如Cloud类似
框架可以理解为是封装好的操作,如方法,具体介绍可以到61章博客里去查看

91-Lucene+ElasticSeach核心技术_第19张图片

Elasticsearch:
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch,从这个官网可以看出,的确是Elastic里面的

91-Lucene+ElasticSeach核心技术_第20张图片

功能:
分布式的搜索引擎:百度、Google、站内搜索
全文检索:提供模糊搜索等自动度很高的查询方式,并进行相关性排名,高亮等功能
数据分析引擎(分组聚合):如电商网站,一周内手机销量Top10
对海量数据进行近乎实时处理:水平扩展,每秒钟可处理海量事件
同时能够自动管理索引和查询在集 群中的分布方式,以实现极其流畅的操作
如上所述,Elasticsearch具备以下特点:
高速、扩展性、最相关的搜索结果
分布式:节点对外表现对等,每个节点都可以作为入门,加入节点自动负载均衡
JSON:输入输出格式是JSON
Restful风格:一切API都遵循Rest原则,容易上手
近实时搜索:数据更新在Elasticsearch中几乎是完全同步的,数据检索近乎实时
安装方便:没有其它依赖,下载后安装很方便,简单修改几个参数就可以搭建集群
支持超大数据:可以扩展到PB级别的结构化和非结构化数据,单位:B-KB-MB-GB-TB-PB-EB,他们之间的倍数是1024的倍数换算
所以PB是很大的
版本:
目前Elasticsearch最新的版本是7.x或者以上(当然自然会随着时间的推移而有更加的新版本)
企业内目前用的比较多是6.x,我们以6.2.4进行讲解,需要JDK1.8及以上,也可以说是"Java8"
具体的资源地址和解释:
资源地址:
链接:https://pan.baidu.com/s/1cXFLIMIgmQ3zfy6lOGkm7g
提取码:alsk
解释:

91-Lucene+ElasticSeach核心技术_第21张图片

上面图中,成为可以说是称为
安装和配置:
为了快速看到效果,我们直接在本地window下安装Elasticsearch
环境要求:JDK8及以上版本,如果不是,那么可能启动不了,因为使用到了对应的新特性
第一步:把资料文件夹中的准备好的软件放到一个没有中文且没有空格的路径位置,然后解压即可
如果路径有中文或者有空格,可能启动会失败,但也不一定,所以最好放到一个没有中文且没有空格的路径位置
对应的elasticsearch-6.2.4.zip解压后,可以找到如下目录

91-Lucene+ElasticSeach核心技术_第22张图片

依赖,组件,框架,他们里面都可以说是存放jar包的说明
上面的第三方插件,一般是空的,也一般在es启动时加载,其他的不用加载的依赖默认是自己有的,而不是加载
就如java自己操作默认的类,而不是操作我们写的类(需要导入)
第二步:修改配置文件
修改索引数据和日志数据存储的路径

91-Lucene+ElasticSeach核心技术_第23张图片

进入elasticsearch.yml文件,找到如下(一般在33行到37行那里):
#path.data: /path/to/data
#
# Path to log files:
#
#path.logs: /path/to/logs

#修改成(即解除了对应的注释)
path.data: d:\class\es\data
#
# Path to log files:
#
path.logs: d:\class\es\lo

#即他们定义了路径,即索引的数据在data文件夹里面,日志的数据在lo文件夹里面
#当启动时,若对应的目录不存在,那么会进行创建,如果存在,那么不变(即不会覆盖)
#而正是因为这样,所以最好不要修改里面的数据(添加好像并没有关系,前提是不会被识别),因为不会是覆盖的
#当然,这里的d也可以是D,但最好不要
#防止一些规定小写的,使得可能会出现问题,或者启动不了

#注意:启动后,会占用上面的文件目录信息,自然并不是都占用,可能也需要指定删除某些文件才可解除占用
#比如删除node.lock文件,在data里面可以找到,那么就可以删除data了
#这是为了可以随时的操作请求来操作索引库的原因,即我要为主

#当然这里了解即可,并不需要说明为什么
第三步:进入bin目录中直接双击 图下的命令文件,即elasticsearch.bat文件

91-Lucene+ElasticSeach核心技术_第24张图片

如果启动失败,那么一般是内存(不是磁盘,内存一般是针对整个机器的)不够,需要修改虚拟机内存的大小
找到config里面的jvm.options文件,找到如下(一般在22行到23行那里):
-Xms1g
-Xmx1g
#默认是上面的,即1GB,可以修改成
-Xms256m 	#初始分配的内存
-Xmx256m	#允许分配的内存,即加起来总共就是512m(m就是mb)
#这里与普通的虚拟机不同,在启动时,如果没有空闲的他们的总和,那么启动一般会失败,而不是虚拟机的启动成功
#因为他并不是固定占用,只是一个上限,可能以前也说过是固定(注意即可)

#但也要注意:不要设置的太少,因为基本的功能也是需要内存的,否则可能也会导致启动不了,或者,可能有兜底的最少值
#用来防止你设置的太少,即使用该最小的值
Xms 是指设定程序启动时占用内存大小,一般来讲,大点,程序会启动的快一点
但是也可能会导致自己的机器变慢,因为内存占用的多
Xmx 是指设定程序运行期间最大可占用的内存大小
如果程序运行需要占用更多的内存,超出了 这个设置值,就会抛出OutOfMemory异常
通常情况下,他们都最好大一点,其中Xmx最好更大一点
访问:

91-Lucene+ElasticSeach核心技术_第25张图片

我们可以看到,启动时,有绑定了两个端口
9300:集群节点间通讯接口,接收tcp协议
9200:客户端访问接口,接收Http协议
即我们在浏览器中访问:http://127.0.0.1:9200,若出现如下,则启动成功:

91-Lucene+ElasticSeach核心技术_第26张图片

其中不同的服务器,即主机,对应的实例名称以及唯一编号基本是不同,这里只要知道对应的信息是什么就可以了
安装kibana :
什么是Kibana:

91-Lucene+ElasticSeach核心技术_第27张图片

Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具
可以利用Elasticsearch的聚合功能, 生成各种图表,如柱形图,线状图,饼图等
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法
安装:
因为Kibana依赖于node,需要在windows下先安装Node.js,对应的安装包地址如下(虽然以前安装过了):
链接:https://pan.baidu.com/s/14J1jWknioP87mTSFbfJIag
提取码:alsk
一路下一步即可安装成功,然后在任意DOS窗口输入名:
node -v
可以查看到node版本,如下:

在这里插入图片描述

然后安装kibana,最新版本与elasticsearch保持一致,也是6.2.4
如果不一致,可能会出现问题,比如会导致es关闭(可能的,但一般不会)
下载地址:
链接:https://pan.baidu.com/s/1Ycoz_ktWpX3EFTBiI-CrEQ
提取码:alsk
我们直接解压kibana-6.2.4-windows-x86_64.zip即可
配置运行:
进入安装(即解压)目录下的config目录,修改kibana.yml文件的如下位置:
elasticsearch.url: "http://localhost:9200"
#即解除了对应注释,一般在21行那里,一般默认是主机的9200端口,所以不解除也可以
#但通常情况下,即实际开发中,对应的es不会放在本机,所以那个时候就需要指定地址了
进入安装目录下的bin目录:

91-Lucene+ElasticSeach核心技术_第28张图片

双击运行kibana.bat:

91-Lucene+ElasticSeach核心技术_第29张图片

可以看到kibana的监听端口是5601
我们访问:http://127.0.0.1:5601,若出现如下,则启动成功:

91-Lucene+ElasticSeach核心技术_第30张图片

控制台:
选择左侧的DevTools菜单(我们称为控制台),即可进入控制台页面:

91-Lucene+ElasticSeach核心技术_第31张图片

若出现上面的,往下滑,找到一个按钮(这个按钮自己寻找,一般是最后面的一个,即下图中的"Get to work"按钮),如下:

91-Lucene+ElasticSeach核心技术_第32张图片

点击后,可以出现如下(光标闪烁完后的截图,先不要理解这里的语法,后面会进行说明):

91-Lucene+ElasticSeach核心技术_第33张图片

上面右边是执行了运行按钮的结果,而左边就是请求条件,这里注意即可
左边操作一般是像postman和浏览器一样的发送地址(上面的请求方式是get)
对应的访问地址已经指定了
即前面的kibana.yml文件的配置操作,在该配置文件的21行那里,前面修改成了elasticsearch.url: “http://localhost:9200”
且对应条件已经指定好了,那么我们操作地址,然后使得得到ES的信息,所以我们也说kibana是操作es的可视化软件,如操作条件
而不用我们操作之前的使用程序了,使得原来我们使用程序操作文件,然后通过程序查询,变成了先占用文件,然后操作文件查询
实际上就是看成对应的地址,相当于操作url(比如web项目),所以上面的请求中因为有对应的地址,并加上条件
通过请请求发送信息,使得es操作了该条件,从而操作对应的索引库(即占用的文件目录)
注意:kibana启动时,最好先启动对应的指定的地址,即这里是es
否则可能对应的上面的控制台那里一般操作不了,虽然可以访问,可以看到提示
当然若你后启动也可以的,会有时间一直进行连接(每过一段间隔时间连接一次),直到连接为止,即连接后,那么就不会连接了
安装ik分词器:
Lucene的IK分词器早在2012年已经没有维护了,现在我们要使用的是在其基础上维护升级的版本
并且开发为Elasticsearch的集成插件了,与Elasticsearch一起维护升级
所以版本也最好保持一致,否则可能会出现问题,或者操作不了
对应的地址:https://github.com/medcl/elasticsearch-analysis-ik
安装:
在这之前,我们先写上如下(先不要理解这里的语法,后面会进行说明):
GET /_analyze
{
 "text": "我是中国人"

}


不写分词器的指定,那么一般是操作默认的分词器,即lucene的分词器,因为es就是封装了lucene
进行访问,出现如下:

91-Lucene+ElasticSeach核心技术_第34张图片

可以发现后面都是一个词,接下来
解压elasticsearch-analysis-ik-6.2.4.zip后,将解压后的文件夹拷贝到elasticsearch-6.2.4\plugins下,并重命名文件夹为ik
对应的文件在前面的地址中已经有了
最后如图:

91-Lucene+ElasticSeach核心技术_第35张图片

最好不要将plugin-descriptor.properties文件和elasticsearch-analysis-ik-6.2.4.jar文件放入到一个文件夹中,否则可能启动不了
他们是主要的文件,即ik里面就要存在他们,而不是再次的下一级
重启对应的es使得加载,然后输入如下:
GET /_analyze
{
 "analyzer": "ik_max_word", #使用了分词器,而不是默认的
 "text": "我是中国人"

}


出现如下:

91-Lucene+ElasticSeach核心技术_第36张图片

即不是对应的单独的词了
其中若没有对应的分词器,那么访问时,会返回错误信息
因为没有对应的分词器,如果不写,那么就是默认的,所以之前的是单个词
但我们也可以直到,并不是全部的组合,即有效的组合,所以没有什么"我是中",这样的组词
一般这样的偏门的词,需要我们去扩展,前面说过的扩展(这里的扩展一般需要他的操作,具体可以百度)
最后注意:由于他加载了我们的中文分词,所以对应的文件是占用的
里面的文件基本都是占用的,除了个别的,比如config目录,所以是防止你随时的操作删除
也可以操作这个:
GET /_analyze
{
 "analyzer": "ik_smart", #使用了分词器
 "text": "我是中国人"

}

结果是:

91-Lucene+ElasticSeach核心技术_第37张图片

发现少了一点分词,即更加的操作主要的词语,即正常的合理,而不是非常的合理(即ik_max_word,中国人也再次的分词)
换言之就是ik_max_word是操作更加细度的拆分
最后,上面只是进行测试,具体的语法介绍会在后面进行说明
安装Head插件 :
elasticsearch-head 简介:
elasticsearch-head是一个界面化的集群操作和管理工具,可以对集群进行傻瓜式操作
你可以通过 插件把它集成到es(首选方式),也可以安装成一个独立webapp
es-head主要有三个方面的操作:
1:显示集群的拓扑,并且能够执行索引和节点级别操作
2:搜索接口能够查询集6群中原始json或表格格式的检索数据
3:能够快速访问并显示集群的状态
官方的文档:https://github.com/mobz/elasticsearch-head
elasticsearch-head 安装 :
直接下载压缩包,地址:https://files.cnblogs.com/files/sanduzxcvbnm/elasticsearch-head.7z
或者使用前面给的地址
解压后,在谷歌浏览器中点击"加载已解压的压缩程序",找到解压后的elasticsearch-head文件夹
选择后,即可进 行安装,步骤如下:

91-Lucene+ElasticSeach核心技术_第38张图片

91-Lucene+ElasticSeach核心技术_第39张图片

如果有这个:

91-Lucene+ElasticSeach核心技术_第40张图片

点击出现蓝色的就会出现了,主要是为了防止太多插件,而出现的功能
我们带点击对应的这个:

在这里插入图片描述

那就会出现如下:

91-Lucene+ElasticSeach核心技术_第41张图片

至此我们操作成功,注意:他会自动的连接http://localhost:9200/(第一次默认是这个)
只要你刷新,或者重新加载就会访问他,无论是否修改该值,会自动变成http://localhost:9200/
除非你在其他的路径下,可以访问后(是可以,不可以的不会变,或者说,出现节点信息)
那么刷选或者重新加载时,即自动变成该访问地址
一般我们都会使用elasticsearch-head 而不是kibana,因为kibana太大且太复杂了
但是越复杂的,功能也是越多的,所以具体使用那一个主要看你
注意:对于这里的笔记来说,使用ctrl+f查询时,是不分大小写的,这里提一下
使用kibana对索引库操作:
基本概念:
节点、集群、分片及副本
节点 (node):
一个节点是一个Elasticsearch的实例,在服务器上启动Elasticsearch之后,就拥有了一个节点
如果在另一台服务器上启动Elasticsearch,这 就是另一个节点
甚至可以通过启动多个Elasticsearch进程,在同一台服务器上拥有多个节点
集群(cluster):
多个协同工作的Elasticsearch节点的集合被称为集群,在多节点的集群上,同样的数据可以在多台服务器上传播,这有助于性能
这同样有助于稳定性,如果 每个分片至少有一个副本分片,那么任何一个节点宕机后
Elasticsearch依然可以进行服务,返回所有数据
但是它也有缺点:必须确定节点之间能够足够快速地通信
并且不会产生脑裂效应(集群的2个部分不 能彼此交流,都认为对方宕机了)
分片 (shard):
索引可能会存储大量数据,这些数据可能超过单个节点的硬件限制
例如,十亿个文档的单个索引占用 了1TB的磁盘空间,可能不适合单个节点的磁盘
或者可能太慢而无法单独满足来自单个节点的搜索请求
为了解决此问题,Elasticsearch提供了将索引细分为多个碎片的功能
创建索引时,只需定义所需的分 片数量即可
每个分片本身就是一个功能齐全且独立的"索引",可以托管在群集中的任何节点上
分片很重要,主要有两个原因:
1:它允许您水平分割/缩放内容量
2:它允许您跨碎片(可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量
分片如何分布以及其文档如何聚合回到搜索请求中的机制完全由Elasticsearch管理,并且对您作为用户 是透明的
在随时可能发生故障的网络/云环境中,非常有用,强烈建议您使用故障转移机制,以防碎片/节点因某 种原因脱机或消失
为此,Elasticsearch允许您将索引分片的一个或多个副本制作为所谓的副本分片 (简称副本)
通常情况下,我们写入分片集群一般会根据对应的分片规则(即分片策略)来写入
当然了若对方集群都没有了,那么一般不会操作分片,即添加不了
当然可能也在添加,若有添加,那么就有两种情况
第一:将没有宕机的进行分片策略的分片
第二:假装他没有宕机,让他参与分片策略,只是分给他的数据存不了而已
一般情况下,是第二种,因为动态的改变设置的集群地址,是很难的
而读取分片集群,一般是进行全部依次读取累加显示(通常是默认操作这个的)
因为总不能将查询的数据先保存再给前端吧(虽然大多数是先保存然后再给前端),自然是依次的给
但有时也只会读取其中少部分分片节点(一般在数据量特别大的时候操作的,甚至可能只读取一个)
所以整体看来,我们就可以将分片集群(虽然可能还会让分片集群)看成一个机器即可
副本(replica):
分片处理允许用户推送超过单机容量的数据至Elasticsearch集群
副本则解决了访问压力过大时单机无 法处理所有请求的问题
分片可以是主分片,也可以是副本分片,其中副本分片是主分片的完整副本
副本分片用于搜索,或者 是在原有的主分片丢失后成为新的主分片,这样防止对应的分片宕机,使得可能丢失一部分数据
注意:可以在任何时候改变每个分片的副本分片的数量,因为副本分片总是可以被创建和移除的
这并 不适用于索引划分为主分片的数量,在创建索引之前,必须决定主分片的数量,或者说需要有主分片
通常的选择一个主分片,比如操作选举
过少的分片将限制可扩 展性,但是过多的分片会影响性能,默认设置的5份是一个不错的开始
注意:实际上副本有两个解释,大多数情况下是第二种,这里是第一种
第一,同样的当前分片集群的某个分片的分片集群
第二,分片集群的分片,而不是他们的集群,这可以使得一个分片(节点)有多个主分片,如其中的副本分片被选举成了主
但也只是逻辑上的主,因为地址变了,或者分给了主分片
这里与其他的副本不同的是,使用的是分片集群的其他分片(第二种),当然,可能也有第一种(这也是大多数的使用方式)
简单来说就是一个是给分片做集群,来保存数据副本数据,另外一个是以现有的集群来保存副本数据
但若是分给主分片,那么第一种是添加节点(虽然不是当前的分片集群)
第二种,任然是以现有的集群来保存分片数据(虽然是主分片)
具体解释在后面会体会到,且对应的优点和缺点在后面也会说明
文档、类型、索引及映射:
文档 (document):
Elasticsearch是面向文档的,这意味着索引和搜索数据的最小单位是文档,1在Elasticsearch中文档有几个重要的属性
1:它是自我包含的,一篇文档同时包含字段和它们的取值,即Field和他所对应的值,换言之,就是域名和域值
2:它可以是层次的,文档中还包含新的文档信息,字段还可以包含其他字段信息
例如,"location"字 段可以同时包含"city"和"street"两个字段,即地址,可以由城市和街道组成
其中的值也可以是对应的值的信息(包含其他文档的值信息)
总体来说就是,包含信息或者逻辑上是包含的
3:它拥有灵活的结构,文档不依赖于预先定义的模式,并非所有的文档都需要拥有相同的字段,它们 不受限于同一个模式
类型 (type):
类型是文档的逻辑容器,类似于表格是行的容器,在不同的类型中,最好放入不同结构的文档
例如, 可以用一个类型定义聚会时的分组,而另一个类型定义人们参加的活动
在前面我们多次的执行(不删除所以),出现的不同一个段的数据,那么相同的可以称为类型
索引 (index):
索引是映射类型的容器,一个Elasticsearch索引是独立的大量的文档集合
每个索引存储在磁盘上的同 组文件中,索引存储了所有映射类型的字段,还有一些设置
映射(mapping):
所有文档在写入索引前都将被分析,用户可以设置一些参数,决定如何将输入文本分割为词条,哪些词条应该被过滤掉
或哪些附加处理有必要被调用(比如移除HTML标签)
这就是映射扮演的角色:存储分析链所需的所有信息
简单来说就是一些其他信息,以及是否操作索引,是否存储的功能,有时也可以有是否操作分词
换言之就是相当于对应的对象,比如LongField对象和StringField对象等等对象
Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的
对比关系:

91-Lucene+ElasticSeach核心技术_第42张图片

详细说明:

91-Lucene+ElasticSeach核心技术_第43张图片

创建索引库 :
现在开始说明语法:
语法 :
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
而由于是Rest风格,那么应该有如下:
查询:GET
删除:DELETE
新建:POST,但是在这里,变成了PUT,你可以试一下,就知道了,返回如下:

91-Lucene+ElasticSeach核心技术_第44张图片

修改(也是操作新建的):PUT
上面的请求方式在编写时一般是大小写忽略的
且要注意:如果不加"/“开头或者结尾(即"PUT 索引库名/”,会是他这样的),那么默认是加上"/",除非你已经有了
所以后面的像这样的语法格式,即"PUT 索引库名",也可以,当然这些注意即可
创建索引的请求格式:
请求方式:PUT
请求路径:/索引库名
请求参数:json格式
格式:
PUT /索引库名
使用kibana创建:
kibana的控制台,可以对http请求进行简化,示例:
PUT /lagou

91-Lucene+ElasticSeach核心技术_第45张图片

相当于是省去了elasticsearch的服务器地址 而且还有语法提示,非常舒服
执行后,可以到这里:

91-Lucene+ElasticSeach核心技术_第46张图片

下面又进行了同样的操作,所以时间是不同的

91-Lucene+ElasticSeach核心技术_第47张图片

其中indices是创建的索引文件,里面包含了对应的索引信息,而上面的图中global-2.st操作了你操作的信息
当然但这些并不需要考虑
查看索引库 :
Get请求可以帮我们查看索引信息,格式:
GET /索引库名

91-Lucene+ElasticSeach核心技术_第48张图片

上面中aliases代表别名,mappings代表映射,settings代表设置信息,一般情况下,除了有特殊的变量
比如_mapping可以进行修改mappings的值外,其他的一般需要在创建时,进行修改,通常settings一般要在创建时进行修改
删除索引库 :
删除索引使用DELETE请求
格式:
DELETE /索引库名

91-Lucene+ElasticSeach核心技术_第49张图片

再次查看lagou:

91-Lucene+ElasticSeach核心技术_第50张图片

当然,我们也可以用HEAD请求,查看索引是否存在:

91-Lucene+ElasticSeach核心技术_第51张图片

为了验证请求方式在编写时是否一般是大小写忽略的,也就是不区分大小写,看如下:
下面查看成功的,可以都是小写(实际上是不区分大小写的)

91-Lucene+ElasticSeach核心技术_第52张图片

在这里插入图片描述

实际上是不区分大小写的

91-Lucene+ElasticSeach核心技术_第53张图片

注意:虽然不区分大小写,但是会有提示报错,那么因为识别时报错,但是运行时,还是不会变的,也就相当于都操作大写了
使用kibana对类型及映射操作:
有了 索引库 ,等于有了数据库中的 database,接下来就需要索引库中的类型了,也就是数据库中的表
创建数据库表需要设置字段约束,索引库也一样,在创建索引库的类型时
需要知道这个类型下 有哪些字段,每个字段有哪些约束信息,这就叫做 字段映射(mapping)
注意:Elasticsearch7.x取消了索引type类型的设置,不允许指定类型,默认为_doc
但字段仍然是有的,我们需要设置字段的约束信息,叫做字段映射(mapping)
字段的约束我们在学习Lucene中我们都见到过,包括到不限于:
1:字段的数据类型
2:是否要存储
3:是否要索引
4:是否分词
5:分词器是什么
那么为什么要叫做字段映射呢,实际上我们可以称为字段设置信息,而不是字段映射
当然既然是设置,那么一点是有对应,所以叫做字段映射也可以
我们一起来看下创建的语法:
创建字段映射:
请求方式依然是PUT:
PUT /索引库名/_mapping/typeName
{
 "properties": {
   "字段名": {
     "type": "类型",
     "index": true"store": true"analyzer": "分词器"
   }
 }
}

91-Lucene+ElasticSeach核心技术_第54张图片

类型名称:就是前面的type的概念,类似于数据库中的表
字段名(也就是Field):任意填写,下面指定许多属性,下面的属性也称为字段映射,所以也就是创建字段映射了,例如:
type:类型,可以是text、keyword、long、short、date、integer、object等
index:是否索引,默认为true
store:是否存储,默认为false
analyzer:分词器,若是 ik_max_word,则使用他的ik分词器
发起请求:
注意:对应的lagou库需要存在,否则操作不了,且需要换行但不能使得行隔开
而不是将"{",放在路径后面和路径的下两行及其以上后面,,必须是刚好在下一行,否则也操作不了
其中可以是PUT,POST,GET,其中若是多次执行,那么任然返回正确数据
若改变执行,那么需要改变字段名,那么查询时,会多出字段名称,即我们只会添加字段名,而不会修改字段名,否则报错
其中HEAD也可以(DELETE不可以)
虽然不会操作,但操作后,若要索引库基本不会有错误,否则会有错误(即200 - OK或者404 - Not Found的返回)
无论是否改变,但并不会改变数据,即只是检查是否存在索引库
PUT lagou/_mapping/goods
{
  "properties": {
    "title":{
      "type": "text",
      "store": true,
      "analyzer": "ik_max_word"
    },
    "images":{
      "type": "keyword",
      "store": true,
      "index": false
    },
    "price":{
      "type": "float"
    }
  }
}

上面操作了映射的信息
响应结果:
{
  "acknowledged": true
}
上述案例中,就给lagou这个索引库添加了一个名为 goods 的类型,并且在类型中设置了3个字段:
title:商品标题
images:商品图片
price:商品价格
并且给这些字段设置了一些属性,至于这些属性对应的含义,我们在后续会详细介绍
查看映射关系 (只能是GET):
GET /索引库名/_mapping

#比如
GET /lagou/_mapping
查看某个索引库中的所有类型的映射,如果要查看某个类型映射,可以再路径后面跟上类型名称,即:
GET /索引库名/_mapping/类型名

#比如
GET /lagou/_mapping/goods

上面两个的响应如下:
{
  "lagou": {
    "mappings": {
      "goods": {
        "properties": {
          "images": {
            "type": "keyword",
            "index": false,
            "store": true
          },
          "price": {
            "type": "float"
          },
          "title": {
            "type": "text",
            "store": true,
            "analyzer": "ik_max_word"
          }
        }
      }
    }
  }
}
实际上对应的一个索引库里面,基本只能有一个类型,虽然字段可以一直增加(字段映射信息也加上)
但不能修改字段里面的信息,既不能添加或者修改或者删除属性,即只能查看
当然,添加字段和删除字段,以及查看字段可以,当然,删除实际上不是真的删除,只是覆盖而已,只是语句没有对应的字段
但并不是删除,只是将剩下的覆盖(虽然一样)
即可以加上不一样的字段名,自己再次的加上一个字段名,其他的不变,执行就知道了
我们也可以看出来,对应的类型的确是显示了字段的信息,里面有多个字段及其对应的字段映射的信息
而正是因为类型只能是一个,所以上面的两个方法的结果是一样的,实际上在以前是可以有多个类型的,越高版本
那么一般是只有一个类型的,主要是对应的索引存放的原因,虽然我们说明类型相当于表,但并不是完全一样的
我们知道,他最终会变成词,而词自然对应文档,所以他们实际上操作的内容会是一起的,而不是与表一样相互独立
既然这样,那么为什么不直接的操作一个类型呢,而节省类型的创建来释放空间呢,所以一般只有一个类型了
映射属性详解:
在说明之前,对应的有些请求,并不是一定只有一种,但最好按照下面主请求来操作见名知意
比如添加文档时,可以使用POST和GET,但最好使用POST
因为看起来POST就是添加的操作,GET是得到的操作,虽然这里是作用一样
所以后面的我们说明主请求(即最好的,如这里的POST),因为你知道副请求有用吗,这是没有必要的,顶多提一下
其中,一般HEAD可以检验路径的存在是否合理,通常是检验索引的存在,而不会操作其他的操作,你可以试一下将索引
比如lagou修改成lagouu,后面的操作不变,很明显返回的结果是404 - Not Found,而不是200 - OK
即是检验索引的存在,或者说索引库
所以若返回正确的数据,那么一般是可以操作的请求,否则基本不可以
type(是字段的类型,而不是索引的类型,即属于字段映射里面的):
Elasticsearch中支持的数据类型非常丰富:

91-Lucene+ElasticSeach核心技术_第55张图片

91-Lucene+ElasticSeach核心技术_第56张图片

91-Lucene+ElasticSeach核心技术_第57张图片

91-Lucene+ElasticSeach核心技术_第58张图片

我们说几个关键的(注意:在复制代码时,可能有对应的隐藏的信息,可以将代码之前的空格删除,然后执行看看结果):
String类型,又分两种:
text:使用文本数据类型的字段,它们会被分词,其他的类型一般不会被分词,如数值类型或者其值,值基本只包括数字
主要是数字基本不会被分词,这也使得其他的类型基本不会分词,虽然并不是只包括数字
所以现在基本规定,当设置为数值类型或者其他不分词的类型时
(并不是只包括数字的类型,虽然这里并没有说明,虽然上面解释了很多的类型,但具体可以百度查看)
不能加上分词器的属性,否则操作不了,即创建不了
文本字段不用于排序,很少用于聚合,如 文章标题、正文
keyword:关键字数据类型,用于索引结构化内容的字段,不会被分词(也就不能加上分词的属性,比如analyzer)
必须完整匹配的内 容,如邮箱,身份证号,支持聚合
这两种类型都是比较常用的,但有的时候,对于一个字符串字段,我们可能希望他两种都支持,此时
可以利用其多字段特性,也就是之前说的合并,虽然是合并,但是实际上还是有两个,通过例外一个也可以操作合并后的域名
但以第一个为主,比如"还会",和"还早",还早是后执行的,那么得到的信息是还会(第一个)
除非得到合并后的信息(原来的通过id得到的文档可以获取到,即操作getFields可以得到两个值,否则一般只会操作第一个)
当然,是不能越界的,得到值的办法如下:使用得到的数组中的其中一个执行这个stringValue()即可得到数据(一般操作TextField)
具体的数字匹配,可以百度进行查看,一般我们只操作字符串的匹配
"properties": {
           "my_index"
               "analyzer": "ik_max_word",
               "fields": {
                   "sort":{
                       "type": "keyword"
                   }
               },
               "index": true #boolean类型可以不加"

           }
       }
Numerical:数值类型,分两类
基本数据类型:long、interger、short、byte、double、float、half_float
double 双精度64位
float 单精度32位
half_ float 半精度16位
浮点数的高精度类型:scaled_float
带有缩放因子的缩放类型浮点数,依靠一个 long 数字类型通过一个固定的(double 类型)缩放因数进行缩放
需要指定一个精度因子,比如10或100,elasticsearch会把真实值乘以这个因子后存储,取出时再还原,比如原来的值是3.15
那么乘以100,变成315,那么就减少小数了,也就提高了精确度,然后变回来时,在除以100即可
这样的好处为可以使得存放的位置的精度不会使得数据发生变化
Date:日期类型
elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省 空间
Array:数组类型
进行匹配时,任意一个元素满足,都认为满足,比如之前合并的Field,他操作的数组,但也只是对应于那个域名,这是自然的
排序时,如果升序则用数组中的最小值来排序,如果降序则用数组中的最大值来排序
字符串数组:["one", "two"]
整数数组:[1,2]
数组的数组:[1, [2, 3]],等价于[1,2,3]
对象数组:[ { "name": "Mary", "age": 12 }, { "name": "John", "age": 10 }]
Object:对象
JSON文档本质上是分层的:文档包含内部对象,内部对象本身还包含内部对象
{
 "region": "US",
 "manager.age": 30,
 "manager.name ": "John Smith"
}
#当然,还可以更加的加上层级

#索引方法如下:
{
   "mappings": {
       "properties": {
           "region": { "type": "keyword" },
           "manager": {
               "properties": {
                       "age": { "type": "integer" },
                       "name": { "type": "text" }
                 }
           }
       }
   }
}

mappings可以说是映射,即映射关系
如果存储到索引库的是对象类型,例如上面的manager,会把manager编程两个字段:manager.name和manager.age
ip地址:
#在这之前要注意,{}最好在路径的下一行,不要隔开行,否则一般执行不了

PUT my_index
{
 "mappings": { 
   "_doc": { 
   #上面两个也就相当于,my_index/_mapping/_doc,只是唯一的区别就是,他这里可以创建然后赋值
   #而my_index/_mapping/_doc我们手动创建
   #注意:以"_"开头,代表使用变量,所以对应的值基本是固定的,比如_mapping,就代表mappings
   #所以这里的_doc中的doc不能改变,除非解除了"_"开头
   #而以"_"开头的,基本可以是{}里面的属性参数(如这里的_mapping,就代表mappings属性参数)
   #否则,也只能写在路径后面,比如后面的添加文档,如POST /lagou/goods/
     "properties": {
       "ip_addr": {
         "type": "ip"
       }
     }
   }
 }
}

#后面的注释,可以在学习后面的内容时,就会明白了,可以大致的看一下,实际上也说明了后面的内容

PUT my_index/_doc/1 #POST也可以,若不写1,那么基本只有POST和GET了
#添加数据,指定使用_doc类型里面映射类型信息,没有会报错(但通常会智能判断,即自动创建,所以也通常不会报错)
#对应的1代表文档编号(不写默认生成)
#也就是说,虽然上面设置了字段映射
#但是与程序不同的是他是定义类型信息,我们去使用,而我们在程序里面操作时,是直接的使用类型信息
#实际上也是一样的,只是程序那里基本是固定的写法,比如基本只能改变存储,所以说es扩展了lucene,虽然es必须先指定
{
    "ip_addr": "192.168.1.1" 
#基本上对应的名称可以改变,比如ip_addrr,即指定的字段
#如果没有前面的设置属性,那么操作默认的设置
}

GET my_index/_search
{
 "query": {
   "term": {
     "ip_addr": "192.168.0.0/16" 
     #查看ip_addr字段的信息
     #实际上是精确查询,所有也可以是192.168.1.1值,但是192.168.1.0或者其他的不可,因为不是相等的
     #由于192.168.0.0/16代表192.168.0.1 ~ 192.168.255.254,包括了192.168.1.1值,所以也能匹配成功
   }
 }
}

GET my_index/_search
{
 "query": {
   "match_all": {} #查询所有,相当于GET my_index/_search,既然是所有,自然也包括除了文档信息外的其他信息
   #具体在后面会说明到
 }
}
index:
index影响字段的索引情况
true:字段会被索引,则可以用来进行搜索过滤,默认值就是true
只有当某一个字段的index值 设置为true时,检索ES才可以作为条件去检索
false:字段不会被索引,不能用来搜索
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引
但是有些字段是我们不希望被索引的,比如商品的图片信息(URL),就需要手动设置index为false
store:
是否将数据进行额外存储
在学习lucene时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值
用户的搜索结果中不会显示出来,但是在Elasticsearch中,即便store设置为false,也可以搜索到结果
原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做 _source 的属性 中
而且我们可以通过过滤 _source 来选择哪些要显示,哪些不显示
而如果设置store为true,就会在 _source 以外额外存储一份数据,多余,因此一般我们都会将store设 置为false
事实上,store的默认值就是false
在某些情况下,这对 store 某个领域可能是有意义的
例如,如果您的文档包含一个 title ,一个date 和一个非常大的 content 字段
则可能只想检索title 和date,而不必从一个大 _source字段(即包括了非常大的 content 字段)中提取这些字段
即直接在额外的地方取:
PUT my_index
{
 "mappings": {
   "_doc": {
     "properties": {
       "title": {
         "type": "text",
         "store": true 
       },
       "date": {
         "type": "date",
         "store": true 
       },
       "content": {
         "type": "text"
       }
     }
   }
 }
}

boost:
网站权重:网站权重是指搜索引擎给网站(包括网页)赋予一定的权威值,对网站(含网页)权威的评估评价
一 个网站权重越高,在搜索引擎所占的份量越大,在搜索引擎排名就越好
提高网站权重,不但利于网站(包括网页)在搜索引擎的排名更靠前,还能提高整站的流量,提高网站信任度
所以提高网站的权重具有相当重要的意义,权重即网站在SEO中的重要性,权威性
英文:Page Strength
1:权重不等于排名
2:权重对排名有着 非常大的影响
3:整站权重的提高有利于内页的排名
权重,新增数据时,可以指定该数据的权重,权重越高,得分越高,排名越靠前
PUT my_index
{
 "mappings": {
   "_doc": {
     "properties": {
       "title": {
         "type": "text",
         "boost": 2 
      },
       "content": {
         "type": "text"
       }
     }
  }
 }
}

title 字段上的匹配项的权重是字段上的匹配项的权重的两倍 content ,默认 boost 值为 1.0 或者说是1
提升(即操作权重)基本仅适用于Term查询(不提升prefix,range和模糊查询)
一次创建索引库和类型:
第一步:
PUT /lagou

第二步:
PUT lagou/_mapping/goods
{
 "properties": {
   "title": {
     "type": "text",
     "analyzer": "ik_max_word"
   },
   "images": {
     "type": "keyword",
     "index": "false"
   },
   "price": {
     "type": "float"
   }
 }
}

刚才 的案例中我们是把创建索引库和类型分开来做,其实也可以在创建索引库的同时,直接制定索引库 中的类型,基本语法:
put /索引库名

{
    "settings":{
        "索引库属性名":"索引库属性值"
   },
    "mappings":{ #这个前面已经说过了
        "类型名":{
            "properties":{
                "字段名":{
                    "映射属性名":"映射属性值"

               }
           }
       }
   }
}
来试一下吧:
PUT /lagou2
{
 "settings": {}, 
 "mappings": {
   "goods": {
     "properties": {
       "title": {
         "type": "text",
         "analyzer": "ik_max_word"
      }
     }
   }
 }
}

结果:
{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "lagou2"
}
但是他这样的操作却不能进行添加字段名,只能是创建,因为他需要创建索引库,否则若有则执行失败
使用kibana对文档操作:
文档,即索引库中某个类型下的数据,会根据规则创建索引,将来用来搜索。可以类比做数据库中的每 一行数据
新增文档:
新增并随机生成id :
通过POST请求(GET好像也可以,没有说明的,那么基本只有一个),可以向一个已经存在的索引库中添加文档数据
POST /索引库名/类型名
{
   "key":"value"
}

示例:
POST /lagou/goods/
{
   "title":"小米手机",
   "images":"http://image.lagou.com/12479122.jpg",
   "price":2699.00
}

响应:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "-sZkU4MBXyiY5LSSA6ws",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}
下面是再次的操作一次

91-Lucene+ElasticSeach核心技术_第59张图片

可以看到结果显示为: created ,应该是创建成功了
另外,需要注意的是,在响应结果中有个 _id 字段,这个就是这条文档数据的 唯一标识
以后的增删改 查都依赖这个id作为唯一标示
可以看到id的值为: -sZkU4MBXyiY5LSSA6ws(前面的生成,不以图片为准)
这里我们新增时没有指定id,所以是ES帮我们随机生成 的id
查看文档:
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上
通过kibana查看数据:
GET /lagou/goods/-sZkU4MBXyiY5LSSA6ws
查看结果:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "CsZpU4MBXyiY5LSSl63y", 
  #我又操作了一次,当然是随机生成的,所以你的一般与我不一样,通常操作时间戳或者UUID的类似的操作
  "_version": 1,
  "found": true,
  "_source": {
    "title": "小米手机",
    "images": "http://image.lagou.com/12479122.jpg",
    "price": 2699
  }
}

91-Lucene+ElasticSeach核心技术_第60张图片

_source :源文档信息,所有的数据都在里面
_id :这条文档的唯一标示 自动生成的id,长度为20个字符,URL安全,base64编码,GUID(全局唯一标识符)
分布式系统并行生成时不可能会发生冲突,在实际开发中不建议使用ES生成的ID
因为生成的ID太长且为字符串类型(不是整型,我们在程序里,一般是整型的,如前面代码里面的int类型作为doc的参数)
所以检索时效率低,主要还是太长了,所以即不建议使用
建议:将数据表中 唯一的ID,作为ES的文档ID
也可以查询所有的,即goods类型的所有id的数据
GET /lagou/goods/_search

91-Lucene+ElasticSeach核心技术_第61张图片

其中"total": 4代表有4个数据,即4个文档数据
新增文档并自定义id :
如果我们想要自己新增的时候指定id,可以这么做(前面操作过一次):
POST /索引库名/类型/id值

{
   ...
}

示例:
POST /lagou/goods/2
{
 "title":"大米手机",
 "images":"http://image.lagou.com/12479122.jpg",
 "price":2899.00 
 #00不会显示除了,即结果还是2899,但是有数字就不一定了,比如2899.231,那么显示2899.231
 #对于integer或者其他整型,那么只会显示2899,所以换句话说就是,结尾的0省略,若都是0,那么没有小数
}

得到的数据:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "2",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}
修改数据:
PUT:新增文档/修改文档
POST:新增文档/修改文档
GET:新增文档/修改文档
再次的执行就是修改(需要id相同,不指定的话,一般是创建,因为id随机的且唯一)
但若把刚才新增的请求方式改为PUT,必须指定id,否则报错
即:
id对应文档存在,则修改
id对应文档不存在,则新增
比如,我们把使用id为3,不存在,则应该是新增:
PUT /lagou/goods/3 
{
 "title":"超米手机",
 "images":"http://image.lagou.com/12479122.jpg",
 "price":3899.00,
 "stock": 100,
 "saleable":true

}
结果:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0

 },
  "_seq_no": 1,
  "_primary_term": 1

}
可以看到是 created ,是新增
执行查询(查看):
GET /lagou/goods/3
查看结果:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "_version": 5,
  "found": true, #找到数据,返回true
  "_source": {
    "title": "超米手机",
    "images": "http://image.lagou.com/12479122.jpg",
    "price": 3899,
    "stock": 100,
    "saleable": true
  }
}
我们再次执行刚才的请求,不过把数据改一下:
PUT /lagou/goods/3 
{
 "title":"超米手机",
 "images":"http://image.lagou.com/12479122.jpg",
 "price":3899.23, #修改的地方
 "stock": 100,
 "saleable":true

}
查看结果:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "_version": 4, #代表我操作了几次(我测试时操作了2次)
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}
可以看到结果是: updated ,显然是更新数据
执行查询:
GET /lagou/goods/3
结果如下:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "_version": 4,
  "found": true,
  "_source": {
    "title": "超米手机",
    "images": "http://image.lagou.com/12479122.jpg",
    "price": 3899.23, #这里改变了
    "stock": 100,
    "saleable": true
  }
}
删除数据:
删除使用DELETE请求,同样,需要根据id进行删除:
DELETE /索引库名/类型名/id值

91-Lucene+ElasticSeach核心技术_第62张图片

再次的查看,返回的结果如下:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "found": false #没找到数据,返回false
}

#如果没有对应的id,那么返回的结果中一般会出现"result": "not_found",代码没有对应的id
智能判断:
刚刚我们在新增数据时,添加的字段都是提前在类型中定义过的,如果我们添加的字段并没有提前定义 过,能够成功吗?
事实上Elasticsearch非常智能,你不需要给索引库设置任何mapping映射
它也可以根据你输入的数据 来判断类型,动态添加或者操作数据映射
也就是我之前说明的默认的操作(当然,该默认有点智能,会根据你的值来判断类型),测试一下:
POST /lagou/goods/3 
#若直接的创建,那么可以不用先创建映射(无论是否是第一次)
#因为会自己判断,所以也就不需要一定要先操作_mapping了
#即若第一次创建,会自动有创建mappings属性(也包括goods),然后操作,后续就是补充
#所以说"无论是否是第一次"
{
 "title":"超大米手机",
 "images":"http://image.lagou.com/12479122.jpg",
 "price":3299.00,
 "stock": 200,
 "saleable":true, 
 "subTitle":"大米"

}

我们额外添加了stock库存,saleable是否上架,subtitle副标题、3个字段
来看结果:
{
  "_index": "lagou",
  "_type": "goods",
  "_id": "3",
  "_version": 2,
  "result": "updated", #也是更新,因为是存在id的
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}
查看映射:
GET /lagou/_mapping
#或者执行
GET /lagou
下图中,出入是插入,部分词是不分词,即有错误,这里修改一下

91-Lucene+ElasticSeach核心技术_第63张图片

发现的确操作了映射关系,我们也可以将这样的操作称为默认
因为对应的类型也差不多是操作默认给出的类型的,比如上面的long
即stock、saleable、subtitle都被成功映射了
subtitle是String类型数据,ES无法智能判断,它就会存入两个字段
例如:
subtitle:text类型
subtitle.keyword:keyword类型
这种智能映射,底层原理是动态模板映射,但通常不建议使用,就比如上面的long,明明可以更小
如我给200这个值,那么long占了空间了,实际上int就可以了
但如果我们想修改这种智能映射的规则,其实只要修改动态模 板即可,也就是修改默认
动态映射模板:
动态模板的语法(也就是格式,或者说语句,简称为语法):

91-Lucene+ElasticSeach核心技术_第64张图片

1:模板名称,随便起
2:匹配条件,凡是符合条件的未定义字段,都会按照这个规则来映射
3:映射规则,匹配成功后的映射规则
举例,我们可以把所有未映射的string类型数据自动映射为keyword类型:
PUT lagou3
{
 "mappings": {
   "goods": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
       }
     },
      "dynamic_templates": [
       {
          "strings": {
            "match_mapping_type": "string", #位置无关紧要,只要与mapping平级即可
            "mapping": {
              "type": "keyword",
              "index":false,
              "store":true
           }
        }
      }
    ]
  }
 }
}
在这个案例中,我们把做了两个映射配置:
title字段:统一映射为text类型,并制定分词器
其它字段:只要是string类型,统一都处理为keyword类型,以及其他的索引和存储的设置
这样,未知的string类型数据就不会被映射为text和keyword并存,而是统一以keyword来处理
我们试试看新增一个数据:
POST /lagou3/goods/1
{
    "title":"超大米手机",
    "images":"http://image.lagou.com/12479122.jpg",
    "price":3299.00

}

我们只对title做了配置,现在来看看images和price会被映射为什么类型呢:
GET /lagou3/_mapping
结果:
{
  "lagou3": {
    "mappings": {
      "goods": {
        "dynamic_templates": [
          {
            "strings": {
              "match_mapping_type": "string",
              "mapping": {
                "index": false,
                "store": true,
                "type": "keyword"
              }
            }
          }
        ],
        "properties": {
          "images": {
            "type": "keyword",
            "index": false,
            "store": true 
            #可以发现,的确是对应的我们设置的配置
          },
          "price": {
            "type": "float"
          },
          "title": {
            "type": "text",
            "analyzer": "ik_max_word"
    #而这里就是使用我们设置的类型,否则那么会使用默认的,即操作模板,即自己设置的优先
    #而操作模板的后操作,当然也只会操作一个,而没有覆盖
          }
        }
      }
    }
  }
}


可以看到images被映射成了keyword,而非之前的text和keyword并存,说明我们的动态模板生效了
查询:
前面的查询,都只是顺便提一下,且基本只查询文档数据,基本没有具体的或者细节的查询,接下来说明真正的具体的查询
基本查询:
GET /索引库名/_search 
#一般查询操作中(即_search),POST基本也可以,与GET一样,可以理解为后台使用@RequestMapping来针对_search
{
    "query":{
        "查询类型":{
            "查询条件":"查询条件值"
       }
   }
}

这里的query代表一个查询对象,基本是固定这样写的,里面可以有不同的查询属性
查询类型:
例如: match_all , match , term , range 等等,当然还有很多,如分页的查询,后面会说明的
查询条件:查询条件会根据类型的不同,写法也有差异,后面会有语句,自行体会
即我们在语句中可以体会到,即也可以说是详细讲解
注意:操作查询条件时,基本只能选择一个查询条件
即match和match_all,不可以一起,或者match和range不可以一起,否则执行会报错(可能有其他的方式可以进行结合,但一般没有,注意即可)
查询所有(match_all):
GET /lagou3/_search
{
    "query":{
      "match_all": {}
  }
}

#实际上也就相当于
GET /lagou3/_search
#或者相当于
GET /lagou3/_search
{

}

#上面三个的结果是一样的,实际上后面两个最终也会自动变成第一个,所以也说明结果一样
query :代表查询对象
match_all :代表查询所有
既然是所有,自然并不会只包含文档信息的,也有其他的信息的
{
  "took": 0, #检索所消耗的时间,也就是查询所消耗的时间
  "timed_out": false, #是否超时
  "_shards": {  #分片信息
    "total": 5, #5个分片
    "successful": 5, 
    "skipped": 0,
    "failed": 0
  },
  "hits": { #命中结果,即查询的结果信息
    "total": 1, #有几个文档
    "max_score": 1,
    "hits": [
      {
        "_index": "lagou3", #索引名称
        "_type": "goods", #类型
        "_id": "1", #id值
        "_score": 1, #评分
        "_source": { #原始数据
          "title": "超大米手机",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3299
        }
      }
    ]
  }
}
took:查询花费时间,单位是毫秒
time_out:是否超时
_shards:分片信息
hits:搜索结果总览对象(下面是他里面的):
total:搜索到的总条数
max_score:所有结果中文档得分的最高分
hits:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息(下面是他里面的):
_index:索引库
_type:文档类型
_id:文档id
_score:文档得分
_source:文档的源数据
文档得分:使用ES时,对于查询出的文档无疑会有文档相似度之别,而理想的排序是和查询条件相关性有关
越高排序越靠前,而这个排序的依据就是_score,比如说我们要查询"5G内存",可能他会给你显示"内存条"出来
这对用户是不友好的,所以该评分也可以说是相似度,一般相似度高的,也就是评分高的,会优先显示
但是一般查询所有,那么就不会考虑相似度,所以也就相当于默认是1,这时就看id排名了
一般以创建顺序或者说创建的时间为主,后创建的在后面,但通常情况下,是随机的,而不是创建顺序
但该随机可能也操作了根据文档信息的排序,所以有一定的规则,比如文档id,1可能一定在3前面,当然这些我们并不需要考虑
否则就看相似度,从而显示谁在前面,当然,权重越大,对应的评分也会使得越大,所以权重也是一个因素
当然创建时间也是一个因素(实际上创建时间基本上是最二因素,通常来说,我们的条件因素是最大的因素)
权重一般需要与其他因素组合才可以超过(大于)创建时间或者条件这两个因素
由于条件最主要,所以可以说不同的条件,可能相同的文档,先后顺序不同,且对应评分不同
条件中,我们指定的字段的字段类型也是一个因素
整型或者说数值类型(并不一定是整数)一般就以权重为主,评分值一般就是权重,即也就是1,因为权重默认为1
而其他的类型,如字符串的类型,比如说text类型,那么会有很多的因素,如权重,创建时间,文档数量等等
当然这些因素的操作可以百度,这里就不说明了,一般来说文档数量的变化很明显(多数情况下)
在少数的情况下,可能部分不会变,自己查看即可,当然,这些因素只需要了解,一般还会有其他的因素的
而正是因为评分,所以前面我们也说过,权重不等于排名,实际上在大多数的情况下,权重代表几率的作用
但是在要保持排名时,即先后顺序时,权重是排名的一个重要因素,而不是几率,如这里
匹配查询(match) :
我们先加入一条数据,便于测试:
PUT /lagou3/goods/3
{
    "title":"小米电视4A",
    "images":"http://image.lagou.com/12479122.jpg",
    "price":3899.00

}

#再次的执行

PUT /lagou3/goods/2
{
    "title":"小米手机4A",
    "images":"http://image.lagou.com/12479124.jpg",
    "price":3799.00

}

PUT /lagou3/goods/9
{
    "title":"电视4A",
    "images":"http://image.lagou.com/12479120.jpg",
    "price":3499.00

}

现在,索引库中有2部手机,2台电视:
or关系:
match 类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or的关系
GET /lagou3/_search
{
    "query":{
      "match":{
        "title":"小米电视" 
        #这里与之前的程序不同,查询条件会进行切分词,即会将查询条件进行切分词(即分词操作)
        #对应的分词只会分词为小米和电视,而没有小米电视
        #因为这是对应的映射的字段操作(没有设置分词,那么使用默认的)
        #只是该映射操作程序之前,即访问后与到程序执行之前
        #他会将该语句条件分开(分词的分开),依次的给程序去匹配,然后操作完后
        #将结果合并(自然是合并到第一个结果返回的数组里面,看程序里就知道有一个数组返回的)
        #即ScoreDoc[] scoreDoc = topDocs.scoreDocs;
        #返回给我们看时,自然是使用这些数组里面包含的id,来进行操作的结果
        #所以会让你以为是程序里面有操作分词(实际上没有,可能现在有了,具体可以百度)
        #当然他这个字段也可能是设置了程序的切分词(先给程序,然后分词匹配)
        #而不是es的语句条件切分词(先分词,然后给程序匹配),但不管是哪一种情况,结果都一样
        #这里就认为是es语句条件切分词
        #这是因为分开的原因,你可以试着将对应的字段的切分词属性删除,添加一个"小",再次的匹配
        #可以发现,匹配了单独的单个汉字,即还是可以匹配,再次的变回来就不可以了
        #所以他是使用es设置的字段的分词(可以操作汉字的分词),从而说明是将语句分开

#所以与程序里面不同的是,他是分词后的匹配,那么可能没有"小米电视"这个整体,当然es也解决了这样问题
#比如后面的精确匹配(即词条匹配,即term 查询)
#我们可以将他说成es不操作分词了,直接给与程序,使得与程序创建的分词结果直接去匹配

#注意:我们去程序匹配时,那么自然是匹配程序里面的创建的索引的分词操作,这里只是指定我们给出的分词与不分词的匹配
#即分词匹配和整体匹配等说明

#大多数的情况下,无论是什么匹配,基本上分词的匹配多一点,因为match用的多
#我们在实际情况下搜索时,也基本是使用match

#注意:写在字段的分词,是操作我们保存文档的,我们可以试着操作就知道了
    }
  }
}


#查询条件:
#在match下查询,查询条件的值会进行分词,小米电视-->小米,电视,前面的这个字段在创建时,操作了ik分词器
#所以会查询对应的结果集,集根据词找到对应的文档数据

在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 or 的关系
结果如下:
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 0.5753642,
        "_source": {
          "title": "小米电视4A", #被分成的小米匹配,电视在后面,自然就没有匹配了
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        }
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "9",
        "_score": 0.2876821,
        "_source": {
          "title": "电视4A", #被分成的电视匹配
          "images": "http://image.lagou.com/12479120.jpg",
          "price": 3499
        }
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "2",
        "_score": 0.2876821,
        "_source": {
          "title": "小米手机4A", #被分成的小米匹配
          "images": "http://image.lagou.com/12479124.jpg",
          "price": 3799
        }
      }
    ]
  }
}
#有三个数据
#但也要注意:他是操作title的域名,自然其他的域名是不会操作的,所以我们也说
#只有该组字段有对应的分词信息才可,其他字段的没有用(前面也说明过了)
and关系:
某些情况下,我们需要更精确查找:
比如在电商平台精确搜索商品时,我们希望这个关系(查询条件切分词之后的关系)变成 and (既要满足你,又要满足我)
而不是只满足切分词中的其中一个即可
可以这样做:
GET /lagou3/_search
{
   "query":{
     "match":{
        "title":{"query":"小米电视","operator":"and"}
     }
   } 
}
本例中,只有同时包含 小米 和 电视 的词条才会被搜索到,实际上相当于精确匹配,因为"小米电视",自然包括了他所有的分词
但与精确匹配不同的是,只要其他的一个组合中,也有小米电视的所有分词,那么他也会匹配
比如"小米电视aa",所以与精确匹配还是有不同的地方的
所以结果如下:
{
  "took": 24,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 0.5753642,
        "_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        }
      }
    ]
  }
}
#只有一个了,而不是两个,即只有都满足,才会合并
一般match需要后面的bool才可以操作多个字段,否则一般只能指定一个字段
虽然bool里面的match属性也只能指定一个字段
就如程序里面的Query query = new TermQuery(new Term(“companyName”,“北”));一样,基本只能指定一个字段和其对应的值作为条件
词条匹配(term):
term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串,keyword类型的字符串
效果类似于:select * from tableName where colName=‘value’;,相当于这样的直接对比匹配
GET /lagou3/_search
{
    "query":{
       "term":{
         "price":3899.00

     }
 }
}
结果如下:
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 1,
        "g_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        }
      }
    ]
  }
}
布尔组合(bool):
bool 把各种其它查询通过 must (与)、 must_not (非)、 should (或)的方式进行组合
他们只能有一个,所以就是说must 不能出现两个及其以上,其他的也是如此
但要注意:虽然match只能有一个,但是对应的must和must_not 和should 使用使用"[]"(中括号)来操作多个macth
举个栗(例,嘿嘿(●ˇ∀ˇ●)请你吃个栗子,简称板栗)子,部分代码比如:
 GET /lagou3/_search
 {
 "query":{
     "bool":{ 
 "should": [
 { 
 "match": 
 { "title": "大米" }
 },
 
 { 
 "match": 
 { "title": "手机" }
 }
       
      ]
      }
      }
      }
从而实现多个字段匹配,而解决了只能操作一个字段的问题
注意:若是空的数组,那么默认没有条件,即相当于查询所有,但是空的集合就会报错,所以基本也只有数组有默认的空操作
实际上可以操作数组的都是如此,比如后面的includes属性,若是空数组,那么也是空操作
即不会过滤掉指定的信息了,即不会不显示指定的字段信息了
即空数组就是空操作,或者说没有操作(因为并不是所以的属性都可以操作数组,比如之前的query,所以他也只能操作单个字段)
所以到那时自己测试即可,但并不绝对,所以注意即可
GET /lagou3/_search
{
  "query":{
     "bool":{ 
     #如果里面没有值,也就是"bool":{},那么代表查询所有,所以自然的
     #评分也就是1了(若操作过滤,那么评分是0)
     #指定的查询,若是数值类型,那么评分一般是权重,否则需要考虑很多因素,比如字符串类型,即如text类型
       "must": { "match": { "title": "大米" }}, 
       "must_not": { "match": { "title": "电视" }}, 
       "should": { "match": { "title": "手机" }}
     }
  }
}

#上面操作的是,包含(可以说必须包含)大米,不包含电视,可以包含手机
#注意:虽然是可以包含,但并不是真正的"或",该"或"是可以的意思,而不是整体的"或"
#在java中"与"优先级大于"或",在外面没有使用括号的情况下
#我们基本也知道该"或"差不多是整体的,也就是只要他"或"操作的结果有一方是true,那么返回的基本就是true
#无论你另外一方的是否是false,但这里的可以包含的意思不是这样
#他只是有该值的都可以匹配(在数组中,只要匹配一个即可,而不必像must一样都进行匹配,这是主要的区别)
#即可以的意思,但没有的自然也是不会匹配
#所以说首先的前提是包含大米,否则后面的自然是不会匹配

#注意:其中操作的是match,也就是说操作分词
#而对应的must和must_not和should是操作分词后来过滤总结果,否则相当于默认操作must(即不写这三个属性的默认)
返回结果:
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "1",
        "_score": 0.5753642,
        "_source": {
          "title": "超大米手机",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3299
        }
      }
    ]
  }
}
那么must和should的主要区别是什么(上面的注释也说明过)
我们可以发现,当他们一起操作时,很明显,should的条件就无关紧要了,因为无论你怎么操作,查询的都是一样的结果
因为必须满足must,但是具体的区别在于单独的操作
由于should是可以的意思,那么在数组中,只需要满足一个即可,而must必须都满足
范围查询(range):
range 查询找出那些落在指定区间内的数字或者时间
GET /lagou3/_search
{
    "query":{
       "range": { #renge里面必须有字段,否则报错,即不能是空的,就如query也不能是空的一样
       #当然这里并不需要理会,注意即可,因为这是语法的规定
       #他也会匹配对应的字段分词
       #这是自然的,因为我们创建文档时,就是操作分词的,这里只是再次的提一下,前面就已经多次说明过了
       #如通过分词来找文档id,域值是分词的(基本会分词,因为有默认或者操作我们自己的分词),这是前面的基础
       #你可以试着将对应的字段修改成title(不是数值类型的,那么会有如下)
       #再添加一个有数字的title,比如"小米手33机",该33会乘100-1,即实际上是3299
       #发现分词的数字若在范围里面也匹配了
          "price": { #如果是空值,即{},那么相当于是所有的范围,即所有的price,相当于没有指定范围
          #可以加上[],但是基本只能是一个{},否则查询不了(查询不了的,可以理解为报错了),所以也通常不加
             "gte": 1000.0,
             "lt": 3700.00 
             #对应的可以是"3700.00",因为若对方是字符串,那么会变成数字类型
             #实际上在真正的存放数据的情况下,是没有数值类型一说的(如记事本的数据),但在程序里,一般都会有
             #因为程序这样设置,主要是为了进行操作计算的
     }
    }
  }
}

返回的结果:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "9",
        "_score": 1,
        "_source": {
          "title": "电视4A",
          "images": "http://image.lagou.com/12479120.jpg",
          "price": 3499
        }
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "超大米手机",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3299
        }
      }
    ]
  }
}
range 查询允许以下字符:

91-Lucene+ElasticSeach核心技术_第65张图片

模糊查询(fuzzy):
fuzzy 查询是 term 查询的模糊等价,很少直接使用它,最主要的就是数据真实查询的问题
当然这是对用户来说的,但有时我们也会使用,虽然很少使用
我们新增一个商品:
POST /lagou3/goods/10
{
 "title":"apple手机", #分词时,分为apple,和手机,没有其他
 "images":"http://image.lagou.com/12479122.jpg",
 "price":6899.00

}

fuzzy 查询是 term 查询的模糊等价,它允许用户搜索词条与实际词条的拼写出现偏差,但是偏差的 编辑距离不得超过2:
GET /lagou3/_search
{
 "query": {
 "fuzzy": {
 "title": "appla" #可以是applaa,正好两个错误,但是applaaa就查不到了

   }
 }
}
#注意:偏差规则是,组合的偏差
#举个例子,假如上面的是查询appee,那么文档的applee,可以匹配吗,答:可以
#因为applee,变成appee只需要删除l即可,然后偏移一个距离,也就是相当于移动了两个e,即编辑距离为2
#那么文档的appue呢,直接的改变代表编辑距离为2
#上面是将文档进行匹配,实际上也是如此,虽然反过来说也可以解释

#注意:偏移距离是可以改变的,操作如下:
"title": 
{
"value":" appee" ,
 "fuzziness": "1"
}
#将这个设置为1,那么你会发现,匹配不到了,因为他的编辑距离是2

#现在设置4及其以上的的编辑距离,看如下的解释
#那么又有一个疑问,那么文档appaa可以吗,答:不可以(特殊情况)
#但是若查询例子是appaa,那么文档appee可以吗,答:也不可以
#所以修改两个及其以上的那么是不会匹配的,并不绝对
#但是,有些时候,却可以有如下的情况
#若查询例子是appaa,那么文档appee可以,但是若查询例子是appee,而文档appaa却不可以
#且一般只有在5个字符以上才可以匹配成功,可能只发现ee有问题,可能还有其他问题
#具体原因,大概是底层的原因,具体可以百度,当然,这并不重要,可能是他的这个相似有什么算法导致的
#这里的解释可以不用理会,因为这是我通过测试得来的,可能是一些其他问题导致的
返回结果:
{
  "took": 85,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.60393023,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "10",
        "_score": 0.60393023,
        "_source": {
          "title": "apple手机",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 6899
        }
      }
    ]
  }
}
那么有个疑问,若有多个是对应的2个或者以内的偏差的数据,是查询所有的还是根据顺序查询一个,答:经过测试,是所有的
也就是说,如果是"applf手机"和"apple手机",同时存在,那么查询出来
结果过滤:
这个过滤是针对于语句或者说结果的过滤,后面有个filter是针对显示的,他们两个虽然都是过滤,但是却是不同的
到后面学习时注意即可
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在 _source 的所有字段都返回
如果我们只想获取其中的部分字段,我们可以添加 _source 的过滤
直接指定字段 :
GET /lagou3/_search
{
 "_source": ["title","price"], #若是空数组,根据前面说过的,则是空操作,即不会过滤
 #只查看这两个数据,操作匹配,好像只能是数组,即[],而不能是对象,即{}
 #但是又有其他的属性操作,那么可以是{},如后面的指定includes和excludes,如果是空的{},那么也是操作所有
 "query": {
   "term": {
     "price": 3899

 }
 }
}
返回的结果:
{
  "took": 16,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 1,
        "_source": {
          "price": 3899,
          "title": "小米电视4A" #发现,的确只查看了这两个数据
        }
      }
    ]
  }
}
那么如果对应的_source的值是空集合,那么是什么情况
答:如果没有指定,那么默认是显示所有,所以对应的三个属性应该都会显示
指定includes和excludes:
我们也可以通过:
includes:来指定想要显示的字段,若有这个,那么只会显示这里面的字段,而不会显示他没有指定的
excludes:来指定不想要显示的字段,无论是否有includes,他都会操作不显示指定字段,自己测试就知道了
二者都是可选的
GET /lagou3/_search
{
  "_source": {
    "includes":["title","price"],  
    "excludes":["title"]
     #上面两个若是空数组,前面说过的,则是空操作,即不会过滤,不能是空集合{},否则报错
 },
  "query": {
    "term": {
      "price": 3899

  }
 }
}

#之前的_source的指定显示可以理解成默认为操作includes,且只操作includes
#那么上面的title会显示吗,答:不会,即excludes优先
#对于优先的理解,大多数情况下(一起的说明也基本是这样)是只操作优先的
#而不会操作不优先的,即名额只有一个,所以也只能操作优先的
#所以可以这样说,excludes除了是操作普通的不显示外,主要是限制includes的显示的
返回的结果:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 1,
        "_source": {
          "price": 3899
        }
      }
    ]
  }
}
过滤(filter):
Elasticsearch 使用的查询语言(DSL)拥有一套查询组件,这些组件可以以无限组合的方式进行搭配
这套组件可以在以下两种情况下使用:过滤情况(filtering context)和查询情况(query context)
如何选择查询与过滤:
通常的规则是,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响相关性得分的搜索
除此以外的情况都使用过滤(filters)
条件查询中进行过滤:
所有的查询基本都会影响到文档的评分及排名(条件的因素,基本是最大的因素)
如果我们需要在查询结果中进行过滤,并且不希望过滤条件(即改变条件) 影响评分
那么就不要把过滤条件的具体操作,变为查询条件来用,而是使用 filter 方式:
#先执行或者更新如下:
PUT /lagou3/goods/1
{
    "title":"小米手机",
    "images":"http://image.lagou.com/12479122.jpg",
    "price":3899.00

}


GET /lagou3/_search
{
    "query":{
        "bool":{
       "must":{ "match": { "title": "小米手机" }},
       "filter":{ #一般与bool的属性一起操作,其他的操作,好像并没有结合,具体自己可以百度查看
                "range":{"price":{"gt":2000.00,"lt":3800.00}}
       }
       }
   }
}

#上面的查询不到,刷选的没有满足,将3800修改成3900,即可
简单的理解:假设你在京东或者拼多多里查询"手机",那么首先,"手机"这个是我们的查询条件
得到结果后,我们可以进行刷选,而这个刷选的过程中我们也知道查询的条件是没有变化的
所以这些刷选的操作就是过滤的操作,一般来说,过滤是对数据的一种显示或者不显示的一种操作
比如不会显示指定的,或者说只会显示我们指定的,当然,这些是看当时的解释的,所以并不绝对
而这里的解释很明显,是只会显示我们指定的,所以上面的filter就是类似于刷选的操作
由于是刷选结果,那么肯定的,没有满足的自然不会显示,当然,是在返回给用户看之前进行的刷选
无查询条件,直接过滤:
如果一次查询只有过滤,没有查询条件,不希望进行评分(评分默认为1,而不是权重的值,因为没主体)
我们可以使用 constant_score 取代只有 filter 语句的 bool 查询
在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助
GET /lagou3/_search
{
    "query":{
        "constant_score": { #不操作bool,那么相当于查询所有,只是评分是1,而不是0
     "filter":{
            "range":{"price":{"gt":2000.00,"lt":3900.00}}
        }
        
     }
  
}
}

#这样也可以,只是评分默认为0
GET /lagou3/_search
{
 "query":{
 "bool":{
 
"filter":{  
#过滤里面一般要指定值,否则报错,一般也可以操作多个的,即加上[]
#相当于返回代表对应的都要满足的结果
"range":{"price":{"gt":2000.00,"lt":3900.00}}
}
}
 }
}


#在评分相同的情况下,一般就是看创建顺序或者说创建的时间了
#一般是随机的,但有一定的规则,比如文档id,1可能一定在3前面,2一定在1上面等等,即是有序的,虽然也可以说是随机
#当然这些我们并不需要考虑
#虽然他这个顺序或者说创建的时间也影响评分,但也只是在他不使得因素评分相同的操作的情况下,否则就是上面的情况
排序:
单字段排序 :
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式
#重置lagou3的数据
DELETE lagou3

PUT lagou3
{
 "mappings": {
   "goods": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
       }
     },
      "dynamic_templates": [
       {
          "strings": {
            "match_mapping_type": "string", 
            "mapping": {
              "type": "keyword",
              "index":false,
              "store":true
           }
        }
      }
    ]
  }
 }
}

PUT /lagou3/goods/3
{
    "title":"小米电视4A",
    "images":"http://image.lagou.com/12479122.jpg",
    "price":3899.00

}

PUT /lagou3/goods/2
{
    "title":"小米手机4A",
    "images":"http://image.lagou.com/12479124.jpg",
    "price":3799.00

}

#上面的是重新给值

GET /lagou3/_search
{
  "query": {
    "match": {
      "title": "小米手机"
   }
  },
 "sort": [ 
 #如果是单独的,那么可以去掉中括号,但一般也只能排序float类型的字段,(默认智能判断时,有小数的是该类型)
 #否则查询失败,这里注意即可
  {
    "price": {
        "order": "desc" #从大到小,即降序,若是asc,即是从小到大,那么就是升序
    }
   }
 ]
}

返回结果:
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": null,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": null,
        "_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899 
        },
        "sort": [
          3899
        ]
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "2",
        "_score": null,
        "_source": {
          "title": "小米手机4A",
          "images": "http://image.lagou.com/12479124.jpg",
          "price": 3799
        },
        "sort": [
          3799
        ]
      }
    ]
  }
}

#上面是3899到3799,我们可以将desc修改成asc,那么结果是3799到3899,即从小到大
#一般说什么从大到下,和从小到大的,都是从上往下看,或者从左往右看

#最后注意:他基本只能是操作整型(也说成数值类型,并不一定是整数)和不分词的字符串
#即keyword(若操作这个,那么基本只看顺序了)
#所以这里将字段修改成title是会报错的

#当然,由于是数组,那么可以指定多个,多个中,谁在前面,优先考虑谁,然后在考虑后面的,当都相同时
#那么按照之前的创建顺序的介绍(基本随机但却有序)排列
多字段排序:
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按 照相关性得分排序:
GET /lagou3/_search
{
    "query":{
      "bool":{
       "must":{ "match": { "title": "小米手机" }},
        "filter":{
          "range":{"price":{"gt":2000,"lt":300000}} #没有price自然不会是匹配的
       }
    }
   },
   "sort": [
  { "price": { "order": "desc" }},
  { "_score": { "order": "desc" }}
 ]
}



返回结果:
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": null,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 0.2876821,
        "_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        },
        "sort": [
          3899,
          0.2876821
        ]
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "2",
        "_score": 0.5753642,
        "_source": {
          "title": "小米手机4A",
          "images": "http://image.lagou.com/12479124.jpg",
          "price": 3799
        },
        "sort": [
          3799,
          0.5753642
        ]
      }
    ]
  }
}

#先考虑price,然后考虑_score,类似于mysql一样,有先后的顺序考虑
分页:
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回
那么,如果要实现分页查询该怎么办呢?
elasticsearch的分页与mysql数据库非常相似,都是指定两个值:
from:目标数据的偏移值(开始位置),默认from为0
size:每页大小
GET /lagou3/_search
{
  "query": {
    "match_all": {}
 },
  "sort": [
   {
      "price": {
        "order": "asc"

     }
   }
 ],
  "from": 3, 
  #若from不写,默认是0,若是小于0的,也默认是0,若是大于0的,则是大于0的,取整数,小数不算
  #也需要符合数据,所以01.5不可以,但1.5可以
  "size": 3

#代表从3开始,选择3个,这看起来就是第二页
#当然,你最好将from设置为0,size设置为10,来查看数据,然后依次根据自己来进行改变测试即可
}

返回结果(from设置为0,size设置为10):
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2, 
    #注意:这个值与分页没有关系,比如我们不通过分页查询的是4条,但分页只查看2条,那么这个值是4,而不是2
    #注意即可
    "max_score": null,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "2",
        "_score": null,
        "_source": {
          "title": "小米手机4A",
          "images": "http://image.lagou.com/12479124.jpg",
          "price": 3799
        },
        "sort": [
          3799
        ]
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": null,
        "_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        },
        "sort": [
        #出现的排序信息,当然,若没有对应的字段,那么一般是Infinity(asc是这个)或者-Infinity(desc是这个)
        #那么这时在相同的结果排序中,那么就操作顺序了(随机却有序的)
          3899
        ]
      }
    ]
  }
}
高亮:
高亮原理:
服务端搜索数据,得到搜索结果
把搜索结果中,搜索的关键字都加上约定好的标签
前端页面获得该文档数据时,由于对应的数据加上了写好的标签的CSS样式
那么浏览器或者其他可以渲染的,自然就会进行渲染,即可实现高亮或者其他的样式显示
elasticsearch中实现高亮的语法比较简单:
GET /lagou3/_search
{
  "query": {
    "match": {
      "title": "小米手机"

   }
 },
  "highlight": {
  "pre_tags": "",
    "post_tags": "", 
    #上面两个值基本可以随便写,也就是说,并不是必须是标签格式,只是为了满足前端渲染,所以一般我们加上标签格式
    #但是基本只能是String类型,即必须加上"",否则执行时报错(无论是否有查询)
    #但一般我们都以查询为主的解释报错
    #其他的出来高亮的操作基本也是如此,这里注意即可
    #因为查询是主要的,其他的增删改基本很少操作,虽然报错与查询无关
    "fields": {
      "title": {}  
      #将上面的设置给该标签对应的所有分词或者整体
      #或者说即指定我们的条件(若条件中没有,自然不会操作)中的某一个字段的分词操作或者整体操作作为样式的添加
      
      #所以说,上面的条件是操作title字段,那么操作样式的字段也要是title,否则基本操作不了
      #且要注意:基本只能操作一个字段,也就是说,只能操作一个macth的字段
     
   }
 }
}

 #但是如果通过bool,那么可以操作多个字段
 #注意:下面是测试,测试完后,可以删除或者重新的操作lagou3,即后面的就是删除的
    # 比如:
      "fields": {
     "title": {},
      "ii":{}
     }
#实际上这样也可
"fields": [{
      "title": {}},{
      "ii":{}
      
    }]

#一样的结果
#对应的bool是:
GET /lagou3/_search
 {
 "query":{
     "bool":{ 
 "should": [
 { 
 "match": 
 { "title": "大米" }
 },
 
 { 
 "match": 
 { "ii": "手机" }
 }
       
      ]
      }
      },
       "highlight": {
    "pre_tags": "",
    "post_tags": "",
    "fields": [{
      "title": {}},{
      "ii":{}
      
    }]
    
  }
      }
      
      #添加的ii字段
       PUT /lagou3/_mapping/goods
   {
     "properties": {
       "ii":{
         "type": "text",
         "analyzer": "ik_max_word"
       }
     }
   }
   
   #添加的值
   PUT /lagou3/goods/444
  {
    "title":"超大米手机大米手机",
    "ii":"手机"
  }

#返回的文档具体结果是:
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1.2118783,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "444",
        "_score": 1.2118783,
        "_source": {
          "title": "超大米手机大米手机",
          "ii": "手机"
        },
        "highlight": {
          "ii": [
            "手机"
          ],
          "title": [
            "超大米手机大米手机"
          ]
        }
        #可以发现,有两个数据,即的确可以操作多个字段
      }
    ]
  }
}
在使用match查询的同时,加上一个highlight属性:
pre_tags:前置标签
post_tags:后置标签
fields:需要高亮的字段
title:这里声明title字段需要高亮,需要指定我们查询的字段,否则其他的字段基本不会操作
结果(也就是返回结果):
#这里是没有测试的,或者测试的痕迹没有了,即删除了
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "24",
        "_score": 0.5753642,
        "_source": {
          "title": "小米手机4A",
          "images": "http://image.lagou.com/12479124.jpg"
        },
        "highlight": {
          "title": [
            "小米手机4A"
          ]
        }
      },
      {
        "_index": "lagou3",
        "_type": "goods",
        "_id": "3",
        "_score": 0.2876821,
        "_source": {
          "title": "小米电视4A",
          "images": "http://image.lagou.com/12479122.jpg",
          "price": 3899
        },
        "highlight": {
          "title": [
            "小米电视4A"
          ]
        }
      }
    ]
  }
}

#我们可以发现,返回结果他多出了一个属性
#他将我们匹配中的字段都进行了高亮,即小米手机的分词的小米和手机都加上了标签
#所以如果给到前端,那么自然是可以使用这个属性,从而使用这些标签的,那么如何使用这个标签:
#我们可以让前端自行解析文档,我们也可以让后端使用程序解析
#所以我们完全可以通过程序,使得覆盖返回的数据中,使得返回高亮属性里面的title字段而不是正常的title字段
#虽然这个em标签是使得字体斜体,但也说明了高亮的原理
#就是改变原来的返回数据,自然渲染时,也使用了标签

#但也要注意:一般他的分词并不会全部满足,比如"中国人"一般来说"中国人"会分成"中国人","国人","中国"
#但是如果你查询的数据中,是"中国人",那么只会是"中国人"整体,即在"中国人",前后加上标签
#而不会在"国人","中国"加上标签,这是因为"中国人",比"国人","中国"更加的有效
#一般我们将这样的,称为最佳分词,基本上所有的高亮操作都是操作最佳分词的,这里注意即可

#当然了,如果是没有使用分词器的,那么一般都会加上标签,所以实际上最佳分词是分词器给高亮的一种特殊操作
#即这是分词器的操作,而不全是es自己的规定,或者也可以说不是es的规定

聚合aggregations:
聚合可以让我们极其方便的实现对数据的统计、分析
例如: 什么品牌的手机最受欢迎? 这些手机的平均价格、最高价格、最低价格? 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果
基本概念 :
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫 桶 ,一个叫 度量 :
桶(bucket) 类似于 group by
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个 桶
例如我们根据国籍对人 划分,可以得到 中国桶 、 英国桶 , 日本桶等等
或者我们按照年龄段对人进行划分:0 ~ 10,10 ~ 20,20 ~ 30,30 ~ 40等
Elasticsearch中提供的划分桶的方式有很多:
Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
Histogram Aggregation:根据数值阶梯分组,与日期类似,需要知道分组的间隔(interval)
Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
当然还有其他,就不依次的说明了,具体可以百度(虽然也并不是非要百度,只要是可以查询问题的地方都可以)
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算
因此往往bucket中 往往会嵌套另一种聚合:metrics aggregations即度量
度量(metrics) 相当于聚合的结果
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为 度量
比较常用的一些度量聚合方式:
Avg Aggregation:求平均值
Max Aggregation:求最大值
Min Aggregation:求最小值
Percentiles Aggregation:求百分比
Stats Aggregation:同时返回avg、max、min、sum、count等
Sum Aggregation:求和
Top hits Aggregation:求前几
Value Count Aggregation:求总数
当然还有其他,就不依次的说明了,具体可以百度
为了测试聚合,我们先批量导入一些数据
创建索引:
PUT /car
{
  "mappings": {
    "orders": {
      "properties": {
        "color": {
          "type": "keyword"
       },
       "make": {
         "type": "keyword"
      }
    }
  }
 }
}

注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词,必须使用keyword 或 数值类型
前面在排序的时候也说明过
其中过滤(即filter属性,一般在bool属性里面,即在他的bool的下一级)还要特殊一些(本质上还是自己设置的属性)
所以他过滤的操作中,是可以操作分词的,这也使得,只要匹配自己设置的属性,那么也会匹配
而为了真正的过滤,所以过滤也最好是使用keyword 或 数值类型来整体匹配
而使得不会误过滤(不小心过滤其他的了,因为分词),即总体来说必须使用keyword 或 数值类型
实际上数值类型好像并不能分词,所以实际上操作,数值类型就是整体,所以会以为过滤是操作整体的
这里我们将color和make这两个文字类型的字段设置为keyword类型,这个类型 不会被分词,将来就可以参与聚合
导入数据,这里是采用批处理的API,大家直接复制到kibana运行即可:
POST /car/orders/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2020-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2020-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2020-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2020-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2020-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2020-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2020-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2020-02-12" }

#对应的id是随机生成的,但是该随机生成的,却是也会操作顺序的随机(虽然也是有序)
#大概是id的原因吧,就如1基本在3上面一样,2在1上面(前面说过了),即是有序的,虽然也可以说是随机

#也可以像下面这样依次执行

POST /car/orders/1
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2020-10-28" }

POST /car/orders/2
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2020-11-05" }

POST /car/orders/3
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2020-05-18" }

POST /car/orders/4
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2020-07-02" }

POST /car/orders/5
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2020-08-19" }

POST /car/orders/6
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2020-11-05" }

POST /car/orders/7
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2020-01-01" }

POST /car/orders/8
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2020-02-12" }

聚合为桶:
首先,我们按照 汽车的颜色 color来 划分 桶 ,按照颜色分桶,最好是使用Terms Aggregation类型,按 照颜色的名称来分桶
GET /car/_search
{
    "size" : 0,
    "aggs" : {  #一般可以操作多个名称,实现多聚合,但是就如mysql一样,不能指定多字段
    #因为总不是不同的结果是一个组吧,比如分组性别,假如可以两次字段或者以上,那么分为女和男
    #很明显,女和男是不能同一组的,所以一般分组只能是一个字段
       "popular_colors" : { 
           "terms" : { 
             "field" : "color"
             #"size": 2,可以加上这个语句,与field平级,代表显示分组个数
             #则默认都显示,必须大于0,否则报错
             #自然是取整的,即1.5是1

     }
   }
  }
}
#除了定义多个名称,可以给分组后的再次分组,但我们通常也只会操作到度量,而不会分组(除非是很复杂的需求)


#注意:我们之前的语句中,只要是平级的,基本不限先后位置,注意即可
size: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率
aggs:声明这是一个聚合查询,是aggregations的缩写
popular_colors:给这次聚合起一个名字,可任意指定
terms:聚合的类型,这里选择terms,是根据词条内容(这里是颜色)划分
field:划分桶时依赖的字段
返回结果:
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 8,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "popular_colors": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "红",
          "doc_count": 4
        },
        {
          "key": "绿",
          "doc_count": 2
        },
        {
          "key": "蓝",
          "doc_count": 2
        }
      ]
    }
  }
}
#的确将字段color进行分组了,且是根据内容来分组的
hits:查询结果为空,因为我们设置了size为0,否则一般是根据查询所有的结果来进行排序的,自己测试就知道了
aggregations:聚合的结果
popular_colors:我们定义的聚合名称
buckets:查找到的桶,每个不同的color字段值都会形成一个桶
key:这个桶对应的color字段的值
doc_count:这个桶中的文档数量 通过聚合的结果我们发现,目前红色的小车比较畅销!
桶内度量:
前面的例子告诉我们每个桶里面的文档数量,这很有用
但通常,我们的应用需要提供更复杂的文档度量
例如,每种颜色汽车的平均价格是多少? 因此,我们需要告诉Elasticsearch 使用哪个字段 , 使用何种度量方式 进行运算
这些信息要嵌套在 桶内, 度量 的运算会基于 桶 内的文档进行 现在,我们为刚刚的聚合结果添加 求价格平均值的度量:
GET /car/_search
{
    "size" : 0,
    "aggs" : { 
        "popular_colors" : { 
            "terms" : { 
            "field" : "color"
           },
            "aggs":{
                "avg_price": { 
                   "avg": {
                      "field": "price" 
                   }
               }
           }
       }
   }
}

#实际上就是在分组后的显示里面再次给出显示
aggs:我们在上一个aggs(popular_colors)中添加新的aggs,可见度量也是一个聚合
avg_price:聚合的名称
avg:度量的类型,这里是求平均值
field:度量运算的字段
返回的结果:
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 8,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "popular_colors": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "红",
          "doc_count": 4,
          "avg_price": {
            "value": 32500
          }
        },
        {
          "key": "绿",
          "doc_count": 2,
          "avg_price": {
            "value": 21000
          }
        },
        {
          "key": "蓝",
          "doc_count": 2,
          "avg_price": {
            "value": 20000
          }
        }
      ]
    }
  }
}

#可以看到每个桶中都有自己的 avg_price 字段,这是度量聚合的结果,简称对聚合的聚合操作
如果是这样:
GET /car/_search
{
    "size" : 0,
  
              "aggs":{
               "avg_price": { 
                   "avg": {
                      "field": "price" 
                    }
             
         
    }
 }
}

那么返回的结果是:
{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 8,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "avg_price": {
      "value": 26500
      #返回的结果有点不同而已,但具体信息已经给出,实际上是分组类型的原因导致的返回信息不同
      #因为一个操作组或者说操作多个,一个操作单个值或者说操作一个
      #你可以将对应的算平均值的avg,改成terms就知道了
      #但通常来说,我们不会再次的分组(除非是很复杂的需求),否则基本没有意义
      #因为这时基本只针对上次的一样分组的,那么基本是同样的结果
      #如根据颜色分一次,然后再根据颜色还分一次(不复杂的,基本没有其他的可分,就算要分,也是没有意义的)
      #第二次的结果自然与第一次一样,所以没有意义
      #其实就算不操作同一个分组操作,也是没有意义,因为我们并不需要
      #所以一般第二个aggs操作的是度量,即计算的操作
    }
  }
}
所以,一个aggs代表操作对象,即上面的是以全部文档进行操作
而aggs里面的aggs,代表操作第一个aggs操作后的文档,也就是对应的组
而正是因为这个size在最外面,所以是显示查询所有的,即全部的文档数据(所以评分也就是1,如是0,那么评分自然也是0)
自然是从上到下的依次取得数据
但是size也基本只能与第一个aggs平级或者与field平级
所以基本是看不到对应部分(分组后)的文档了,当然,size可以设置显示,前面也说过
注意:只要size不是大于等于0的,默认是查询所有,自然也是取整的,即1.5是1
当然,size是设置上限的,即最大多少,自然没有超过的,也会显示
Elasticsearch集群:
在之前的操作中,我们都是使用单点的elasticsearch,接下来我们会学习如何搭建Elasticsearch的集 群
单点的问题 :
单点的elasticsearch存在哪些可能出现的问题呢?
单台机器存储容量有限,无法实现高存储
单服务器容易出现单点故障,无法实现高可用
单服务的并发处理能力有限,无法实现高并发
所以,为了应对这些问题,我们需要对elasticsearch搭建集群
集群的结构:
数据分片:
首先,我们面临的第一个问题就是数据量太大,单点存储量有限的问题。
大家觉得应该如何解决? 没错,我们可以把数据拆分成多份,每一份存储到不同机器节点(node)
从而实现减少每个节点数 据量的目的,这就是数据的分布式存储,也叫做: 数据分片(Shard)

91-Lucene+ElasticSeach核心技术_第66张图片

数据备份(注意这是备份,而不是同步复制,即不算节点之间的同步复制):
数据分片解决了海量数据存储的问题,但是如果出现单点故障或者分片集群全部故障(这个基本不会)
那么分片数据可能就不再完整,这又该如何 解决呢?
没错,就像大家为了备份手机数据,会额外存储一份到移动硬盘一样
我们可以给每个分片数据进行备 份,存储到其它节点
防止数据丢失,这就是数据备份,也叫 数据副本(replica)
数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高 了!
为了在高可用和成本间寻求平衡,我们可以这样做:
首先对数据分片,存储到不同节点
然后对每个分片进行备份,放到对方(可以是其他的节点或者是分片节点也或者是下一个分片节点)节点
完成互相备份,这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:

91-Lucene+ElasticSeach核心技术_第67张图片

我们可以发现,对应的0到2中,外框框颜色深的,代表原数据,颜色不深的代表副本数据
上面有三个数据,0,1,2
很明显,0将他自己复制给了下一个分片(即分片节点,简称分片)
1将他自己复制给了下一个分片,最后一个将自己复制给第一个分片
但实际上我们也通常是复制给除了自己的其他所有节点,实现真正的保存,但这些都就与分片的初衷违背了
因为这样说的话,那不就是一个节点拥有全部的数据了,所以违背了分片,所以一般是部分的复制,通常是给下一个节点
所以在这个集群中,如果出现单节点故障,并不会导致数据缺失,所以保证了集群的高可用,同时也减少了 节点中数据存储量
并且因为是多个节点存储数据,因此用户请求也会分发到不同服务器,并发能力也 得到了一定的提升
实际上对分片再次的集群也是可以的,但就如我们说的,需要多个节点,实际上也可以防止分片集群都挂掉的情况
即这种是与分片集群同样的一种方式,虽然比对应的单个节点有更多数据了,但也少了节点
但在以后的扩展中(性能也会少点),扩展性还是没有集群在集群里的好扩展(虽然需要多个节点)
各有利弊,或者说,就是用性能换成本,实际上可以理解为将对方看成我对应的存放节点
通常他们是没有同步的,需要我们手动复制,但是并不绝对
现在大概有可以复制节点到集群的了(注意不是同步,一般可以通过配置就可以解决)
我们就以这个为例子
搭建集群 :
集群需要多台机器,我们这里用一台机器来模拟,因此我们需要在一台虚拟机中部署多个elasticsearch
节点,每个elasticsearch的端口都必须不一样
一台机器进行模拟:将我们的ES的安装包复制三份,修改端口号,data和log存放位置的不同
但是在实际开发中,我们最好是将每个ES节点放在不同的服务器上
我们计划集群名称为:lagou-elastic,部署3个elasticsearch节点,分别是:
node-01:http端口9201,TCP端口9301
node-02:http端口9202,TCP端口9302
node-03:http端口9203,TCP端口9303
http:表示使用http协议进行访问时使用 端口,如使用elasticsearch-head、kibana、postman操作的端口,默认端口号 是9200
tcp:集群间的各个节点进行通讯的端口,默认9300
第一步:复制es软件粘贴3次,分别改名

91-Lucene+ElasticSeach核心技术_第68张图片

第二步:修改每一个节点的配置文件 config下的elasticsearch.yml
下面已第一份配置文件为例,三个节点的配置文件几乎一致
除了:node.name、path.data、path.log、http.port、transport.tcp.port
注意:由于对应的配置,基本都是注释的,所以我们可以将对应的elasticsearch.yml配置文件内容全部删除
直接复制下面的配置文件粘贴即可
node-01(9201的配置):
#允许跨域名访问
http.cors.enabled: true
#当设置允许跨域,默认为*(一般不写,那么默认是这个),表示支持所有域名
http.cors.allow-origin: "*"
#允许所有节点访问
network.host: 0.0.0.0
# 集群的名称,同一个集群下所有节点的集群名称应该一致
cluster.name: lagou-elastic
#当前节点名称 每个节点不一样,如果是一样,虽然也可以访问,但是对于节点机器的判别(查看),不友好
node.name: node-01
#数据的存放路径 每个节点不一样,不同es服务器对应的data和log存储的路径不能一样
path.data: d:\class\es-9201\data
#日志的存放路径 每个节点不一样
path.logs: d:\class\es-9201\logs
#上面的两个路径也基本不能一样,否则报错,因为就如我们之前说的被占用了,如果不占用,自然可以设置
#当然,若你非要一样,那么你不指定对应的占用的文件就可以了,也可以指定占用文件里面的没占用的,比如\data\t
#或者解除对应data的占用(删除node.lock文件就不会占用了,这时我们也可以删除data,也可以使得占用)
#当然,前面说过了这个data的占用解除,删除后,然后就可以设置对应的data
#当然logs里面的内容基本都是占用的,所以大概是一直占用的,基本解除不了
#上面这些了解即可,并不需要死磕

# http协议的对外端口 每个节点不一样,默认:9200
http.port: 9201
# TCP协议对外端口 每个节点不一样,默认:9300
transport.tcp.port: 9301
#三个节点相互发现,包含自己,使用tcp协议的端口号
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1
discovery.zen.minimum_master_nodes: 2
# 是否为主节点,对应的真正的主节点,还是需要上面的选举算法,一般是半数机制,即半数以上的为主
# 如果是false,那么无论怎么选举,都基本不会是主节点,或者说,就算有票,也不会变成主
# 那么第二个(下一个)的基本就是主了

#注意:一般说的半数机制,只是针对于选举来说的,而不是机器的半数(基本只有zookeeper独有)
#但这里需要集群,而集群需要主,所以这里最少是2台机器
#而不是3台(zookeeper规定的半数,也针对于机器半数以上,而不只是投票,其他的基本都只是投票)
node.master: true

#当然,yml由于与properties类似的操作,所以也可以直接的指定,不用有对应的格式,即可以node.master: true
#但是properties反过来不可以有yml的格式(yaml与yml一样的解释,所以说明yml也就相当于说明yaml)
node-02(9202的配置):
#允许跨域名访问
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 0.0.0.0
# 集群的名称
cluster.name: lagou-elastic
#当前节点名称 每个节点不一样
node.name: node-02
#数据的存放路径 每个节点不一样
path.data: d:\class\es-9202\data
#日志的存放路径 每个节点不一样
path.logs: d:\class\es-9202\logs
# http协议的对外端口 每个节点不一样
http.port: 9202
# TCP协议对外端口 每个节点不一样
transport.tcp.port: 9302
#三个节点相互发现
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1
discovery.zen.minimum_master_nodes: 2
# 是否为主节点
node.master: true
node-03(9203的配置):
#允许跨域名访问
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 0.0.0.0
# 集群的名称
cluster.name: lagou-elastic
#当前节点名称 每个节点不一样
node.name: node-03
#数据的存放路径 每个节点不一样
path.data: d:\class\es-9203\data #d和D是一样的,大多数情况下,都可以,但最好是d,前面说过了
#日志的存放路径 每个节点不一样
path.logs: d:\class\es-9203\logs
# http协议的对外端口 每个节点不一样
http.port: 9203
# TCP协议对外端口 每个节点不一样
transport.tcp.port: 9303
#三个节点相互发现
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1
discovery.zen.minimum_master_nodes: 2
# 是否为主节点
node.master: true
注意:由于是yml文件,所以对应的":"后面需要空格隔开(无论多少空格,但必须要有,换行也不可以)
这是yml文件的语法,就不多说了,如果不遵守,否则可能启动不了,自己测试即可
第三步:启动集群
把三个节点分别启动,启动时不要着急,要一个一个地启动(从9201,9202,9203,依次启动过去)
实际上并没有先后之分,主要是为了操作选举的
所以启动的顺序可能会导致谁为主,当然在这里,谁为主,并不重要,所以这里就不分先后的启动也可以
但也要注意,文件的编码最好是UTF-8,如果改变的编码且内容改变,那么不要动
否则复制时,需要改成UTF_8,否则你可能启动不了,因为文件到程序,是自然需要经过文件对应的编码来操作的
使用head插件查看(前面在浏览器中安装了):

91-Lucene+ElasticSeach核心技术_第69张图片

上面的星星符号代表为主节点,一般是启动的先后造成的,很明显,他大概是第二次的启动
即一般是根据启动的先后进行选举,但就算是后启动的
也可能不会是主节点(因为可能已经选举完了,或者是结合随机的有序指定,注意:在集群中,最少两个才会显示)
所以并不绝对,注意即可
注意:等待完全启动,否则可能启动过慢,后启动的可能会变成主节点
注意:若他们之间的连接是已经有互相发现了,那么在这个基础上,必须名称一致,否则启动失败
如果没有互相发现,那么自然不会是在一个集群,且可以启动,但是不能访问
还需要将没有投票的这个discovery.zen.minimum_master_nodes: 2,删除或者注释
这也就可以访问了
测试集群中创建索引库:
配置kibana的kibana.yml文件,再重启
配置如下:
elasticsearch.url: "http://localhost:9201"
#需要解除注释修改了,虽然默认是9200
搭建集群以后就要创建索引库了,那么问题来了,当我们创建一个索引库后,数据会保存到哪个服务节 点上呢?
如果我们对索引库分片,那么每个片会在哪个节点呢?
这个要亲自尝试才知道,点击如下:

91-Lucene+ElasticSeach核心技术_第70张图片

分片默认是5片,即一般最大的分片节点是5,若不足,那么自然是操作对应的分片
而一般会放在一起,即一个节点可能有两个主分片
副本默认是1个,副本数为1,表示为每一个分片做一个副本
也就是5+5=10个分片数据了,但我们通常不会设置成分片数及其以上的副本,即在这里不会设置5及其以上,最大设置为4
因为这里设置5及其以上,那就相当于没有操作分片,或者多余分片,使得看起来原来的数据做集群了,或者再次的保存部分信息
所以我们也通常,不能大于等于分片数,当然你也可以大于等于分片数,无非就是多保存
只是使得节点数据更多了而已(这样对性能不太友好)
因为虽然我们是用性能换成本,但也不能浪费太多的性能,要在合理范围内,否则得不偿失,所以也通常不大于等于分片数
注意:对应的设置,也基本只能是整型数值,而不能是小数或者其他类型的数,否则报错
注意:若点击ok并没有作用,那么可以进入kibana创建(但这是刷选的界面是有问题的),然后都重启,那么就可以了
大概是因为head之间的联系有问题吧或者配置的作用也出现了问题,但这些基本是框架的问题,重启即可
具体可以百度,或者下载其他版本的框架来解决这些问题,一般的解决方案是,在对应的head里面找到vendor.js
将对应的6886行和7573行的application/x-www-form-urlencoded修改成application/json;charset=UTF-8即可
然后点击ok,等待许久一般就会出现提示(也就是kibana的创建成功的返回数据)
或者直接的在kibana查看,会发现,创建成功了
还记得创建索引库的API吗?
我们输入如下:

91-Lucene+ElasticSeach核心技术_第71张图片

点击ok,那么就相当于执行后面的代码
请求方式:PUT
请求路径:/索引库名
请求参数:json格式:
{
    "settings": { #之前没有操作过该属性的内容,现在开始操作
        "属性名": "属性值"

     }
}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认
这里给搭建看看集群中分片和备份的设置方式,示例:
DELETE /lagou

#下面就是上面的图片中点击ok执行的操作,只是变成了图形化而已,而不用我们输入命令
PUT /lagou  #可能集群环境下,如果是有问题的
#那么创建索引可能不会有返回值出现,一般是有问题的返回值,等待即可,但还是创建了,与原来的创建成功返回值不同
#虽然head不是该返回值,即kibana的该地方是特殊的,注意一下即可
#因为基本要在创建时,才可以操作settings,所以上面先进行删除
#防止已经有了,虽然没有(因为我这里是第一次)
{
  "settings": {
    "number_of_shards": 3, #一般默认为5
    "number_of_replicas": 1 #一般默认为1

 }
}
这里有两个配置:
number_of_shards:分片数量,这里设置为3
number_of_replicas:副本数量,这里设置为1,每个分片一个备份,一个原始数据,共2份
通过chrome浏览器的head查看,我们可以查看到分片的存储结构:
当没有分片成功时,一般会出现如下:

91-Lucene+ElasticSeach核心技术_第72张图片

91-Lucene+ElasticSeach核心技术_第73张图片

上面是我操作了分片数为3,所以对应的分片是0到2,由于副本是1,所以对应的0到2都有一个副本
我们可以发现,对应的0到2中,外框框颜色深的,代表原数据,颜色不深的代表副本数据
上面的Unassigned代表没有分片的显示,简称为未分配的分片
实际上若我们操作大于等于分片数的副本,他还是会继续的给分片的,自己测试就知道了
当然,具体如何分,一般是随机但有序的,这些并不需要在意
如果分片成功,那么会出现如下:

91-Lucene+ElasticSeach核心技术_第74张图片

91-Lucene+ElasticSeach核心技术_第75张图片

我们可以看到右上角是6 of 6,在前面的图片中,那么没有分配的,一般是0 of 6代表没有健康的
那么为什么会没有分配呢,这有很多种情况,当然,大多数的情况
是磁盘空间的太小,只要该磁盘(通常是盘符,如c盘,d盘等)占用了90%空间
那么就会导致不会分配分片,默认上限是90%,包括90%
当然也是可以设置的,如设置成上限是85%,具体可以百度查看,这些了解即可
可以看到,lagou这个索引库,有三个分片,分别是0、1、2,每个分片有1个副本,共6份
node-01上保存了1号分片和0号分片的副本
node-02上保存了2号分片和1号分片的副本
node-03上保存了0号分片和2号分片的副本
很明显,他将副本分片给下一个分片,但是起始的分片基本还是随机的(重启就知道了)
现在我们看看若是多个索引,那么节点是如果操作的,首先回到kibana删除lagou索引,创建如下:
PUT /lagou1
{

    "number_of_shards": 3,
    "number_of_replicas": 1

 
}

PUT /lagou2
{

    "number_of_shards": 5,
    "number_of_replicas": 1

 
}

对应的显示:

91-Lucene+ElasticSeach核心技术_第76张图片

91-Lucene+ElasticSeach核心技术_第77张图片

由于是集群,那么访问其中一个节点地址,对应的都会显示,这是肯定的
我们可以看到,在对应的节点中,有多个相同的0,或者其他,实际上我们可以发现,中间是隔开的
也就是说,虽然有相同的o,或者其他,但是归属索引不同,所以还是有唯一的区别的
而要删除,可以点击如下:

91-Lucene+ElasticSeach核心技术_第78张图片

输入"删除"即可,否则不会删除
集群工作原理:
shad与replica机制 :
1:一个index包含多个shard,也就是一个index存在多个服务器上
2:每个shard都是一个最小工作单元,承载部分数据,比如有三台服务器,现在有三条数据,这三条数 据在三台服务器上各方一条
3:增减节点时,shard会自动在nodes中负载均衡 ,也就是会分给他
当然,并不是会直接的重新分配,因为这是在启动后的增减,否则自然是重新分配的
4:primary shard(主分片)和replica shard(副本分片)
每个document肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard
5:replica shard是primary shard的副本,负责容错,即主分片挂了,副分片选举(一般直接会变成为主,因为只有一个)
所以一个节点中,对应的索引信息可以存在多个主分片,也可以承担读请求负载 ,即主分片和副分片是同步关系
当然,虽然分片节点之间是数据的分开,不是同步,但是在语句的操作中
是以分片集群为主的(而不是主节点,后面的协调节点就是如此,mongo则是以路由为主来操作语句),所以语句不分节点
6:primary shard的数量在创建索引的时候就固定了,replica shard的数量可以随时修改
7:primary shard的默认数量是5,replica默认是1(每个主分片一个副本分片)
默认有10个shard,5个primary shard,5个replica shard,主分片对于数据来说,只有一个(对于的0,1,2等等)
8:primary shard不能和自己的replica shard放在同一个节点上(通常也不会放在一起,因为是下一个节点的操作)
否则节点宕机,primary shard和 副本都丢失,起不到容错的作用,但是可以和其他primary shard的replica shard放在同一个节点上
集群写入数据(也就是以分片集群为主的,即语句不分节点):
1:客户端选择一个node发送请求过去(基本是随机的),这个node就是coordinating node (协调节点)
2:coordinating node,对document进行路由,将请求转发给对应的node
他是根据一定的算法选择 对应的节点进行存储,类似于mongodb里面的分片策略,下面会说明该路由算法
3:实际上的node上的primary shard处理请求,将数据保存在本地,然后将数据同步到replica node
4:coordinating node,如果发现primary node和所有的replica node都搞定之后,就会返回请求到 客户端
这个路由的算法(可以说是分片策略)简单的说就是取模算法
比如说现在有3台服务器,这个时候传过来的id是5,那么5%3=2,就放在第2台服务器
最后说明:虽然我们启动es,但实际上显示的是我们设置的目录的文件
也就是说,如果没有文件,那么就算你启动,对于的分片也不会给你,但通常情况下,因为占用的原因,所以是不会出现这种情况
ES查询数据 :
倒排序算法 :
查询有个算法叫倒排序:简单的说就是通过分词把词语出现的id进行记录下来,再查询的时候先去查到哪些id包含这个数据
然后再根据id把数据查出来
查询过程:
1:客户端发送一个请求给coordinate node(基本是随机的)
2:协调节点将搜索的请求转发给所有的shard对应的primary shard 或replica shard
3:query phase(查询阶段):每一个shard 将自己搜索的结果(其实也就是一些唯一标识,如程序里面的文档id)
返回 给协调节点,有协调节点进行数据的合并,排序,分页等操作,产出最后的结果
也就是前面说的读取所有然后合并(也可以说是累加,因为合并就类似于慢慢的加上去的)操作
4:fetch phase(获取阶段) ,接着由协调节点
根据唯一标识去各个节点进行拉取数据,也操作合并(也可以说是累加,因为合并就类似于慢慢的加上去的),最终返回 给客户端
正是因为这样,所有我们也将分片集群看成一个整体(大多数操作分片的也是如此,比如mongodb)
Elasticsearch客户端(也可以说是java客户端,即一般用来操作索引库的):
客户端介绍:
在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html
注意点击进入后,选择版本到 6.2.4 ,因为我们之前按照的都是 6.2.4 版本:

在这里插入图片描述

里面可以选择版本,自己找即可,这里就不给出了,因为在不同的时间,可能显示的不同的,他们总会维护的
一般会出现如下:

91-Lucene+ElasticSeach核心技术_第79张图片

上面的版本自己指定(一般有个其他的版本出现,点击即可显示),然后点击High,之后的界面先不说明,后面会给出
创建Demo工程(Spring Boot项目,所以记得有父工程):
初始化项目:
目录如下:

91-Lucene+ElasticSeach核心技术_第80张图片

上面有两个配置文件application.properties,application.yml,共存的,在88章博客有说明覆盖操作以及先后读取顺序
一般是application.properties覆盖他们,即以application.properties为主
pom.xml文件:
<properties>
        <java.version>11java.version>
    properties>
     <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>junitgroupId>
             <artifactId>junitartifactId>
            <version>4.12version>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-loggingartifactId>
        dependency>
        <dependency>
            
            <groupId>com.google.code.gsongroupId>
            <artifactId>gsonartifactId>
            <version>2.8.5version>
        dependency>
        <dependency>
           
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.8.1version>
        dependency>
        
        <dependency>
               
            <groupId>commons-beanutilsgroupId>
            <artifactId>commons-beanutilsartifactId>
            <version>1.9.1version>
        dependency>
          
       <dependency>
           
         <groupId>org.elasticsearch.clientgroupId>
         <artifactId>elasticsearch-rest-high-level-clientartifactId>
         <version>6.2.4version> 
           
       dependency>
         
         
               
         <dependency>
             
             <groupId>org.elasticsearchgroupId>
             <artifactId>elasticsearchartifactId>
             <version>6.2.4version>
      
         dependency>
            <dependency>
                
             <groupId>org.elasticsearch.clientgroupId>
             <artifactId>elasticsearch-rest-clientartifactId>
             <version>6.2.4version> 
                
             <scope>compilescope>
         dependency>

    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
配置文件:
我们在resource下创建application.yml,先不操作
索引库及映射:
我们在上面点击High后的界面会有类似于如下的显示(不同的时间一般会不同,因为下面是以前的):

91-Lucene+ElasticSeach核心技术_第81张图片

创建索引库的同时,我们也会创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因 如下:
索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好
官方提供的创建索引库及映射API非常繁琐,需要通过字符串拼接json结构:

91-Lucene+ElasticSeach核心技术_第82张图片

因此,这些操作建议还是使用我们学习的Rest风格API去实现(如先让前面的kibana操作好)
我们接下来以这样一个商品数据类Product为例来创建索引库:
package com.lagou.pojo;

/**
 *
 */
public class Product {
    private Long id;
    private String title; //标题
    private String category;// 分类
    private String brand; // 品牌
    private Double price; // 价格
    private String images; // 图片地址
}

分析一下数据结构:
id:可以认为是主键,将来判断数据是否重复的标示,不分词,可以使用keyword类型
title:搜索字段,需要分词,可以用text类型
category:商品分类,这个是整体,不分词,可以使用keyword类型
brand:品牌,与分类类似,不分词,可以使用keyword类型
price:价格,这个是double类型
images:图片,用来展示的字段,不搜索,index为false,不分词,可以使用keyword类型
我们可以编写这样的映射配置(在kibana里进行操作,而不是在java客户端进行):
DELETE /lagou

PUT /lagou
        {
         "settings": {
          "number_of_shards": 3,
          "number_of_replicas": 1
         },
         "mappings": {
           "item": {
             "properties": {
               "id": {
                 "type": "keyword"
               },
               "title": {
                 "type": "text",
                 "analyzer": "ik_max_word"
               },
               "category": {
                 "type": "keyword"
               },
               "brand": {
                 "type": "keyword"
               },
               "images": {
                 "type": "keyword",
                 "index": false
               },
               "price": {
                 "type": "double"
               }
             }
           }
         }
        }
        
        

#index:是否索引,默认为true
#store:是否存储,默认为false,在es中是决定是否额外存储的,lucene则是真正的是否存储
#这里是es的解释,因为我们操作的是es
若有隐藏的,我们可以复制到一个地方,比如java程序里,然后进行查找删除,然后复制回来即可,比如:
先ctrl+f,出现如下:

91-Lucene+ElasticSeach核心技术_第83张图片

复制其中一个NBSP(实际上是一个隐藏值),然后点击如下:

91-Lucene+ElasticSeach核心技术_第84张图片

点击后,然后删除(键盘上的backspace,回删),这样就可以了,然后复制粘贴,这就是没有隐藏值的语句了
索引数据操作:
有了索引库,我们接下来看看如何新增索引数据
操作MYSQL数据库:
1:获取数据库连接
2:完成数据的增删改查
3:释放资源
初始化客户端:
完成任何操作都需要通过HighLevelRestClient客户端,看下如何创建
我们先编写一个测试类ElasticSearchTest,然后再@Before的方法中编写client初始化:
package com.lagou.test;

import com.google.gson.Gson;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

/**
 *
 */
//@RunWith(SpringRunner.class) 可以不加,因为Spring Boot基本什么运行环境都可以,虽然有默认
//实际上是对于@SpringBootTest这个的环境,在Spring中,对应的读取配置,则需要对应的环境
//比如@RunWith(SpringJUnit4ClassRunner.class)和@RunWith(SpringRunner.class),其他的环境基本不可以
@SpringBootTest
public class ElasticSearchTest {

    private RestHighLevelClient client;

    // Json工具
    private Gson gson = new Gson();

    @Before 
    //实际上对应的依赖,基本只在测试时(测试资源文件夹)可以使用
    //所以在java资源文件夹里面,不能使用,这是设置的
    /*
    即我们设置了
        junit
         junit
         4.12
         test
         加了这个test ,基本只在测试时(测试资源文件夹)可以使用
         若test变成了compile,那么基本都可以,无论是测试资源文件夹还是java资源文件夹都可以使用
     */
    public void init() {
        // 初始化HighLevel客户端
        //也可以说是连接索引库(可以是集群或者不是集群,即可以连接多个,多个之间可以没有关系)
        //我们操作时,也就相当于给他们都进行操作,如果是集群,那么集群的他们只会给一个出来
        //所以下面可以指定一个或者三个都可以,也可以不是集群,也会给
        
        //但具体给的方式,解释如下:
        //如果他是属于集群的:那么对应的集群必须都写上
        //可以只写其中一个,多写也没有关系(因为协调,只会操作其中一个)
        //但是如果集群没有启动,即只启动一个,那么添加时
        //虽然报错,但还是使得会创建并添加文档
        //有时候不会添加,即在错误出来之前,还没有操作,这种情况是很少的(如通信太慢的问题)
        //当然了,在集群启动时,那么就不会报错了(至少两个)
        //而不是集群的:那么没有限制,基本不会报错,只要存在,即都给
        //如果我们创建文档出现了过久的结果返回,第一,集群没有启动,第二,磁盘空间不足(对应的盘符)
        //如果是集群没有启动,那么报错还是会创建索引以及添加文档(虽然有时不会添加)
        //如果是磁盘空间不足,那么基本只创建索引,而不会给文档数据
        //除非没有一丝的空间,那么索引也不会创建,当然这种情况基本是没有的
        
        //上面的错误,基本是超时的错误,即如下:
        //java.io.IOException: listener timeout after waiting for [30000] ms
        
       
        RestClientBuilder restClientBuilder = RestClient.builder(
                new HttpHost("127.0.0.1", 9201, "http"),
                new HttpHost("127.0.0.1", 9202, "http"),
                new HttpHost("127.0.0.1", 9203, "http")
        );
        
        //这样就初始化完成,以后我们操作语句时,使用他,那么就相当于使用kibana操作es一样
        //这是这里是程序里面操作,而不是在浏览器上的执行,即称为java客户端,而不是浏览器上的客户端
        //但都是相对于es来说是客户端,es则是服务端
        client = new RestHighLevelClient(restClientBuilder);


    }

    @After
    public void close() throws IOException {
        // 关闭客户端

        client.close();
    }

    @Test
    public void test(){
        System.out.println(client);
    }


}

执行test方法,若返回了数据,代表操作成功
新增文档:
到测试类里面加上testInsert方法:
/*
    新增文档,多次的执行就是更新,前面说过的,同一个id,那么就是更新
    当然,他除了创建文档之外,若没有索引,他也会创建,执行对应的字段映射是操作模板的而已
    但这里还有个不同,他是判断是否有索引,即没有索引会创建,但有索引,那么就是操作文档,即自然就是更新的
    而不是报错,这里与之前的kibana不同,他操作了判断,而那个只是语句,而没有判断
    所以也可以看出来,程序比较灵活,具体判断方式,是可以使用try来操作的,当然可能还有其他判断,这里就不说明了
     */
    @Test
    public void testInsert() throws IOException {

        //1.定义文档数据
        Product product = new Product();
        product.setId(1L);
        product.setTitle("华为P50就是棒");
        product.setCategory("手机");
        product.setBrand("华为");
        product.setPrice(5999.99);
        product.setImages("http://image.huawei.com/1.jpg");

        //由于在es里面,存的是json的值,也就是字符串,所以我们需要转化成json,这里与lucene是不同的
        //我们通过之前的学习也是知道,es保存数据使用的就是json的格式来进行的
        //2.将文档数据转换为json格式
        String source = gson.toJson(product);

        //3.创建索引请求对象 访问哪个索引库、哪个type、指定文档ID
        //public IndexRequest(String index, String type, String id)
        IndexRequest request =
                new IndexRequest("lagou","item",product.getId().toString());
        //指定了对方的索引类型,并也指定了是那个文档id,那么接下来进行创建
        //创建传递我们要创建的数据
        //参数:
        //参数1:我们的json格式的数据
        //参数2:代表操作json类型的数据,一般是保证是json,不写的话就会报错
        request.source(source, XContentType.JSON);

         //实际上上面只是定义请求,以及定义数据,可以理解为,他们变成一个语句,语句里面有我们要传递的数据
        //一般需要其他的参数数据的或者说自己没有指定的通常需要request.source来操作添加,这里就需要
        //后面的查询,删除等等操作就不需要了,因为并不需要其他参数数据或者需要我们来指定语句
        //因为他们自己就已经准备好了
        //其他参数数据或者指定语句可以简称为:需要添加的语句信息
        //接下来执行语句,也就是发出请求
        //4.发出请求
        IndexResponse response = client.index(request);

        //既然我们执行了请求,也就是执行了语句,那么应该有响应信息,这里打印出来
        System.out.println(response);
        //返回IndexResponse[index=lagou,type=item,id=1,version=8,result=created,seqNo=7,primaryTerm=1,shards={"total":2,"successful":2,"failed":0}]
        //"total":2  主分片和副本分片,"successful":2 成功两个,即他们两个正好给了
        //其中主副分片规定是2,没有变化,主要是成功的这个,如果是单独的,那么successful值是1,否则一般是2
        //total基本都是2,所以单独的也就只操作一个主,而没有副,所以是1,而集群操作了副,所以是2
        
        
        
        //对应的返回信息,的确与kibana里操作时类似


    }

//可以在kibana里执行GET /lagou/item/1,看看结果
上面是我们自己首先创建了映射(虽然他会自动补全,即智能判断,即操作模板)
那么我们来看看,如果不在kibana里创建映射,那么有多么麻烦:
  /*
    创建映射
     */
    //注意:如果有对应的索引的话,会报错,这是语法的问题
    @Test
    public void testMapping() throws IOException {
        //定义索引库名
        CreateIndexRequest request = new CreateIndexRequest("twitter");

        //操作分片和副本的数量
        request.settings(Settings.builder()
                .put("index.number_of_shards", 3)
                .put("index.number_of_replicas", 2)
        );

        //只要符合json格式,那么就算都在同一行也可,但为了好观察,所以这里给出好的格式
        request.mapping("tweet", //该名称需要与类型一样,否则报错,这是为了确定的意思
                "  {" +
                        "    \"tweet\": {" +
                        "      \"properties\": {" +
                        "        \"message\": {" +
                        "          \"type\": \"text\"" +
                        "        }" +
                        "      }" +
                        "    }" +
                        "  }",XContentType.JSON); //确定为json,否则不写的话,就会报错


        //执行请求,使得创建索引
        CreateIndexResponse createIndexResponse = client.indices().create(request);
        System.out.println(createIndexResponse);
        //org.elasticsearch.action.admin.indices.create.CreateIndexResponse@44da7eb3
        //当然多次的执行,该返回结果肯定是不同的,就如多次的创建相同类型的对象一样
    }

//可以在kibana里执行GET /twitter,看看结果
我们可以看到,如果是我们自己编写映射,那么非常麻烦,你可能现在看起来不麻烦,那你试着将前面的lagou索引编写一下
会发现,在mapping里面编写,的确太麻烦了,因为对应的明明可以在kibana里操作,那么为什么不在里面操作呢
而之所以我们创建文档不在kibana操作,那是因为对应的数据是在程序获得的,而不是固定的数据,所以需要编写程序
查看文档:
根据rest风格,查看应该是根据id进行get查询,难点是对结果的解析:
在测试类里面加上testFindIndex方法:
/*
    查询文档
     */
    @Test
    public void testFindIndex() throws IOException {
        // 创建get请求,并指定id
        GetRequest request = new GetRequest("lagou", "item", "1");

        // 查询,得到响应
        GetResponse response = client.get(request);
        System.out.println(response);
        //{"_index":"lagou","_type":"item","_id":"1","_version":2,"found":true,"_source":{"id":1,"title":"华为P50就是棒","category":"手机","brand":"华为","price":5999.99,"images":"http://image.huawei.com/1.jpg"}}

        // 解析响应,应该是json,很明显,就是得到"_source"属性里面的值
        //我们通过kibana里也可以看到上面输出的结构
        String source = response.getSourceAsString();
        System.out.println(source); 
        //{"id":1,"title":"华为P50就是棒","category":"手机","brand":"华为","price":5999.99,"images":"http://image.huawei.com/1.jpg"}
        
        //如果没有文档,那么最终的结果,也一定是null
        

        // 转换json数据
        Product item = gson.fromJson(source, Product.class);
        System.out.println(item);
        //Product(id=1, title=华为P50就是棒, category=手机, brand=华为, price=5999.99, images=http://image.huawei.com/1.jpg)
        
        //如果没有响应数据是null,那么最终的结果,也一定是null

    }

//至此,我们也应该知道,对应的操作client的方法得到的返回数据,其实就是我们操作kibana的返回数据
//当然,可能程序会再次的进行修改,比如删除的返回值,但大致是相同的
//至于如何进行分割的取,就看你如何操作了,比如操作上面的分割出"_source"属性里面的值,来进行获取数据
//从而得到类的数据
修改文档,只要再次的执行前面的新增即可,因为相同的id就是修改,自己可以修改一下我们设置的信息,然后启动查看即可
删除文档:
根据id删除:
在测试类里加上testDeleteIndex方法:
  /*
    删除文档
     */
    @Test
    public void testDeleteIndex() throws IOException {
      // 准备删除的请求,参数为id
      DeleteRequest request = new DeleteRequest("lagou", "item", "1");

      // 发起请求
      DeleteResponse response = client.delete(request);
      System.out.println("response = " + response);
      //response = DeleteResponse[index=lagou,type=item,id=1,version=2,result=deleted,shards=ShardInfo{total=2, successful=2, failures=[]}]
        
        //如果没有数据删除,那么返回如下:
        //response = DeleteResponse[index=lagou,type=item,id=1,version=3,result=not_found,shards=ShardInfo{total=2, successful=2, failures=[]}]
    }
搜索数据:
查询所有match_all:
在测试类里加上testMatchAll方法:
  /*
    查询所有
     */
    @Test
    public void testMatchAll() throws IOException {
      //创建搜索对象,我们在前面查询时,我们知道,基本都是使用"_search"(他是用来操作查询的)
      //所以这里就是操作"_search"来操作查询
      SearchRequest request = new SearchRequest();

      // 查询构建工具,里面包含了查询的属性信息,以及入口确认的查询(比如query属性,对应query方法)
      //当然也有其他,比如sort属性(对应sort方法)
      //_source属性(操作是否显示字段,也可以说是过滤,对应fetchSource方法)等等
      SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

      // 添加查询条件,通过QueryBuilders获取各种查询
        //比如match_all属性,对应与QueryBuilders.matchAllQuery方法,没有参数
        //比如match属性,对应与QueryBuilders.matchQuery方法,有参数的
        //比如range属性,对应与QueryBuilders.rangeQuery方法,有参数的
        //且QueryBuilders.rangeQuery方法还需要继续调用,比如lt方法,有参数的
        //其他的看后面的学习就知道了
        //这里就是查询所有的条件,并进行确认
      // 实际上也就是查询时,需要的query属性,这是固定的,也就是将查询的属性信息,给到query里面
        //从而出现整体的语句
      sourceBuilder.query(QueryBuilders.matchAllQuery());

      //语句已经确认,那么使用"_search",来进行查询,先添加语句
      //因为这个查询属性是我们指定的,而不是像查询和删除一样,他自己指定,需要添加语句
        //当然基本除了单独创建索引外(前面的很麻烦的代码),基本都是使用他来添加语句
      request.source(sourceBuilder);

      // 搜索,添加后,我们就需要执行了
      SearchResponse response = client.search(request);
        System.out.println(response);
        //{"took":4,"timed_out":false,"_shards":{"total":8,"successful":8,"skipped":0,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"lagou","_type":"item","_id":"1","_score":1.0,"_source":{"id":1,"title":"华为P50就是","category":"手机","brand":"华为","price":5999.99,"images":"http://image.huawei.com/1.jpg"}}]}}
//实际上,该查询的结果,不只是查询一个,相当于这个:
        /*
        这个在之前没有说过,之前的一般是POST /lagou/_search,代表只对lagou索引查询所有
        而下面代表对所有的索引都进行查询,你可以自己测试,当然,他也可以作用与其他的条件,这是自然的
        由于我们读取信息,基本是从所有节点读取,但条件就是条件,自然是全部判断
        POST /_search
{
    "query":{
      "match_all": {}
  }
}
         */

      // 解析
      SearchHits hits = response.getHits();
        System.out.println(hits);
        //org.elasticsearch.search.SearchHits@df0e2a86
        //虽然是这样的结果,但通过返回的结果我们也知道,是得到第一个hits
        //里面有多个hits,自然是对应了所有的文档
        //我们也可以看到,其中的hits有明确的索引库和类型的信息,你可以自己测试
        //添加其他索引库,自然他们都是在下面一个的hits里面
        



      SearchHit[] searchHits = hits.getHits();
        System.out.println(searchHits);
        //[Lorg.elasticsearch.search.SearchHit;@d969452
        //虽然是这样结果,但通过上面的分析,我们也知道,这里是得到对应的索引文档信息
        //即所有的索引库的文档信息,而不是其中一个
        //所以得到的是数组的信息
        
        //上面是直接的得到数据集合,实际上我们可以这样:
        for(SearchHit searchHits:hits){ 
            //因为SearchHits实现了Iterable,所以是可以看成集合的,自然也实现了对应方法,从而可以得到数据
    System.out.println(searchHits);
}
        //将原来的数据,进行遍历,他是可以这样操作的,虽然他不是集合,这样就可以得到其中的所有数据
        //相当于后面的for (SearchHit hit : searchHits) {


        System.out.println("----------------");

      for (SearchHit hit : searchHits) {
        // 取出source数据
        String json = hit.getSourceAsString();
          System.out.println(json);
          //与前面的查询一样,实际上前面的查询,就是该里面的hits的数据,所以这里取得"_source"属性的信息
          //{"id":1,"title":"华为P50就是","category":"手机","brand":"华为","price":5999.99,"images":"http://image.huawei.com/1.jpg"}

        // 反序列化,这个的意思,并不是对类的序列,而是对json的序列,即可以称为序列
        Product item = gson.fromJson(json, Product.class);
        //转化为类返回
        System.out.println("item = " + item);
        //item = Product(id=1, title=华为P50就是, category=手机, brand=华为, price=5999.99, images=http://image.huawei.com/1.jpg)

          //通过层层的关系,我们知道,基本上查询的信息是hits里面的,而他的上面也是hits
          //包括一般有其他的总体情况
          //一般的查询,即具体的查询,也就是根据条件而查询到的其中一个hits的值

          //所以说,通过_search的查询,基本上是给出整体面貌,而不是只是数据
          //所有后面的查询操作基本都是如下,都是给出整体面貌
          
          //也说明了的确是kibana的返回数据,然后通过程序来选择读取
          //如层级的解析,比如response.getHits()的操作

      }
        }

/*
相当于
GET /_search
{
  "query": {
    "match_all": {}
  }
  
}
或者
GET /_search
{
 
}

单独的GET /_search也算
*/

注意,上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery())来添加的
这个 query() 方法接受的参数是: QueryBuilder 接口类型
这个接口提供了很多实现类,分别对应我们在之前中学习的不同类型的查询
例如:term查询、match查询、range查询、bool查询等,如图(实现类的实现类,也算他的实现类),这里只给出部分:

91-Lucene+ElasticSeach核心技术_第85张图片

因此,我们如果要使用各种不同查询,其实仅仅是传递给 sourceBuilder.query() 方法的参数不同而已
而这些实现类不需要我们去 new ,官方提供了 QueryBuilders 工厂帮我们构建各种实现类(这里也只给出部分):

91-Lucene+ElasticSeach核心技术_第86张图片

关键字搜索match:
其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已
因为实际上也只是查询方式不同,但结果的格式层级是一样的(因为使用_search),即其他代码基本一致:
比如:
//将sourceBuilder.query(QueryBuilders.matchAllQuery());修改成如下:
sourceBuilder.query(QueryBuilders.matchQuery("title","华为"));
/*
也就相当于:
GET /_search 
{
  "query": {
    "match": {
      "title": "华为"
    }
  }
  
}

自然也是对其所有的索引都查询(GET /_search ),当然,获取时,不只是查询该一个节点(因为分片的)
无论是查询一个索引还是多个索引,都归属于条件,然后条件取查询节点
*/
因此,我们可以把这段代码封装,然后把查询条件作为参数传递:
在测试类里加上basicQuery方法(一般执行语句的方法,基本需要操作异常,这里基本都是抛出):
  /*
        定义公用方法,来操作添加语句并执行
         */
    private void basicQuery(SearchSourceBuilder sourceBuilder) throws IOException {

        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        
        //添加语句
        request.source(sourceBuilder);
        
        // 搜索
        SearchResponse response = client.search(request);
        // 解析

        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
          // 取出source数据
          String json = hit.getSourceAsString();
            System.out.println(json); 
          // 反序列化
          Product item = gson.fromJson(json, Product.class); 
            //json的属性,一般是类操作直接赋值,且必须一样,否则不会有数据,即操作默认的
            //通常来说,主体是类,而不是我们传递的属性,其他的博客说明基本也是这样
            //我之所以在这里提一下,是因为后面操作时,你查看打印时,可能需要这里的解释
          System.out.println("item = " + item);
        }
      }
调用封装的方法,并传递查询条件:
在测试类里加上testMatchQuery方法:
 /*
      调用公用方法,传递语句
       */
    @Test
    public void testMatchQuery() throws IOException {
      // 查询构建工具
      SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
      // 添加查询条件,通过QueryBuilders获取各种查询
      sourceBuilder.query(QueryBuilders.matchQuery("title", "华为"));

      //传递语句并执行
      basicQuery(sourceBuilder);
        }
范围查询range:
sourceBuilder.query(QueryBuilders.rangeQuery("price"));
与页面上一样,支持下面的范围关键字:

91-Lucene+ElasticSeach核心技术_第87张图片

在测试类里面加上testRangeQuery方法:
     /*
        范围查询
         */
    @Test
    public void testRangeQuery() throws IOException {
        // 查询构建工具
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.rangeQuery("price").gt(1000).lt(6000));
        //当然,你可以得到值,然后通过值来调用gt或者lt,虽然他们也返回自身

        basicQuery(sourceBuilder);
        
        //那么有没有可能,gt和lt后面继续增加对应的gt,lt,lte,gte,那么这样可以吗
        //答:可以,实际上在这种问题,不只是这里出现,其他的框架一般可能也会出现,但基本他们都会是一个原则
        //也就是gt和gte或者他们自己,以及lt和lte或则他们自己,是赋值关系,相关的操作也是如此
        //这也使得,若出现gt(1000).lt(6000).gte(3000),那么就是3000<=x<6000,即1000被覆盖了
        //即可以说是:大于覆盖大于或者也算大于的(如大于等于),小于覆盖小于或者也算小于的(如小于等于)
        
        
        //上面的代码相当于:
        /*
        GET /_search
{
  "query": {
    "range": {
      "price": {
        "gt": 1000,
        "lt": 6000
      }
    }
  }
  
}
        */
    }
source过滤:
_source:存储原始文档
默认情况下,索引库中所有数据都会返回,如果我们想只返回部分字段,可以通过source filter来控 制
在测试类里加上testSourceFilter方法:
   /*
    source过滤
     */
    @Test
    public void testSourceFilter() throws IOException {

        // 创建搜索对象
        SearchRequest request = new SearchRequest();

        // 查询构建工具
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchAllQuery());

        // 添加source过滤
        sourceBuilder.fetchSource(new String[]{"id", "title", "price"}, null); 
        //参数1:前面的是操作指定(即includes,且只会显示他指定的)
        //参数2:后面的是excludes操作不指定显示(无论是否有includes都不会指定显示,这里就设置为null了)
        //当然,若都是null,自然就相当于没有操作,即都显示
        //虽然第一个或者第二个可能需要是(String) null
        //即(String)写上(其中一个写上即可,至少一个,即至少要有一个明面上是String类型的参数)
        //具体的方法,fetchSource(@Nullable String[] includes, @Nullable String[] excludes)
        //一般是@Nullable注解的原因,可能也是其他原因,具体可以百度
        
        //很明显,他不是在query里面添加的,所以与query平级
        basicQuery(sourceBuilder);
    }

/*
相当于:
GET /_search #后面多是这个的/_search,即查询所有的索引
{
  "query": {
    "match_all": {}
    },
    "_source": ["id","title","price"]
  }
  
*/
排序:
依然是通过sourceBuilder来配置:
在测试类里加上testSortQuery方法:
 /*
    排序
     */
    @Test
    public void testSortQuery() throws IOException {
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 查询构建工具
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        // 添加排序
        sourceBuilder.sort("price", SortOrder.ASC);
        basicQuery(sourceBuilder);
    }
/*
相当于:
GET /_search
{
  "query": {
    "match_all": {}
    },
 "sort": {
     "price": {
       "order": "asc"
     }
   }
 
}
*/
分页:
分页需要视图层传递两个参数给我们: 当前页:page,每页大小:size
而elasticsearch中需要的不是当前页,而是起始位置,还好有公式可以计算出:
给出具体的分页操作:
"from": 3, #起始位置,0表示第一条
"size" 3
#起始位置:start = (page - 1) * size

#比如:
#第一页:(1-1)*3 = 0
#第二页:(2-1)*3 = 3
代码:
在测试类里加上testSortAndPageQuery方法:
 /*
    分页
     */
      @Test
      public void testSortAndPageQuery() throws IOException {
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 查询构建工具
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        // 添加排序
        sourceBuilder.sort("price", SortOrder.ASC);
        // 添加分页
        int page = 1; //第几页
        int size = 3; //每页条数
        int start = (page - 1) * size; //起始位置,这里就是0
        // 配置分页
        sourceBuilder.from(start);
        sourceBuilder.size(3);
        basicQuery(sourceBuilder);
      }
      /*
      相当于:
      GET /_search
{
  "query": {
    "match_all": {}
    },
 "sort": {
     "price": {
       "order": "asc"
     }
   },
   "from": 0,
   "size": 3

}
       */

Spring Data Elasticsearch :
接下来我们学习Spring提供的elasticsearch组件:Spring Data Elasticsearch,之前的是es自己的组件,现在学习第三方的组件
什么是SpringDataElasticsearch:
Spring Data Elasticsearch(以后简称SDE)是Spring Data项目下的一个子模块
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是 非关系数据库(如Redis)
或者类似Elasticsearch这样的索引数据库,从而简化开发人员的代码,提高开发效率
Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/
特征:
支持Spring的基于 @Configuration 的java配置方式,或者XML配置方式
提供了用于操作ES的便捷工具类 ElasticsearchTemplate,包括实现文档到POJO之间的自动智能映射
利用Spring的数据转换服务实现的功能丰富的对象映射
基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
如可以直接定义JavaBean:类名、属性,对他们加上注解来实现索引数据与类的映射
根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自 动得到实现)
当然,也支持人工定制方法,如查询
配置SpringDataElasticsearch:
我们在pom文件中,引入SpringDataElasticsearch的启动器:
<dependency>
    
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-elasticsearchartifactId>
    
    
    
    
dependency>
然后,只需要在resources下新建application.yml文件(如果有的话,那么自然是不需要的),引入elasticsearch的host和port即可:
spring:
 data:
   elasticsearch:
     cluster-name: lagou-elastic #需要是我们设置的集群名称,否则可能执行失败
     #默认是elasticsearch,你可以点击进去看看就知道了
     #虽然并不是所有的都可以进去,因为有些只是被读取信息
     #而不是自己有信息(有默认或者是没有默认,即类型默认值,一般是类型默认值,通常是null,也就会使得没有操作)
     cluster-nodes: 127.0.0.1:9301,127.0.0.1:9302,127.0.0.1:9303 #集群节点
     #只要有一个正确,即可,否则可能执行失败,即正确里面加上错误也行
     #并不会随机选择(不与zookeeper一样,虽然他也有可能与这里一样,即他也并不绝对)
     #逗号的隔开与以前说的Spring Cloud注册一样
     #但是这里与Spring Cloud不同的是能在逗号前面加空格,而Spring Cloud不能加,但他们都可以在后面加
     #一般是解决空格的方案不同导致的,但这些并不需要理会,注意即可
     
     #实际上为了美观,最好不加空格
     
     #如果cluster-name和cluster-nodes爆红,那么一般说是版本太高,即过时了,修改成低版本即可
     #比如是2.1.6.RELEASE(只需要修改父依赖的Spring Boot版本即可)
     #一般该版本没有org.junit.jupiter操作测试类,而只有org.junit.Test,虽然高版本两个都有
     #可是虽然过时了,但还是可以使用的,只是有爆红而已
      #只是在后面的操作高亮的SearchResultMapper接口以及AggregatedPage类等等信息,需要低版本,高版本一般没有该接口或者该类,所以最好写上低版本,比如2.1.6.RELEASE这个版本
需要注意的是,SpringDataElasticsearch底层使用的不是Elasticsearch提供的RestHighLevelClient, 而是TransportClient
并不采用Http协议通信,而是访问elasticsearch对外开放的tcp端口,我们之前 集群配置中,设置的分别是:9301,9302,9303
另外,SpringBoot已经帮我们配置好了各种SDE配置,并且注册了一个ElasticsearchTemplate供我们使用,接下来一起来试试吧
在测试资源文件夹的test目录下,创建ElasticSearchTest2测试类:
package com.lagou.test;

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.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;



/**
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticSearchTest2 {

    @Autowired
    private ElasticsearchTemplate template;

    @Test
    public void check(){
        System.out.println(template);
        //org.springframework.data.elasticsearch.core.ElasticsearchTemplate@3c17794e
        //像这样的地址,多次执行,自然是不同的,注意即可
    }


}

/*
ok,这里就有一个实际开发中,依赖冲突的重要问题,你可以发现,在启动时,会报错
我们将之前的6.2.4都修改成6.3.2即可,也就是说,一个版本,可能对以前的依赖操作起作用
但是对导入并使用该版本的框架不起作用,也幸好6.3.2都可以满足

这就是版本的冲突的实际上情况,所以可能我们引入依赖,要想使得该依赖起作用
可能会使得以前的依赖版本发生改变,但也要注意,改变的版本也可以使用以前的依赖

三个依赖,elasticsearch-rest-high-level-client,elasticsearch,elasticsearch-rest-client
正如:如果依赖是6.2.4,6.3.2,6.2.4可以操作这里,但不能操作以前的操作
而6.2.4,6.2.4,6.2.4可以操作以前的,但不能操作这里	
6.3.2,6.3.2,6.2.4两个都可以操作
这就是依赖的冲突的解决方式,通常需要有一个整体来解决两方

因为版本使用不同,这里只需要第二个是6.3.2即可,而以前的需要他们两个相同
*/

索引库操作 :
创建索引库:
到刚才创建的测试类ElasticSearchTest2里(后面的测试类说的就是这个,注意一下,不要以为是ElasticSearchTest)
然后加上testCreateIndex方法:
@Test
    public void testCreateIndex(){
        // 创建索引库,并指定实体类的字节码
        template.createIndex(Product.class);
        
        //我们直接的执行,会发现,报错,因为我们并没有指定索引库名称
    }

发现没有,创建索引库需要指定的信息
比如:索引库名、类型名、分片、副本数量、还有映射信息,但这里都没有填写,这是怎么回事呢?
实际上,与我们自定义工具类类似,SDE也是通过实体类上的注解来配置索引库信息的
我们需要在Product类上添加下面的一些注解:
package com.lagou.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
 *
 */
@Data
//声明索引库lagou,类型product,分片3,副本1,因为product的存在
//也就是说,是指定类型的查询,而不是所有类型,或者说所有的索引
@Document(indexName = "lagou", type = "product", shards = 3, replicas = 1)
public class Product {
    @Id
    private Long id;
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title; //标题
    @Field(type = FieldType.Keyword)
    private String category;// 分类
    @Field(type = FieldType.Keyword)
    private String brand; // 品牌
    @Field(type = FieldType.Double)
    private Double price; // 价格
    @Field(type = FieldType.Keyword, index = false)
    private String images; // 图片地址
}


几个用到的注解:
@Document:声明索引库配置:
indexName:索引库名称
type:类型名称,默认是"docs",即如果注解里没有写或者是"",那么默认是docs
shards:分片数量,默认5
replicas:副本数量,默认1
@Id:声明实体类的id,也通常指定文档的id
虽然这里可以不用声明,即可以删除,现在并不会使用到(使用到时,自己可以加上进行测试即可)
你可以百度查看具体作用和使用的操作
@Field:声明字段属性:
type:字段的数据类型
analyzer:指定分词器类型
index:是否创建索引
虽然store默认是不保存,但es只是不保存在其他地方中(即不额外存储一份数据),原始数据还是有的
现在我们将索引库都删除,执行上面的testCreateIndex方法
前提是有索引库名称,其他的注解基本只有分片和副分片的属性会有用(类型不会操作)
其余的注解或者信息则基本不会考虑,即不会操作
然后查看索引库,会发现的确创建了,即他也只创建索引库,如果多次执行他并不会报错,但也不会修改,大概是什么都没操作
因为如果执行成功,那么应该返回错误信息,这是因为多次的创建是会报错的
创建映射:
刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样:
在测试类里面加上testMapping方法:
 @Test
    public void testMapping(){
        // 创建索引库的映射,并制定实体类的字节码
        template.putMapping(Product.class);
        //注意:他并不会创建索引库,他只是在创建的索引库里进行添加映射,所以如果没有索引库,那么会报错
        //那么自然,对于的注解需要写上(分片和副本不会使用,可以删除),来确认是操作哪一个索引库的
    }

//虽然对应的分片副本可以不写,但实际上类型名称也可以不写,虽然他有默认,但实际上如果不写的话,并不会走默认
//而是以类名的小写(全部小写),作为映射的类型名称
//当然,也符合修改的定义,只能添加,而不能修改映射,且只能是一个映射

//虽然对应的创建索引库,也基本只操作索引库名和分片以及副本

//但是我们可以看出来,对应的就算写上也不会操作,所以我们最好都写上
//这也使得,无论我们执行那一个方法,我们都不需要去修改注解信息了
//所以前面就是都写上
然后你可以进行通过浏览器的客户端head或者kibana查看,会发现有字段映射了
索引数据CRUD :
SDE(Spring Data Elasticsearch)的索引数据CRUD并没有封装在ElasticsearchTemplate中
而是有一个叫做ElasticsearchRepository的接口:

91-Lucene+ElasticSeach核心技术_第88张图片

我们需要自定义接口,继承ElasticsearchRespository:
在lagou包下创建repository包,然后再repository包下创建ProductRepository接口,并继承ElasticsearchRepository接口:
package com.lagou.repository;


import com.lagou.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;


/**
 * 当SDK访问索引库时,需要定义一个持久层的接口去继承ElasticsearchRepository接口即可,无需实现
 */
//泛型一般是指定,返回类型以及文档id类型,后面的查询中,可以看到结果,基本贯穿相关的所有方法或者说全文
public interface ProductRepository extends ElasticsearchRepository<Product,Long> {
    
    //这里就如mybatis-plus一样,对应的sdk会找当前项目所有继承了ElasticsearchRepository的接口
    //所以可以被找到,从而进行一系列的操作,使得可以被使用

}

创建索引数据 :
创建索引有单个创建和批量创建之分,先来看单个创建:
在测试类里面加上如下:
 @Autowired
    private ProductRepository productRepository;  

@Test
    public void addDocument(){
        //再Product里创建有参构造
        //无参构造也要,否则没有无参,上一个的测试类那么基本会有报错,因为他们操作的是无参)
        //然后才可以操作如下
        Product product = new Product(1L, "小米手机9", " 手机","小米", 3499.00, "http://image.lagou.com/13123.jpg");
        // 添加索引数据
        productRepository.save(product);
    }


/*
这里我就要提一提@Id的注解了,前面说过他通常指定文档id,在这之前,我们首先看id这个变量
如果没有@Id注解,且删除这个id
那么请问,对应的文档id是随机的,还是不会创建
答:不会创建,也就是说,该id必须存在,即如果不删除,那么只能是都小写,即是id,他会作为文档id,否则报错
但如果加上了@Id注解,那么可以删除id,因为以@Id为主了,且@Id注解对应的变量是大小写忽略的
因为@Id注解通常指定代表就是文档id,所以也通常给id变量
但是如果@Id注解给其他的变量,那么就是该变量就作为文档id的值(可以删除id)
当然如果没有@Id注解,默认是都是小写的id作为文档id
当然其他的字段,就看成普通的操作字段映射的字段即可,所以如果是title和titLe
那么就是两个字段,即以变量名来做为字段映射的字段名



最后说明一下:赋值时,基本只会赋值给全部小写的字段,也就是说,如果Product类的变量有大写的,比如Price
那么在添加时,不会进行添加,会发现,没有对应的值(特别是更新的时候,没有价格这个字段以及值了,这是很危险的)
所以最好变量都是小写,虽然可以创建映射
*/
执行后,我们可以查看数据即可,会发现一般会有数据
再来看批量创建:
再测试类里面加上如下:

    @Test
    public void addDocuments(){
        // 准备文档数据:
        List<Product> list = new ArrayList<>();
        list.add(new Product(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
        list.add(new Product(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
        list.add(new Product(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
        list.add(new Product(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
        list.add(new Product(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));
        // 添加索引数据

        productRepository.saveAll(list); 
        //一般操作list集合即可,虽然可能其他的集合也可以,主要看参数是否符合,比如使用Set集合也可以
        //但对于大多数的集合情况来说,我们只会使用list集合
        
        //且自然的,如果是相同的文档id,那么肯定是修改
    }
那么接下来我们可以继续查看,当然,查看方式有很多,可以使用浏览器客户端,或者kibana都可
查询索引数据:
默认提供了根据id查询,查询所有两个功能:
再测试类里面加上如下:
  @Test
    public void testQueryById(){
        //因为泛型那里是对应的Product和Long,所以若类型不是Long类型,那么这里会报错
        Optional<Product> goodsOptional = productRepository.findById(3L);
        System.out.println(goodsOptional);
        //Optional[Product(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=/13123.jpg)]
        //如果没有文档,那么返回Optional.empty
        
        Product product = goodsOptional.get();
        System.out.println(product);
        //Product(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=/13123.jpg)
        
        /*
         public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }
        */

        //一般来说,我们基本只会使用orElse方法,因为可以操作兜底数据,而不是直接的使用get方法获取值
        System.out.println(goodsOptional.orElse(null)); 
        //参数类型在泛型中已经指定,即这里就是Product类型,如果不是,自然是类型不匹配
        //当然,null,自然也算的,因为可以赋值
        
        //该goodsOptional.orElse(null)方法代表,如果没有找到对应的文档,那么返回参数值null
        //所以这里可以给兜底数据,比如goodsOptional.orElse(new Product())
        //这样的,给个空的对象(即都是操作默认值的对象)
        /*
        看看他的源码就知道了:
           public T orElse(T other) {
        return value != null ? value : other;
    }
    若没有找到数据,那么返回other,也就是返回我们的参数
        */
        
        //上面源码可以看到,value就是存放查询到的值的变量
        //private final T value;
        
        //执行结果:
        //Product(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=/13123.jpg)
    }

 @Test
    public void testQueryAll(){
        Iterable<Product> list = productRepository.findAll();
        list.forEach(System.out::println);
        /*
        Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)
        Product(id=5, title=荣耀V10, category=手机, brand=华为, price=2799.0, images=/13123.jpg)
        Product(id=4, title=小米Mix2S, category=手机, brand=小米, price=4299.0, images=/13123.jpg)
        Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg)
        Product(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=/13123.jpg)
前面说过,添加的顺序是随机且有序的(一般是有序的,可能是某些算法导致),这里就可以看到结果

         */
    }
自定义方法查询:
ProductRepository接口继承的ElasticsearchRepository接口提供的查询方法有限,基本不能够操作复杂的查询
但是它却提供了非常强大的自定义查询功能:
只要遵循SpringData提供的语法(后面会说明),我们可以任意定义方法声明:
我们回到ProductRepository接口,加上如下:
   /**
     * 根据价格区间查询
     * @param from 开始价格
     * @param to 结束价格
     * @return 符合条件的Product
     */

List<Product> findByPriceBetween(Double from, Double to);
List<Product> findByIdBetween(Long from, Long to); //这个是为了测试语法
无需写实现,SDE会自动帮我们实现该方法,我们只需要用即可:
然后在测试类里加上如下:
   @Test
    public void testQueryByPrice(){
        List<Product> list = productRepository.findByPriceBetween(1000d, 4000d);
        List<Product> list1 = productRepository.findByIdBetween(0l, 4000l);


        //新特性:可以到31章查看
        Consumer action = System.out::println;
        action.accept("1");
        //返回1这个值

        list.forEach(System.out::println); //这里面只是操作了多个的accept执行,且是循环的
        System.out.println("---");
        list1.forEach(System.out::println);

        /*
        Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)
        Product(id=5, title=荣耀V10, category=手机, brand=华为, price=2799.0, images=/13123.jpg)
        Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg)
        ---
        Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)
        Product(id=5, title=荣耀V10, category=手机, brand=华为, price=2799.0, images=/13123.jpg)
        Product(id=4, title=小米Mix2S, category=手机, brand=小米, price=4299.0, images=/13123.jpg)
        Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg)
        Product(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=/13123.jpg)
         */
    }
那么为什么我们执行自己定义的方法,他知道是什么作用呢?
在上面我们说过了,只要遵循SpringData提供的语法,我们可以任意定义方法声明,那么这个语法是什么呢,看如下:
现在看一看支持的一些语法示例:
我们可以看到下面的findByPriceBetween,在前面的ProductRepository接口中,有findByPriceBetween和findByIdBetween
所以我们可以看出,下面图中的Price是自定义的
当然,其他的方法中,可能有多个自定义,比如findByNameAndPrice的Name和Price
具体的观察,主要看后面的语句即可确定谁是自定义,一般对应示例语句中,值或者值里面是" ? "的,就说明该字段是自定义
结合语句和方法名称,就能知道谁是自定义了
比如findByNameAndPrice的Name和Price,以及findByPriceBetween的Price(没有form和to对应的自定义)
经过测试,该Price(或者其他自定义的)一般是类的变量(首字母忽略大小写)
当然,变量也最好不要是有大写,否则不会进行操作,一般这里会报错,虽然添加那里不会,这是语法的原因
因为有些不加字段,不会报错,比如添加(相当于空的)
而有些需要指定字段,比如这里对应的range,他里面必须要指定字段,否则报错,看后面的语法示例就知道了)
与前面的添加一样,这是SDK规定的,大多数的情况下,基本都是如此
所以在操作SDK时,最好变量都是小写的,这也基本不会出现特殊问题
在其他的框架中,一般也有首字母的对应,通常会仔细的说明对应
无论是从哪个方向出发,从己方还是甲方,比如在61章博客中#里面的说明就是如此
可能其他地方的说明有欠缺,但注意即可,或者自己测试补全即可
即该变量就是操作文档的对应的值

91-Lucene+ElasticSeach核心技术_第89张图片

91-Lucene+ElasticSeach核心技术_第90张图片

91-Lucene+ElasticSeach核心技术_第91张图片

通过上面我们可以知道,后面的语句我们都可以进行操作,从而可以实现复杂的语句执行
当然,可能还有更多的示例语句的支持,这里可能并不是全部,具体可以百度查看
原生查询 :
如果觉得上述接口依然不符合你的需求,比如超级复杂的语句,那么SDE也支持原生查询
这个时候还是使用ElasticsearchTemplate,而查询条件的构建是通过一个名为 NativeSearchQueryBuilder 的类来完成的
不过这个类的底层还 是使用的原生API(之前的测试类的java客户端)中的如下工具:
比如QueryBuilders 、 AggregationBuilders 、 HighlightBuilders 等等工具
需求: 查询title中包含小米手机的商品,以价格升序排序
分页查询:每页展示2条,查询第1页
对查询结果进行聚合分析:获取品牌及个数
在测试类里加上如下:
  @Test
    public void testNativeQuery(){
        // 原生查询构建器
        // 实际上大多数的构建或者说明,都只是一个对象,而该对象里面有操作
        // 从而也称为对应的特殊名称,就比如这里是原生查询构建器,这里就提一下
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // source过滤,public FetchSourceFilter(String[] includes, String[] excludes) {
        //都是空数组,自然就是查询所有,就如对应的过滤字段那里是空数组一样
        //当然,如果是null,那么相当于没有字段或者也是空数组
        //很明显他自带对应的_search查询,然后添加source过滤的字段,也就是_source
        //并在参数里指定对应的两个属性,即includes和excludes
        //这里就不做过滤,即设置为new String[0]
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
        // 搜索条件
        //是import org.elasticsearch.index.query.QueryBuilders;,这个包,其他的基本操作不了下面的参数(可能有独有的操作,这里我们就使用这个包)
        //添加query属性,并在里面加上match属性,然后操作字段条件
        queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
        // 分页及排序条件,添加分页的条件,即from为0,size为3,然后并加上sort属性
        //(虽然在里面,但还是可以在最外层加上sort属性,这种方式有很多,比如说传递要添加的原来的对象等等)
        // 使得来操作对应的字段,操作asc,即是从小到大,那么就是升序
        queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
        // 高亮显示,这里之所以注释掉,是因为原生的查询基本不支持,会报错的,后面会说明实现方式
        //可能以后会支持,但也要到支持的时候去了,这里就不加上了,自己可以进行测试
        //或者查看百度,查看是否可以支持以及支持的代码如何操作,或者使用其他的方法来操作等等
        // queryBuilder.withHighlightBuilder(new HighlightBuilder().field("title"));
         //注意:他只是说明这里的Spring Data Elasticsearch的原生
        //之前的es的elasticsearch-rest-high-level-client这个是有操作方式的
        //只是具体高亮需要我们来提取出来,这里一般操作不了他的方式
        //如果有,那么自然也可以操作,即也可以提取,具体百度看看有没有这样的操作即可
        //一般没有,但后面给出了一种操作方式,好像也是主要的操作方式(可能有其他方式)

 // 聚合,添加aggs属性,并指定聚合名称brandAgg,并添加terms属性
        //虽然方法名称是terms,但名称并不是一定是优先,可能是一起,这里就是以参数优先
        // 然后添加field属性对brand操作聚合,注意:前面并没有说明过与查询结合
        // 若是与查询结合,那么默认是以查询结果为主,也就是我们操作一系列的过滤,或者分词后,然后再操作聚合
// 因为之前的,并没有操作查询,而不操作查询,也就是只有{}以及其他的操作,那么实际上是默认操作查询所有的
        queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
// 构建查询条件,并且查询,也就是执行语句,并得到数据,该数据,一般只会保存聚合相关的数据
        //查询的文档数据可能并没有,具体可以看api,或者百度查看,可能也是有的
        //并指定一个类的字节码文件,主要是用来指定是哪个索引库
        AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
        System.out.println(result); //Page 1 of 2 containing com.lagou.pojo.Product instances
        // 解析结果:这里只是保存了具体数据,而不是操作返回结果
        // 分页结果
        long total = result.getTotalElements(); //包含的文档数量,即总共的条数
//获取页码,总共的页数,这里并不是根据结果来找的(因为返回结果里一般都没有)
        //他是根据计算来得出的,因为有总条数存在,所以可以根据总条数以及查询的条数来确定页数一般向上取整
        //举个例子:比如总共有5条,查2条,那么5/2=2.5
        //向上取整:如果有小数,那么向上取整,这里即为3,如果是2,那么就是2,即2.1也是3(因为有小数)
        //所以根据上面的解释,那么这个例子的结果就是3页
        int totalPages = result.getTotalPages(); 
        List<Product> list = result.getContent(); //获取本页数据的集合
        System.out.println("总条数 = " + total); //总条数 = 3
        System.out.println("总页数 = " + totalPages); //总页数 = 2
        System.out.println(list); 
        //[Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg), Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)]
        // 聚合结果,根据查询的结果,我们可以得到聚合属性里面的值
        //也就是aggregations属性的值,看结果就知道了
        Aggregations aggregations = result.getAggregations();
        
        //也可以直接的获得aggregations属性里面的单个值
        //只是只能获得值了,基本不能操作提取聚合内容,即桶的内容
        //所以还是最好以上面的方式为主,可能也是有作用的,具体可以百度
        Aggregation aggregation = result.getAggregation("brandAgg");
        System.out.println(aggregation+"---");
        //{"brandAgg":{"doc_count_error_upper_bound":0,"sum_other_doc_count":0,"buckets":[{"key":"小米","doc_count":2},{"key":"锤子","doc_count":1}]}}---
        
        //然后得到对应的聚合名称是brandAgg的值,因为可以有多个聚合名称,所以需要指定名称
        //然后就可以得到对应聚合名称的结果集了
        //记得导入import org.elasticsearch.search.aggregations.bucket.terms.Terms;
        //否则后面的方法,比如getBuckets()没有
        Terms terms = aggregations.get("brandAgg");
        System.out.println(terms);
        //{"brandAgg":{"doc_count_error_upper_bound":0,"sum_other_doc_count":0,"buckets":[{"key":"小米","doc_count":2},{"key":"锤子","doc_count":1}]}}
        
        //得到buckets属性里面的值,看查询的返回结果就知道了
        //是聚合名称结果集中的属性,一般是包括了聚合的数据
        terms.getBuckets().forEach(b -> {
            //下面是取得buckets属性里面的值
            System.out.println("品牌 = " + b.getKeyAsString()); //取出key值
            System.out.println("count = " + b.getDocCount()); //取出doc_count值
            /*
            有两个数据:
            品牌 = 小米
            count = 2
            
            品牌 = 锤子
            count = 1
             */
     });
        //我们可以发现,他们对于语句和返回值的解析,是一层一层的,所以程序也是一个顺序的执行,即程序也是严谨的
    }

    //最后说明一下:虽然对应的代码中,我将浏览器客户端代码进行比较来理解
//但实际上并不是完全一样(用方式不同),因为是直接的作用与es(作用方式不同而已)
    //只是操作一样而已(实际上也差不多,因为一个是浏览器上的,一个是程序里的
//但都是相对于es来说是客户端),所以你也可以看成语句或者说都是语句的操作,即可以看成一样

/*
语句相当于这样:
复制粘贴时,记得删除下面的解释,虽然前面也多次的这样介绍

GET /lagou/product/_search 因为指定有类型和索引,所以是操作对应的索引和类型的(虽然他们是一体的)
那么可以这样,GET /lagou/_search和GET /lagou/product/_search是一样的
当然,因为访问的原因,一般/可以多加,但\却只能在后面加,因为实际上除了浏览器更正外,服务器也会继续更正
这里并不需要理会,因为并没有什么作用,只要保持"/"即可
{
  "query": {
    "match": {
      "title": "小米手机"
    }
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand"
      }
    }
  }
}
*/

/*
因为他使用了类的注解信息,实际上只会识别索引名或者类型名,如果没有类型,那么结果也是一样
因为GET /lagou/_search和GET /lagou/product/_search是一样的

但要注意,索引名和类型名不能都不写,虽然认为是GET /_search,即查询所有,因为如果数据太多
特别是大的公司里面,那么会非常的卡,所以为了防止这种情况
一般不能不写索引名
不写索引名就相当于都不写了,因为类型名需要索引名,所以索引名没了,自然类型名也要没了,否则会报错
*/
注:上述查询不支持高亮结果
高亮展示:
通过测试,如果你不支持,那么一般是如下的代码出现错误:
AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
/*
也就是说,实际上我们是设置高亮的,但在我们却不会识别,通过debug可以发现
具体的代码报错是queryForPage方法里面的如下:

这里是入口(两个参数),里面的queryForPage方法回到后面的queryForPage方法里面去,因为是三个参数而不是两个参数
public  AggregatedPage queryForPage(SearchQuery query, Class clazz) {
        return this.queryForPage((SearchQuery)query, clazz, this.resultsMapper);
    }

    public  AggregatedPage queryForPage(
    SearchQuery query, Class clazz, SearchResultMapper mapper) {
        SearchResponse response = this.doSearch(this.prepareSearch(query, clazz), query);
        return mapper.mapResults(response, clazz, query.getPageable());
    }
    
    
    其中 SearchResponse response = this.doSearch(this.prepareSearch(query, clazz), query);
    他里面doSearch方法里面是报错的根源
    如果是设置了高亮,那么就会报错,否则不会
    
    实际上他是因为this.resultsMapper(ResultsMapper的类型)的缘故
    虽然他并没有看到传递,但是因为this的原因,自然是会使用到的
    而三个参数中是以SearchResultMapper接收ResultsMapper的,所以我们需要操作这个
    因为SearchResultMapper是ResultsMapper的父类(可以这样说,虽然他们都是接口)
    
*/
上面找到了具体的错误,接下来我们来解决该错误
自定义搜索结果映射:
在lagou包下创建resultmapper.ESSearchResultMapper类,并实现SearchResultMapper接口:
package com.lagou.resultmapper;

import com.google.gson.Gson;
import com.lagou.pojo.Product;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 自定义结果映射,主要用来处理高亮
 */
public class ESSearchResultMapper implements SearchResultMapper {

    /**
     * 完成结果映射
     * 由于高亮,无非就是将我们查询的词添加上对应的标签
     * 所以操作的重点就是将原有的结果"_source"属性下的数据取出来,放入高亮的标签即可(一般只操作对应词)
     * 然后生成一个list集合,我们之前原生查询中,可以知道
     * 有这样的一行代码List list = result.getContent(); 获取本页数据的集合
     * 也就是说,我们生成的list集合也就是他这个list集合,所以从而得到高亮数据
     * 所以这里就是设置他之前的方法,从而操作高亮,并可以使得支持(可能原生的没有,所以报错)
     * 一般我们在执行代码中,加上这个类的对象
     * 从而可以支持高亮的操作,后面会说明的
     * 虽然这里是斜体字,但idea是可以修改的,具体可以百度,我这里使用斜体字
     * @param searchResponse
     * @param aClass
     * @param pageable
     * @param 
     * @return AggregatedPage 需要三个参数进行构建,一般是pageable(分页相关的),list集合,总记录数
     */
    @Override
    public <T> AggregatedPage<T> mapResults(
        SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
        
        System.out.println(aClass); //class com.lagou.pojo.Product
        System.out.println("--1");
        System.out.println(searchResponse);
        /*
        {"took":41,"timed_out":false,"_shards":{"total":5,"successful":5,"skipped":0,"failed":0},"_clusters":{"total":0,"successful":0,"skipped":0},"hits":{"total":3,"max_score":null,"hits":[{"_index":"lagou","_type":"product","_id":"1","_version":1,"_score":null,"_source":{"id":1,"title":"小米手机7","category":"手机","brand":"小米","price":3299.0,"images":"/13123.jpg"},"highlight":{"title":["小米手机7"]},"sort":[3299.0]},{"_index":"lagou","_type":"product","_id":"2","_version":1,"_score":null,"_source":{"id":2,"title":"坚果手机R1","category":"手机","brand":"锤子","price":3699.0,"images":"/13123.jpg"},"highlight":{"title":["坚果手机R1"]},"sort":[3699.0]}]},"aggregations":{"brandAgg":{"doc_count_error_upper_bound":0,"sum_other_doc_count":0,"buckets":[{"key":"小米","doc_count":2},{"key":"锤子","doc_count":1}]}}}
         */
        
        //所以可以看出,他实际上是得到执行语句的返回结果,然后我们操作
        //所以后面的返回值的介绍中,也说返回了分页的结果
        
        System.out.println(pageable); //Page request [number: 0, size 2, sort: price: ASC]
        //获得总记录数
        long totalHits = searchResponse.getHits().getTotalHits();
        System.out.println(totalHits); //3
        //记录列表,用来保存返回的结果
        List<T> list = new ArrayList<>();

        // 获取搜索结果(真正的的记录),因为我们获取的是操作好的结果
        //可以发现,在之前的原生查询中,并没有操作具体的返回结果,而是直接的使用
        //即已经操作好的,即他自带的已经操作好的,这里我们也要自己操作,所以searchResponse自然也是返回结果
        //前面有类似的,比如SearchResponse response = client.search(request);
        SearchHits hits = searchResponse.getHits();
        System.out.println(hits); //org.elasticsearch.search.SearchHits@66097225
        for (SearchHit hit : hits) {
            //下面的输出结果,我只给出一个,这只是让你知道,他返回了上面而已,所以注意即可
            System.out.println("2");
            System.out.println(hit); //org.elasticsearch.search.SearchHit@b9a709f4
            // 像这样的地址,应该要知道,多次的执行,一般都不相同,除非已经指定,或者运气好
            
            //这里是为了防止突然出现的情况
            //比如并发下的情况且操作同一个地址(虽然大多数的情况下不会,这里也并没有设置)
            // 所以虽然看起来是不起作用的,因为如果是0,那么自然不会执行这个循环
            //所以他只是为了防止特殊的情况的
            if (hits.getHits().length <= 0) {
                return null;
            }
            String json = hit.getSourceAsString();

            System.out.println(json);
            /*
           {"id":1,"title":"小米手机7","category":"手机","brand":"小米","price":3299.0,"images":"/13123.jpg"}
             */
            System.out.println("----------");
            //获得_source的数据
          // 将原本的JSON对象(即_source的数据)转换成Map对象
          Map<String, Object> map = hit.getSourceAsMap();
            System.out.println(map);
            /*
            map的key一般是无序的,但也可以说是有序,因为同样的结果还是一样
        {images=/13123.jpg, price=3299.0, id=1, title=小米手机7, category=手机, brand=小米}
             */
            System.out.println("1");
          // 获取高亮的字段Map,通过返回值可以知道是获得highlight属性
          Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            System.out.println(highlightFields);
            //{title=[title], fragments[[小米手机7]]}
          //每个高亮字段都需要进行设置,因为可以有多个高亮的字段,前面高亮操作说明过了
          for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
            // 获取高亮的Key
            String key = highlightField.getKey();
            // 获取高亮的Value,这就是我们操作添加的高亮信息,就是再原来的_source基础上加上了高亮操作的值
            HighlightField value = highlightField.getValue();
            // 实际fragments[0]就是高亮的结果,无需遍历拼接,因为返回结果中,基本只有一个字段对应的值
              // 所以数组的值基本就是一个,即长度为1,所以fragments[0]就是高亮的结果
              //如果是fragments[1]或者是更大的下标,自然是超出了数据的大小,自然也会报错
            Text[] fragments = value.getFragments();
            // 因为高亮的字段必然存在于_source的数据的Map中(可以通过返回结果就知道了),就是key值
            // 所以如果我们这时再次的放入,那么根据map,相同的key,则是覆盖
              //从而使得_source里面的值变成了高亮的值,但要注意:虽然说的是_source的值
              //但实际上已经取出来了,所以返回结果操作的是上面设置的list,所以实际上并不是修改了_source的值
              // 所以这里就是我们之前说的(学习高亮时有说明),我们也可以让后端使用程序解析
              //从而,返回给前端的_source的数据是对应的高亮操作的数据
              //不要混淆程序与前端,实际上我们并不是改变语句的返回结果
              //我们只是先得到,然后通过java程序给前端,而不是直接的将返回结果给前端
              //前面的操作基本都是在程序里得到返回结果,然后进行操作的,这里要注意一下

              System.out.println("22");
              System.out.println(fragments[0]); //小米手机7
              System.out.println("333");
            map.put(key, fragments[0].toString()); 
              //这里需要加上toString(),否则类型可能不同,一般需要String类型,否则可能会报错
              
            //正是因为map的覆盖原理,所以这里使用map集合,而不会返回字符串(json)
              //即String json = hit.getSourceAsString();
          }
          // 把Map转换成对象,我们可以将map先变成json,然后变成对象
            //如果有map变成对象的方法,那么自然也可以进行操作
            //只是这里导入的依赖中,好像并没有这样的操作,所以需要先变成json,
            //当然,你可以自行的解决map到对象,百度上肯定有依赖
            //或者你自己写一个嘿嘿(●ˇ∀ˇ●)
          Gson gson = new Gson();

          T item = gson.fromJson(gson.toJson(map), aClass);
          list.add(item);
            System.out.println(list);
            /*
            [Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg), Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)]
            */
        }
        // 返回的是带分页的结果,但是对应的返回数据结果变成了这里的list
        //所以我们需要将原理的聚合的代码删除,这里要注意,否则执行会找不到的,即报错
        return new AggregatedPageImpl<>(list, pageable, totalHits);
        //这里对比一下这个
        /*
        AggregatedPage result = template.queryForPage(queryBuilder.build(), Product.class);
        我们可以发现,返回的是这里的值,即给result
        所以List list = result.getContent();得到的集合自然也是这里的集合list集合
        他会将我们的list变成对应的数据的(并不是返回结果,因为他已经操作了返回结果)
        所以我们得到的数据里面就有对应的高亮的信息了
         */

        //实际上上面的泛型并没有动态的操作,但是运行时,自然是Object的,所以也就是使得基本都可以操作
      }
}






然后修改原来的原生查询的代码,修改如下:
  @Test
    public void testNativeQuery(){
     
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
      
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));

        queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
        
            queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
        
        //这里是主要修改或者添加的代码
        
        // 高亮显示,他会添加highlight属性,然后指定对应要操作的字段(对应与查询的,也就是分词的字段)
        HighlightBuilder.Field title = new HighlightBuilder.Field("title");
        title.preTags("");
        title.postTags("");
        queryBuilder.withHighlightFields(title);
        /*
        当然,也可以这样 queryBuilder.withHighlightBuilder(
        new HighlightBuilder().field("title").preTags("").postTags(""));
        因为执行的方法,返回本身了,前面的范围查询也可以,虽然并没有测试
         */


        queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
     
        //这里加上了new ESSearchResultMapper(),来操作我们自定义的设置,而不是使用他本来的
        AggregatedPage<Product> result = template.queryForPage(
            queryBuilder.build(), Product.class,new ESSearchResultMapper());
        System.out.println(result); //Page 1 of 2 containing com.lagou.pojo.Product instances
        // 解析结果:
        // 分页结果,这里的操作在上一个测试类里说明过了
        long total = result.getTotalElements(); 
        int totalPages = result.getTotalPages();
        List<Product> list = result.getContent(); 
        System.out.println("总条数 = " + total); 
        System.out.println("总页数 = " + totalPages); 
        System.out.println(list); 
        //[Product(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=/13123.jpg), Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=/13123.jpg)]
        //我们可以发现,操作了高亮的添加,即高亮操作成功
       
        
        //因为list集合是使用我们操作的,所以没有了聚合的值,因为我们也并没有加上对应的聚合值
        Aggregations aggregations = result.getAggregations();
        Aggregation aggregation = result.getAggregation("brandAgg");
        System.out.println(aggregation+"---"); //null---
     
//        Terms terms = aggregations.get("brandAgg");
//        System.out.println(terms);
//        terms.getBuckets().forEach(b -> {
//            System.out.println("品牌 = " + b.getKeyAsString());
//            System.out.println("count = " + b.getDocCount());
//     });
      
    }


//所以总体来说,自带的与我们自己写的SearchResultMapper接口的区别
//他们都是操作了查询的数据,然后给一个AggregatedPage接口来存放具体的值,然后我们使用他的方法来获取

//至此,只要我们不会报错,自然也会执行SearchResultMapper接口的mapResults方法
//所以如果是高亮操作原生查询,那么报错也并没有打印信息(没有执行mapResults方法),我们的不会报错
//实际上原生的使用我们的也不会报错,只是不能使用他自己的,使用他自己的会报错,只是不会操作我们的高亮执行而已
//原生的是:withHighlightBuilder
//我们的是:withHighlightFields

//所以原生查询的原生在这里,就是SearchResultMapper接口的原生接口,而不是我们自定义的接口

//具体操作是,我们传递SearchResultMapper接口对应的实现类时,会执行方法,并传递操作好的数据给出
//然后我们再操作,而原生的,如果是高亮,则不会执行,我们的就会执行
//或者说,实际上原生的也执行了,只是里面判断不能操作高亮,且没有什么打印的信息而已
执行后,我们可以看看数据,发现操作了高亮,那么我们也可以知道,实际上主要是第三个参数是否可以操作高亮的原因
换言之,就是原来自带的,不能操作高亮的查询,因为我们自定义的方法是查询了高亮的数据
至此我们操作完毕
特别的内容:
ik分词器下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
具体的elasticsearch-rest-high-level-client操作高亮的其中一个方式(只给出主要代码):
 SearchRequest request = new SearchRequest();
 
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery("name", "中国人哈哈"));

        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("name");//高亮的字段
		//前缀
        highlightBuilder.preTags("");
	//后缀
        highlightBuilder.postTags("");
        sourceBuilder.highlighter(highlightBuilder);
        
//注意:得到的返回消息里面可以看到已经操作了高亮
//但本来的数据,还是没有变成高亮效果,需要你手动的覆盖,比如如下的操作(给出部分的完整代码):

 @Test
    //查询文档
    public void sele() throws IOException {
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 查询构建工具
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery("name", "哈哈"));
        sourceBuilder.sort("price", SortOrder.ASC);

        sourceBuilder.from(0);
        sourceBuilder.size(5);

        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("name");//高亮的字段
        highlightBuilder.requireFieldMatch(false);//是否多个字段都高亮
        highlightBuilder.preTags("");//前缀后缀
        highlightBuilder.postTags("");
        sourceBuilder.highlighter(highlightBuilder);

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(0).lt(0));

        sourceBuilder.postFilter(boolQueryBuilder);
        //传递语句并执行
        basicQuery(sourceBuilder);
    }
    /*
      定义公用方法,来操作添加语句并执行
       */
    private void basicQuery(SearchSourceBuilder sourceBuilder) throws IOException {

        // 创建搜索对象
        SearchRequest request = new SearchRequest();

        //添加语句
        request.source(sourceBuilder);

        // 搜索
        SearchResponse response = client.search(request);
        System.out.println(response);


        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
            //这里就是主要的,实际上与在前面操作SDE时的高亮操作类似
            
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
                // 获取高亮的Key
                String key = highlightField.getKey();
                // 获取高亮的Value,这就是我们操作添加的高亮信息
                HighlightField value = highlightField.getValue();
                Text[] fragments = value.getFragments();
                sourceAsMap.put(key, fragments[0].toString());
            }

            Products item = gson.fromJson(gson.toJson(sourceAsMap), Products.class);
            System.out.println("item = " + item);
        }
    }

这里了解即可

你可能感兴趣的:(笔记,es,Lucene,ElasticSeach,java)