ElasticSearch - 全文檢索服務(wù) - RestHightLevel版

一、引言


1.1 數(shù)據(jù)庫(kù)查詢?yōu)槭裁催€要ElasticSearch?

數(shù)據(jù)庫(kù)一般只適合保存搜索結(jié)構(gòu)化的數(shù)據(jù),對(duì)于非結(jié)構(gòu)化的數(shù)據(jù)( 比如文章內(nèi)容)哨鸭,只能通過like%%模糊查詢,但是在大量的數(shù)據(jù)面前娇妓,like%%有兩個(gè)弊端:

1)搜索效率會(huì)很差像鸡,因?yàn)槭亲鲆粋€(gè)全表掃描(like%%會(huì)讓索引失效)

2)搜索沒辦法通過相關(guān)度匹配排序(可能返回的是用戶不關(guān)心的結(jié)果)

ElasticSearch就可以解決這些問題

1.2 什么是全文檢索?

全文檢索 將非結(jié)構(gòu)化數(shù)據(jù)中的一部分信息提取出來(lái)哈恰,重新組織只估,使其變得具有一定結(jié)構(gòu),然后對(duì)此有一定結(jié)構(gòu)的數(shù)據(jù)進(jìn)行搜索蕊蝗,從而達(dá)到搜索相對(duì)較快的目的仅乓。這部分從非結(jié)構(gòu)化數(shù)據(jù)中提取出的然后重新組織的信息,我們稱之索引蓬戚。

例如字典的拼音表和部首檢字表就相當(dāng)于字典的索引,通過查找拼音表或者部首檢字表就可以快速的查找到我們要查的字宾抓。

這種先建立索引子漩,再對(duì)索引進(jìn)行搜索的過程就叫全文檢索(Full-text Search)。

1.3 全文檢索的流程


image.png

1.4 構(gòu)建索引的過程

索引過程石洗,對(duì)要搜索的原始內(nèi)容進(jìn)行索引構(gòu)建一個(gè)索引庫(kù)幢泼,索引過程包括:

獲得文檔→創(chuàng)建文檔→分析文檔→索引文檔。

1.4.1 獲得原始文檔

原始文檔是指要索引和搜索的內(nèi)容讲衫。原始內(nèi)容包括互聯(lián)網(wǎng)上的網(wǎng)頁(yè)缕棵、數(shù)據(jù)庫(kù)中的數(shù)據(jù)、磁盤上的文件等涉兽。

1.4.2 創(chuàng)建文檔對(duì)象(Document)

獲取原始文檔的目的是為了索引招驴,在索引前需要將原始內(nèi)容創(chuàng)建成文檔(Document),文檔中包括一個(gè)一個(gè)的域(Field)枷畏,域中存儲(chǔ)內(nèi)容别厘。

1.4.3 分析文檔(分詞)

將原始內(nèi)容創(chuàng)建為包含域(Field)的文檔(document),需要再對(duì)域中的內(nèi)容進(jìn)行分析拥诡,分析的過程是經(jīng)過對(duì)原始文檔提取單詞触趴、將字母轉(zhuǎn)為小寫氮发、去除標(biāo)點(diǎn)符號(hào)、去除停用詞等過程生成最終的語(yǔ)匯單元冗懦。

1.4.4 創(chuàng)建索引

創(chuàng)建索引是對(duì)語(yǔ)匯單元索引爽冕,通過詞語(yǔ)找文檔,這種索引的結(jié)構(gòu)叫倒排索引結(jié)構(gòu)披蕉。

1.5 倒排索引

1.5.1 正向索引

簡(jiǎn)單來(lái)說(shuō)颈畸,正向索引就是根據(jù)文件ID找到該文件的內(nèi)容,在文件內(nèi)容中匹配搜索關(guān)鍵字嚣艇,這種方法是順序掃描方法承冰,數(shù)據(jù)量大、搜索慢食零。


image.png

1.5.2 反向(倒排)索引

倒排索引和正向索引剛好相反困乒,是根據(jù)內(nèi)容(詞語(yǔ))找文檔


image.png

二、ElasticSearch


2.1 ElasticSearch簡(jiǎn)介

ElasticSearch基本概念

2.1.1 索引庫(kù)(index)

索引庫(kù)是ElasticSearch存放數(shù)據(jù)的地方贰谣,可以理解為關(guān)系型數(shù)據(jù)庫(kù)中的一個(gè)數(shù)據(jù)庫(kù)娜搂。事實(shí)上,我們的數(shù)據(jù)被存儲(chǔ)和索引在分片(shards)中吱抚,索引只是一個(gè)把一個(gè)或多個(gè)分片分組在一起的邏輯空間百宇。

2.1.2 映射類型(type)

映射類型用于區(qū)分同一個(gè)索引下不同的數(shù)據(jù)類型,相當(dāng)于關(guān)系型數(shù)據(jù)庫(kù)中的表。

注意:在 6.0 的index下是無(wú)法創(chuàng)建多個(gè)type秘豹,并且會(huì)在 7.0 中完全移除携御。

2.1.3 文檔(documents)

文檔是ElasticSearch中存儲(chǔ)的實(shí)體,類比關(guān)系型數(shù)據(jù)庫(kù)既绕,每個(gè)文檔相當(dāng)于數(shù)據(jù)庫(kù)表中的一行數(shù)據(jù)啄刹。

2.1.4 字段(fields)

文檔由字段組成,相當(dāng)于關(guān)系數(shù)據(jù)庫(kù)中列的屬性凄贩。

2.1.5 分片與副本

如果一個(gè)索引具有很大的數(shù)據(jù)量誓军,它的數(shù)據(jù)量可能會(huì)超出單個(gè)節(jié)點(diǎn)的容量限制(硬盤容量),而且單個(gè)節(jié)點(diǎn)數(shù)據(jù)量過大疲扎,執(zhí)行性能也會(huì)隨之下降昵时,每個(gè)搜索請(qǐng)求的執(zhí)行效率都會(huì)降低。 為了解決上述問題椒丧, Elasticsearch 提出了分片的概念壹甥,索引將劃分成多份,稱為分片瓜挽。每個(gè)分片都可以創(chuàng)建對(duì)應(yīng)的副本盹廷,以便保證服務(wù)的高可用性。

2.2 ElasticSearch的安裝

1)準(zhǔn)備ElasticSearch的docker-compose.yml文件

version: '3.1'
services:
  elasticsearch:
    image: elasticsearch:6.8.5
    restart: always
    container_name: elasticsearch
    ports:
      - 9200:9200
      - 9300:9300
    environment:
      discovery.type: single-node
    volumes:
      - ./es/data:/usr/share/elasticsearch/data:rw
      - ./es/logs:/usr/share/elasticsearch/logs:rw
      - ./es/plugins:/usr/share/elasticsearch/plugins
      - config:/usr/share/elasticsearch/config
  kibana:
    image: kibana:6.8.5
    container_name: kibana
    restart: always
    environment:
      SERVER_NAME: kibana
      ELASTICSEARCH_URL: http://193.168.195.135:9200
    ports:
      - 5601:5601
volumes:
  config:

注意:第23行必須寫elasticsearch所在機(jī)器的ip地址,不能寫127.0.0.1

2)執(zhí)行docker-compose up -d 命令啟動(dòng)容器
docker-compose up -d

注意:第一次創(chuàng)建容器會(huì)失敗俄占,需要給.es文件夾賦予權(quán)限管怠,執(zhí)行chmod 777 -R ./es命令,然后重啟容器

3)安裝中文分詞器

進(jìn)入elasticsearch容器缸榄,執(zhí)行中文分詞器相關(guān)安裝命令

docker exec -it elasticsearch bash

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.5/elasticsearch-analysis-ik-6.8.5.zip

注意:可以訪問

https://github.com/medcl/elasticsearch-analysis-ik/releases

查找和當(dāng)前es匹配的版本

4)手動(dòng)安裝中文分詞器

如果第3步安裝超時(shí)失敗渤弛,可以嘗試進(jìn)行手動(dòng)安裝,如果第三步成功則跳過該步驟

4.1)手動(dòng)下載對(duì)應(yīng)的IK分詞器版本https://github.com/medcl/elasticsearch-analysis-ik/releases

4.2)直接將分詞器壓縮包上傳到容器的plugins路徑(上傳到宿主機(jī)的容器卷路徑即可)

4.3)解壓分詞器

unzip elasticsearch-analysis-ik-6.8.5.zip -d ./ik-analyze
5)重啟容器
docker-compose restart
6)訪問kibana服務(wù)
image.png

7)測(cè)試IK分詞器

POST _analyze
{
  "analyzer":"ik_smart",
  "text":"殲10系列戰(zhàn)斗機(jī)"
}


POST _analyze
{
  "analyzer":"ik_max_word",
  "text":"殲10系列戰(zhàn)斗機(jī)"
}
image.png

2.3 索引庫(kù)(Index)相關(guān)操作

2.3.1 概述

ElasticSearch采用Rest風(fēng)格的API甚带,因此其API就是一次Http請(qǐng)求

請(qǐng)求分為: PUT POST GET DELETE

GET:查詢數(shù)據(jù)

PUT:插入數(shù)據(jù)

POST:更新數(shù)據(jù)她肯,實(shí)際上很多情況下 es 不是很清晰你到底要作什么,有些時(shí)候POST也可用于新增或者查詢

DELETE: 刪除數(shù)據(jù)

2.3.2 新增索引庫(kù)語(yǔ)法

PUT /索引庫(kù)名稱
{
  "settings":{
    "number_of_shards": 3, #分片的數(shù)量
    "number_of_replicas": 2 #副本的數(shù)量
  }
}

settings:表示索引庫(kù)的設(shè)置 number_of_shards:表示分片的數(shù)量 number_of_replicas:副本數(shù)量

2.3.3 查詢索引信息

GET /索引庫(kù)名稱

2.3.4 判斷索引庫(kù)是否存在

HEAD /索引庫(kù)名稱

2.3.5 刪除索引庫(kù)

DELETE /索引庫(kù)名稱

注意
1)索引庫(kù) 類似于 MySQL中數(shù)據(jù)庫(kù)的概念
2)如果創(chuàng)建索引不指定settings鹰贵,默認(rèn)會(huì)有5個(gè)分片晴氨,1個(gè)副本

2.4 映射類型(type)相關(guān)操作
2.4.1 新增映射類型語(yǔ)法

PUT /索引庫(kù)名/_mapping/類型名稱
{
    "properties": {
        "字段名1": {
            "type": "類型",
            "index": true,
            "store": true碉输,
            "analyzer": "分詞器"
        },
        "字段名2": {
            "type": "類型",
            "index": true籽前,
            "store": true,
            "analyzer": "分詞器"
        }
    }
}

type:類型敷钾,可以是text枝哄、long、date阻荒、integer挠锥、object、keyword(表示關(guān)鍵字侨赡,不能被分詞)
index:是否參與索引蓖租,默認(rèn)為true
store:是否參與存儲(chǔ),默認(rèn)為false
analyzer:分詞器羊壹,可選 “ik_max_word”或者“ik_smart”菜秦,表示使用ik分詞器

2.4.2 查看映射類型信息

GET /索引庫(kù)名稱/_mapping/類型名稱

2.4.3 字段屬性詳解

type

String類型,又分兩種: text:可分詞舶掖,不可參與聚合 keyword:不可分詞,數(shù)據(jù)會(huì)作為完整字段進(jìn)行匹配尔店,可以參與聚合
Numerical數(shù)值類型眨攘,分兩類 基本數(shù)據(jù)類型:long、interger嚣州、short鲫售、byte、double该肴、flfloat情竹、half_flfloat 浮點(diǎn)數(shù)的高精度類型:scaled_float,需要指定一個(gè)精度因子匀哄,比如10或100秦效,elasticsearch會(huì)把真實(shí)值乘以這個(gè)因子后存儲(chǔ)雏蛮,取出時(shí)再還原
Date:日期類型 elasticsearch可以對(duì)日期格式化為字符串存儲(chǔ),但是建議我們存儲(chǔ)為毫秒值阱州,存儲(chǔ)為long挑秉,節(jié)省空間
boolean: 設(shè)置字段類型為boolean后,可以填入的值為:true苔货、false犀概、"true"、"false"
binary: binary類型接受base64編碼的字符串
geo_point: 地理點(diǎn)類型用于存儲(chǔ)地理位置的經(jīng)緯度對(duì)
更多類型參考:https://www.elastic.co/guide/en/elasticsearch/reference/6.5/mapping-types.html

index

index影響字段的索引情況夜惭。
true:字段會(huì)被索引姻灶,則可以用來(lái)進(jìn)行搜索。默認(rèn)值就是true
false:字段不會(huì)被索引诈茧,不能用來(lái)搜索
index的默認(rèn)值就是true产喉,也就是說(shuō)你不進(jìn)行任何配置,所有字段都會(huì)被索引若皱。但是有些字段是我們不希望被索引的镊叁,比如商品的圖片信息脖镀,就需要手動(dòng)設(shè)置index為false订晌。

store

是否將數(shù)據(jù)進(jìn)行額外存儲(chǔ)。在學(xué)習(xí)lucene和solr時(shí)习勤,我們知道如果一個(gè)字段的store設(shè)置為false互广,那么在文檔列表中就不會(huì)有這個(gè)字段的值敛腌,用戶的搜索結(jié)果中不會(huì)顯示出來(lái)。
但是在Elasticsearch中惫皱,即便store設(shè)置為false像樊,也可以搜索到結(jié)果。
原因是Elasticsearch在創(chuàng)建文檔索引時(shí)旅敷,會(huì)將文檔中的原始數(shù)據(jù)備份生棍,保存到一個(gè)叫做 _source 的屬性中。而且我們可以通過過濾 _source 來(lái)選擇哪些要顯示媳谁,哪些不顯示涂滴。而如果設(shè)置store為true,就會(huì)在 _source 以外額外存儲(chǔ)一份數(shù)據(jù)晴音,多余柔纵,因此一般我們都會(huì)將store設(shè)置為false,事實(shí)上锤躁,store的默認(rèn)值就是false搁料。

analyzer

定義的是該字段的分析器,默認(rèn)的分析器是 standard 標(biāo)準(zhǔn)分析器,這個(gè)地方可定義為自定義的分析器郭计。
比如IK分詞器為: ik_max_word 或者 ik_smart

boost

激勵(lì)因子霸琴。這個(gè)與lucene中一樣,我們可以通過指定一個(gè)boost值來(lái)控制每個(gè)查詢子句的相對(duì)權(quán)重。
該值默認(rèn)為1拣宏。一個(gè)大于1的boost會(huì)增加該查詢子句的相對(duì)權(quán)重
比如:

GET /_search {
    "query": {
        "bool": {
            "must": {
                "match": {
                    "content": {
                        "query": "full text search",
                        "operator": "and"
                    }
                }
            },
            "should": [{
                    "match": {
                        "content": {
                            "query": "Elasticsearch",
                            "boost": 3
                        }
                    }
                },
                {
                    "match": {
                        "content": {
                            "query": "Lucene",
                            "boost": 2
                        }
                    }
                }
            ]
        }
    }
}

注意
1)映射類型(type) 類似于 MySQL數(shù)據(jù)庫(kù)中表的概念
2)從ElasticSearch 6.x之后沈贝,一個(gè)Index下只能有一個(gè)type

2.5 文檔相關(guān)(document)操作
2.5.1 添加文檔

#指定id的添加方式
PUT /索引庫(kù)名/類型名稱/id #id需要自己指定
{
  "field1":"value1",
  "field2":"value2",
  ...
}
#自動(dòng)生成id的添加方式
POST /索引庫(kù)名/類型名稱  #使用POST無(wú)需指定id
{
  "field1":"value1",
  "field2":"value2",
  ...
}
#批量添加文檔
PUT /索引庫(kù)名稱/類型名稱/_bulk
{"index":{"_id":id值1}}
{"field1":"value1", "field2":"value2"...}
{"index":{"_id":id值2}}
{"field1":"value1", "field2":"value2"...}
....

2.5.2 更新文檔

#全局更新,會(huì)將所有字段更新勋乾,沒有指定的字段會(huì)自動(dòng)刪除
PUT /索引庫(kù)名/類型名稱/id #需要更新的id宋下,id必須存在,如果不存在就變成了添加
{
  "field1":"value1",
  "field2":"value2",
  ...
}
#局部更新辑莫,只更新需要更新的字段
POST /索引庫(kù)名/類型名稱/id/_update
{
  "doc":{
    "field1": "新的value"
  }
}

2.5.3 刪除文檔

DELETE /索引庫(kù)名/類型名稱/id

2.5.4 查詢文檔

#查詢索引庫(kù)全部數(shù)據(jù)
GET /索引庫(kù)名稱/_search 
#根據(jù)id查詢
GET /索引庫(kù)名稱/類型名稱/id
#批量查詢
GET /_mget
{
    "docs": [
        {
            "_index": "索引庫(kù)名稱1",
            "_type": "映射類型1",
            "_id":"查詢文檔id1"
        },
        {
            "_index": "索引庫(kù)名稱2",
            "_type": "映射類型2",
            "_id":"查詢文檔id2"
        }
    ]
}

注意
1)文檔(document)類似于 數(shù)據(jù)庫(kù)中表的一條記錄
2)當(dāng)添加的文檔中学歧,設(shè)置的field,而type中沒有時(shí)各吨,type會(huì)自動(dòng)的添加該field的映射記錄枝笨,
這是elasticsearch的自動(dòng)映射功能

三、SpringBoot操作ElasticSearch(elasticsearch-rest-high-level-client)
3.1 配置ElasticSearch
1)添加依賴

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.8.5</version>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.8.5</version>
</dependency>

2)配置application.yml

spring:
  elasticsearch:
    rest:
      uris: http://192.168.195.135:9200

3)在需要的地方注入RestHighLevelClient對(duì)象

@Autowired
 private RestHighLevelClient restHighLevelClient;

3.2 使用SpringBoot操作索引庫(kù)(Index)

 @Autowired
    private RestHighLevelClient client;


    /**
     * 創(chuàng)建索引
     * @param indexName
     * @return
     */
    @Override
    public boolean createIndex(String indexName) {

        CreateIndexRequest indexRequest = new CreateIndexRequest(indexName);
        //設(shè)置索引庫(kù)的相關(guān)屬性
        Settings settings = Settings.builder()
                .put("number_of_shards", 1)//設(shè)置分片數(shù)量
                .put("number_of_replicas", 0)//設(shè)置副本數(shù)量
                .build();
        indexRequest.settings(settings);

        try {
            CreateIndexResponse response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
            //返回結(jié)果
            return response.isAcknowledged();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

    /**
     * 判斷索引是否存在
     * @param indexName
     * @return
     */
    @Override
    public boolean isExistsIndex(String indexName) {
        GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
        try {
            boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
            return exists;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

    /**
     * 刪除索引
     * @param indexName
     * @return
     */
    @Override
    public boolean deleteIndex(String indexName) {
        DeleteIndexRequest request = new DeleteIndexRequest(indexName);
        try {
            AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
            return response.isAcknowledged();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

3.3 使用SpringBoot操作映射類型(type)

/**
     * 添加映射
     * PUT /partform_hotal/_mapping/hotal
     * {
     *   "properties": {
     *     "hotalName":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "hotalImage":{
     *       "type": "keyword",
     *       "index": false
     *     },
     *     "type":{
     *       "type": "integer"
     *     },
     *     "hotalInfo":{
     *       "type":"text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "keyword":{
     *       "type":"text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "location":{
     *       "type": "geo_point"
     *     },
     *     "star":{
     *       "type": "integer"
     *     },
     *     "brand":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "address":{
     *       "type": "keyword"
     *     },
     *     "openTime":{
     *       "type": "date",
     *       "format": "yyyy-MM-dd"
     *     },
     *     "cityname":{
     *       "type": "keyword"
     *     },
     *     "regid":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     }
     *   }
     * }
     *
     *
     * @return
     */
    @Override
    public boolean createMapping(String index) {

        PutMappingRequest putMappingRequest = new PutMappingRequest(index);

        try {
            XContentBuilder builder = JsonXContent.contentBuilder();
            builder
                    .startObject()
                    .startObject("properties")

                    .startObject("hotalName")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .startObject("hotalImage")
                    .field("type", "keyword")
                    .field("index", "false")
                    .endObject()

                    .startObject("type")
                    .field("type", "integer")
                    .endObject()

                    .startObject("hotalInfo")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .startObject("keyword")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .startObject("location")
                    .field("type", "geo_point")
                    .endObject()

                    .startObject("star")
                    .field("type", "integer")
                    .endObject()

                    .startObject("brand")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .startObject("address")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .startObject("openTime")
                    .field("type", "date")
                    .field("format", "yyyy-MM-dd")
                    .endObject()

                    .startObject("cityname")
                    .field("type", "keyword")
                    .endObject()

                    .startObject("regid")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()

                    .endObject().endObject();
            //設(shè)置到Request對(duì)象中
            putMappingRequest.source(builder);
            client.indices().putMapping(putMappingRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

3.4 使用SpringBoot操作文檔(document)
新增文檔

/**
     * 給索引庫(kù)添加文檔
     * @param indexName
     * @param hotal
     * @return
     */
    @Override
    public boolean insertDco(String indexName, Hotal hotal) {

        String json = JSON.toJSONString(hotal);
        System.out.println(json);

        IndexRequest indexRequest = new IndexRequest(indexName, "_doc")
                .id(hotal.getId() + "")
                .source(json, XContentType.JSON);
        try {
            IndexResponse index = client.index(indexRequest, RequestOptions.DEFAULT);
            long seqNo = index.getSeqNo();
            String lowercase = index.getResult().getLowercase();
            int status = index.status().getStatus();
            System.out.println("狀態(tài):" + status);
            System.out.println("返回:" + lowercase);
            System.out.println("序號(hào):" + seqNo);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

刪除文檔

 /**
     * 根據(jù)ID刪除
     * @param indexName
     * @param id
     * @return
     */
    @Override
    public boolean deleteDco(String indexName, Integer id) {

        DeleteRequest deleteRequest = new DeleteRequest(indexName, "_doc", id + "");

        try {
            DeleteResponse resp = client.delete(deleteRequest, RequestOptions.DEFAULT);
            int status = resp.status().getStatus();
            System.out.println("結(jié)果:" + status);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

更新文檔

 /**
     * 根據(jù)id修改信息
     * @param indexName
     * @param hotal
     * @return
     */
    @Override
    public boolean updateDco(String indexName, Hotal hotal) {

        String json = JSON.toJSONString(hotal);
        System.out.println(json);

//        Map map = new HashMap();
//        map.put("hotalInfo", "xxxx");

        UpdateRequest updateRequest = new UpdateRequest(indexName, "_doc", hotal.getId() + "");
        updateRequest.doc(json, XContentType.JSON);

        try {
            UpdateResponse response = client.update(updateRequest, RequestOptions.DEFAULT);
            int status = response.status().getStatus();
            System.out.println("狀態(tài):" + status);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

四揭蜒、基本查詢
4.1 term横浑、terms查詢
什么是term查詢?
term是代表完全匹配屉更,也就是精確查詢徙融,搜索前不會(huì)再對(duì)搜索詞進(jìn)行分詞,所以我們的搜索詞必須是文檔分詞集合中的一個(gè)瑰谜。 比如文檔內(nèi)容為:"美的微波爐"欺冀,被分詞為"美的"和"微波爐",term搜索的關(guān)鍵字必須為"美的"或者"微波爐"才能搜索出這個(gè)文檔萨脑,搜索"美的微波爐"搜索不出來(lái)
語(yǔ)法

#term查詢
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "term": {
      "字段名稱": {
        "value": "搜索關(guān)鍵字"
      }
    }
  }
}
#terms查詢 - 可以同時(shí)查詢多個(gè)關(guān)鍵詞
GET /索引庫(kù)/映射類型/_search
{
  "query":{
    "terms": {
      "字段名稱": [
        "關(guān)鍵字1","關(guān)鍵字2"...
      ]
    }
  }  
}

注意:terms查詢 多個(gè)關(guān)鍵字之間是或者的關(guān)系隐轩,也就是說(shuō)只要符合一個(gè)關(guān)鍵字的文檔就會(huì)被查詢出來(lái)

4.2 match查詢

什么是match查詢?

match 查詢是高層查詢渤早,它們了解字段映射的信息:

1.如果查詢 日期(date) 或 整數(shù)(integer) 字段职车,它們會(huì)將查詢字符串分別作為日期或整數(shù)對(duì)待。

2.如果查詢一個(gè)( not_analyzed )未分詞的精確值字符串字段鹊杖, 它們會(huì)將整個(gè)查詢字符串作為單個(gè)詞項(xiàng)對(duì)待提鸟。

3.但如果要查詢一個(gè)( analyzed )已分析的全文字段, 它們會(huì)先將查詢字符串傳遞到一個(gè)合適的分析器仅淑,然后生成一個(gè)供查詢的詞項(xiàng)列表。 一旦組成了詞項(xiàng)列表胸哥,這個(gè)查詢會(huì)對(duì)每個(gè)詞項(xiàng)逐一執(zhí)行底層的查詢涯竟,再將結(jié)果合并,然后為每個(gè)文檔生成一個(gè)最終的相關(guān)度評(píng)分。 match查詢其實(shí)底層是多個(gè)term查詢庐船,最后將term的結(jié)果合并银酬。

語(yǔ)法

#match_all查詢 - 查詢指定庫(kù)的指定類型的所有文檔
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "match_all": {}
  }
}
#match查詢 - 根據(jù)關(guān)鍵字查詢
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "match": {
      "字段名稱": "搜索關(guān)鍵字"
    }
  }
}
#布爾match查詢
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "match": {
      "字段名稱": {
        "query": "搜索關(guān)鍵字",
        "operator": "OR或者AND"  
      }
    }
  }
}

注意:operator值為
and表示關(guān)鍵詞分詞后的結(jié)果,必須全部匹配上
or表示需要一個(gè)分詞匹配上即可筐钟,默認(rèn)為or

#mulit_match查詢 - 可以查詢多個(gè)字段
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "multi_match": {
      "query": "搜索關(guān)鍵字",
      "fields": ["字段名稱1^2.0", "字段名稱2^0.5"],
      "operator": "or"
    }
  }
}

注意:
^2.0表示這個(gè)字段在搜索中的權(quán)重揩瞪,值越高權(quán)重越大,可以不設(shè)置篓冲。

image.png
#match_phrase查詢 - 短語(yǔ)查詢
GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "match_phrase": {
      "字段名稱": "關(guān)鍵詞1 關(guān)鍵詞2"
    }
  }
}

注意:

match_phrase查詢李破,只會(huì)匹配關(guān)鍵詞1 和關(guān)鍵詞2 挨在一起的文檔,如果兩個(gè)關(guān)鍵詞分開太遠(yuǎn)的文檔是不會(huì)匹配上的

4.3 ids查詢
什么ids查詢壹将?
ids查詢是一類簡(jiǎn)單的查詢嗤攻,它過濾返回的文檔只包含其中指定標(biāo)識(shí)符的文檔,
該查詢默認(rèn)指定作用在“_id”上面诽俯。
語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "ids": {
      "values": ["1","3","6"...]
    }
  }
}
image.png

4.4 prefix前綴查詢
什么是prefix查詢妇菱?
前綴查詢,可以使我們找到某個(gè)字段以給定前綴開頭的文檔暴区。最典型的使用場(chǎng)景闯团,一般是在文本框錄入的時(shí)候的聯(lián)想功能
語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "prefix": {
      "字段名稱": {
        "value": "前綴"
      }
    }
  }
}

注意:前綴查詢并不是和搜索字段的內(nèi)容前綴匹配,而是和搜索字段的所有分詞的前綴匹配仙粱,匹配上一個(gè)分詞后房交,就會(huì)查詢出該文檔,建議和keyword類型的字段結(jié)合使用


image.png

4.5 fuzzy查詢

什么是fuzzy查詢缰盏?

fuzzy(模糊)查詢是一種模糊查詢涌萤,term 查詢的模糊等價(jià)。

語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "fuzzy": {
      "字段名稱": {
        "value": "關(guān)鍵詞",
        "fuzziness": "2"
      }
    }
  }
}

注意:
1口猜、fuzzy搜索以后负溪,會(huì)自動(dòng)嘗試將你的搜索文本進(jìn)行糾錯(cuò),然后去跟文本進(jìn)行匹配
2济炎、fuzziness屬性表示關(guān)鍵詞最多糾正的次數(shù)川抡, 比如空條 -> 空調(diào),需要糾正一次须尚,fuzziness需要設(shè)置為1
3崖堤、prefix_length屬性表示不能被 “模糊化” 的初始字符數(shù)。 大部分的拼寫錯(cuò)誤發(fā)生在詞的結(jié)尾耐床,而不是詞的開始密幔。 例如通過將prefix_length 設(shè)置為 3 ,你可能夠顯著降低匹配的詞項(xiàng)數(shù)量撩轰。(前面3個(gè)字不能出錯(cuò)胯甩,否則查不到)


image.png

4.6 wildcard查詢
什么是wildcard查詢昧廷?
wildcard(通配符)查詢意為通配符查詢

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "wildcard": {
      "字段名稱": {
        "value": "關(guān)鍵詞? *"
      }
    }
  }
}

注意:
*表示匹配0或者多個(gè)字符
?表示匹配一個(gè)字符
wildcard查詢不注意查詢性能,應(yīng)盡可能避免使用偎箫。

4.7 range查詢
什么range查詢木柬?
range查詢既范圍查詢,可以對(duì)某個(gè)字段進(jìn)行范圍匹配

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "range": {
      "字段名稱": {
        "gte": 0,
        "lte": 2000,
      }
    }
  }
}
image.png

4.8 regexp查詢

什么是regexp查詢淹办?
正則表達(dá)式查詢眉枕,wildcard和regexp查詢的工作方式和prefix查詢完全一樣。它們也需要遍歷倒排索引中的詞條列表來(lái)找到所有的匹配詞條怜森,然后逐個(gè)詞條地收集對(duì)應(yīng)的文檔ID速挑。它們和prefix查詢的唯一區(qū)別在于它們能夠支持更加復(fù)雜的模式。
語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "regexp": {
      "字段名稱": "正則表達(dá)式"
    }
  }
}

注意:
1塔插、prefix(前綴)梗摇,wildcard(通配符)以及regexp(正則)查詢基于詞條進(jìn)行操作。如果你在一個(gè)analyzed字段上使用了它們想许,它們會(huì)檢查字段中的每個(gè)詞條伶授,而不是整個(gè)字段。
2流纹、對(duì)一個(gè)含有很多不同詞條的字段運(yùn)行這類查詢是非常消耗資源的糜烹。應(yīng)該避免使用一個(gè)以通配符開頭的模式(比如,*foo)

image.png

4.9 使用JavaAPI實(shí)現(xiàn)以上查詢
查詢的基礎(chǔ)結(jié)構(gòu)漱凝,通過不同的QueryBuilder對(duì)象疮蹦,可以實(shí)現(xiàn)不同的查詢

@Override
public List<Hotal> queryHotals(String indexName, QueryBuilder queryBuilder) {
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.query(queryBuilder);
        
    SearchRequest searchRequest = new SearchRequest(indexName);
    searchRequest.source(searchSourceBuilder);

    try {
        SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();

        //循環(huán)結(jié)果
        for (SearchHit hit : hits) {
            System.out.println("------------------------------------------");
            Map<String, DocumentField> fields = hit.getFields();
            for (Map.Entry<String, DocumentField> entry : fields.entrySet()) {
                System.out.println(entry.getKey() + ":" +  entry.getValue().getValue());
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}
//term
TermQueryBuilder termQueryBuilder = 
            QueryBuilders.termQuery("hotalName", "連鎖");
//terms
TermsQueryBuilder termsQueryBuilder = 
            QueryBuilders.termsQuery("hotalName", "7天", "連鎖");
//match
MatchQueryBuilder matchQueryBuilder = 
            QueryBuilders.matchQuery("hotalName", "愛麗絲");

//match查詢
        MultiMatchQueryBuilder matchBuilder = QueryBuilders.multiMatchQuery("大中華")
                .field("hotalName", 1.0f)
                .field("hotalInfo", 2.0f);

//matchall
MatchAllQueryBuilder matchAllQueryBuilder = 
            QueryBuilders.matchAllQuery();
//Ids
IdsQueryBuilder idsQueryBuilder = 
            QueryBuilders.idsQuery().addIds("2","3");
//prefix
PrefixQueryBuilder prefixQueryBuilder = 
            QueryBuilders.prefixQuery("hotalName", "連");
//fuzzy
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("regid", "平三區(qū)")
                .fuzziness(Fuzziness.TWO)
                .prefixLength(0);
//wildcard
WildcardQueryBuilder wildcardQueryBuilder = 
            QueryBuilders.wildcardQuery("hotalName", "愛麗*");
//range
RangeQueryBuilder rangeQuery = 
            QueryBuilders.rangeQuery("star").gte(0).lt(3);
//regexp
RegexpQueryBuilder regexQuery = 
            QueryBuilders.regexpQuery("hotalName", "\\S{0,}[0-9]{1}.*");

五、復(fù)合查詢

5.1 bool查詢
bool 過濾器茸炒。 這是個(gè) 復(fù)合過濾器(compound fifilter) 愕乎,它可以接受多個(gè)其他過濾器作為參數(shù),并將這些過濾器結(jié)合成各式各樣的布爾(邏輯)組合壁公。
語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "content": {
              "value": "性價(jià)比"
            }
          }
        },{
          "match": {
            "title": "微波爐"
          }
        }
      ],
      "must": [
        {
          "match": {
            "content": "格力造"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gte": 300,
              "lte": 3000
            }
          }
        }
      ],
      "filter": {
        "match": {
          "title": "美的"
        }
      }
    }
  }
}

例子

#bool查詢 - 將多個(gè)基本查詢組合在一起
GET /hotal_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "brand": {
              "value": "7天"
            }
          }
        }
      ], 
      "must": [
        {
          "match": {
            "hotalName": "連鎖"
          }
        },{
          "range": {
            "price": {
              "gte": 500,
              "lte": 3000
            }
          }
        }
      ],
      ##minimum_should_match:1
      "filter": {
          "multi_match": {
            "query": "大中華",
            "fields": ["hotalName^2.0", "hotalInfo"]
          }
      }
    }
  }
}

屬性含義

must: 返回的文檔必須滿足must子句的條件感论,并且參與計(jì)算分值,與 AND 等價(jià)

must_not:所有的語(yǔ)句都 不能(must not) 匹配紊册,與 NOT 等價(jià)

should: 返回的文檔可能滿足should子句的條件比肄。在一個(gè)Bool查詢中,如果沒有must或者filter囊陡,有一個(gè)或 者多個(gè)should子句芳绩,那么只要滿足一個(gè)就可以返回,與 OR 等價(jià)

minimum_should_match:用來(lái)指定should至少需要匹配幾個(gè)語(yǔ)句

filter:返回的文檔必須滿足filter子句的條件撞反。但是不會(huì)像Must一樣妥色,參與計(jì)算分值

注意

如果查詢中沒有must語(yǔ)句,那么至少要匹配一個(gè)should語(yǔ)句

5.1.1 什么是filter遏片?

filter vs query
filter 垛膝,僅僅只是按照搜索條件過濾出需要的數(shù)據(jù)而已鳍侣,不計(jì)算任何相關(guān)度分?jǐn)?shù),對(duì)相關(guān)度沒有任何影響吼拥;
query ,會(huì)去計(jì)算每個(gè)document相對(duì)于搜索條件的相關(guān)度线衫,并按照相關(guān)度進(jìn)行排序凿可;
一般來(lái)說(shuō),如果你是在進(jìn)行搜索授账,需要將最匹配搜索條件的數(shù)據(jù)先返回枯跑,那么用query;如果你只是要根據(jù)一些條件篩選出一部分?jǐn)?shù)據(jù)白热,不關(guān)注其排序敛助,那么用filter; 除非是你的這些搜索條件屋确,你希望越符合這些搜索條件的document越排在前面返回纳击,那么這些搜索條件要放在query中;如果你不希望一些搜索條件來(lái)影響你的document排序攻臀,那么就放在filter中即可焕数;
filter和query的性能對(duì)比
filter ,不需要計(jì)算相關(guān)度分?jǐn)?shù)刨啸,不需要按照相關(guān)度分?jǐn)?shù)進(jìn)行排序堡赔,同時(shí)還有內(nèi)置的自動(dòng)cache最常使用filter的數(shù)據(jù)
query ,相反设联,要計(jì)算相關(guān)度分?jǐn)?shù)善已,按照分?jǐn)?shù)進(jìn)行排序,而且無(wú)法cache結(jié)果
所以filter查詢性能會(huì)高于query

5.3 boosting查詢
什么是boosting查詢离例?

該查詢用于將兩個(gè)查詢封裝在一起换团,并降低其中一個(gè)查詢所返回文檔的分值。它接受一個(gè)positive查詢和一個(gè)negative查詢粘招。只有匹配了positive查詢的文檔才會(huì)被包含到結(jié)果集中啥寇,但是同時(shí)匹配了negative查詢的文檔會(huì)被降低其相關(guān)度,通過將文檔原本的score和negative_boost參數(shù)進(jìn)行相乘來(lái)得到新的score洒扎。因此辑甜,negative_boost參數(shù)必須小于1.0
(positive作為查詢條件,如果查詢條件同時(shí)符合negative條件袍冷,則會(huì)降低分值)
"negative_boost": 會(huì)把原來(lái)的分值乘以negative_boost的值作為最后的分值
如果negative_boost>1:提高分值
如果negative_boost<1:降低分值

運(yùn)用場(chǎng)景

例如磷醋,在互聯(lián)網(wǎng)上搜索"蘋果"也許會(huì)返回,水果或者各種食譜的結(jié)果胡诗。但是用戶可能只想搜索到蘋果手機(jī)等電子產(chǎn)品邓线,當(dāng)然我們可以通過排除“水果 喬木 維生素”和這類單詞淌友,結(jié)合bool查詢中的must_not子句,將結(jié)果范圍縮小到只剩蘋果手機(jī)骇陈,但是這種做法難免會(huì)排除掉那些真的想搜索水果的用戶震庭,這時(shí)可以通過boosting查詢,通過降低“水果 喬木 維生素”等關(guān)鍵詞的評(píng)分你雌,讓蘋果等電子產(chǎn)品的排名靠前

語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "title": "性價(jià)比"
        }
      },
      "negative": {
        "match": {
          "content": "性價(jià)比"
        }
      },
      "negative_boost": 0.1
    }
  }
}

5.4 使用JavaAPI實(shí)現(xiàn)以上查詢

//bool
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .must(....)
                .mustNot(...)
                .should(...)
                .filter(...)
                .minimumShouldMatch(1);
//boosting
BoostingQueryBuilder boostingQueryBuilder = QueryBuilders
                .boostingQuery(..., ...)
                .negativeBoost(0.2f);

例子:


image.png

六器联、排序

ElasticSearch默認(rèn)會(huì)有一套相關(guān)性分?jǐn)?shù)計(jì)算,分?jǐn)?shù)越高婿崭,說(shuō)明文檔相關(guān)性越大拨拓,也就越會(huì)排在前面。除了相關(guān)性排序之外氓栈,開發(fā)者也可以通過自己的需要渣磷,通過某些規(guī)則設(shè)置查詢文檔的排序
如果進(jìn)行手動(dòng)排序,則評(píng)分為null

語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段1": {
        "order": "asc"
      }
    },
    {
      "排序字段2":{
        "order": "desc"
      }
    }
  ]
}
例子:
#手動(dòng)排序
GET /hotal_index/_search
{
  "query": {
    "match": {
      "hotalName": "品牌連鎖酒店"
    }
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    }
  ]
}
//創(chuàng)建查詢構(gòu)建器
QueryBuilder queryBuilder = .........
..................

//執(zhí)行查詢
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
     .query(queryBuilder)
      //設(shè)置排序
     .sort("star", SortOrder.DESC)
     .sort("type", SortOrder.ASC);

七授瘦、高亮

什么是高亮醋界?
許多應(yīng)用都傾向于在每個(gè)搜索結(jié)果中 高亮 顯示搜索的關(guān)鍵詞,比如字體的加粗,改變字體的顏色等.以便讓用戶知道為何該文檔符合查詢條件奥务。在 Elasticsearch 中檢索出高亮片段也很容易物独。高亮顯示需要一個(gè)字段的實(shí)際內(nèi)容。 如果該字段沒有被存儲(chǔ)(映射mapping沒有將存儲(chǔ)設(shè)置為 true)氯葬,則加載實(shí)際的source挡篓,并從source中提取相關(guān)的字段。

語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
  "query": {
    ....
  },
  "highlight": {
    "fields": {
      "待高亮字段1": {},
      "待高亮字段2": {}
    },
    "post_tags": ["</font>"],
    "pre_tags": ["<font color='red'>"],
    //功能:搜索的摘要顯示
    "number_of_fragments": 5,
    "fragment_size": 3
  }
}

例子:


image.png

摘要顯示


image.png

參數(shù)含義
number_of_fragments: fragment 是指一段連續(xù)的文字帚称。返回結(jié)果最多可以包含幾段不連續(xù)的文字官研。
默認(rèn)是5。
fragment_size: 某字段的值闯睹,長(zhǎng)度是1萬(wàn)戏羽,但是我們一般不會(huì)在頁(yè)面展示這么長(zhǎng),可能只是展示一部分楼吃。設(shè)置要顯示出來(lái)的fragment文本判斷的長(zhǎng)度始花,默認(rèn)是100
noMatchSize: 搜索出來(lái)的這個(gè)文檔這個(gè)字段已經(jīng)顯示出高亮的情況,可是其它字段并沒有任何顯示孩锡,設(shè)置這個(gè)屬性可以顯示出來(lái)酷宵。
pre_tags: 標(biāo)記 highlight 的開始標(biāo)簽。
post_tags: 標(biāo)記 highlight 的結(jié)束標(biāo)簽躬窜。

JavaAPI

//設(shè)置高亮信息
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder
    .field("title", 100)
    .field("content", 100)
    .preTags("<font color='red'>")
    .postTags("</font>");

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
     .query(queryBuilder)
     .highlighter(highlightBuilder)
     .sort("star", SortOrder.DESC)
     .sort("type", SortOrder.ASC);

........     
//獲得高亮結(jié)果
Map<String, HighlightField> highlightFields 
                = hit.getHighlightFields();
for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {
    System.out.println(entry.getKey() + "--" 
                + entry.getValue().getFragments()[0].string());
}

八浇垦、地理位置搜索
地理位置在ElasticSearch中的字段類型geo-point
8.1 地理位置類型的相關(guān)操作

#創(chuàng)建映射類型:
PUT /soufang/_mapping/house
{
  "properties": {
    "name": {
      "type": "text"
    },
    "location": {
      "type": "geo_point"
    }
  }
}
#添加坐標(biāo)點(diǎn)數(shù)據(jù):
PUT /soufang/house/1
{
  "name": "市民中心",
  "location": {
    "lat": 22.54737, #lat代表緯度
    "lon": 114.067531 #lon代表經(jīng)度
  }
}

8.2 通過geo_distance過濾器搜索坐標(biāo)

geo_distance:地理距離過濾器( geo_distance )以給定位置為圓心畫一個(gè)圓,來(lái)找出那些地理坐標(biāo)落在其中的文檔

GET /soufang/house/_search
{
  "query": {
    "geo_distance":{
      "location": {
        "lat": 22.551013,
        "lon": 114.065432
      },
      "distance": "1km",
      "distance_type": "arc"
    }
  }
}

distance:中心點(diǎn)的半徑距離
distance_type:兩點(diǎn)間的距離計(jì)算的精度算法
arc - 最慢但是最精確是弧形(arc)計(jì)算方式荣挨,這種方式把世界當(dāng)作是球體來(lái)處理
plane - 平面(plane)計(jì)算方式男韧,把地球當(dāng)成是平坦的朴摊。 這種方式快一些但是精度略遜

8.3 通過geo_bounding_box過濾器搜索坐標(biāo)
geo_bounding_box: 查找某個(gè)長(zhǎng)方形區(qū)域內(nèi)的位置

GET /soufang/house/_search
{
  "query": {
    "geo_bounding_box":{
      "location":{
        "top_left": {
          "lat": 22.628427,
          "lon": 114.009234
        },
        "bottom_right": {
          "lat": 22.521103,
          "lon": 114.148939
        }
      }
    }
  }
}

top_left:代表矩形左上角
bottom_right:代表矩形右下角

8.4 通過geo_polygon過濾器搜索坐標(biāo)
geo_polygon:查找位于多邊形內(nèi)的地點(diǎn)

GET /soufang/house/_search
{
  "query": {
    "geo_polygon": {
      "location":{
        "points": [
          [113.908911, 22.613748],
          [114.056952,22.634298],
          [114.031368,22.575843],
          [114.097196,22.500803],
          [113.9,22.493591]
        ]
      }
    }
  }
}

8.5 過濾結(jié)果通過距離排序
query:是查詢這個(gè)區(qū)域的house
sort:這個(gè)區(qū)域的house為location里的坐標(biāo)為零點(diǎn),按距離排序返回?cái)?shù)據(jù)

GET /soufang/house/_search
{
  "query": {
    "geo_distance":{
      "location": {
        "lat": 22.551013,
        "lon": 114.065432
      },
      "distance": "1km",
      "distance_type": "arc"
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "order": "asc",
        "location": {
          "lat": 22.551013,
          "lon": 114.065432
        },
        "unit": "km",
        "distance_type": "arc"
      }
    }
  ]
}

unit:以 公里(km)為單位此虑,將距離設(shè)置到每個(gè)返回結(jié)果的 sort 鍵中

8.6 JavaApi的執(zhí)行方式

//geo_distance查找方式
QueryBuilders.geoDistanceQuery("location")
                    .point(22.55243, 114.044335)
                    .distance(2.8, DistanceUnit.KILOMETERS)

//geo_bounding_box查找方式
QueryBuilders.geoBoundingBoxQuery("location")
        .setCorners(
        new GeoPoint(22.628427, 114.009234), 
        new GeoPoint(22.521103, 114.148939)) 

//geo_polygon查找方式
List<GeoPoint> points = new ArrayList<>();
    points.add(new GeoPoint(22.613748, 113.908911));
    points.add(new GeoPoint(22.634298, 114.056952));
    points.add(new GeoPoint(22.575843,114.031368));
    points.add(new GeoPoint(22.500803,114.097196));
    points.add(new GeoPoint(22.493591,113.9));
QueryBuilders.geoPolygonQuery("location", points)


//根據(jù)距離排序
SortBuilders
    .geoDistanceSort("location", 22.586737, 113.960829)
    .order(SortOrder.DESC)
    .unit(DistanceUnit.KILOMETERS)

九甚纲、function_score自定義文檔相關(guān)性

9.1 什么是function_score?

在使用ES進(jìn)行全文搜索時(shí)朦前,搜索結(jié)果默認(rèn)會(huì)以文檔的相關(guān)度進(jìn)行排序贩疙,而這個(gè) "文檔的相關(guān)度",是可以通過 function_score 自己定義的况既,也就是說(shuō)我們可以透過使用function_score,來(lái)控制 "怎么樣的文檔相關(guān)度更高" 這件事

9.2 文檔相關(guān)度評(píng)分默認(rèn)大概規(guī)則

1组民、關(guān)鍵詞詞頻越高棒仍,評(píng)分越高
2、關(guān)鍵詞在所有文檔中出現(xiàn)的頻率越高臭胜,評(píng)分越低
3莫其、搜索的關(guān)鍵詞與目標(biāo)文檔中分詞匹配個(gè)數(shù)越多,評(píng)分越高
4耸三、匹配的字段權(quán)重越高乱陡,評(píng)分越高

9.3 function_score的基本用法

9.3.1 function_score提供的加強(qiáng)_score的函數(shù)

1、weight:設(shè)置權(quán)重提升值仪壮,可以用于任何查詢
2憨颠、field_value_factor: 將某個(gè)字段的值乘上old_score
3、random_score : 為每個(gè)用戶都使用一個(gè)不同的隨機(jī)評(píng)分對(duì)結(jié)果排序积锅,但對(duì)某一具體用戶來(lái)說(shuō)爽彤,看到的順序始終是一致的
4、衰減函數(shù) (linear缚陷、exp适篙、guass) : 以某個(gè)字段的值為基準(zhǔn),距離某個(gè)值越近得分越高
5箫爷、script_score : 當(dāng)需求超出以上范圍時(shí)嚷节,可以用自定義腳本完全控制評(píng)分計(jì)算,不過因?yàn)檫€要額外維護(hù)腳本不好維護(hù)虎锚,因此盡量使用ES提供的評(píng)分函數(shù)硫痰,需求真的無(wú)法滿足再使用script_score

image.png

9.3.2 function_score其他輔助的參數(shù)

boost_mode
決定 old_score 和 加強(qiáng)score 如何合并
可選值:

multiply(默認(rèn)) : new_score = old_score * 加強(qiáng)score
sum : new_score = old_score + 加強(qiáng)score
min : old_score 和 加強(qiáng)score 取較小值,new_score = min(old_score, 加強(qiáng)score)
max : old_score 和 加強(qiáng)score 取較大值翁都,new_score = max(old_score, 加強(qiáng)score)
replace : 加強(qiáng)score直接替換掉old_score碍论,new_score = 加強(qiáng)score

score_mode

決定functions里面的加強(qiáng)score們?cè)趺春喜ⅲ瑫?huì)先合并加強(qiáng)score們成一個(gè)總加強(qiáng)score柄慰,再使用總加強(qiáng)score去和old_score做合并鳍悠,換言之就是會(huì)先執(zhí)行score_mode税娜,再執(zhí)行boost_mode

可選值:

multiply (默認(rèn)):將所有加強(qiáng)score相乘
sum:求和
avg:取平均值
first : 使加強(qiáng)首個(gè)函數(shù)(可以有filter,也可以沒有)的結(jié)果作為最終結(jié)果
max:取最大值
min:取最小值

max_boost
限制加強(qiáng)函數(shù)的最大效果藏研,就是限制加強(qiáng)score最大能多少敬矩,但要注意不會(huì)限制old_score

9.3.3 function_score語(yǔ)法
單加強(qiáng)函數(shù)語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
    "query": {
        "function_score": {
            //主查詢,查詢完后這裡自己會(huì)有一個(gè)評(píng)分蠢挡,就是old_score
            "query": {.....}, 
            //在old_score的基礎(chǔ)上弧岳,給他加強(qiáng)其他字段的評(píng)分,這裡會(huì)產(chǎn)生一個(gè)加強(qiáng)score业踏,如果只有一個(gè)加強(qiáng)function時(shí)禽炬,直接將加強(qiáng)函數(shù)名寫在query下面就可以了
            "field_value_factor": {...}, 
            //指定用哪種方式結(jié)合old_score和加強(qiáng)score成為new_score
            "boost_mode": "multiply", 
            //限制加強(qiáng)score的最高分,但是不會(huì)限制old_score
            "max_boost": 1.5 
        }
    }
}

多加強(qiáng)函數(shù)語(yǔ)法

GET /索引庫(kù)/映射類型/_search
{
    "query": {
        "function_score": {
            "query": {.....},
            "functions": [
                //可以有多個(gè)加強(qiáng)函數(shù)(或是filter+加強(qiáng)函數(shù))勤家,每一個(gè)加強(qiáng)函數(shù)會(huì)產(chǎn)生一個(gè)加強(qiáng)score腹尖,因此functions會(huì)有多個(gè)加強(qiáng)score
                { "field_value_factor": ... },
                { "gauss": ... },
                { "filter": {...}, "weight": ... }
            ],
            //決定加強(qiáng)score們?cè)趺春喜?            "score_mode": "sum", 
            //決定總加強(qiáng)score怎么和old_score合并
            "boost_mode": "multiply" 
        }
    }
}

weight加強(qiáng)函數(shù)用法

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"filter": {
          "range": {
            "price": {
              "gte": 1000,
              "lte": 3000
            }
          }
        }, "weight": 3}
      ],
      "boost_mode": "sum"
    }
  }
}

解析:查詢所有文檔,如果某個(gè)文檔的價(jià)格在1000~3000范圍內(nèi)伐脖,文檔評(píng)分就會(huì)*3热幔,并且new_score會(huì)和old_score相加得到最終評(píng)分

image.png

random_score加強(qiáng)函數(shù)使用案例

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"random_score": {
          "seed": 2
        }}
      ]
    }
  }
}

解析:不同的用戶,可以設(shè)置不同的seed值(比如用戶的id號(hào))讼庇,實(shí)現(xiàn)隨機(jī)排序的效果绎巨,但是對(duì)同一個(gè)用戶排序結(jié)果又是恒定的

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"field_value_factor": {
          "field": "price"
        }}
      ]
    }
  }
}

解析:查詢所有文檔,并且將所有文檔的old_score蠕啄,乘以本身的價(jià)格场勤,得到new_score,默認(rèn)將new_score * old_score介汹,得到最終評(píng)分

9.3.4 衰減函數(shù)評(píng)分

什么是衰減函數(shù)却嗡?

以某一個(gè)范圍為基準(zhǔn),距離這個(gè)范圍越遠(yuǎn)嘹承,評(píng)分越低窗价。 比如以100為基準(zhǔn),那么大于100叹卷,或者小于100評(píng)分都將變得越來(lái)越低撼港。

為什么需要衰減函數(shù)?

在一次搜索中骤竹,某個(gè)條件并不一定是線性增長(zhǎng)或者遞減來(lái)影響最終結(jié)果評(píng)分的帝牡。比如搜索商品,并不是價(jià)格越低就意味著越好蒙揣,用戶就會(huì)越感興趣靶溜,往往可能維系在一個(gè)價(jià)格區(qū)間的用戶會(huì)更感興趣一些。比如原來(lái)做過一個(gè)調(diào)查,如果買車大概會(huì)買什么價(jià)位的罩息,最后有40%的人選擇的是10~15w之間的車型嗤详。所以我們會(huì)發(fā)現(xiàn),價(jià)格這個(gè)因素瓷炮,對(duì)用戶來(lái)說(shuō)并不是越高越好葱色,同時(shí)也不意味著越低越好,而衰減函數(shù)就是為了對(duì)這一類的數(shù)據(jù)進(jìn)行評(píng)分的

衰減函數(shù)的分類

linear娘香、exp 和 gauss苍狰,三種衰減函數(shù)的差別只在于衰減曲線的形狀,在DSL的語(yǔ)法上的用法完全一樣
linear : 線性函數(shù)是條直線烘绽,一旦直線與橫軸相交淋昭,所有其他值的評(píng)分都是0
exp : 指數(shù)函數(shù)是先劇烈衰減然后變緩
gauss(最常用) : 高斯函數(shù)則是鐘形的,他的衰減速率是先緩慢安接,然后變快响牛,最后又放緩
field_value_factor加強(qiáng)函數(shù)使用案例

image.png

衰減函數(shù)的支持參數(shù)

origin : 中心點(diǎn),或是字段可能的最佳值赫段,落在原點(diǎn)(origin)上的文檔評(píng)分_score為滿分1.0,支持?jǐn)?shù)值矢赁、時(shí)間 以及 "經(jīng)緯度地理座標(biāo)點(diǎn)"等類型字段 _
offset : 從 origin 為中心糯笙,為他設(shè)置一個(gè)偏移量offset覆蓋一個(gè)范圍,在此范圍內(nèi)所有的評(píng)分_score也都是和origin一樣滿分1.0
scale : 衰減率撩银,即是一個(gè)文檔從origin下落時(shí)给涕,_score改變的速度

衰減函數(shù)案例

GET /mytest/doc/_search
{
    "query": {
        "function_score": {
            "functions": [
                //第一個(gè)gauss加強(qiáng)函數(shù),決定距離的衰減率
                {
                    "gauss": {
                        "location": {
                            "origin": {  //origin點(diǎn)設(shè)成酒店的經(jīng)緯度座標(biāo)
                                "lat": 51.5,
                                "lon": 0.12
                            },
                            "offset": "2km", //距離中心點(diǎn)2km以內(nèi)都是滿分1.0额获,2km外開始衰減
                            "scale": "3km"  //衰減率
                        }
                    }
                },
                //第二個(gè)gauss加強(qiáng)函數(shù)够庙,決定價(jià)格的衰減率,因?yàn)橛脩魧?duì)價(jià)格更敏感抄邀,所以給了這個(gè)gauss                      加強(qiáng)函數(shù)2倍的權(quán)重
                {
                    "gauss": {
                        "price": {
                            "origin": "50", 
                            "offset": "50",
                            "scale": "20"
                        }
                    },
                    "weight": 2
                }
            ]
        }
    }
}

JavaAPI設(shè)置評(píng)分

/**
* 自定義評(píng)分 
*/
@Test
public void functionScore() throws IOException {
    List<FunctionScoreQueryBuilder.FilterFunctionBuilder> list 
        = new ArrayList<>();
    list.add(new FunctionScoreQueryBuilder.
        FilterFunctionBuilder(ScoreFunctionBuilders.
            gaussDecayFunction("location", new GeoPoint(22.586203, 114.031687),             "6km", "5km")));

    SearchRequest searchRequest = new SearchRequest("soufang").types("house");
    searchRequest.source().query(
        QueryBuilders.functionScoreQuery(QueryBuilders.matchAllQuery(),                     list.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[0]))
            .boostMode(CombineFunction.REPLACE));

    SearchResponse response = 
        restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    SearchHits hits = response.getHits();
    for (SearchHit hit : hits) {
        System.out.println("查詢結(jié)果:" + hit.getSourceAsString() + " 評(píng)分:" + hit.getScore());
    }
}

例子:
注意:如果price用“scaled_float”會(huì)報(bào)錯(cuò)耘眨。需要改成double


image.png

十、分頁(yè)
任何搜索都可以加兩個(gè)參數(shù)“from”/"size"境肾,相當(dāng)于數(shù)據(jù)庫(kù)的limit ?,?
總條數(shù)在結(jié)果中的“total”參數(shù)中剔难,可以自己運(yùn)算出分頁(yè)數(shù)


image.png

十一、ElasticSearch集群搭建

1奥喻、創(chuàng)建基本目錄/usr/local/es-cluster

image.png

2偶宫、在master/conf/elasticsearch.yml添加如下內(nèi)容

bootstrap.memory_lock: false
cluster.name: "es-cluster"
node.name: es-master
node.master: true
node.data: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.zen.ping.unicast.hosts: ["es-master:9300"]
discovery.zen.minimum_master_nodes: 1

path.logs: /usr/share/elasticsearch/logs
http.cors.enabled: true
http.cors.allow-origin: "*"
xpack.security.audit.enabled: true

3、在node1&node2/conf/elasticsearch.yml添加如下內(nèi)容

cluster.name: "es-cluster"
node.name: node1 #這里注意替換
node.master: false
node.data: true
network.host: 0.0.0.0
http.port: 9202
transport.tcp.port: 9302
discovery.zen.ping.unicast.hosts: ["es-master:9300"]

path.logs: /usr/share/elasticsearch/logs

4环鲤、編寫docker-compose.yml

version: '3.1'
services:
     es-master:
       image:  elasticsearch:6.8.5
       container_name: es-master
       restart: always
       volumes:
         - ./master/data:/usr/share/elasticsearch/data:rw
         - ./master/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
         - ./master/logs:/user/share/elasticsearch/logs:rw
       ports:
         - 9200:9200
         - 9300:9300
       networks:
         - es-network
     es-node1:
       image:  elasticsearch:6.8.5
       container_name: es-node1
       restart: always
       networks:
         - es-network
       volumes:
         - ./node1/data:/usr/share/elasticsearch/data:rw
         - ./node1/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
         - ./node1/logs:/user/share/elasticsearch/logs:rw
     es-node2:
       image:  elasticsearch:6.8.5
       container_name: es-node2
       restart: always
       networks:
         - es-network
       volumes:
         - ./node2/data:/usr/share/elasticsearch/data:rw
         - ./node2/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
         - ./node2/logs:/user/share/elasticsearch/logs:rw
     es-head:
       image: mobz/elasticsearch-head:5
       container_name: es-head
       restart: always
       ports:
         - 9100:9100
       networks:
         - es-network
     kibana:
       image: kibana:6.8.5
       restart: always
       container_name: kibana 
       environment:
         SERVER_NAME: kibana
         ELASTICSEARCH_URL: http://192.168.195.135:9200
       ports:
         - 5601:5601
       networks:
         - es-network
networks:
  es-network:

5纯趋、啟動(dòng)docker-compose.yml

#授權(quán)
chmod 777 -R master node1 node2

#啟動(dòng)
docker-compose up -d

安裝中遇到問題

問題:max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
解決:在宿主機(jī)執(zhí)行sysctl -w vm.max_map_count=262144,重啟docker容器

十二、綜合使用

12.1吵冒、案例1纯命、


image.png

image.png

代碼實(shí)現(xiàn)

/**
     * 通過查詢條件,執(zhí)行相應(yīng)的查詢
     * @param searchParams
     * @return
     * @throws IOException
     */
    @Override
    public List<Hotal> query(SearchParams searchParams) throws IOException {
        //構(gòu)建查詢構(gòu)建器

        //城市查詢
        TermQueryBuilder cityQuery = QueryBuilders.termQuery("cityname", searchParams.getCityName());
        //通過關(guān)鍵詞匹配多個(gè)字段
        QueryBuilder keywordQuery = null;
        if (StringUtils.isNotEmpty(searchParams.getKeyword())) {
            //用戶關(guān)鍵字不為空
            keywordQuery = QueryBuilders
                    .multiMatchQuery(searchParams.getKeyword())
                    .field("hotalName").boost(2)
                    .field("brand").boost(2)
                    .field("regid")
                    .field("keyword")
                    .field("hotalInfo");
        } else {
            keywordQuery = cityQuery;
        }
        //通過價(jià)格限制條件查詢
        RangeQueryBuilder priceQuery = QueryBuilders.rangeQuery("price")
                .gte(searchParams.getMinPirce() != null ? searchParams.getMinPirce().doubleValue() : 0)
                .lte(searchParams.getMaxPirce() != null ? searchParams.getMaxPirce().doubleValue() : Integer.MAX_VALUE);

        //bool查詢將以上兩個(gè)查詢整合
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(keywordQuery)
                .must(priceQuery);

        //降級(jí)查詢 - 符合這個(gè)條件的文檔桦锄,評(píng)分會(huì)降低
        //bool對(duì)城市查詢?nèi)》?        BoolQueryBuilder boolQuery2 = QueryBuilders.boolQuery().mustNot(cityQuery);
        //使用boosting查詢將以上兩個(gè)bool查詢組合起來(lái)
        BoostingQueryBuilder execQuery = QueryBuilders.boostingQuery(
                boolQuery,
                boolQuery2
        ).negativeBoost(0.2f);
        return execQuery;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載扎附,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末结耀,一起剝皮案震驚了整個(gè)濱河市留夜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌图甜,老刑警劉巖碍粥,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異黑毅,居然都是意外死亡嚼摩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門矿瘦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)枕面,“玉大人,你說(shuō)我怎么就攤上這事缚去〕泵兀” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵易结,是天一觀的道長(zhǎng)枕荞。 經(jīng)常有香客問我,道長(zhǎng)搞动,這世上最難降的妖魔是什么躏精? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鹦肿,結(jié)果婚禮上矗烛,老公的妹妹穿的比我還像新娘。我一直安慰自己箩溃,他們只是感情好高诺,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碾篡,像睡著了一般虱而。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上开泽,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天牡拇,我揣著相機(jī)與錄音,去河邊找鬼。 笑死惠呼,一個(gè)胖子當(dāng)著我的面吹牛导俘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播剔蹋,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼旅薄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了泣崩?” 一聲冷哼從身側(cè)響起少梁,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎矫付,沒想到半個(gè)月后凯沪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡买优,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年妨马,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杀赢。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烘跺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脂崔,到底是詐尸還是另有隱情液荸,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布脱篙,位于F島的核電站,受9級(jí)特大地震影響栅迄,放射性物質(zhì)發(fā)生泄漏掸掏。R本人自食惡果不足惜寞秃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秤朗。 院中可真熱鬧,春花似錦笔喉、人聲如沸取视。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)作谭。三九已至,卻和暖如春奄毡,著一層夾襖步出監(jiān)牢的瞬間折欠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锐秦,地道東北人咪奖。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像酱床,于是被迫代替她去往敵國(guó)和親羊赵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345