Lucene+ElasticSeach
什么是全文检索:
数据分类:
我们生活中的数据总体分为两种:结构化数据和非结构化数据
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等
非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件
结构化数据搜索:
常见的结构化数据也就是数据库中的数据
在数据库中搜索很容易实现,通常都是使用 sql语句进行查询,而且能很快的得到查询结果
为什么数据库搜索很容易:
因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的
非结构化数据查询方法:
顺序扫描法(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架构:
全文检索的应用场景:
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索
单机软件的搜索:word、markdown
站内搜索:京东、淘宝、拉勾,索引源是数据库
搜索引擎:百度、Google,索引源是爬虫程序抓取的数据
最后:主要与mysql的区别就在于模糊的查询,mysql模糊查询基本不能使用索引,所以在这方面比lucene要差很多
Lucene 实现全文检索的流程说明:
索引和搜索流程图:
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)之后,需要再对域中的内容进行分析
分析的过程 是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单 元
可以将语汇单元理解为一个一个的单词,比如:
分好的词会组成索引库中最小的单元:term,一个term由域名和词组成
第四步:创建索引
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索
最终要实现只搜索被索引的语汇单 元从而找到 Document(文档)
注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫 倒排索引结构
倒排索引结构是根据内容(词语)找文档,如下图:
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合 较大
倒排索引:
倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的 文档及出现的位置
文档:索引库中的每一条原始数据,例如一个商品信息、一个职位信息
词条:原始数据按照分词算法进行分词,得到的每一个词 创建倒排索引,分为以下几步:
创建文档列表:
lucene首先对原始文档数据进行编号(DocID),形成列表,就是一个文档列表
创建倒排索引列表:
对文档中数据进行分词,得到词条(分词后的一个又一个词)
对词条进行编号,以词条创建索引,然后记录下包含该词条的所有文档编号(及其它信息)
搜索的过程:
当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条
然后拿着这 些词条去倒排索引列表中进行匹配,找到这些词条就能找到包含这些词条的所有文档的编号
然后根据这些编号去文档列表中找到文档
查询索引 :
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的 过程
根据关键字搜索索引,根据索引找到对应的文档
第一步:创建用户接口:用户输入关键字的地方
第二步:创建查询 指定查询的域名和关键字
第三步:执行查询
第四步:渲染结果 (结果内容显示到页面上 关键字需要高亮)
Lucene实战:
需求说明:
生成职位信息索引库,从索引库检索数据
分词算法也称为分词器
创建数据库es,将sql脚本导入数据库执行
数据库地址:
链接:https://pan.baidu.com/s/1I1zyzDwaWMnHVSWKuoYvfg
提取码:alsk
建议直接的执行他,而不是打开赋值执行或者查看,因为文件很大
准备开发环境 :
第一步:创建一个maven工程,已经学过Spring Boot,我们就创建一个SpringBoot项目
目录如下:
参照这个目录创建并编写如下:
第二步:导入依赖
<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")
public class JobInfo {
@Id
private long id;
private String companyName;
private String companyAddr;
private String companyInfo;
private String jobName;
private String jobAddr;
private String jobInfo;
private 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;
@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"));
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig Config = new IndexWriterConfig(Version.LATEST,analyzer);
IndexWriter indexWriter = new IndexWriter(directory,Config);
indexWriter.deleteAll();;
List<JobInfo> jobInfos = jobInfoService.selectAll();
for(JobInfo jobInfo : jobInfos){
Document document = new Document();
document.add(new LongField("id",jobInfo.getId(), Field.Store.YES));
document.add(new TextField("companyName",jobInfo.getCompanyName(),Field.Store.YES));
document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(),Field.Store.YES));
document.add(new TextField("companyInfo",jobInfo.getCompanyInfo(),Field.Store.YES));
document.add(new TextField("jobName",jobInfo.getJobName(),Field.Store.YES));
document.add(new TextField("jobAddr",jobInfo.getJobAddr(),Field.Store.YES));
document.add(new TextField("jobInfo",jobInfo.getJobInfo(),Field.Store.YES));
document.add(new 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);
}
indexWriter.close();
System.out.println("创建成功");;;;;;;;;;;;;;;;
}
}
所以可以看出,Field的确是Document里面的信息,且Document里面的信息以Field存储
一般我们分词时,分词的对象是Field的域值,然后分好后,多出的term,代表分好的词和从那个分词对象的Field的名称的组合
即域名和词组成,该域名由于是Field的名称,那么自然是会对应文档,多个文档的域名可能有相同的
所以我们也说,一个词可以找到很多文档,大多数情况下,就是这样的说明
当然,有时候我们只会根据词来查找,也就是说,查询对应的文档中的所有Field域名
当然,我们也可以指定域名查找,这样其他的Field域名就不会查找了
具体的实际情况,就比如说,公司名称或者公司职位等信息,域名就代表他们的意思
上面的代码总体介绍是:
在生成的索引目录E:\class\index中创建索引
索引(Index): 在Lucene中一个索引是放在一个文件夹中的,如下图,同一文件夹中的所有的文件构成一个Lucene索引
我们也可以发现,对应的索引库信息还是挺大的,因为存放了对应的索引对应的信息,或者说,查询的文档信息,分词信息等
且在硬盘(或者磁盘,硬盘只是磁盘的一种,即硬磁盘,以前一般是使用软磁盘)里面
段(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类型:
查询索引 :
回到测试类,添加如下方法:
@Test
public void query() throws IOException {
Directory directory = FSDirectory.open(new File("E:/class/index"));
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Query query = new TermQuery(new Term("companyName","北"));
TopDocs topDocs = indexSearcher.search(query, 100);
int totalHits = topDocs.totalHits;
System.out.println("符合条件的文档数:" + totalHits);
ScoreDoc[] scoreDoc = topDocs.scoreDocs;
for(ScoreDoc s : scoreDoc){
int doc = s.doc;
Document doc1 = indexSearcher.doc(doc);
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 IKAnalyzer();
把原来的索引数据删除,再重新生成索引文件,再使用关键字"北京"进行测试,发现可以查询到结果了
考虑一个问题:一个大型网站中的索引数据会很庞大的,所以使用lucene这种原生的写代码的方式就不合适了
比如说添加时,添加域名和域值等信息的代码,和获取时,得到对应的域名对应的域值信息的代码,编写超级麻烦
或者我们需要看看索引库的文档信息总共有多少个(条),一般没有这样的API
通常需要我们来操作编写,比如慢慢的将参数从0开始,一直加1得到文档,并操作try来解决没有编号的错误等等
或者我们要看看对应的根据条件查询的信息占总信息的多少,即命中率等等信息,即或多或少有些操作是没有的或者很麻烦的
所以需要借助一个成熟的项目或软件来实现,目前比较有名是solr和elasticSearch,所以接下来我们学习elasticSearch的使用
Elastic search介绍和安装:
Elasticsearch是一个需要安装配置的软件,可以说是封装了lucene的框架
并在他的基础上进行了一系列的扩展,比如上面的查询总文档有多少个(条),所以他Elasticsearch也可以称为索引库
虽然lucene和Elasticsearch都可以称为索引库,但实际上只是他们的创建索引的位置的地方(文件目录),才是真正的索引库
这里对他们的称呼是一个整体操作,所以也可以将索引库称为是整体操作的集合
ELK技术栈说明:
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等
前面说的三个就是大家常说的ELK技术栈(开源实时日志分析平台)
Logstash 的作用就是一个数据收集器,将各种格式各种渠道的数据通过它收集解析之后格式化输出到 Elasticsearch
最后再由Kibana 提供的比较友好的 Web 界面进行汇总、分析、搜索。
ELK 内部实际就是个管道结构,数据从 Logstash 到 Elasticsearch 再到 Kibana 做可视化展示
这三个 组件各自也可以单独使用,比如 Logstash 不仅可以将数据输出到Elasticsearch ,也可以到数据库、缓 存等
在安装之前,首先说明一下Elastic
简介:
Elastic官网:https://www.elastic.co/cn/
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈
所以对应的资源可以说是Elastic里面的,即基本可以说Elastic是他们的父辈,或者说Elastic是一系列框架的集合体,就如Cloud类似
框架可以理解为是封装好的操作,如方法,具体介绍可以到61章博客里去查看
Elasticsearch:
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch,从这个官网可以看出,的确是Elastic里面的
功能:
分布式的搜索引擎:百度、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
解释:
上面图中,成为可以说是称为
安装和配置:
为了快速看到效果,我们直接在本地window下安装Elasticsearch
环境要求:JDK8及以上版本,如果不是,那么可能启动不了,因为使用到了对应的新特性
第一步:把资料文件夹中的准备好的软件放到一个没有中文且没有空格的路径位置,然后解压即可
如果路径有中文或者有空格,可能启动会失败,但也不一定,所以最好放到一个没有中文且没有空格的路径位置
对应的elasticsearch-6.2.4.zip解压后,可以找到如下目录
依赖,组件,框架,他们里面都可以说是存放jar包的说明
上面的第三方插件,一般是空的,也一般在es启动时加载,其他的不用加载的依赖默认是自己有的,而不是加载
就如java自己操作默认的类,而不是操作我们写的类(需要导入)
第二步:修改配置文件
修改索引数据和日志数据存储的路径
进入elasticsearch.yml文件,找到如下(一般在33行到37行那里):
path.data: d:\class\es\data
path.logs: d:\class\es\lo
第三步:进入bin目录中直接双击 图下的命令文件,即elasticsearch.bat文件
如果启动失败,那么一般是内存(不是磁盘,内存一般是针对整个机器的)不够,需要修改虚拟机内存的大小
找到config里面的jvm.options文件,找到如下(一般在22行到23行那里):
-Xms1g
-Xmx1g
-Xms256m
-Xmx256m
Xms 是指设定程序启动时占用内存大小,一般来讲,大点,程序会启动的快一点
但是也可能会导致自己的机器变慢,因为内存占用的多
Xmx 是指设定程序运行期间最大可占用的内存大小
如果程序运行需要占用更多的内存,超出了 这个设置值,就会抛出OutOfMemory异常
通常情况下,他们都最好大一点,其中Xmx最好更大一点
访问:
我们可以看到,启动时,有绑定了两个端口
9300:集群节点间通讯接口,接收tcp协议
9200:客户端访问接口,接收Http协议
即我们在浏览器中访问:http://127.0.0.1:9200,若出现如下,则启动成功:
其中不同的服务器,即主机,对应的实例名称以及唯一编号基本是不同,这里只要知道对应的信息是什么就可以了
安装kibana :
什么是Kibana:
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"
进入安装目录下的bin目录:
双击运行kibana.bat:
可以看到kibana的监听端口是5601
我们访问:http://127.0.0.1:5601,若出现如下,则启动成功:
控制台:
选择左侧的DevTools菜单(我们称为控制台),即可进入控制台页面:
若出现上面的,往下滑,找到一个按钮(这个按钮自己寻找,一般是最后面的一个,即下图中的"Get to work"按钮),如下:
点击后,可以出现如下(光标闪烁完后的截图,先不要理解这里的语法,后面会进行说明):
上面右边是执行了运行按钮的结果,而左边就是请求条件,这里注意即可
左边操作一般是像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
进行访问,出现如下:
可以发现后面都是一个词,接下来
解压elasticsearch-analysis-ik-6.2.4.zip后,将解压后的文件夹拷贝到elasticsearch-6.2.4\plugins下,并重命名文件夹为ik
对应的文件在前面的地址中已经有了
最后如图:
最好不要将plugin-descriptor.properties文件和elasticsearch-analysis-ik-6.2.4.jar文件放入到一个文件夹中,否则可能启动不了
他们是主要的文件,即ik里面就要存在他们,而不是再次的下一级
重启对应的es使得加载,然后输入如下:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
出现如下:
即不是对应的单独的词了
其中若没有对应的分词器,那么访问时,会返回错误信息
因为没有对应的分词器,如果不写,那么就是默认的,所以之前的是单个词
但我们也可以直到,并不是全部的组合,即有效的组合,所以没有什么"我是中",这样的组词
一般这样的偏门的词,需要我们去扩展,前面说过的扩展(这里的扩展一般需要他的操作,具体可以百度)
最后注意:由于他加载了我们的中文分词,所以对应的文件是占用的
里面的文件基本都是占用的,除了个别的,比如config目录,所以是防止你随时的操作删除
也可以操作这个:
GET /_analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
结果是:
发现少了一点分词,即更加的操作主要的词语,即正常的合理,而不是非常的合理(即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文件夹
选择后,即可进 行安装,步骤如下:
如果有这个:
点击出现蓝色的就会出现了,主要是为了防止太多插件,而出现的功能
我们带点击对应的这个:
那就会出现如下:
至此我们操作成功,注意:他会自动的连接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类似的
对比关系:
详细说明:
创建索引库 :
现在开始说明语法:
语法 :
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
而由于是Rest风格,那么应该有如下:
查询:GET
删除:DELETE
新建:POST,但是在这里,变成了PUT,你可以试一下,就知道了,返回如下:
修改(也是操作新建的):PUT
上面的请求方式在编写时一般是大小写忽略的
且要注意:如果不加"/“开头或者结尾(即"PUT 索引库名/”,会是他这样的),那么默认是加上"/",除非你已经有了
所以后面的像这样的语法格式,即"PUT 索引库名",也可以,当然这些注意即可
创建索引的请求格式:
请求方式:PUT
请求路径:/索引库名
请求参数:json格式
格式:
PUT /索引库名
使用kibana创建:
kibana的控制台,可以对http请求进行简化,示例:
PUT /lagou
相当于是省去了elasticsearch的服务器地址 而且还有语法提示,非常舒服
执行后,可以到这里:
下面又进行了同样的操作,所以时间是不同的
其中indices是创建的索引文件,里面包含了对应的索引信息,而上面的图中global-2.st操作了你操作的信息
当然但这些并不需要考虑
查看索引库 :
Get请求可以帮我们查看索引信息,格式:
GET /索引库名
上面中aliases代表别名,mappings代表映射,settings代表设置信息,一般情况下,除了有特殊的变量
比如_mapping可以进行修改mappings的值外,其他的一般需要在创建时,进行修改,通常settings一般要在创建时进行修改
删除索引库 :
删除索引使用DELETE请求
格式:
DELETE /索引库名
再次查看lagou:
当然,我们也可以用HEAD请求,查看索引是否存在:
为了验证请求方式在编写时是否一般是大小写忽略的,也就是不区分大小写,看如下:
下面查看成功的,可以都是小写(实际上是不区分大小写的)
实际上是不区分大小写的
注意:虽然不区分大小写,但是会有提示报错,那么因为识别时报错,但是运行时,还是不会变的,也就相当于都操作大写了
使用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": "分词器"
}
}
}
类型名称:就是前面的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中支持的数据类型非常丰富:
我们说几个关键的(注意:在复制代码时,可能有对应的隐藏的信息,可以将代码之前的空格删除,然后执行看看结果):
String类型,又分两种:
text:使用文本数据类型的字段,它们会被分词,其他的类型一般不会被分词,如数值类型或者其值,值基本只包括数字
主要是数字基本不会被分词,这也使得其他的类型基本不会分词,虽然并不是只包括数字
所以现在基本规定,当设置为数值类型或者其他不分词的类型时
(并不是只包括数字的类型,虽然这里并没有说明,虽然上面解释了很多的类型,但具体可以百度查看)
不能加上分词器的属性,否则操作不了,即创建不了
文本字段不用于排序,很少用于聚合,如 文章标题、正文
keyword:关键字数据类型,用于索引结构化内容的字段,不会被分词(也就不能加上分词的属性,比如analyzer)
必须完整匹配的内 容,如邮箱,身份证号,支持聚合
这两种类型都是比较常用的,但有的时候,对于一个字符串字段,我们可能希望他两种都支持,此时
可以利用其多字段特性,也就是之前说的合并,虽然是合并,但是实际上还是有两个,通过例外一个也可以操作合并后的域名
但以第一个为主,比如"还会",和"还早",还早是后执行的,那么得到的信息是还会(第一个)
除非得到合并后的信息(原来的通过id得到的文档可以获取到,即操作getFields可以得到两个值,否则一般只会操作第一个)
当然,是不能越界的,得到值的办法如下:使用得到的数组中的其中一个执行这个stringValue()即可得到数据(一般操作TextField)
具体的数字匹配,可以百度进行查看,一般我们只操作字符串的匹配
"properties": {
"my_index"
"analyzer": "ik_max_word",
"fields": {
"sort":{
"type": "keyword"
}
},
"index": true
}
}
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": {
"properties": {
"ip_addr": {
"type": "ip"
}
}
}
}
}
PUT my_index/_doc/1
{
"ip_addr": "192.168.1.1"
}
GET my_index/_search
{
"query": {
"term": {
"ip_addr": "192.168.0.0/16"
}
}
}
GET my_index/_search
{
"query": {
"match_all": {}
}
}
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
}
下面是再次的操作一次
可以看到结果显示为: created ,应该是创建成功了
另外,需要注意的是,在响应结果中有个 _id 字段,这个就是这条文档数据的 唯一标识
以后的增删改 查都依赖这个id作为唯一标示
可以看到id的值为: -sZkU4MBXyiY5LSSA6ws(前面的生成,不以图片为准)
这里我们新增时没有指定id,所以是ES帮我们随机生成 的id
查看文档:
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上
通过kibana查看数据:
GET /lagou/goods/-sZkU4MBXyiY5LSSA6ws
查看结果:
{
"_index": "lagou",
"_type": "goods",
"_id": "CsZpU4MBXyiY5LSSl63y",
"_version": 1,
"found": true,
"_source": {
"title": "小米手机",
"images": "http://image.lagou.com/12479122.jpg",
"price": 2699
}
}
_source :源文档信息,所有的数据都在里面
_id :这条文档的唯一标示 自动生成的id,长度为20个字符,URL安全,base64编码,GUID(全局唯一标识符)
分布式系统并行生成时不可能会发生冲突,在实际开发中不建议使用ES生成的ID
因为生成的ID太长且为字符串类型(不是整型,我们在程序里,一般是整型的,如前面代码里面的int类型作为doc的参数)
所以检索时效率低,主要还是太长了,所以即不建议使用
建议:将数据表中 唯一的ID,作为ES的文档ID
也可以查询所有的,即goods类型的所有id的数据
GET /lagou/goods/_search
其中"total": 4代表有4个数据,即4个文档数据
新增文档并自定义id :
如果我们想要自己新增的时候指定id,可以这么做(前面操作过一次):
POST /索引库名/类型/id值
{
...
}
示例:
POST /lagou/goods/2
{
"title":"大米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":2899.00
}
得到的数据:
{
"_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,
"_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,
"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值
再次的查看,返回的结果如下:
{
"_index": "lagou",
"_type": "goods",
"_id": "3",
"found": false
}
智能判断:
刚刚我们在新增数据时,添加的字段都是提前在类型中定义过的,如果我们添加的字段并没有提前定义 过,能够成功吗?
事实上Elasticsearch非常智能,你不需要给索引库设置任何mapping映射
它也可以根据你输入的数据 来判断类型,动态添加或者操作数据映射
也就是我之前说明的默认的操作(当然,该默认有点智能,会根据你的值来判断类型),测试一下:
POST /lagou/goods/3
{
"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",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
查看映射:
GET /lagou/_mapping
GET /lagou
下图中,出入是插入,部分词是不分词,即有错误,这里修改一下
发现的确操作了映射关系,我们也可以将这样的操作称为默认
因为对应的类型也差不多是操作默认给出的类型的,比如上面的long
即stock、saleable、subtitle都被成功映射了
subtitle是String类型数据,ES无法智能判断,它就会存入两个字段
例如:
subtitle:text类型
subtitle.keyword:keyword类型
这种智能映射,底层原理是动态模板映射,但通常不建议使用,就比如上面的long,明明可以更小
如我给200这个值,那么long占了空间了,实际上int就可以了
但如果我们想修改这种智能映射的规则,其实只要修改动态模 板即可,也就是修改默认
动态映射模板:
动态模板的语法(也就是格式,或者说语句,简称为语法):
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": {
"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
{
"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,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "lagou3",
"_type": "goods",
"_id": "1",
"_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":"小米电视"
}
}
}
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 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
}
}
]
}
}
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":{
"must": { "match": { "title": "大米" }},
"must_not": { "match": { "title": "电视" }},
"should": { "match": { "title": "手机" }}
}
}
}
返回结果:
{
"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": {
"price": {
"gte": 1000.0,
"lt": 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 查询允许以下字符:
模糊查询(fuzzy):
fuzzy 查询是 term 查询的模糊等价,很少直接使用它,最主要的就是数据真实查询的问题
当然这是对用户来说的,但有时我们也会使用,虽然很少使用
我们新增一个商品:
POST /lagou3/goods/10
{
"title":"apple手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":6899.00
}
fuzzy 查询是 term 查询的模糊等价,它允许用户搜索词条与实际词条的拼写出现偏差,但是偏差的 编辑距离不得超过2:
GET /lagou3/_search
{
"query": {
"fuzzy": {
"title": "appla"
}
}
}
"title":
{
"value":" appee" ,
"fuzziness": "1"
}
返回结果:
{
"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"],
"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
}
}
}
返回的结果:
{
"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":{
"range":{"price":{"gt":2000.00,"lt":3800.00}}
}
}
}
}
简单的理解:假设你在京东或者拼多多里查询"手机",那么首先,"手机"这个是我们的查询条件
得到结果后,我们可以进行刷选,而这个刷选的过程中我们也知道查询的条件是没有变化的
所以这些刷选的操作就是过滤的操作,一般来说,过滤是对数据的一种显示或者不显示的一种操作
比如不会显示指定的,或者说只会显示我们指定的,当然,这些是看当时的解释的,所以并不绝对
而这里的解释很明显,是只会显示我们指定的,所以上面的filter就是类似于刷选的操作
由于是刷选结果,那么肯定的,没有满足的自然不会显示,当然,是在返回给用户看之前进行的刷选
无查询条件,直接过滤:
如果一次查询只有过滤,没有查询条件,不希望进行评分(评分默认为1,而不是权重的值,因为没主体)
我们可以使用 constant_score 取代只有 filter 语句的 bool 查询
在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助
GET /lagou3/_search
{
"query":{
"constant_score": {
"filter":{
"range":{"price":{"gt":2000.00,"lt":3900.00}}
}
}
}
}
GET /lagou3/_search
{
"query":{
"bool":{
"filter":{
"range":{"price":{"gt":2000.00,"lt":3900.00}}
}
}
}
}
排序:
单字段排序 :
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式
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": [
{
"price": {
"order": "desc"
}
}
]
}
返回结果:
{
"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
]
}
]
}
}
多字段排序:
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按 照相关性得分排序:
GET /lagou3/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机" }},
"filter":{
"range":{"price":{"gt":2000,"lt":300000}}
}
}
},
"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
]
}
]
}
}
分页:
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回
那么,如果要实现分页查询该怎么办呢?
elasticsearch的分页与mysql数据库非常相似,都是指定两个值:
from:目标数据的偏移值(开始位置),默认from为0
size:每页大小
GET /lagou3/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"from": 3,
"size": 3
}
返回结果(from设置为0,size设置为10):
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 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": [
3899
]
}
]
}
}
高亮:
高亮原理:
服务端搜索数据,得到搜索结果
把搜索结果中,搜索的关键字都加上约定好的标签
前端页面获得该文档数据时,由于对应的数据加上了写好的标签的CSS样式
那么浏览器或者其他可以渲染的,自然就会进行渲染,即可实现高亮或者其他的样式显示
elasticsearch中实现高亮的语法比较简单:
GET /lagou3/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"highlight": {
"pre_tags": "",
"post_tags": "",
"fields": {
"title": {}
}
}
}
"fields": {
"title": {},
"ii":{}
}
"fields": [{
"title": {}},{
"ii":{}
}]
GET /lagou3/_search
{
"query":{
"bool":{
"should": [
{
"match":
{ "title": "大米" }
},
{
"match":
{ "ii": "手机" }
}
]
}
},
"highlight": {
"pre_tags": "",
"post_tags": "",
"fields": [{
"title": {}},{
"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"
]
}
}
]
}
}
聚合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" }
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" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
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
}
]
}
}
}
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
}
}
]
}
}
}
如果是这样:
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
}
}
}
所以,一个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)
数据备份(注意这是备份,而不是同步复制,即不算节点之间的同步复制):
数据分片解决了海量数据存储的问题,但是如果出现单点故障或者分片集群全部故障(这个基本不会)
那么分片数据可能就不再完整,这又该如何 解决呢?
没错,就像大家为了备份手机数据,会额外存储一份到移动硬盘一样
我们可以给每个分片数据进行备 份,存储到其它节点
防止数据丢失,这就是数据备份,也叫 数据副本(replica)
数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高 了!
为了在高可用和成本间寻求平衡,我们可以这样做:
首先对数据分片,存储到不同节点
然后对每个分片进行备份,放到对方(可以是其他的节点或者是分片节点也或者是下一个分片节点)节点
完成互相备份,这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:
我们可以发现,对应的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次,分别改名
第二步:修改每一个节点的配置文件 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
path.data: d:\class\es-9201\data
path.logs: d:\class\es-9201\logs
http.port: 9201
transport.tcp.port: 9301
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
discovery.zen.minimum_master_nodes: 2
node.master: true
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.port: 9202
transport.tcp.port: 9302
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
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
path.logs: d:\class\es-9203\logs
http.port: 9203
transport.tcp.port: 9303
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
discovery.zen.minimum_master_nodes: 2
node.master: true
注意:由于是yml文件,所以对应的":"后面需要空格隔开(无论多少空格,但必须要有,换行也不可以)
这是yml文件的语法,就不多说了,如果不遵守,否则可能启动不了,自己测试即可
第三步:启动集群
把三个节点分别启动,启动时不要着急,要一个一个地启动(从9201,9202,9203,依次启动过去)
实际上并没有先后之分,主要是为了操作选举的
所以启动的顺序可能会导致谁为主,当然在这里,谁为主,并不重要,所以这里就不分先后的启动也可以
但也要注意,文件的编码最好是UTF-8,如果改变的编码且内容改变,那么不要动
否则复制时,需要改成UTF_8,否则你可能启动不了,因为文件到程序,是自然需要经过文件对应的编码来操作的
使用head插件查看(前面在浏览器中安装了):
上面的星星符号代表为主节点,一般是启动的先后造成的,很明显,他大概是第二次的启动
即一般是根据启动的先后进行选举,但就算是后启动的
也可能不会是主节点(因为可能已经选举完了,或者是结合随机的有序指定,注意:在集群中,最少两个才会显示)
所以并不绝对,注意即可
注意:等待完全启动,否则可能启动过慢,后启动的可能会变成主节点
注意:若他们之间的连接是已经有互相发现了,那么在这个基础上,必须名称一致,否则启动失败
如果没有互相发现,那么自然不会是在一个集群,且可以启动,但是不能访问
还需要将没有投票的这个discovery.zen.minimum_master_nodes: 2,删除或者注释
这也就可以访问了
测试集群中创建索引库:
配置kibana的kibana.yml文件,再重启
配置如下:
elasticsearch.url: "http://localhost:9201"
搭建集群以后就要创建索引库了,那么问题来了,当我们创建一个索引库后,数据会保存到哪个服务节 点上呢?
如果我们对索引库分片,那么每个片会在哪个节点呢?
这个要亲自尝试才知道,点击如下:
分片默认是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吗?
我们输入如下:
点击ok,那么就相当于执行后面的代码
请求方式:PUT
请求路径:/索引库名
请求参数:json格式:
{
"settings": {
"属性名": "属性值"
}
}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认
这里给搭建看看集群中分片和备份的设置方式,示例:
DELETE /lagou
PUT /lagou
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
这里有两个配置:
number_of_shards:分片数量,这里设置为3
number_of_replicas:副本数量,这里设置为1,每个分片一个备份,一个原始数据,共2份
通过chrome浏览器的head查看,我们可以查看到分片的存储结构:
当没有分片成功时,一般会出现如下:
上面是我操作了分片数为3,所以对应的分片是0到2,由于副本是1,所以对应的0到2都有一个副本
我们可以发现,对应的0到2中,外框框颜色深的,代表原数据,颜色不深的代表副本数据
上面的Unassigned代表没有分片的显示,简称为未分配的分片
实际上若我们操作大于等于分片数的副本,他还是会继续的给分片的,自己测试就知道了
当然,具体如何分,一般是随机但有序的,这些并不需要在意
如果分片成功,那么会出现如下:
我们可以看到右上角是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
}
对应的显示:
由于是集群,那么访问其中一个节点地址,对应的都会显示,这是肯定的
我们可以看到,在对应的节点中,有多个相同的0,或者其他,实际上我们可以发现,中间是隔开的
也就是说,虽然有相同的o,或者其他,但是归属索引不同,所以还是有唯一的区别的
而要删除,可以点击如下:
输入"删除"即可,否则不会删除
集群工作原理:
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 版本:
里面可以选择版本,自己找即可,这里就不给出了,因为在不同的时间,可能显示的不同的,他们总会维护的
一般会出现如下:
上面的版本自己指定(一般有个其他的版本出现,点击即可显示),然后点击High,之后的界面先不说明,后面会给出
创建Demo工程(Spring Boot项目,所以记得有父工程):
初始化项目:
目录如下:
上面有两个配置文件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后的界面会有类似于如下的显示(不同的时间一般会不同,因为下面是以前的):
创建索引库的同时,我们也会创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因 如下:
索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好
官方提供的创建索引库及映射API非常繁琐,需要通过字符串拼接json结构:
因此,这些操作建议还是使用我们学习的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"
}
}
}
}
}
若有隐藏的,我们可以复制到一个地方,比如java程序里,然后进行查找删除,然后复制回来即可,比如:
先ctrl+f,出现如下:
复制其中一个NBSP(实际上是一个隐藏值),然后点击如下:
点击后,然后删除(键盘上的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;
@SpringBootTest
public class ElasticSearchTest {
private RestHighLevelClient client;
private Gson gson = new Gson();
@Before
public void init() {
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")
);
client = new RestHighLevelClient(restClientBuilder);
}
@After
public void close() throws IOException {
client.close();
}
@Test
public void test(){
System.out.println(client);
}
}
执行test方法,若返回了数据,代表操作成功
新增文档:
到测试类里面加上testInsert方法:
@Test
public void testInsert() throws IOException {
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");
String source = gson.toJson(product);
IndexRequest request =
new IndexRequest("lagou","item",product.getId().toString());
request.source(source, XContentType.JSON);
IndexResponse response = client.index(request);
System.out.println(response);
}
上面是我们自己首先创建了映射(虽然他会自动补全,即智能判断,即操作模板)
那么我们来看看,如果不在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)
);
request.mapping("tweet",
" {" +
" \"tweet\": {" +
" \"properties\": {" +
" \"message\": {" +
" \"type\": \"text\"" +
" }" +
" }" +
" }" +
" }",XContentType.JSON);
CreateIndexResponse createIndexResponse = client.indices().create(request);
System.out.println(createIndexResponse);
}
我们可以看到,如果是我们自己编写映射,那么非常麻烦,你可能现在看起来不麻烦,那你试着将前面的lagou索引编写一下
会发现,在mapping里面编写,的确太麻烦了,因为对应的明明可以在kibana里操作,那么为什么不在里面操作呢
而之所以我们创建文档不在kibana操作,那是因为对应的数据是在程序获得的,而不是固定的数据,所以需要编写程序
查看文档:
根据rest风格,查看应该是根据id进行get查询,难点是对结果的解析:
在测试类里面加上testFindIndex方法:
@Test
public void testFindIndex() throws IOException {
GetRequest request = new GetRequest("lagou", "item", "1");
GetResponse response = client.get(request);
System.out.println(response);
String source = response.getSourceAsString();
System.out.println(source);
Product item = gson.fromJson(source, Product.class);
System.out.println(item);
}
修改文档,只要再次的执行前面的新增即可,因为相同的id就是修改,自己可以修改一下我们设置的信息,然后启动查看即可
删除文档:
根据id删除:
在测试类里加上testDeleteIndex方法:
@Test
public void testDeleteIndex() throws IOException {
DeleteRequest request = new DeleteRequest("lagou", "item", "1");
DeleteResponse response = client.delete(request);
System.out.println("response = " + response);
}
搜索数据:
查询所有match_all:
在测试类里加上testMatchAll方法:
@Test
public void testMatchAll() throws IOException {
SearchRequest request = new SearchRequest();
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
request.source(sourceBuilder);
SearchResponse response = client.search(request);
System.out.println(response);
SearchHits hits = response.getHits();
System.out.println(hits);
SearchHit[] searchHits = hits.getHits();
System.out.println(searchHits);
for(SearchHit searchHits:hits){
System.out.println(searchHits);
}
System.out.println("----------------");
for (SearchHit hit : searchHits) {
String json = hit.getSourceAsString();
System.out.println(json);
Product item = gson.fromJson(json, Product.class);
System.out.println("item = " + item);
}
}
注意,上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery())来添加的
这个 query() 方法接受的参数是: QueryBuilder 接口类型
这个接口提供了很多实现类,分别对应我们在之前中学习的不同类型的查询
例如:term查询、match查询、range查询、bool查询等,如图(实现类的实现类,也算他的实现类),这里只给出部分:
因此,我们如果要使用各种不同查询,其实仅仅是传递给 sourceBuilder.query() 方法的参数不同而已
而这些实现类不需要我们去 new ,官方提供了 QueryBuilders 工厂帮我们构建各种实现类(这里也只给出部分):
关键字搜索match:
其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已
因为实际上也只是查询方式不同,但结果的格式层级是一样的(因为使用_search),即其他代码基本一致:
比如:
sourceBuilder.query(QueryBuilders.matchQuery("title","华为"));
因此,我们可以把这段代码封装,然后把查询条件作为参数传递:
在测试类里加上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) {
String json = hit.getSourceAsString();
System.out.println(json);
Product item = gson.fromJson(json, Product.class);
System.out.println("item = " + item);
}
}
调用封装的方法,并传递查询条件:
在测试类里加上testMatchQuery方法:
@Test
public void testMatchQuery() throws IOException {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("title", "华为"));
basicQuery(sourceBuilder);
}
范围查询range:
sourceBuilder.query(QueryBuilders.rangeQuery("price"));
与页面上一样,支持下面的范围关键字:
在测试类里面加上testRangeQuery方法:
@Test
public void testRangeQuery() throws IOException {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.rangeQuery("price").gt(1000).lt(6000));
basicQuery(sourceBuilder);
}
source过滤:
_source:存储原始文档
默认情况下,索引库中所有数据都会返回,如果我们想只返回部分字段,可以通过source filter来控 制
在测试类里加上testSourceFilter方法:
@Test
public void testSourceFilter() throws IOException {
SearchRequest request = new SearchRequest();
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.fetchSource(new String[]{"id", "title", "price"}, null);
basicQuery(sourceBuilder);
}
排序:
依然是通过sourceBuilder来配置:
在测试类里加上testSortQuery方法:
@Test
public void testSortQuery() throws IOException {
SearchRequest request = new SearchRequest();
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.sort("price", SortOrder.ASC);
basicQuery(sourceBuilder);
}
分页:
分页需要视图层传递两个参数给我们: 当前页:page,每页大小:size
而elasticsearch中需要的不是当前页,而是起始位置,还好有公式可以计算出:
给出具体的分页操作:
"from": 3,
"size" 3
代码:
在测试类里加上testSortAndPageQuery方法:
@Test
public void testSortAndPageQuery() throws IOException {
SearchRequest request = new SearchRequest();
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.sort("price", SortOrder.ASC);
int page = 1;
int size = 3;
int start = (page - 1) * size;
sourceBuilder.from(start);
sourceBuilder.size(3);
basicQuery(sourceBuilder);
}
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
cluster-nodes: 127.0.0.1:9301,127.0.0.1:9302,127.0.0.1:9303
需要注意的是,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);
}
}
索引库操作 :
创建索引库:
到刚才创建的测试类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
@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的接口:
我们需要自定义接口,继承ElasticsearchRespository:
在lagou包下创建repository包,然后再repository包下创建ProductRepository接口,并继承ElasticsearchRepository接口:
package com.lagou.repository;
import com.lagou.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ProductRepository extends ElasticsearchRepository<Product,Long> {
}
创建索引数据 :
创建索引有单个创建和批量创建之分,先来看单个创建:
在测试类里面加上如下:
@Autowired
private ProductRepository productRepository;
@Test
public void addDocument(){
Product product = new Product(1L, "小米手机9", " 手机","小米", 3499.00, "http://image.lagou.com/13123.jpg");
productRepository.save(product);
}
执行后,我们可以查看数据即可,会发现一般会有数据
再来看批量创建:
再测试类里面加上如下:
@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);
}
那么接下来我们可以继续查看,当然,查看方式有很多,可以使用浏览器客户端,或者kibana都可
查询索引数据:
默认提供了根据id查询,查询所有两个功能:
再测试类里面加上如下:
@Test
public void testQueryById(){
Optional<Product> goodsOptional = productRepository.findById(3L);
System.out.println(goodsOptional);
Product product = goodsOptional.get();
System.out.println(product);
System.out.println(goodsOptional.orElse(null));
}
@Test
public void testQueryAll(){
Iterable<Product> list = productRepository.findAll();
list.forEach(System.out::println);
}
自定义方法查询:
ProductRepository接口继承的ElasticsearchRepository接口提供的查询方法有限,基本不能够操作复杂的查询
但是它却提供了非常强大的自定义查询功能:
只要遵循SpringData提供的语法(后面会说明),我们可以任意定义方法声明:
我们回到ProductRepository接口,加上如下:
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);
Consumer action = System.out::println;
action.accept("1");
list.forEach(System.out::println);
System.out.println("---");
list1.forEach(System.out::println);
}
那么为什么我们执行自己定义的方法,他知道是什么作用呢?
在上面我们说过了,只要遵循SpringData提供的语法,我们可以任意定义方法声明,那么这个语法是什么呢,看如下:
现在看一看支持的一些语法示例:
我们可以看到下面的findByPriceBetween,在前面的ProductRepository接口中,有findByPriceBetween和findByIdBetween
所以我们可以看出,下面图中的Price是自定义的
当然,其他的方法中,可能有多个自定义,比如findByNameAndPrice的Name和Price
具体的观察,主要看后面的语句即可确定谁是自定义,一般对应示例语句中,值或者值里面是" ? "的,就说明该字段是自定义
结合语句和方法名称,就能知道谁是自定义了
比如findByNameAndPrice的Name和Price,以及findByPriceBetween的Price(没有form和to对应的自定义)
经过测试,该Price(或者其他自定义的)一般是类的变量(首字母忽略大小写)
当然,变量也最好不要是有大写,否则不会进行操作,一般这里会报错,虽然添加那里不会,这是语法的原因
因为有些不加字段,不会报错,比如添加(相当于空的)
而有些需要指定字段,比如这里对应的range,他里面必须要指定字段,否则报错,看后面的语法示例就知道了)
与前面的添加一样,这是SDK规定的,大多数的情况下,基本都是如此
所以在操作SDK时,最好变量都是小写的,这也基本不会出现特殊问题
在其他的框架中,一般也有首字母的对应,通常会仔细的说明对应
无论是从哪个方向出发,从己方还是甲方,比如在61章博客中#里面的说明就是如此
可能其他地方的说明有欠缺,但注意即可,或者自己测试补全即可
即该变量就是操作文档的对应的值
通过上面我们可以知道,后面的语句我们都可以进行操作,从而可以实现复杂的语句执行
当然,可能还有更多的示例语句的支持,这里可能并不是全部,具体可以百度查看
原生查询 :
如果觉得上述接口依然不符合你的需求,比如超级复杂的语句,那么SDE也支持原生查询
这个时候还是使用ElasticsearchTemplate,而查询条件的构建是通过一个名为 NativeSearchQueryBuilder 的类来完成的
不过这个类的底层还 是使用的原生API(之前的测试类的java客户端)中的如下工具:
比如QueryBuilders 、 AggregationBuilders 、 HighlightBuilders 等等工具
需求: 查询title中包含小米手机的商品,以价格升序排序
分页查询:每页展示2条,查询第1页
对查询结果进行聚合分析:获取品牌及个数
在测试类里加上如下:
@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")));
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
System.out.println(result);
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);
Aggregations aggregations = result.getAggregations();
Aggregation aggregation = result.getAggregation("brandAgg");
System.out.println(aggregation+"---");
Terms terms = aggregations.get("brandAgg");
System.out.println(terms);
terms.getBuckets().forEach(b -> {
System.out.println("品牌 = " + b.getKeyAsString());
System.out.println("count = " + b.getDocCount());
});
}
注:上述查询不支持高亮结果
高亮展示:
通过测试,如果你不支持,那么一般是如下的代码出现错误:
AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
上面找到了具体的错误,接下来我们来解决该错误
自定义搜索结果映射:
在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 {
@Override
public <T> AggregatedPage<T> mapResults(
SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
System.out.println(aClass);
System.out.println("--1");
System.out.println(searchResponse);
System.out.println(pageable);
long totalHits = searchResponse.getHits().getTotalHits();
System.out.println(totalHits);
List<T> list = new ArrayList<>();
SearchHits hits = searchResponse.getHits();
System.out.println(hits);
for (SearchHit hit : hits) {
System.out.println("2");
System.out.println(hit);
if (hits.getHits().length <= 0) {
return null;
}
String json = hit.getSourceAsString();
System.out.println(json);
System.out.println("----------");
Map<String, Object> map = hit.getSourceAsMap();
System.out.println(map);
System.out.println("1");
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println(highlightFields);
for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
String key = highlightField.getKey();
HighlightField value = highlightField.getValue();
Text[] fragments = value.getFragments();
System.out.println("22");
System.out.println(fragments[0]);
System.out.println("333");
map.put(key, fragments[0].toString());
}
Gson gson = new Gson();
T item = gson.fromJson(gson.toJson(map), aClass);
list.add(item);
System.out.println(list);
}
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
}
然后修改原来的原生查询的代码,修改如下:
@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")));
HighlightBuilder.Field title = new HighlightBuilder.Field("title");
title.preTags("");
title.postTags("");
queryBuilder.withHighlightFields(title);
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
AggregatedPage<Product> result = template.queryForPage(
queryBuilder.build(), Product.class,new ESSearchResultMapper());
System.out.println(result);
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);
Aggregations aggregations = result.getAggregations();
Aggregation aggregation = result.getAggregation("brandAgg");
System.out.println(aggregation+"---");
}
执行后,我们可以看看数据,发现操作了高亮,那么我们也可以知道,实际上主要是第三个参数是否可以操作高亮的原因
换言之,就是原来自带的,不能操作高亮的查询,因为我们自定义的方法是查询了高亮的数据
至此我们操作完毕
特别的内容:
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) {
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
String key = highlightField.getKey();
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);
}
}
这里了解即可