上一篇《分布式搜索引擎Elasticsearch——基础》
https://keyboard-dog.blog.csdn.net/article/details/103875978
一、深度分页
1、什么是深度分页
es 默认采用的分页方式是 from+ size 的形式,当from值非常大的时候,比如10000、5000,我们就称作深度分页。在深度分页的情况下,查询效率下是非常低的,比如from = 5000, size=10, es需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条,如果有5个分片,那么es就需要汇总2.5w条数据,进行排序,然后再获取最后10个。
除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误
2、简单的处理方案(淘宝就是这样的)
(1)限制分页数量
那么如何解决深度分页带来的性能呢?其实我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深,我们平时搜索淘宝或者百度,一般也就看个10来页就顶多了。
(2)修改搜索量
配置文件修改
核心配置文件(elasticsearch.yml)的最下方添加配置
#设置搜索量,注意冒号后面有个空格
index.max_result_window: 100000000
api修改
linux下使用curl访问api接口
curl -XPUT http://127.0.0.1:9200/_settings -d '{ "index" : { "max_result_window" : 100000000}}‘
或者使用工具发POST请求到ip:9200/索引名/_settings
在body中添加参数
{
"index.max_result_window": 100000000
}
3、滚动搜索处理方案(scroll api)
官方文档:https://www.elastic.co/guide/cn/elasticsearch/guide/current/scroll.html
滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记,随后再次滚动搜索会需要上一次搜索的锚标记,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的,搜索的内容还是快照中的数据。
(1)发起滚动查询
scroll=1m,相当于是一个session会话时间,搜索保持的上下文时间为1分钟,也就是这次的滚动搜索的有效时间是1分钟,所有的操作必须在1分钟里完成
使用get/post请求访问 ip:9200/_search?scroll=1m
{
"query": {
"match_all": { }
},
"sort" : ["_doc"],
<!-- 每次滚动查询出来的次数 -->
"size": 1000
}
在返回结果中,除了查询的结果还会包含一个“scroll_id”属性
,它是一个base64编码的长字符串 ,我们需要使用他进行后续查询的“滚动”
(2)进行后续的滚动查询
使用get/post请求访问 ip:9200/_search
{
"scroll": "1m",
"scroll_id" : "your last scroll_id"
}
这个游标查询将返回下一批结果。 尽管我们指定字段 size 的值为1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为size * number_of_primary_shards 。
注:注意游标查询每次返回一个新字段 _scroll_id。每次我们做下一次游标查询, 我们必须把前一次查询返回的字段 _scroll_id 传递进去。 当没有更多的结果返回的时候,我们就处理完所有匹配的文档了
二、批量操作
1、批量查询(_mget)
使用get/post请求访问 ip:9200/索引名/_doc/_mget
{
<!-- 查询的字段 -->
"ids": [
"1001",
"1002",
"1003"
]
}
这种查询的时候,即使没有数据他也会返回一条数据
{
"_index":"索引名",
"_type":"_doc",
"_id":"搜索的id",
<!-- false:没有查到 true:查询到了-->
"found":false
}
2、批量操作(bulk)
官方文档:https://www.elastic.co/guide/cn/elasticsearch/guide/current/bulk.html
(1)基本语法
bulk操作和以往的普通请求格式有区别。不要格式化json,bulk的语法是一行算作一个命令,同时也不是完全正确的json格式,个别的工具可能有json校验,会报错,这个需要注意
,
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
- { action: { metadata }}代表批量操作的类型,可以是新增、删除或修改\n
- \n是每行结尾必须填写的一个规范,每一行包括最后一行都要写,用于es的解析
- { request body }是请求body,增加和修改操作需要,删除操作则不需要
(2)批量操作的类型
action 必须是以下选项之一:
- create:如果文档不存在,那么就创建它。存在会报错。发生异常报错不会影响其他操作。
- index:创建一个新文档或者替换一个现有的文档。
- update:部分更新一个文档。
- delete:删除一个文档。
metadata 中需要指定要操作的文档的_index 、 _type 和 _id,_index 、 _type也可以在url中指定
(3)批量新增
1)create批量新增
如果文档不存在,那么就创建它。存在会报错。发生异常报错不会影响其他操作
不同索引的批量新增
向shop1、shop2、shop3三个索引,分别增加1条数据
使用post请求访问 ip:9200/_bulk
{"create": {"_index": "shop1", "_type": "_doc", "_id": "2001"}}
{"id": "2001", "name": "name2001"}
{"create": {"_index": "shop2", "_type": "_doc", "_id": "2002"}}
{"id": "2002", "name": "name2002"}
{"create": {"_index": "shop3", "_type": "_doc", "_id": "2003"}}
{"id": "2003", "name": "name2003"}
相同索引的批量新增
使用post请求访问 ip:9200/索引名/_doc/_bulk
{"create": {"_id": "2001"}}
{"id": "2001", "name": "name2001"}
{"create": {"_id": "2002"}}
{"id": "2002", "name": "name2002"}
{"create": {"_id": "2003"}}
{"id": "2003", "name": "name2003"}
2)index批量新增
已有文档id会被覆盖,不存在的id则新增
使用post请求访问 ip:9200/索引名/_doc/_bulk
{"index": {"_id": "2001"}}
{"id": "2001", "name": "name2001"}
{"index": {"_id": "2002"}}
{"id": "2002", "name": "name2002"}
{"index": {"_id": "2003"}}
{"id": "2003", "name": "name2003"}
同样支持不同索引批量新增,只需要把create改成index就可以了
(4)批量跟新部分文档数据
使用post请求访问 ip:9200/索引名/_doc/_bulk
{"update": {"_id": "2001"}}
{"doc":{ "id": "3004"}}
{"update": {"_id": "2007"}}
{"doc":{ "name": "nameupdate"}}
(5)批量删除
使用post请求访问 ip:9200/索引名/_doc/_bulk
{"delete": {"_id": "2004"}}
{"delete": {"_id": "2007"}}
(6)混合批量各种操作
使用post请求访问 ip:9200/索引名/_doc/_bulk
{"create": {"_id": "8001"}}
{"id": "8001", "name": "name8001"}
{"update": {"_id": "2001"}}
{"doc":{ "id": "20010"}}
{"delete": {"_id": "2003"}}
{"delete": {"_id": "2005"}}
三、Elasticsearch集群
学习了过单机的ES后,我们可以把注意力转移到高可用上,一般我们可以把es搭建成集群,2台以上就能成为es集群了。集群不仅可以实现高可用,也能实现海量数据存储的横向扩展。
1、分片机制
在之前的文章中(分布式搜索引擎Elasticsearch——基础),我们的备份分片出现了一点问题,是因为没有节点可用。
下面我们举例说明一下Elasticsearch在集群情况下的默认分片机制。
现在我们有3台服务器,ip分别是192.168.85.200、192.168.85.201、192.168.85.202,然后新建了一个索引叫test,他有5个分片(分别叫:主1,主2,主3,主4,主5),每个分片又有一个备份分片(分别叫:备1,备2,备3,备4,备5)。
ES会先分配主分片,按照主1-主5的顺序依次分配到3台服务器上,200服务器上分配主1、主4,201服务器上分配主2、主5,202服务器上分配主3,情况大致如下:
ip地址 | 分片名 | 分片名 |
---|---|---|
192.168.85.200 | 主1 | 主4 |
192.168.85.201 | 主2 | 主5 |
192.168.85.202 | 主3 |
然后分配备份分片,会接着上次分配到的服务器,继续按照备1-备5的顺序依次分配到3台服务器上,有所不同的是如果备份分片和主分片将要被分配到同一个服务器上时(比如:3个服务器,6个分片的时候,主1和备1就会被分配到同一个服务器上),ES会跳过这个服务器,再继续分配,情况大致如下:
ip地址 | 分片名 | 分片名 | 分片名 | 分片名 |
---|---|---|---|---|
192.168.85.200 | 主1 | 主4 | 备2 | 备5 |
192.168.85.201 | 主2 | 主5 | 备3 | |
192.168.85.202 | 主3 | 备1 | 备4 |
副本分片是主分片的备份,主挂了,备份还是可以访问。同一个分片的主与副本是不会放在同一个服务器里的,因为一旦宕机,这个分片就没了,如果现在200服务器宕机了,备1和备4就会接替主1和主4继续提供访问,我们系统还是保持了完整的服务提供。
2、搭建Elasticsearch集群
装备3台服务器装好ES,我这里ip分别是192.168.85.200、192.168.85.201、192.168.85.202
ES的安装请查看:分布式搜索引擎Elasticsearch——基础
(1)清空单机部署时的数据
将ES的数据清空,根据核心配置文件里面的配置找到数据存放的文件夹,我这里进入ES根目录下的data文件夹,将里面的所有数据清空
rm -rf *
(2)修改核心配置文件(elasticsearch.yml)
三台机器的配置,除了node.name其他参数都是一样的
#集群名称,同一集群的节点名称要保持一致
cluster.name: keyboard-dog-cluster
#节点名称,同一集群下每个节点名称应该保持不一样
node.name: node1
#表示当前节点是主节点,true表示当前节点可用作为master,false表示当前节点永远不可以作为master
node.master: true
#表示当前节点是数据节点
node.data: true
#发现集群节点,就是配置所有节点的ip地址
discovery.seed_hosts: ["192.168.85.200","192.168.85.201","192.168.85.202"]
#初始化master的节点,使用节点名
cluster.initial_master_nodes: ["node1"]
配置完之后可以通过命令删除掉所有的注释,看的更清爽一点
more elasticsearch.yml | grep ^[^#]
(3)三台同时启动ES
别忘记切换用户
su esuser
进入ES根目录下的bin文件夹,然后执行启动命令
./elasticsearch
(4)集群脑裂问题
如果发生网络中断或者服务器宕机,那么集群会有可能被划分为两个部分,各自有自己的master来管理,那么这就是脑裂。
master主节点要经过多个master节点共同选举后才能成为新的主节点。就跟班级里选班长一样,并不是你1个人能决定的,需要班里半数以上的人决定。解决实现原理:半数以上的节点同意选举,节点方可成为新的master。
discovery.zen.minimum_master_nodes=(N/2)+1
N为集群的中master节点的数量,也就是那些 node.master=true 设置的那些服务器节点总数
在最新版7.x中,minimum_master_node这个参数已经被移除了,这一块内容完全由es自身去管理,这样就避免了脑裂的问题,选举也会非常快
四、Elasticsearch整合SpringBoot
1、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<!-- 这个版本 只支持到ES6.4.3 ,如果需要更高的版本,需要把这个包里面的elasticsearch排除掉,额外再引用一个更新版本的elasticsearch-->
<version>2.2.2.RELEASE</version>
</dependency>
2、添加配置文件
spring:
data:
elasticsearch:
cluster-name:
#这里端口号是9300,默认就是9300,通过java连接要使用9300
cluster-nodes: 192.168.85.200:9300,192.168.85.201:9300,192.168.85.202:9300
3、解决netty引起的issue问题
创建一个ES配置类ESConfig
@Configuration
public class ESCofig{
@postConstruct
void init(){
System.setProperty("es.set.netty.runtime.available.processors","false");
}
}
4、SpringBoot操作ES
在需要使用的类中依赖注入ES的模板类
@Autowired
private ElasticsearchTemplate esTemplate;
(1)创建索引
1) 创建实体类,并增加ES配置
@Document(indexName = "stu",type = "_doc")
@Data
public class Stu(
@Id
private Long stuId;
//Field里面的属性和ES的是一样的,根据需要自行配置
@Field(store = true)
private String name;
@Field(store = true)
private Integer age;
}
2)创建索引的方法
public void createIndexStu(){
Stu stu = new Stu();
//默认5个分片,各有1个副本,stu里面有值 会直接创建一条文档
IndexQuery indexQuery= new IndexQueryBuilder().withObject(stu).build();
esTemplate.index(indexQuery);
}
(2)mapping更新
只需要更新实体类的数据结构,添加新的属性,在下次插入数据的时候,对应的索引的mapping就会更新
(3)创建文档
只需要在新增索引的时候,将对象里面添加参数值,就能新增文档,多次插入不会增加多个索引
(4)删除索引
public void deleteIndexStu(){
esTemplate.deleteIndex(Stu.class);
}
(5)修改文档数据
public void updateStu(){
Map<String,Object> soureMap = new HashMap<>();
SourceMap.put("name","Ben");
IndexRequest indexRequest = new IndexRequest();
indexRequest.source(soureMap);
//默认5个分片,各有1个副本,stu里面有值 会直接创建一条文档
UpdateQuery updateQuery= new UpdateQueryBuilder().withClass(Stu.class)
.withId("1001")
.withIndexRequest(indexRequest)
.build();
esTemplate.update(updateQuery);
}
(6)查询文档数据
public void getStu(){
GetQuery query = new GetQuery();
query.setId("1001");
Stu stu = esTemplate.queryForObject(query,Stu.class);
}
(7)删除文档数据
public void deleteStu(){
esTemplate.delete(Stu.class,"1001");
}
(8)分页查询
public void searchStuDoc(){
//查询第一页的,每页十条,页数的索引是从0开始的
Pageable pageable = PageRequest.of(0,10);
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("description","Ben jock"))
.withPageable(pageable)
.build();
AggregatedPage<Stu> pagedStu = esTemplate.queryForPage(query,Stu.class);
//总页数
pagedStu.getTotalPages();
//获取查询到的数据
List<Stu> stuList = pagedStu.getContent();
}
(9)高亮查询
public void highLightStuDoc(){
//定义高亮的标签
String preTag ="<font color = 'red'>";
String postTag = "</font>";
//查询第一页的,每页十条,页数的索引是从0开始的
Pageable pageable = PageRequest.of(0,10);
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("description","Ben jock"))
//设置高亮
.withHighlightFields(new HighlightBuilder.Field("description").preTags(preTag).postTags(postTag))
.withPageable(pageable)
.build();
AggregatedPage<Stu> pagedStu = esTemplate.queryForPage(query,Stu.class,new SearchResultMapping(){
//映射处理,对返回的数据进行处理,默认情况下返回数据是没有高亮的,必须从专门的高亮的封装中把值取出
@Override
public <T> AggregatedPage<T> mapResults (SearchResponse response, Class<T> clazz , Pageable pageable){
List<Stu> stuListHighlight = new ArrayList<>();
//获取返回的所有数据信息
SearchHits his =response.getHits();
for(SearchHit h : hits){
//获取对应的高亮对象
HighlighField highlightField = h.getHighlightFields().get("description");
//获取高亮字段值
String deschighlightField = highlightField.getFragments()[0].toString();
//获取其他的属性
Object stuId = h.getSourceAsMap().get("stuId");
Stu stuHL = new Stu();
stuHL.setStuId(Long.valueOf(stuId.toString()));
stuHL.setDescription(description);
stuListHighlight.add(stuHL);
}
if(stuListHighlight.size > 0){
return new AggregatedPageImpl<>((List<T>)stuListHighlight);
}
return null;
}
});
//总页数
pagedStu.getTotalPages();
//获取查询到的数据
List<Stu> stuList = pagedStu.getContent();
}
(10)排序查询
public void searchStuDoc(){
//查询第一页的,每页十条,页数的索引是从0开始的
Pageable pageable = PageRequest.of(0,10);
//定义排序的字段
SortBulder sortBuilder = new FieldSortBuilder("money").order(SortOrder.Desc);
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("description","Ben jock"))
//配置排序的字段、可以配置多少个
.withSort(sortBuilder);
.withPageable(pageable)
.build();
AggregatedPage<Stu> pagedStu = esTemplate.queryForPage(query,Stu.class);
//总页数
pagedStu.getTotalPages();
//获取查询到的数据
List<Stu> stuList = pagedStu.getContent();
}
五、Logstash
Logstash是elastic技术栈中的一个技术。它是一个数据采集引擎,可以从数据库采集数据到es中。我们可以通过设置自增id主键或者时间来控制数据的自动同步,这个id或者时间就是用于给logstash进行识别的
- id:假设现在有1000条数据,Logstatsh识别后会进行一次同步,同步完会记录这个id为1000,以后数据库新增数据,那么id会一直累加,Logstatsh会有定时任务,发现有id大于1000了,则增量加入到es中
- 时间:同理,一开始同步1000条数据,每条数据都有一个字段,为time,初次同步完毕后,记录这个time,下次同步的时候进行时间比对,如果有超过这个时间的,那么就可以做同步,这里可以同步新增数据,或者修改元数据,因为同一条数据的时间更改会被识别,而id则不会。
1、安装Logstash
(1)下载Logstash并配置好环境
下载地址:https://www.elastic.co/cn/downloads/past-releases/logstash-6-4-3
注:使用Logstatsh的版本号与elasticsearch版本号需要保持一致
- 插件 logstash-input-jdbc
本插件用于同步,es6.x起自带,这个是集成在了 logstash中的。所以直接配置同步数据库的配置文件即可 - 创建索引
同步数据到es中,前提得要有索引,这个需要手动先去创建,名字随意。 - JDK
记得安装JDK,java -version检查一下,如果没有安装,需要安装一下 - mysql
还需要准备好mysql的驱动,根据mysql安装的版本自行下载
(2)解压Logstash
tar -zxvf logstash-6.4.3.tar.gz -C/usr/local/
(3)添加配置
#进入logstash根目录
cd /usr/local/logstash-6.4.3
#创建文件夹用于存放同步数据的相关配置
mkdir sync
cd sync
#将mysql的驱动拷贝过来
cp /home/mysql-connector-java-5.1.41.jar .
#创建配置文件(配置信息在后面)
vim logstash-db-sync.conf
在配置文件中添加以下数据
input{
jdbc{
#设置mysql、mariaDB数据库Url以及数据库名称
jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/foo?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true"
#用户名密码
jdbc_user => "root"
jdbc_password => "root"
#数据库驱动的位置,可以是绝对位置也可以是相对位置
jdbc_driver_library => "/usr/local/logstash-6.4.3/sync/mysql-connector-java-5.1.41.jar"
#驱动类名
jdbc_driver_class => "com.mysql.jdbc.Driver"
#开启分类
jdbc_paging_enabled => "true"
#分页每页数量,可以自定义
jdbc_page_size => "10000"
#执行的Sql文件路径
statement_filePath =>"/usr/local/logstash-6.4.3/sync/items.sql
#设置定时任务间隔 含义:分、时、天、月、年,全部为* 表示每分钟跑一次
schedule => " * * * * * "
#索引类型
type="_doc"
#是否开启记录上次追踪的结果,也就是上次更新的时间
use_column_value => true
#记录上次追踪的结果值
last_run_metadata_path => "/usr/local/logstash-6.4.3/sync/track_time"
#如果 use_column_value为true,配置本参数,追踪的column名,可以是自增id或者时间
tracking_column => "updated_time"
#tracking_column对应字段的类型
tracking_column_type => "timestamp"
#是否清理last_run_metadata_path 的记录,true则每次都从头开始查询所有的数据库记录
clean_run =>false
#数据库字段名大写转写小写
lowercase_column_names => false
}
}
output{
elasticsearch{
#es地址,如果是集群 这里就是一个数组
hosts => ["192.168.85.200:9200"]
#同步的索引名
index => "items"
#设置_docID和数据相同
document_id => "%{id}"
}
#日志输出
stdout{
codec => json_lines
}
}
(4)编写sql
SELECT
i.id as itemId,
i.item_name as itemName,
i.sell_counts as sellCounts,
ii.url as imgUrl,
tempSpec.price_discount as price,
i.updated_time as updated_time
FROM items i
LEFT JOIN items_img ii
on i.id = ii.item_id
LEFT JOIN (SELECT item_id,MIN(price_discount) as price_discount from items_spec GROUP BY item_id) tempSpec
on i.id = tempSpec.item_id
WHERE ii.is_main = 1
and i.updated_time >= :sql_last_value
(5)启动Logstash
比较慢 要多等一下,如果根据时间来同步数据,就需要把数据对应的字段的时间修改到当前时间之后,不然无法同步,
如果数据物理删除ES是无法同步的,所以最好使用逻辑删除
./logstash -f /usr/local/logstash-6.4.3/sync/logstash-db-sync.conf
2、自定义模板配置中文分词
目前的数据同步,mappings映射会自动创建,但是分词不会,还是会使用默认的,而我们需要中文分词,这个时候就需要自定义模板功能来设置分词了
(1)配置分词模板
创建/usr/local/logstash-6.4.3/sync/logstash-ik.json文件
添加如下内容
{
"order": 0,
"version": 1,
"index_patterns": ["*"],
"settings": {
"index": { "refresh_interval": "5s" }
},
"mappings": {
"_default_": {
"dynamic_templates": [ {
"message_field": {
"path_match": "message",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"norms": false
}
}
}, {
"string_fields": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"norms": false,
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}],
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "keyword"
},
"geoip": {
"dynamic": true,
"properties": {
"ip": {
"type": "ip"
},
"location": {
"type": "geo_point"
},
"latitude": {
"type": "half_float"
},
"longitude": {
"type": "half_float"
}
}
}
}
}
},
"aliases": {}
}
(2)修改同步配置文件(logstash-db-sync.conf)
在elasticsearch配置选项中添加如下内容
# 定义模板名称
template_name => "myik"
# 模板所在位置
template => "/usr/local/logstash-6.4.3/sync/logstash-ik.json"
# 重写模板
template_overwrite => true
# 默认为true,false关闭logstash自动管理模板功能,如果自定义模板,则设置为false
manage_template => false
(3)重新运行Logstash进行同步
./logstash -f /usr/local/logstash-6.4.3/sync/logstash-db-sync.conf