文章目錄
上一篇《分佈式搜索引擎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