文章内容输出来源:拉勾教育JAVA就业训练营
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
常见的结构化数据也就是数据库中的数据。
在数据库中搜索很容易实现,通常都是使用 sql语句进行查询,而且能很快的得到查询结果。
( 1 ) 顺序扫描法(Serial Scanning)
用户搜索----->文件
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容 ,只是相当的慢。
( 2 ) 全文检索(Full-text Search)
Lucene适用场景:
Lucene架构:
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
核心概念:
Document:
Field
Term:
我们以拉勾招聘网站的搜索为例,在网站上输入关键字搜索显示的内容不是直接从数据库中来的,而是从索引库中获取的,网站的索引数据需要提前创建的。以下是创建的过程:
倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的文档及出现的位置。
搜索的过程:
当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。然后根据这些编号去文档列表中找到文档
2.4 查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档
生成职位信息索引库,从索引库检索数据
第一步:创建一个maven工程 ,已经学过Spring Boot,我们就创建一个SpringBoot项目
第二步:导入依赖
第三步:创建引导类
第四步:配置properties文件
第五步:创建实体类、mapper、service
@RunWith(SpringRunner.class) //springboot做junit测试固定的写法
@SpringBootTest
public class LuceneManager {
@Autowired
private JobInfoServiceImpl jobInfoService;
@Test
public void testCreateIndex() throws Exception{
// 1、指定索引文件存储的位置 D:\class\index
Directory directory = FSDirectory.open(new File("D:\\class\\index"));
// 2、配置版本和分词器
Analyzer analyzer = new StandardAnalyzer();//标准分词器
IndexWriterConfig config = new
IndexWriterConfig(Version.LATEST,analyzer);
// 3、创建一个用来创建索引的对象 IndexWriter
IndexWriter indexWriter = new IndexWriter(directory,config);
indexWriter.deleteAll(); //先删除索引
// 4、获取原始数据
List<JobInfo> jobInfoList = jobInfoService.findAll();
// 有多少的数据就应该构建多少lucene的文档对象document
for (JobInfo jobInfo : jobInfoList) {
Document document = new Document();
// 域名 值 源数据是否存储
document.add(new LongField("id",jobInfo.getId(), Field.Store.YES));
document.add(new TextField("companyName",jobInfo.getCompanyName(),
Field.Store.YES));
document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(),
Field.Store.YES));
document.add(new TextField("jobName",jobInfo.getJobName(),
Field.Store.YES));
document.add(new TextField("jobAddr",jobInfo.getJobAddr(),
Field.Store.YES));
document.add(new IntField("salary",jobInfo.getSalary(),
Field.Store.YES));
document.add(new StringField("url",jobInfo.getUrl(),
Field.Store.YES));
// StringField 不需要分词时使用 举例:url 、电话号码、身份证号
indexWriter.addDocument(document);
}
// 关闭资源
indexWriter.close();
}
}
生成的索引目录:D:\class\index
public class LuceneQuery {
@Test
public void testQueryIndex() throws Exception{
// 1、指定索引文件存储的位置 D:\class\index
Directory directory = FSDirectory.open(new File("D:\\class\\index"));
// 2、 创建一个用来读取索引的对象 indexReader
IndexReader indexReader = DirectoryReader.open(directory);
// 3、 创建一个用来查询索引的对象 IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 使用term查询:指定查询的域名和关键字
Query query = new TermQuery(new Term("companyName","北京"));
TopDocs topDocs = indexSearcher.search(query, 100);//第二个参数:最多显示多
少条数据
int totalHits = topDocs.totalHits; //查询的总数量
System.out.println("符合条件的总数:"+totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs; //获取命中的文档 存储的是文档的id
for (ScoreDoc scoreDoc : scoreDocs) {
int docID = scoreDoc.doc;
// 根据id查询文档
Document document = indexSearcher.doc(docID);
System.out.println( "id:"+ document.get("id"));
System.out.println( "companyName:"+ document.get("companyName"));
System.out.println( "companyAddr:"+ document.get("companyAddr"));
System.out.println( "salary:"+ document.get("salary"));
System.out.println( "url:"+ document.get("url"));
System.out.println("***********************************************************
***");
}
}
}
使用方式:
<dependency>
<groupId>com.janeluogroupId>
<artifactId>ikanalyzerartifactId>
<version>2012_u6version>
dependency>
放入到resources文件夹中。
Elasticsearch是一个需要安装配置的软件。
ELK技术栈说明
Elastic官网:https://www.elastic.co/cn/
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈。
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch
功能:
如上所述,Elasticsearch具备以下特点:
高速、扩展性、最相关的搜索结果
目前Elasticsearch最新的版本是7.x,企业内目前用的比较多是6.x,我们以6.2.4进行讲解,需要JDK1.8及以上。
为了快速看到效果我们直接在本地window下安装Elasticsearch。环境要求:JDK8及以上版本
path.data: d:\class\es\data
#
# Path to log files:
#
path.logs: d:\class\es\log
可以看到绑定了两个端口:
9300:集群节点间通讯接口,接收tcp协议
9200:客户端访问接口,接收Http协议
我们在浏览器中访问:http://127.0.0.1:9200
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
因为Kibana依赖于node,需要在windows下先安装Node.js,双击运行课前资料提供的node.js的安装包:
一路下一步即可安装成功,然后在任意DOS窗口输入名:
进入安装目录下的config目录,修改kibana.yml文件的第21行(注释放开即可)
修改elasticsearch服务器的地址:
elasticsearch.url: "http://127.0.0.1:9200"
进入安装目录下的bin目录:
双击运行
发现kibana的监听端口是5601
我们访问:http://127.0.0.1:5601
在页面右侧,我们就可以输入请求,访问Elasticsearch了。
Lucene的IK分词器早在2012年已经没有维护了,现在我们要使用的是在其基础上维护升级的版本,并且开发为Elasticsearch的集成插件了,与Elasticsearch一起维护升级,版本也保持一致
https://github.com/medcl/elasticsearch-analysis-ik
elasticsearch-head 简介
es-head主要有三个方面的操作:
elasticsearch-head 安装(基于谷歌浏览器)
(1)直接下载压缩包,地址:https://files.cnblogs.com/files/sanduzxcvbnm/elasticsearch-head.7z
(2)解压
(3)在谷歌浏览器中点击“加载已解压的压缩程序”,找到elasticsearch-head文件夹,点击打开即可进行安装。
1、节点 (node)
2、集群(cluster)
3、分片 (shard)
索引可能会存储大量数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文档的单个索引占用了1TB的磁盘空间,可能不适合单个节点的磁盘,或者可能太慢而无法单独满足来自单个节点的搜索请求。
为了解决此问题,Elasticsearch提供了将索引细分为多个碎片的功能。创建索引时,只需定义所需的分片数量即可。每个分片本身就是一个功能齐全且独立的“索引”,可以托管在群集中的任何节点上。
分片很重要,主要有两个原因:
分片如何分布以及其文档如何聚合回到搜索请求中的机制完全由Elasticsearch管理,并且对您作为用户是透明的。
在随时可能发生故障的网络/云环境中,非常有用,强烈建议您使用故障转移机制,以防碎片/节点因某种原因脱机或消失。为此,Elasticsearch允许您将索引分片的一个或多个副本制作为所谓的副本分片(简称副本)。
4、副本(replica)
1、文档 (document)
Elasticsearch是面向文档的,这意味着索引和搜索数据的最小单位是文档。
在Elasticsearch中文档有几个重要的属性。
2、类型 (type)
3、索引 (index)
4、映射(mapping)
索引库(indices) | indices是index的复数,代表许多的索引, |
---|---|
概念 | 说明 |
类型(type) | 类型是模拟mysql中的table概念,一个索引库下可以有不同类型的索引(目前6.X以后的版本只能有一个类型),类似数据库中的表概念。数据库表中有表结构,也就是表中每个字段的约束信息;索引库的类型中对应表结构的叫做 映射(mapping) ,用来定义每个字段的约束。 |
文档(document) | 存入索引库原始的数据。比如每一条商品信息,就是一个文档 |
字段(field) | 文档中的属性 |
映射配置(mappings) | 字段的数据类型、属性、是否索引、是否存储等特性 |
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
创建索引的请求格式:
{
"settings": {
"属性名": "属性值"
}
}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认
kibana的控制台,可以对http请求进行简化,示例:
相当于是省去了elasticsearch的服务器地址
而且还有语法提示,非常舒服。
Get请求可以帮我们查看索引信息,格式:
GET /索引库名
删除索引使用DELETE请求
语法
DELETE /索引库名
有了 索引库 ,等于有了数据库中的 database 。接下来就需要索引库中的 类型 了,也就是数据库中的表 。创建数据库表需要设置字段约束,索引库也一样,在创建索引库的类型时,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做 字段映射(mapping)
注意:Elasticsearch7.x取消了索引type类型的设置,不允许指定类型,默认为_doc,但字段仍然是有的,我们需要设置字段的约束信息,叫做字段映(mapping)
字段的约束我们在学习Lucene中我们都见到过,包括到不限于:
语法
请求方式依然是PUT
PUT /索引库名/_mapping/typeName
{
"properties": {
"字段名": {
"type": "类型",
"index": true,
"store": true,
"analyzer": "分词器"
}
}
}
PUT lagou/_mapping/goods
{
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "float"
}
}
}
响应结果:
{
"acknowledged": true
}
上述案例中,就给lagou这个索引库添加了一个名为 goods 的类型,并且在类型中设置了3个字段:
语法:
GET /索引库名/_mapping
查看某个索引库中的所有类型的映射。如果要查看某个类型映射,可以再路径后面跟上类型名称。即:
GET /索引库名/_mapping/类型名
示例:
GET /lagou/_mapping/goods
响应:
{
"lagou": {
"mappings": {
"goods": {
"properties": {
"images": {
"type": "keyword",
"index": false
},
"price": {
"type": "float"
},
"title": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
}
1)type
Elasticsearch中支持的数据类型非常丰富:
一级分类 | 二级分类 | 具体类型 | 使用 |
---|---|---|---|
核心类型 | 字符串类型 | text,keyword | 结构化搜索,全文文本搜索、聚合、排 |
- | 序等整数类型 | integer,long,short,byte | 字段的长度越短,索引和搜索的效率越高。 |
- | 浮点类型 | double,float,half_float,scaled_float | - |
- | 逻辑类型 | boolean | - |
- | 日期类型 | date | - |
- | 范围类型 | range | - |
- | 二进制类型 | binary | 该 binary 类型接受二进制值作为Base64编码的字符串。该字段默认情况下不存储(store),并且不可搜索 |
复合类型 | 数组类型 | array | - |
- | 对象类型 | object 用于单个JSON对象 | - |
- | 嵌套类型 | nested | 用于JSON对象数组 |
地理类型 | 地理坐标类型 | geo_point 纬度/经度积分 | |
- | 地理地图 | geo_shape 用于多边形等复杂形状 | |
特殊类型 | IP类型 | ip 用于IPv4和IPv6地址 | |
- | 范围类型 | completion | 提供自动完成建议 |
- | 令牌计数类型 | token_count | 计算字符串中令牌的数量 |
我们说几个关键的:
String类型,又分两种:
这两种类型都是比较常用的,但有的时候,对于一个字符串字段,我们可能希望他两种都支持,此时,可以利用其多字段特性
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"sort":{
"type": "keyword"
}
},
"index": true
}
}
Numerical:数值类型,分两类
Date:日期类型
Array:数组类型
字符串数组:["one", "two"]
整数数组:[1,2]
数组的数组:[1, [2, 3]],等价于[1,2,3]
对象数组:[ { "name": "Mary", "age": 12 }, { "name": "John", "age": 10 }]
Object:对象
{
"region": "US",
"manager.age": 30,
"manager.name ": "John Smith"
}
索引方法如下:
{
"mappings": {
"properties": {
"region": { "type": "keyword" },
"manager": {
"properties": {
"age": { "type": "integer" },
"name": { "type": "text" }
}
}
}
}
}
如果存储到索引库的是对象类型,例如上面的girl,会把girl编程两个字段:girl.name和girl.age
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"
}
}
}
2)index
index影响字段的索引情况。
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。
但是有些字段是我们不希望被索引的,比如商品的图片信息(URL),就需要手动设置index为false。
3)store
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "text",
"store": true
},
"date": {
"type": "date",
"store": true
},
"content": {
"type": "text"
}
}
}
}
}
4)boost
权重,新增数据时,可以指定该数据的权重,权重越高,得分越高,排名越靠前。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "text",
"boost": 2
},
"content": {
"type": "text"
}
}
}
}
}
title 字段上的匹配项的权重是字段上的匹配项的权重的两倍 content ,默认 boost 值为 1.0 。
提升仅适用于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":{
"字段名":{
"映射属性名":"映射属性值"
}
}
}
}
}
文档,即索引库中某个类型下的数据,会根据规则创建索引,将来用来搜索。可以类比做数据库中的每一行数据。
通过POST请求,可以向一个已经存在的索引库中添加文档数据。
POST /索引库名/类型名
{
"key":"value"
}
示例:
POST /lagou/goods/
{
"title":"小米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":2699.00
}
响应:
{
"_index": "lagou",
"_type": "goods",
"_id": "tURGznQB29tVfg_iWHfl",
"_version": 1,
"result": "created",
"_shards": {
"total": 3,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 2
}
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上。
通过kibana查看数据:
GET /lagou/goods/tURGznQB29tVfg_iWHfl
查看结果:
{
"_index": "lagou",
"_type": "goods",
"_id": "tURGznQB29tVfg_iWHfl",
"_version": 1,
"found": true,
"_source": {
"title": "小米手机",
"images": "http://image.lagou.com/12479122.jpg",
"price": 2699
}
}
如果我们想要自己新增的时候指定id,可以这么做:
POST /lagou/goods/2
{
"title":"大米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":2899.00
}
PUT:修改文档
POST:新增文档
把刚才新增的请求方式改为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
}
删除使用DELETE请求,同样,需要根据id进行删除:
POST /lagou/goods/3
{
"title":"超大米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":3299.00,
"stock": 200,
"saleable":true,
"subTitle":"大米"
}
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
}
}
}
]
}
}
}
在这个案例中,我们把做了两个映射配置:
基本语法
GET /索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"查询条件值"
}
}
}
这里的query代表一个查询对象,里面可以有不同的查询属性
GET /lagou/_search
{
"query":{
"match_all": {}
}
}
文档得分:使用ES时,对于查询出的文档无疑会有文档相似度之别。而理想的排序是和查询条件相关性越高排序越靠前,而这个排序的依据就是_score
GET /lagou/_search
{
"query":{
"match":{
"title":"小米电视"
}
}
}
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 or 的关系。
GET /lagou/_search
{
"query":{
"match":{
"title":{"query":"小米电视","operator":"and"}
}
}
}
term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串,keyword类型的字符串
GET /lagou/_search
{
"query":{
"term":{
"price":2699.00
}
}
}
bool 把各种其它查询通过 must (与)、 must_not (非)、 should (或)的方式进行组合
GET /lagou/_search
{
"query":{
"bool":{
"must": { "match": { "title": "大米" }},
"must_not": { "match": { "title": "电视" }},
"should": { "match": { "title": "手机" }}
}
}
}
range 查询找出那些落在指定区间内的数字或者时间
GET /lagou/_search
{
"query":{
"range": {
"price": {
"gte": 1000.0,
"lt": 2800.00
}
}
}
}
range 查询允许以下字符:
操作符 | 说明 |
---|---|
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
fuzzy 查询是 term 查询的模糊等价,很少直接使用它。
fuzzy 查询是 term 查询的模糊等价。它允许用户搜索词条与实际词条的拼写出现偏差,但是偏差的编辑距离不得超过2:
GET /lagou/_search
{
"query": {
"fuzzy": {
"title": "appla"
}
}
}
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在 _source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加 _source 的过滤
GET /lagou/_search
{
"_source": ["title","price"],
"query": {
"term": {
"price": 2699
}
}
}
我们也可以通过:
includes:来指定想要显示的字段
excludes:来指定不想要显示的字段
二者都是可选的。
GET /lagou/_search
{
"_source": {
"includes":["title","price"]
},
"query": {
"term": {
"price": 2699
}
}
}
Elasticsearch 使用的查询语言(DSL)拥有一套查询组件,这些组件可以以无限组合的方式进行搭配。
这套组件可以在以下两种情况下使用:过滤情况(filtering context)和查询情况(query context)。
如何选择查询与过滤:
GET /lagou/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机" }},
"filter":{
"range":{"price":{"gt":2000.00,"lt":3800.00}}
}
}
}
}
GET /lagou/_search
{
"query":{
"constant_score": {
"filter": {
"range":{"price":{"gt":2000.00,"lt":3000.00}}
}
}
}
}
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式
GET /lagou/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序
GET /lagou/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机" }},
"filter":{
"range":{"price":{"gt":200000,"lt":300000}}
}
}
},
"sort": [
{ "price": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回。那么,如果要实现分页查询该怎么办呢?
GET /lagou/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"from": 3,
"size": 3
}
高亮原理:
elasticsearch中实现高亮的语法比较简单:
GET /lagou/_search
{
"query": {
"match": {
"title": "手机"
}
},
"highlight": {
"pre_tags": "",
"post_tags": "",
"fields": {
"title": {}
}
}
}
在使用match查询的同时,加上一个highlight属性:
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫 桶 ,一个叫 度量 :
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个 桶 ,例如我们根据国籍对人划分,可以得到 中国桶 、 英国桶 , 日本桶 ……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为 度量
比较常用的一些度量聚合方式:
首先,我们按照 汽车的颜色 color来 划分 桶 ,按照颜色分桶,最好是使用TermAggregation类型,按照颜色的名称来分桶
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
{
"took": 33,
"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
}
]
}
}
}
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
我们都是使用单点的elasticsearch,接下来我们会学习如何搭建Elasticsearch的集群。
单点的elasticsearch存在哪些可能出现的问题呢?
所以,为了应对这些问题,我们需要对elasticsearch搭建集群
首先,我们面临的第一个问题就是数据量太大,单点存储量有限的问题。
大家觉得应该如何解决?
没错,我们可以把数据拆分成多份,每一份存储到不同机器节点(node),从而实现减少每个节点数据量的目的。这就是数据的分布式存储,也叫做: 数据分片(Shard) 。
第一步:复制es软件粘贴3次,分别改名
第二步:修改每一个节点的配置文件 config下的- elasticsearch.yml,下面已第一份配置文件为例三个节点的配置文件几乎一致,除了:node.name、path.data、path.log、http.port、transport.tcp.port
node1
#允许跨域名访问
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
# 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
# 是否为主节点
node.master: true
node2、node3一样
第三步:启动集群
配置kibana,再重启
搭建集群以后就要创建索引库了,那么问题来了,当我们创建一个索引库后,数据会保存到哪个服务节点上呢?如果我们对索引库分片,那么每个片会在哪个节点呢?
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认。这里给搭建看看集群中分片和备份的设置方式,
PUT /lagou
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
这里有两个配置:
通过chrome浏览器的head查看,我们可以查看到分片的存储结构:
可以看到,lagou这个索引库,有三个分片,分别是0、1、2,每个分片有1个副本,共6份。
(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的副本,负责容错,以及承担读请求负载
(6)primary shard的数量在创建索引的时候就固定了,replica shard的数量可以随时修改
(7)primary shard的默认数量是5,replica默认是1(每个主分片一个副本分片),默认有10个
shard,5个primary shard,5个replica shard
(8)primary shard不能和自己的replica shard放在同一个节点上(否则节点宕机,primary shard和副本都丢失,起不到容错的作用),但是可以和其他primary shard的replica shard放在同一个节点上
这个路由简单的说就是取模算法,比如说现在有3太服务器,这个时候传过来的id是5,那么5%3=2,就放在第2台服务器
倒排序算法
查询有个算法叫倒排序:简单的说就是:通过分词把词语出现的id进行记录下来,再查询的时候先去查到哪些id包含这个数据,然后再根据id把数据查出来
查询过程
在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html
注意点击进入后,选择版本到 6.2.4 ,因为我们之前按照的都是 6.2.4 版本:
注意,这里我们直接导入了SpringBoot的启动器,方便后续讲解。不过还需要手动引入elasticsearch的High-level-Rest-Client的依赖:
<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.4.3version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
我们在resource下创建application.yml
创建索引库的同时,我们也会创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因如下:
package com.lagou.es.pojo;
public class Product {
private Long id;
private String title; //标题
private String category;// 分类
private String brand; // 品牌
private Double price; // 价格
private String images; // 图片地址
}
分析一下数据结构:
我们可以编写这样的映射配置
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"
}
}
}
}
}
有了索引库,我们接下来看看如何新增索引数据
操作MYSQL数据库:
1.获取数据库连接
2.完成数据的增删改查
3.释放资源
我们先编写一个测试类:
public class ElasticSearchTest {
private RestHighLevelClient client;
// Json工具
private Gson gson = new Gson();
@Before
public void init(){
// 初始化HighLevel客户端
client = new RestHighLevelClient(
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")
)
);
}
@After
public void close() throws IOException {
// 关闭客户端
client.close();
}
}
@Test
public void testInsert() throws IOException {
//1.文档数据
Product product = new Product();
product.setBrand("华为");
product.setCategory("手机");
product.setId(1L);
product.setImages("http://image.huawei.com/1.jpg");
product.setPrice(5999.99);
product.setTitle("华为P50就是棒");
//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());
request.source(source, XContentType.JSON);
//4.发出请求
IndexResponse response = restHighLevelClient.index(request,
RequestOptions.DEFAULT);
System.out.println(response);
}
@Test
public void testFindIndex() throws IOException {
// 创建get请求,并指定id
GetRequest request = new GetRequest("lagou", "item", "1");
// 查询,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 解析响应,应该是json
String source = response.getSourceAsString();
// 转换json数据
Product item = gson.fromJson(source, Product.class);
System.out.println(item);
}
新增时,如果传递的id是已经存在的,则会完成修改操作,如果不存在,则是新增。
根据id删除:
@Test
public void testDeleteIndex() throws IOException {
// 准备删除的请求,参数为id
DeleteRequest request = new DeleteRequest("lagou", "item", "1");
// 发起请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("response = " + response);
}
@Test
public void testMatchAll() throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 查询构建工具
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
request.source(sourceBuilder);
// 搜索
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 解析
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// 取出source数据
String json = hit.getSourceAsString();
// 反序列化
Product item = gson.fromJson(json, Item.class);
System.out.println("item = " + item);
}
}
其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已,其他代码基本一致:
因此,我们可以把这段代码封装,然后把查询条件作为参数传递:
private void basicQuery(SearchSourceBuilder sourceBuilder) throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
request.source(sourceBuilder);
// 搜索
SearchResponse response = client.search(request,
RequestOptions.DEFAULT);
// 解析
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// 取出source数据
String json = hit.getSourceAsString();
// 反序列化
Product item = gson.fromJson(json, Item.class);
System.out.println("item = " + item);
}
}
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");
方法 | 说明 |
---|---|
gt(Object from) | 大于 |
gte(Object from) | 大于等于 |
lt(Object from) | 小于 |
lte(Object from) | 小于等于 |
_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);
basicQuery(sourceBuilder);
}
依然是通过sourceBuilder来配置
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);
}
分页需要视图层传递两个参数给我们:
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;
// 配置分页
sourceBuilder.from(start);
sourceBuilder.size(3);
basicQuery(sourceBuilder);
}
我们在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
添加引导类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EsApplication {
public static void main(String[] args) {
SpringApplication.run(EsApplication.class,args);
}
}
准备一个pojo对象
然后准备一个新的实体类,作为下面与索引库对应的文档:
我们先创建一个测试类,然后注入ElasticsearchTemplate:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringElasticsearchTest {
@Autowired
private ElasticsearchTemplate esTemplate;
}
下面是创建索引库的API示例:
@Test
public void testCreateIndex(){
// 创建索引库,并制定实体类的字节码
esTemplate.createIndex(Goods.class);
}
@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; // 图片地址
//
几个用到的注解:
刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样
SDE的索引数据CRUD并没有封装在ElasticsearchTemplate中,而是有一个叫做 ElasticsearchRepository的接口:
我们需要自定义接口,继承ElasticsearchRespository:
package com.lagou.es.repository;
import com.lagou.es.pojo.Goods;
import
org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
@Autowired
private GoodsRepository goodsRepository;
@Test
public void addDocument(){
Goods goods = new Goods(1L, "小米手机9", " 手机",
"小米", 3499.00, "http://image.lagou.com/13123.jpg");
// 添加索引数据
goodsRepository.save(goods);
}
批量创建:
@Test
public void addDocuments(){
// 准备文档数据:
List<Goods> list = new ArrayList<>();
list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));
// 添加索引数据
goodsRepository.saveAll(list);
}
默认提供了根据id查询,查询所有两个功能:
@Test
public void testQueryById(){
Optional goodsOptional = goodsRepository.findById(3L);
System.out.println(goodsOptional.orElse(null));
}
@Test
public void testQueryAll(){
Iterable list = goodsRepository.findAll();
list.forEach(System.out::println);
}
ProductRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:
只要遵循SpringData提供的语法,我们可以任意定义方法声明:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
/**
* 根据价格区间查询
* @param from 开始价格
* @param to 结束价格
* @return 符合条件的goods
*/
List<Product> findByPriceBetween(Double from, Double to);
}
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {“bool” : {“must” : [ {“field”: {“name” : “?”}}, {“field” :{“price” : “?”}} ]}} |
Or | findByNameOrPrice | {“bool” : {“should” : [{“field” : {“name” : “?”}},{“field” : {“price” : “?”}}]}} |
Is | findByName | {“bool” : {“must” : {“field” :{“name” : “?”}}}} |
Not | findByNameNot | {“bool” : {“must_not” :{“field” : {“name” : “?”}}}} |
Between | findByPriceBetween | {“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :?,“include_lower” :true,“include_upper” :true}}}}} |
LessThanEqual | findByPriceLessThan | {“bool” : {“must” : {“range” :{“price” : {“from” : null,“to”: ?,“include_lower” :true,“include_upper” :true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :null,“include_lower” :true,“include_upper” :true}}}}} |
Before | findByPriceBefore | {“bool” : {“must” : {“range” :{“price” : {“from” : null,“to”: ?,“include_lower” :true,“include_upper” :true}}}}} |
After | findByPriceAfter{“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :null,“include_lower” :true,“include_upper” :true}}}}} | |
Like | findByNameLike | {“bool” : {“must” : {“field” :{“name” : {“query” : “?*”,“analyze_wildcard” :true}}}}} |
StartingWith | findByNameStartingWith{“bool” : {“must” : {“field” :{“name” : {“query” : “?*”,“analyze_wildcard” :true}}}}} | |
EndingWith | findByNameEndingWith | {“bool” : {“must” : {“field” :{“name” : {“query” :"*?",“analyze_wildcard” :true}}}}} |
Contains/Containing | findByNameContaining | {“bool” : {“must” : {“field” :{“name” : {“query” : “?”,“analyze_wildcard” :true}}}}} |
In | findByNameIn(Collectionnames) | {“bool” : {“must” : {“bool” :{“should” : [ {“field” :{“name” : “?”}}, {“field” :{“name” : “?”}} ]}}}} |
NotIn | findByNameNotIn(Collectionnames) | {“bool” : {“must_not” :{“bool” : {“should” : {“field”: {“name” : “?”}}}}}} |
Near | findByStoreNear | Not Supported Yet |
True | findByAvailableTrue | {“bool” : {“must” : {“field” :{“available” : true}}}} |
False | findByAvailableFalse | {“bool” : {“must” : {“field” :{“available” : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {“sort” : [{ “name” : {“order”: “desc”} }],“bool” : {“must”: {“field” : {“available” :true}}}} |
需求:
查询title中包含小米手机的商品,以价格升序排序,分页查询:每页展示2条,查询第1页。
对查询结果进行聚合分析:获取品牌及个数
@Test
public void testNativeQuery(){
// 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.1 source过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new
String[0]));
// 1.2搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 1.3分页及排序条件
queryBuilder.withPageable(
PageRequest.of(0, 2,
Sort.by(Sort.Direction.ASC, "price")));
// 1.4高亮显示
// queryBuilder.withHighlightBuilder(new HighlightBuilder().field("title"));
// 1.5聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"
));
// 构建查询条件,并且查询
AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(),
Goods.class);
// 2、解析结果:
// 2.1分页结果
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Goods> list = result.getContent();
System.out.println("总条数 = " + total);
System.out.println("总页数 = " + totalPages);
System.out.println(list);
// 2.2.聚合结果
Aggregations aggregations = result.getAggregations();
Terms terms = aggregations.get("brandAgg");
terms.getBuckets().forEach(b -> {
System.out.println("品牌 = " + b.getKeyAsString());
System.out.println("count = " + b.getDocCount());
});
}
高亮展示:
1、自定义搜索结果映射
public class EsResultMapper implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T>
aClass, Pageable pageable) {
// 记录总条数
long totalHits = response.getHits().getTotalHits();
// 记录列表(泛型) - 构建Aggregate使用
List<T> list = new ArrayList<>();
// 获取搜索结果(真正的的记录)
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
if (hits.getHits().length <= 0) {
return null;
}
// 将原本的JSON对象转换成Map对象
Map<String, Object> map = hit.getSourceAsMap();
// 获取高亮的字段Map
Map<String, HighlightField> highlightFields =
hit.getHighlightFields();
for (Map.Entry<String, HighlightField> highlightField :
highlightFields.entrySet()) {
// 获取高亮的Key
String key = highlightField.getKey();
// 获取高亮的Value
HighlightField value = highlightField.getValue();
// 实际fragments[0]就是高亮的结果,无需遍历拼接
Text[] fragments = value.getFragments();
// 因为高亮的字段必然存在于Map中,就是key值
map.put(key, fragments[0].toString());
}
// 把Map转换成对象
Gson gson = new Gson();
T item = gson.fromJson(gson.toJson(map), aClass);
list.add(item);
}
// 返回的是带分页的结果
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
}
2、高亮实现:
@Test
public void testNativeQuery() {
// 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.1 source过滤
//queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new
String[0]));
// 1.2搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 1.3分页及排序条件
queryBuilder.withPageable(
PageRequest.of(0, 6,
Sort.by(Sort.Direction.ASC, "price")));
// 1.4高亮显示
//queryBuilder.withHighlightBuilder(new
HighlightBuilder().field("title"));
// 构建高亮查询
HighlightBuilder.Field field = new
HighlightBuilder.Field("title").preTags("").postTags("
</font>");
queryBuilder.withHighlightFields(field); // 名字高亮
NativeSearchQuery build = queryBuilder.build();
// 1.5聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"
));
// 构建查询条件,并且查询
AggregatedPage<Product> result =
template.queryForPage(queryBuilder.build(), Product.class,new EsResultMapper());
// 2、解析结果:
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Product> list = result.getContent();
list.stream().forEach(product -> System.out.println(product));
}