国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

ElasticSearch內(nèi)容分享(四):ES搜索引擎

這篇具有很好參考價值的文章主要介紹了ElasticSearch內(nèi)容分享(四):ES搜索引擎。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

目錄

ES搜索引擎

1. DSL設(shè)置查詢條件

1.1 DSL查詢分類

1.2 全文檢索查詢

1.2.1 使用場景

1.2.2 match查詢

1.2.3 mulit_match查詢

1.3 精準(zhǔn)查詢

1.3.1 term查詢

1.3.2 range查詢

1.4 地理坐標(biāo)查詢

1.4.1 矩形范圍查詢

1.4.2 附近(圓形)查詢

1.5 復(fù)合查詢

1.5.0 復(fù)合查詢歸納

1.5.1 相關(guān)性算分

1.5.2 算分函數(shù)查詢

function score 查詢

1.5.3 布爾查詢

bool查詢

2. 設(shè)置搜索結(jié)果

2.0 搜索結(jié)果種類

2.1 排序

2.1.1普通字段排序

2.1.2 地理坐標(biāo)排序

2.2 分頁

2.2.1 基本分頁

2.2.2 深度分頁

2.3 高亮

高亮原理

實現(xiàn)高亮

2.4 數(shù)據(jù)聚合

2.4.1 聚合種類

2.4.2 桶(Bucket)聚合

2.4.3 度量(Metric) and 管道(pipeline)聚合

3. RestClient查詢文檔

3.1 快速入門

3.1.1 發(fā)送查詢請求

3.1.2 解析響應(yīng)結(jié)果

3.1.3 完整代碼

3.2 設(shè)置查詢條件

3.2.1 全文檢索查詢

3.2.2 精準(zhǔn)查詢

3.2.3 地理查詢

3.2.4 布爾查詢

3.2.5 算分函數(shù)查詢

3.3 設(shè)置搜索結(jié)果

3.3.1 排序和分頁

3.3.2 高亮

3.3.3 聚合


ES搜索引擎

elasticsearch的查詢依然是基于JSON風(fēng)格的DSL來實現(xiàn)的。

1. DSL設(shè)置查詢條件

1.1 DSL查詢分類

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)來定義查詢。常見的查詢類型包括:

  • 查詢所有:查詢出所有數(shù)據(jù),一般測試用。例如:match_all

  • 全文檢索(full text)查詢:利用分詞器對用戶輸入內(nèi)容分詞,然后去倒排索引庫中匹配。例如:

    • match_query
    • multi_match_query
  • 精確查詢:根據(jù)精確詞條值查找數(shù)據(jù),一般是查找keyword、數(shù)值、日期、boolean等類型字段。例如:

    • ids
    • range
    • term
  • 地理(geo)查詢:根據(jù)經(jīng)緯度查詢。例如:

    • geo_distance
    • geo_bounding_box
  • 復(fù)合(compound)查詢:復(fù)合查詢可以將上述各種查詢條件組合起來,合并查詢條件。例如:

    • bool
    • function_score

查詢的語法基本一致:

GET?/indexName/_search
{
??"query":?{
????"查詢類型":?{
??????"查詢條件":?"條件值"
????}
??}
}

我們以查詢所有為例,其中:

  • 查詢類型為match_all
  • 沒有查詢條件
// 查詢所有
GET?/indexName/_search
{
??"query":?{
????"match_all":?{
    }
??}
}

其它查詢無非就是查詢類型、查詢條件的變化。

1.2 全文檢索查詢

match和multi_match的區(qū)別是什么?

  • match:根據(jù)一個字段查詢【推薦:使用copy_to構(gòu)造all字段】
  • multi_match:根據(jù)多個字段查詢,參與查詢字段越多,查詢性能越差

注:搜索字段越多,對查詢性能影響越大,因此建議采用copy_to,然后單字段查詢的方式。

1.2.1 使用場景

全文檢索查詢的基本流程如下:

  • 對用戶搜索的內(nèi)容做分詞,得到詞條
  • 根據(jù)詞條去倒排索引庫中匹配,得到文檔id
  • 根據(jù)文檔id找到文檔,返回給用戶

比較常用的場景包括:

  • 商城的輸入框搜索
  • 百度輸入框搜索

例如京東:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

因為是拿著詞條去匹配,因此參與搜索的字段也必須是可分詞的text類型的字段。

常見的全文檢索查詢包括:

  • match查詢:單字段查詢
  • multi_match查詢:多字段查詢,任意一個字段符合條件就算符合查詢條件
1.2.2 match查詢

match查詢語法如下:

GET?/indexName/_search
{
??"query":?{
????"match":?{
??????"FIELD":?"TEXT"
????}
??}
}

match查詢示例:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.2.3 mulit_match查詢

mulit_match語法如下:

GET?/indexName/_search
{
??"query":?{
????"multi_match":?{
??????"query":?"TEXT",
??????"fields":?["FIELD1",?" FIELD12"]
????}
??}
}

multi_match查詢示例:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.3 精準(zhǔn)查詢

精準(zhǔn)查詢類型:

  • term查詢:根據(jù)詞條精確匹配,一般搜索keyword類型、數(shù)值類型、布爾類型、日期類型字段
  • range查詢:根據(jù)數(shù)值范圍查詢,可以是數(shù)值、日期的范圍

精確查詢一般是查找keyword、數(shù)值、日期、boolean等類型字段。所以不會對搜索條件分詞。常見的有:

  • term:根據(jù)詞條精確值查詢
  • range:根據(jù)值的范圍查詢
1.3.1 term查詢

因為精確查詢的字段搜時不分詞的字段,因此查詢的條件也必須是不分詞的詞條。查詢時,用戶輸入的內(nèi)容跟自動值完全匹配時才認為符合條件。如果用戶輸入的內(nèi)容過多,反而搜索不到數(shù)據(jù)。

語法說明:

//?term查詢
GET?/indexName/_search
{
??"query":?{
????"term":?{
??????"FIELD":?{
????????"value":?"VALUE"
??????}
????}
??}
}

示例:

當(dāng)我搜索的是精確詞條時,能正確查詢出結(jié)果:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

但是,當(dāng)我搜索的內(nèi)容不是詞條,而是多個詞語形成的短語時,反而搜索不到:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.3.2 range查詢

范圍查詢,一般應(yīng)用在對數(shù)值類型做范圍過濾的時候。比如做價格范圍過濾。

基本語法:

//?range查詢
GET?/indexName/_search
{
??"query":?{
????"range":?{
??????"FIELD":?{
????????"gte":?10, // 這里的gte代表大于等于,gt則代表大于
????????"lte":?20 // lte代表小于等于,lt則代表小于
??????}
????}
??}
}

示例:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.4 地理坐標(biāo)查詢

所謂的地理坐標(biāo)查詢,其實就是根據(jù)經(jīng)緯度查詢,官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html

常見的使用場景包括:

  • 攜程:搜索我附近的酒店
  • 滴滴:搜索我附近的出租車
  • 微信:搜索我附近的人

附近的酒店:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

附近的車:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.4.1 矩形范圍查詢

很少有業(yè)務(wù)有這種需求

矩形范圍查詢,也就是geo_bounding_box查詢,查詢坐標(biāo)落在某個矩形范圍的所有文檔:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

查詢時,需要指定矩形的左上、右下兩個點的坐標(biāo),然后畫出一個矩形,落在該矩形內(nèi)的都是符合條件的點。

語法如下:

//?geo_bounding_box查詢
GET?/indexName/_search
{
??"query":?{
????"geo_bounding_box":?{
??????"FIELD":?{
????????"top_left":?{ // 左上點
??????????"lat":?31.1,
??????????"lon":?121.5
????????},
????????"bottom_right":?{ // 右下點
??????????"lat":?30.9,
??????????"lon":?121.7
????????}
??????}
????}
??}
}
1.4.2 附近(圓形)查詢

附近查詢,也叫做距離查詢(geo_distance):查詢到指定中心點小于某個距離值的所有文檔。

換句話來說,在地圖上找一個點作為圓心,以指定距離為半徑,畫一個圓,落在圓內(nèi)的坐標(biāo)都算符合條件:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

語法說明:

//?geo_distance 查詢
GET?/indexName/_search
{
??"query":?{
????"geo_distance":?{
??????"distance":?"15km", // 半徑
??????"FIELD":?"31.21,121.5" // 圓心
????}
??}
}

示例:

我們先搜索陸家嘴附近15km的酒店:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

發(fā)現(xiàn)共有47家酒店。

1.5 復(fù)合查詢

復(fù)合(compound)查詢:復(fù)合查詢可以將其它簡單查詢組合起來,實現(xiàn)更復(fù)雜的搜索邏輯。常見的有兩種:

  • fuction score:算分函數(shù)查詢,可以控制文檔相關(guān)性算分,控制文檔排名
  • bool query:布爾查詢,利用邏輯關(guān)系組合多個其它的查詢,實現(xiàn)復(fù)雜搜索
1.5.0 復(fù)合查詢歸納
GET?/hotel/_search
{
  "query":?{
????"function_score":?{           
      "query":?{ // 原始查詢,可以是任意條件
          "bool":?{
              "must":?[
                  {"term":?{"city":?"上海"?}}
              ],
              "should":?[
                  {"term":?{"brand":?"皇冠假日"?}},
                  {"term":?{"brand":?"華美達"?}}
              ],
              "must_not":?[
                  {?"range":?{?"price":?{?"lte":?500?}?}}
              ],
              "filter":?[
                  {?"range":?{"score":?{?"gte":?45?}?}}
              ]
          }
      },
??????"functions":?[?//?算分函數(shù)
????????{
??????????"filter":?{?//?滿足的條件,品牌必須是如家【品牌是如家的才加分,這里是加分條件】
????????????"term":?{
??????????????"brand":?"如家"
????????????}
??????????},
??????????"weight":?2?//?算分權(quán)重為2
????????}
??????],
      "boost_mode": "sum" // 加權(quán)模式,求和
????}
??}  
}

1.5.1 相關(guān)性算分

elasticsearch會根據(jù)詞條和文檔的相關(guān)度做打分,算法由兩種:

  • TF-IDF算法
  • BM25算法,elasticsearch5.1版本后采用的算法

當(dāng)我們利用match查詢時,文檔結(jié)果會根據(jù)與搜索詞條的關(guān)聯(lián)度打分(_score),返回結(jié)果時按照分值降序排列。

例如,我們搜索 "虹橋如家",結(jié)果如下:

[
??{
????"_score"?:?17.850193,
????"_source"?:?{
??????"name"?:?"虹橋如家酒店真不錯",
????}
??},
??{
????"_score"?:?12.259849,
????"_source"?:?{
??????"name"?:?"外灘如家酒店真不錯",
????}
??},
??{
????"_score"?:?11.91091,
????"_source"?:?{
??????"name"?:?"迪士尼如家酒店真不錯",
????}
??}
]

在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

在后來的5.1版本升級中,elasticsearch將算法改進為BM25算法,公式如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

TF-IDF算法有一各缺陷,就是詞條頻率越高,文檔得分也會越高,單個詞條對文檔影響較大。而BM25則會讓單個詞條的算分有一個上限,曲線更加平滑:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.5.2 算分函數(shù)查詢

在搜索出來的結(jié)果的分數(shù)基礎(chǔ)上,再手動與指定的數(shù)字進行一定運算來改變算分,從而改變結(jié)果的排序。

function score query定義的三要素是什么?

  • 過濾條件:哪些文檔要加分
  • 算分函數(shù):如何計算function score
  • 加權(quán)方式:function score 與 query score如何運算

根據(jù)相關(guān)度打分是比較合理的需求,但合理的不一定是產(chǎn)品經(jīng)理需要的。

以百度為例,你搜索的結(jié)果中,并不是相關(guān)度越高排名越靠前,而是誰掏的錢多排名就越靠前。如圖:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

要想認為控制相關(guān)性算分,就需要利用elasticsearch中的function score 查詢了。

function score 查詢

1)語法說明

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

function score 查詢中包含四部分內(nèi)容:

  • 原始查詢條件:query部分,基于這個條件搜索文檔,并且基于BM25算法給文檔打分,原始算分(query score)
  • 過濾條件:filter部分,符合該條件的文檔才會重新算分
  • 算分函數(shù):符合filter條件的文檔要根據(jù)這個函數(shù)做運算,得到的函數(shù)算分(function score),有四種函數(shù)
    • weight:函數(shù)結(jié)果是常量
    • field_value_factor:以文檔中的某個字段值作為函數(shù)結(jié)果
    • random_score:以隨機數(shù)作為函數(shù)結(jié)果
    • script_score:自定義算分函數(shù)算法
  • 運算模式:算分函數(shù)的結(jié)果、原始查詢的相關(guān)性算分,兩者之間的運算方式,包括:
    • multiply:相乘
    • replace:用function score替換query score
    • 其它,例如:sum、avg、max、min

function score的運行流程如下:

  • 1)根據(jù)原始條件查詢搜索文檔,并且計算相關(guān)性算分,稱為原始算分(query score)
  • 2)根據(jù)過濾條件,過濾文檔
  • 3)符合過濾條件的文檔,基于算分函數(shù)運算,得到函數(shù)算分(function score)
  • 4)將原始算分(query score)和函數(shù)算分(function score)基于運算模式做運算,得到最終結(jié)果,作為相關(guān)性算分。

2)舉例

需求:給“如家”這個品牌的酒店排名靠前一些

翻譯一下這個需求,轉(zhuǎn)換為之前說的四個要點:

  • 原始條件:不確定,可以任意變化
  • 過濾條件:brand = "如家"
  • 算分函數(shù):可以簡單粗暴,直接給固定的算分結(jié)果,weight
  • 運算模式:比如求和

因此最終的DSL語句如下:

GET?/hotel/_search
{
??"query":?{
????"function_score":?{
??????"query":?{  .... }, // 原始查詢,可以是任意條件
??????"functions":?[?//?算分函數(shù)
????????{
??????????"filter":?{?//?滿足的條件,品牌必須是如家【品牌是如家的才加分,這里是加分條件】
????????????"term":?{
??????????????"brand":?"如家"
????????????}
??????????},
??????????"weight":?2?//?算分權(quán)重為2
????????}
??????],
      "boost_mode": "sum" // 加權(quán)模式,求和
????}
??}
}

測試,在未添加算分函數(shù)時,如家得分如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

添加了算分函數(shù)后,如家得分就提升了:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

1.5.3 布爾查詢

布爾查詢是一個或多個查詢子句的組合,每一個子句就是一個子查詢。子查詢的組合方式有:

  • must:必須匹配每個子查詢,類似“與”
  • should:選擇性匹配子查詢,類似“或”
  • must_not:必須不匹配,不參與算分,類似“非”
  • filter:必須匹配,不參與算分

注意:盡量在篩選的時候多使用不參與算分的must_not和filter,以保證性能良好

比如在搜索酒店時,除了關(guān)鍵字搜索外,我們還可能根據(jù)品牌、價格、城市等字段做過濾:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

每一個不同的字段,其查詢的條件、方式都不一樣,必須是多個不同的查詢,而要組合這些查詢,就必須用bool查詢了。

需要注意的是,搜索時,參與打分的字段越多,查詢的性能也越差。因此這種多條件查詢時,建議這樣做:

  • 搜索框的關(guān)鍵字搜索,是全文檢索查詢,使用must查詢,參與算分
  • 其它過濾條件,采用filter查詢。不參與算分
bool查詢

1)語法

GET?/hotel/_search
{
??"query":?{
????"bool":?{
??????"must":?[
????????{"term":?{"city":?"上海"?}}
??????],
??????"should":?[
????????{"term":?{"brand":?"皇冠假日"?}},
        {"term":?{"brand":?"華美達"?}}
??????],
??????"must_not":?[
????????{?"range":?{?"price":?{?"lte":?500?}?}}
??????],
??????"filter":?[
????????{?"range":?{"score":?{?"gte":?45?}?}}
??????]
????}
??}
}

2)示例

需求:搜索名字包含“如家”,價格不高于400,在坐標(biāo)31.21,121.5周圍10km范圍內(nèi)的酒店。

分析:

  • 名稱搜索,屬于全文檢索查詢,應(yīng)該參與算分。放到must中
  • 價格不高于400,用range查詢,屬于過濾條件,不參與算分。放到must_not中
  • 周圍10km范圍內(nèi),用geo_distance查詢,屬于過濾條件,不參與算分。放到filter中

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2. 設(shè)置搜索結(jié)果

搜索的結(jié)果可以按照用戶指定的方式去處理或展示。

2.0 搜索結(jié)果種類

查詢的DSL是一個大的JSON對象,包含下列屬性:

  • query:查詢條件
  • from和size:分頁條件
  • sort:排序條件
  • highlight:高亮條件
  • aggs:定義聚合

示例:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2.1 排序

在使用排序后就不會進行算分了,根據(jù)排序設(shè)置的規(guī)則排列

普通字段是根據(jù)字典序排序

地理坐標(biāo)是根據(jù)舉例遠近排序

2.1.1普通字段排序

keyword、數(shù)值、日期類型排序的排序語法基本一致。

語法

排序條件是一個數(shù)組,也就是可以寫多個排序條件。按照聲明的順序,當(dāng)?shù)谝粋€條件相等時,再按照第二個條件排序,以此類推
(可以參考下面的圖片案例)

GET?/indexName/_search
{
??"query":?{
????"match_all":?{}
??},
??"sort":?[
????{
??????"FIELD":?"desc"??//?排序字段、排序方式ASC、DESC
????}
??]
}

示例

需求描述:酒店數(shù)據(jù)按照用戶評價(score)降序排序,評價相同的按照價格(price)升序排序

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2.1.2 地理坐標(biāo)排序

地理坐標(biāo)排序略有不同。

語法說明

GET?/indexName/_search
{
??"query":?{
????"match_all":?{}
??},
??"sort":?[
????{
??????"_geo_distance"?:?{
??????????"FIELD"?:?"緯度,經(jīng)度", // 文檔中g(shù)eo_point類型的字段名、目標(biāo)坐標(biāo)點
??????????"order"?:?"asc", // 排序方式
??????????"unit"?:?"km" // 排序的距離單位
??????}
????}
??]
}

這個查詢的含義是:

  • 指定一個坐標(biāo),作為目標(biāo)點
  • 計算每一個文檔中,指定字段(必須是geo_point類型)的坐標(biāo) 到目標(biāo)點的距離是多少
  • 根據(jù)距離排序

示例:

需求描述:實現(xiàn)對酒店數(shù)據(jù)按照到你的位置坐標(biāo)的距離升序排序

提示:獲取你的位置的經(jīng)緯度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/

假設(shè)我的位置是:31.034661,121.612282,尋找我周圍距離最近的酒店。

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2.2 分頁

elasticsearch會禁止from+ size 超過10000的請求

elasticsearch 默認情況下只返回top10的數(shù)據(jù)。而如果要查詢更多數(shù)據(jù)就需要修改分頁參數(shù)了。elasticsearch中通過修改from、size參數(shù)來控制要返回的分頁結(jié)果:

  • from:從第幾個文檔開始
  • size:總共查詢幾個文檔

類似于mysql中的limit ?, ?

2.2.1 基本分頁

分頁的基本語法如下:

GET?/hotel/_search
{
??"query":?{
????"match_all":?{}
??},
??"from":?0,?//?分頁開始的位置,默認為0
??"size":?10,?//?期望獲取的文檔總數(shù)
??"sort":?[
????{"price":?"asc"}
??]
}
2.2.2 深度分頁

原理:elasticsearch內(nèi)部分頁時,必須先查詢 0~1000條,然后截取其中的990 ~ 1000的這10條

現(xiàn)在,我要查詢990~1000的數(shù)據(jù),查詢邏輯要這么寫:

GET?/hotel/_search
{
??"query":?{
????"match_all":?{}
??},
??"from":?990,?//?分頁開始的位置,默認為0
??"size":?10,?//?期望獲取的文檔總數(shù)
??"sort":?[
????{"price":?"asc"}
??]
}

這里是查詢990開始的數(shù)據(jù),也就是 第990~第1000條 數(shù)據(jù)。

集群情況的深度分頁

針對深度分頁,ES提供了兩種解決方案,官方文檔:

  • search after:分頁時需要排序,原理是從上一次的排序值開始,查詢下一頁數(shù)據(jù)。【官方推薦】
  • scroll:原理將排序后的文檔id形成快照,保存在內(nèi)存。

不過,elasticsearch內(nèi)部分頁時,必須先查詢 0~1000條,然后截取其中的990 ~ 1000的這10條:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

查詢TOP1000,如果es是單點模式,這并無太大影響。

但是elasticsearch將來一定是集群,例如我集群有5個節(jié)點,我要查詢TOP1000的數(shù)據(jù),并不是每個節(jié)點查詢200條就可以了。

因為節(jié)點A的TOP200,在另一個節(jié)點可能排到10000名以外了。

因此要想獲取整個集群的TOP1000,必須先查詢出每個節(jié)點的TOP1000,匯總結(jié)果后,重新排名,重新截取TOP1000。

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

那如果我要查詢9900~10000的數(shù)據(jù)呢?是不是要先查詢TOP10000呢?那每個節(jié)點都要查詢10000條?匯總到內(nèi)存中?

當(dāng)查詢分頁深度較大時,匯總數(shù)據(jù)過多,對內(nèi)存和CPU會產(chǎn)生非常大的壓力,因此elasticsearch會禁止from+ size 超過10000的請求。

2.3 高亮

注意:

  • 高亮是對關(guān)鍵字高亮,因此搜索條件必須帶有關(guān)鍵字,而不能是范圍這樣的查詢。
  • 默認情況下,高亮的字段,必須與搜索指定的字段一致,否則無法高亮
  • 如果要對非搜索字段高亮,則需要添加一個屬性:required_field_match=false

使用場景:在百度等搜索后,會對結(jié)果中出現(xiàn)搜索字段的部分進行高亮處理。

高亮原理

高亮顯示的實現(xiàn)分為兩步:

  • 1)給文檔中的所有關(guān)鍵字都添加一個標(biāo)簽,例如<em>標(biāo)簽
  • 2)頁面給<em>標(biāo)簽編寫CSS樣式
實現(xiàn)高亮

1)語法

GET?/hotel/_search
{
??"query":?{
????"match":?{
??????"FIELD":?"TEXT" // 查詢條件,高亮一定要使用全文檢索查詢
????}
??},
??"highlight":?{
????"fields":?{?//?指定要高亮的字段
??????"FIELD":?{ //【要和上面的查詢字段FIELD一致】
????????"pre_tags":?"<em>",??//?用來標(biāo)記高亮字段的前置標(biāo)簽
????????"post_tags":?"</em>"?//?用來標(biāo)記高亮字段的后置標(biāo)簽
??????}
????}
??}
}

2)示例:組合字段all的案例

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2.4 數(shù)據(jù)聚合

類似于mysql中的【度量(Metric)聚合】聚合語句實現(xiàn)AVG,MAX,MIN;以及【桶(Bucket)聚合】GroupBy實現(xiàn)分組

聚合(aggregations可以讓我們極其方便的實現(xiàn)對數(shù)據(jù)的統(tǒng)計、分析、運算。例如:

  • 什么品牌的手機最受歡迎?
  • 這些手機的平均價格、最高價格、最低價格?
  • 這些手機每月的銷售情況如何?

實現(xiàn)這些統(tǒng)計功能的比數(shù)據(jù)庫的sql要方便的多,而且查詢速度非???,可以實現(xiàn)近實時搜索效果。

aggs代表聚合,與query同級,此時query的作用是?

  • 限定聚合的的文檔范圍

聚合必須的三要素:

  • 聚合名稱
  • 聚合類型
  • 聚合字段

聚合可配置屬性有:

  • size:指定聚合結(jié)果數(shù)量
  • order:指定聚合結(jié)果排序方式
  • field:指定聚合字段
2.4.1 聚合種類

注意:參加聚合的字段必須是keyword、日期、數(shù)值、布爾類型

聚合常見的有三類:

  • 桶(Bucket)聚合:用來對文檔做分組

    • TermAggregation:按照文檔字段值分組,例如按照品牌值分組、按照國家分組
    • Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組
  • 度量(Metric)聚合:用以計算一些值,比如:最大值、最小值、平均值等

    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同時求max、min、avg、sum等
  • 管道(pipeline)聚合:其它聚合的結(jié)果為基礎(chǔ)做聚合

    如:用桶聚合實現(xiàn)種類排序,然后使用度量聚合實現(xiàn)各個桶的最大值、最小值、平均值等

2.4.2 桶(Bucket)聚合

以統(tǒng)計酒店品牌種類,并對其進行數(shù)據(jù)分組

GET?/hotel/_search
{
  "query":?{ //限定要聚合的文檔范圍,只要添加query條件【一般在沒搜索關(guān)鍵字時不寫query】
????"range":?{
??????"price":?{
????????"lte":?200 // 只對200元以下的文檔聚合
??????}
????}
??},?
??"size":?0,??//?設(shè)置size為0,結(jié)果中不包含查詢結(jié)果文檔,只包含聚合結(jié)果
??"aggs":?{?//?定義聚合
????"brandAgg":?{?//給聚合起個名字
??????"terms":?{?//?聚合的類型,按照品牌值聚合,所以選擇term
????????"field":?"brand",?//?參與聚合的字段
        "order":?{
??????????"doc_count":?"asc" //?對聚合結(jié)果按照doc_count升序排列
????????},
????????"size":?20?//?希望獲取的聚合結(jié)果數(shù)量【設(shè)置多少就最多只顯示多少】
??????}
????}
??}
}

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

2.4.3 度量(Metric) and 管道(pipeline)聚合

度量聚合很少單獨使用,一般是和桶聚合一并結(jié)合使用

我們對酒店按照品牌分組,形成了一個個桶?,F(xiàn)在我們需要對桶內(nèi)的酒店做運算,獲取每個品牌的用戶評分的min、max、avg等值。

這就要用到Metric聚合了,例如stat聚合:就可以獲取min、max、avg等結(jié)果。

語法如下:

這次的score_stats聚合是在brandAgg的聚合內(nèi)部嵌套的子聚合。因為我們需要在每個桶分別計算。

GET?/hotel/_search
{
??"size":?0,?
??"aggs":?{
????"brandAgg":?{?
??????"terms":?{?
????????"field":?"brand",?
        "order":?{
??????????"scoreAgg.avg":?"desc" //?對聚合結(jié)果按照指定字段降序排列
????????},
????????"size":?20
??????},
??????"aggs":?{?//?是brands聚合的子聚合,也就是分組后對每組分別計算
????????"score_stats":?{?//?聚合名稱
??????????"stats":?{?//?聚合類型,這里stats可以計算min、max、avg等
????????????"field":?"score"?//?聚合字段,這里是score
??????????}
????????}
??????}
????}
??}
}

另外,我們還可以給聚合結(jié)果做個排序,例如按照每個桶的酒店平均分做排序:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

3. RestClient查詢文檔

文檔的查詢同樣適用昨天學(xué)習(xí)的 RestHighLevelClient對象,基本步驟包括:

  • 1)準(zhǔn)備Request對象
  • 2)準(zhǔn)備請求參數(shù)
  • 3)發(fā)起請求
  • 4)解析響應(yīng)
3.1 快速入門

查詢的基本步驟是:

  1. 創(chuàng)建SearchRequest對象

  2. 準(zhǔn)備Request.source(),也就是DSL。

    ① QueryBuilders來構(gòu)建查詢條件

    ② 傳入Request.source() 的 query() 方法

  3. 發(fā)送請求,得到結(jié)果

  4. 解析結(jié)果(參考JSON結(jié)果,從外到內(nèi),逐層解析)

3.1.1 發(fā)送查詢請求

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

代碼解讀:

  • 第一步,創(chuàng)建SearchRequest對象,指定索引庫名

  • 第二步,利用request.source()構(gòu)建DSL,DSL中可以包含查詢、分頁、排序、高亮等

    • query():代表查詢條件,利用QueryBuilders.matchAllQuery()構(gòu)建一個match_all查詢的DSL
  • 第三步,利用client.search()發(fā)送請求,得到響應(yīng)

這里關(guān)鍵的API有兩個,一個是request.source(),其中包含了查詢、排序、分頁、高亮等所有功能:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

另一個是QueryBuilders,其中包含match、term、function_score、bool等各種查詢:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

3.1.2 解析響應(yīng)結(jié)果

響應(yīng)結(jié)果的解析:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

elasticsearch返回的結(jié)果是一個JSON字符串,結(jié)構(gòu)包含:

  • hits:命中的結(jié)果
    • total:總條數(shù),其中的value是具體的總條數(shù)值
    • max_score:所有結(jié)果中得分最高的文檔的相關(guān)性算分
    • hits:搜索結(jié)果的文檔數(shù)組,其中的每個文檔都是一個json對象
      • _source:文檔中的原始數(shù)據(jù),也是json對象

因此,我們解析響應(yīng)結(jié)果,就是逐層解析JSON字符串,流程如下:

  • SearchHits:通過response.getHits()獲取,就是JSON中的最外層的hits,代表命中的結(jié)果
    • SearchHits#getTotalHits().value:獲取總條數(shù)信息
    • SearchHits#getHits():獲取SearchHit數(shù)組,也就是文檔數(shù)組
      • SearchHit#getSourceAsString():獲取文檔結(jié)果中的_source,也就是原始的json文檔數(shù)據(jù)
3.1.3 完整代碼

完整代碼如下:

@Test
void testMatchAll() throws IOException {
    // 1.準(zhǔn)備Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.準(zhǔn)備DSL
    request.source()
        .query(QueryBuilders.matchAllQuery());
    // 3.發(fā)送請求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    // 4.解析響應(yīng)
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    // 4.解析響應(yīng)
    SearchHits searchHits = response.getHits();
    // 4.1.獲取總條數(shù)
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "條數(shù)據(jù)");
    // 4.2.文檔數(shù)組
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍歷
    for (SearchHit hit : hits) {
        // 獲取文檔source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println("hotelDoc = " + hotelDoc);
    }
}
3.2 設(shè)置查詢條件
3.2.1 全文檢索查詢

全文檢索的match和multi_match查詢與match_all的API基本一致。差別是查詢條件,也就是query的部分。

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

因此,Java代碼上的差異主要是request.source().query()中的參數(shù)了。同樣是利用QueryBuilders提供的方法:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

而結(jié)果解析代碼則完全一致,可以抽取并共享。

完整代碼如下:

@Test
void testMatch() throws IOException {
    // 1.準(zhǔn)備Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.準(zhǔn)備DSL
    request.source()
        .query(QueryBuilders.matchQuery("all", "如家"));
    // 3.發(fā)送請求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析響應(yīng)
    handleResponse(response);

}
3.2.2 精準(zhǔn)查詢

精確查詢主要是兩者:

  • term:詞條精確匹配
  • range:范圍查詢

與之前的查詢相比,差異同樣在查詢條件,其它都一樣。

查詢條件構(gòu)造的API如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

3.2.3 地理查詢

DSL格式

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

cn.itcast.hotel.service.implHotelServicesearch方法中,添加一個排序功能:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

完整代碼:

@Override
public PageResult search(RequestParams params) {
    try {
        // 1.準(zhǔn)備Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.準(zhǔn)備DSL
        // 2.1.query
        buildBasicQuery(params, request);

        // 2.2.分頁
        int page = params.getPage();
        int size = params.getSize();
        request.source().from((page - 1) * size).size(size);

        // 2.3.排序
        String location = params.getLocation();
        if (location != null && !location.equals("")) {
            request.source().sort(SortBuilders
                                  .geoDistanceSort("location", new GeoPoint(location))
                                  .order(SortOrder.ASC)
                                  .unit(DistanceUnit.KILOMETERS)
                                 );
        }

        // 3.發(fā)送請求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析響應(yīng)
        return handleResponse(response);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
3.2.4 布爾查詢

布爾查詢是用must、must_not、filter等方式組合其它查詢,代碼示例如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

可以看到,API與其它查詢的差別同樣是在查詢條件的構(gòu)建,QueryBuilders,結(jié)果解析等其他代碼完全不變。

完整代碼如下:

@Test
void testBool() throws IOException {
    // 1.準(zhǔn)備Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.準(zhǔn)備DSL
    // 2.1.準(zhǔn)備BooleanQuery
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    // 2.2.添加term
    boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
    // 2.3.添加range
    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

    request.source().query(boolQuery);
    // 3.發(fā)送請求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析響應(yīng)
    handleResponse(response);

}
3.2.5 算分函數(shù)查詢

java代碼邏輯:添加一個isAD字段,在算分函數(shù)的filter中判斷isAD=ture就進行重新算分

function_score查詢結(jié)構(gòu)如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

對應(yīng)的JavaAPI如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

我們可以將之前寫的boolean查詢作為原始查詢條件放到query中,接下來就是添加過濾條件、算分函數(shù)、加權(quán)模式了。

// 算分控制
FunctionScoreQueryBuilder functionScoreQuery =
    QueryBuilders.functionScoreQuery(
    // 原始查詢,相關(guān)性算分的查詢
    boolQuery,
    // function score的數(shù)組
    new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
        // 其中的一個function score 元素
        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
            // 過濾條件
            QueryBuilders.termQuery("isAD", true),
            // 算分函數(shù)
            ScoreFunctionBuilders.weightFactorFunction(10)
        )
    });
//將查詢請求放入查詢
request.source().query(functionScoreQuery);
3.3 設(shè)置搜索結(jié)果
3.3.1 排序和分頁

由于這兩個比較簡單,所以一起寫了

搜索結(jié)果的排序和分頁是與query同級的參數(shù),因此同樣是使用request.source()來設(shè)置。

對應(yīng)的API如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

完整代碼示例:

@Test
void testPageAndSort() throws IOException {
    // 頁碼,每頁大小
    int page = 1, size = 5;

    // 1.準(zhǔn)備Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.準(zhǔn)備DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchAllQuery());
    // 2.2.排序 sort
    request.source().sort("price", SortOrder.ASC);
    // 2.3.分頁 from、size
    request.source().from((page - 1) * size).size(5);
    // 3.發(fā)送請求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析響應(yīng)
    handleResponse(response);

}
3.3.2 高亮

高亮的代碼與之前代碼差異較大,有兩點:

  • 查詢的DSL:其中除了查詢條件,還需要添加高亮條件,同樣是與query同級。
  • 結(jié)果解析:結(jié)果除了要解析_source文檔數(shù)據(jù),還要解析高亮結(jié)果

1)高亮請求構(gòu)建

高亮請求的構(gòu)建API如下:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

上述代碼省略了查詢條件部分,但是大家不要忘了:高亮查詢必須使用全文檢索查詢,并且要有搜索關(guān)鍵字,將來才可以對關(guān)鍵字高亮。

完整代碼如下:

@Test
void testHighlight() throws IOException {
    // 1.準(zhǔn)備Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.準(zhǔn)備DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchQuery("all", "如家"));
    // 2.2.高亮
    request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
    // 3.發(fā)送請求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析響應(yīng)
    handleResponse(response);
}

2)高亮結(jié)果解析

高亮的結(jié)果與查詢的文檔結(jié)果默認是分離的,并不在一起。

因此解析高亮的代碼需要額外處理:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

代碼解讀:

  • 第一步:從結(jié)果中獲取source。hit.getSourceAsString(),這部分是非高亮結(jié)果,json字符串。還需要反序列為HotelDoc對象
  • 第二步:獲取高亮結(jié)果。hit.getHighlightFields(),返回值是一個Map,key是高亮字段名稱,值是HighlightField對象,代表高亮值
  • 第三步:從map中根據(jù)高亮字段名稱,獲取高亮字段值對象HighlightField
  • 第四步:從HighlightField中獲取Fragments,并且轉(zhuǎn)為字符串。這部分就是真正的高亮字符串了
  • 第五步:用高亮的結(jié)果替換HotelDoc中的非高亮結(jié)果

完整代碼如下:

private void handleResponse(SearchResponse response) {
    // 4.解析響應(yīng)
    SearchHits searchHits = response.getHits();
    // 4.1.獲取總條數(shù)
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "條數(shù)據(jù)");
    // 4.2.文檔數(shù)組
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍歷
    for (SearchHit hit : hits) {
        // 獲取文檔source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        // 獲取高亮結(jié)果
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (!CollectionUtils.isEmpty(highlightFields)) {
            // 根據(jù)字段名獲取高亮結(jié)果
            HighlightField highlightField = highlightFields.get("name");
            if (highlightField != null) {
                // 獲取高亮值
                String name = highlightField.getFragments()[0].string();
                // 覆蓋非高亮結(jié)果
                hotelDoc.setName(name);
            }
        }
        System.out.println("hotelDoc = " + hotelDoc);
    }
}
3.3.3 聚合

聚合條件與query條件同級別,因此需要使用request.source()來指定聚合條件。

聚合條件的語法:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

聚合的結(jié)果也與查詢結(jié)果不同,API也比較特殊。不過同樣是JSON逐層解析:

ElasticSearch內(nèi)容分享(四):ES搜索引擎,ElasticSearch 內(nèi)容分享,elasticsearch,緩存?

舉例:業(yè)務(wù)代碼文章來源地址http://www.zghlxwxcb.cn/news/detail-755045.html

@Override
public Map<String, List<String>> filters(RequestParams params) {
    try {
        // 1.準(zhǔn)備Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.準(zhǔn)備DSL
        // 2.1.query查詢語句
        buildBasicQuery(params, request);
        // 2.2.設(shè)置size
        request.source().size(0);
        // 2.3.聚合
        buildAggregation(request);
        // 3.發(fā)出請求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析結(jié)果
        Map<String, List<String>> result = new HashMap<>();
        Aggregations aggregations = response.getAggregations();
        // 4.1.根據(jù)品牌名稱,獲取品牌結(jié)果
        List<String> brandList = getAggByName(aggregations, "brandAgg");
        result.put("品牌", brandList);
        // 4.2.根據(jù)品牌名稱,獲取品牌結(jié)果
        List<String> cityList = getAggByName(aggregations, "cityAgg");
        result.put("城市", cityList);
        // 4.3.根據(jù)品牌名稱,獲取品牌結(jié)果
        List<String> starList = getAggByName(aggregations, "starAgg");
        result.put("星級", starList);

        return result;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

private void buildAggregation(SearchRequest request) {
    request.source().aggregation(AggregationBuilders
                                 .terms("brandAgg")
                                 .field("brand")
                                 .size(100)
                                );
    request.source().aggregation(AggregationBuilders
                                 .terms("cityAgg")
                                 .field("city")
                                 .size(100)
                                );
    request.source().aggregation(AggregationBuilders
                                 .terms("starAgg")
                                 .field("starName")
                                 .size(100)
                                );
}

private List<String> getAggByName(Aggregations aggregations, String aggName) {
    // 4.1.根據(jù)聚合名稱獲取聚合結(jié)果
    Terms brandTerms = aggregations.get(aggName);
    // 4.2.獲取buckets
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    // 4.3.遍歷
    List<String> brandList = new ArrayList<>();
    for (Terms.Bucket bucket : buckets) {
        // 4.4.獲取key
        String key = bucket.getKeyAsString();
        brandList.add(key);
    }
    return brandList;
}

到了這里,關(guān)于ElasticSearch內(nèi)容分享(四):ES搜索引擎的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包