Elasticsearch系列---幾個高級功能

概要

本篇主要介紹一下搜索模板、映射模板、高亮搜索和地理位置的簡單玩法。

標準搜索模板

搜索模板search tempalte高級功能之一,可以將我們的一些搜索進行模板化,使用現有模板時傳入指定的參數就可以了,避免編寫重複代碼。對常用的功能可以利用模板進行封裝,使用時更簡便。

這點類似於我們編程時的接口封裝,將一些細節處理的東西封裝成接口,供別人調用,使用者就只需要關注參數和響應結果就行,這樣可以更好地提高代碼複用率。

下面我們來看看最基本的幾種用法

參數替換

GET /music/children/_search/template
{
  "source": {
    "query": {
      "match": {
        "{{field}}":"{{value}}"
      }
    }
  },
  "params": {
    "field":"name",
    "value":"bye-bye"
  }
}

該搜索模板編譯後等同於:

GET /music/children/_search
{
  "query": {
    "match": {
      "name":"bye-bye"
    }
  }
}

使用Json格式的條件查詢

{{#toJson}}塊內可以寫稍微複雜一些的條件

GET /music/children/_search/template
{
  "source": "{\"query\":{\"match\": {{#toJson}}condition{{/toJson}}}}",
  "params": {
    "condition": {
      "name":"bye-bye"
    }
  }
}

該搜索模板編譯後等同於如下:

GET /music/children/_search
{
  "query": {
    "match": {
      "name":"bye-bye"
    }
  }
}

join語法

join內的參數names可以寫多個:

GET /music/children/_search/template
{
  "source": {
    "query": {
      "match": {
        "name": "{{#join delimiter=' '}}names{{/join delimiter=' '}}"
      }
    }
  },
  "params": {
    "name":["gymbo","you are my sunshine","bye-bye"]
  }
}

該搜索模板編譯後等同於如下:

GET /music/children/_search
{
  "query": {
    "match": {
      "name":"gymbo you are my sunshine bye-bye"
    }
  }
}

搜索模板的默認值設置

可以對搜索模板進行一些默認值的設置,如{{^end}}500表示如果end參數爲空,默認值爲500

GET /music/children/_search/template
{
  "source":{
    "query":{
      "range":{
        "likes":{
          "gte":"{{start}}",
          "lte":"{{end}}{{^end}}500{{/end}}"
        }
      }
    }
  },
  "params": {
    "start":1,
    "end":300
  }
}

該搜索模板編譯後等同於:

GET /music/children/_search
{
  "query": {
    "range": {
      "likes": {
        "gte": 1,
        "lte": 300
      }
    }
  }
}

條件判斷

在Mustache語言中,它沒有if/else這樣的判斷,但是你可以定section來跳過它如果那個變量是false還是沒有被定義

{{#param1}}
    "This section is skipped if param1 is null or false"
{{/param1}}

示例:創建mustache scripts對象

POST _scripts/condition
{
  "script": {
    "lang": "mustache",
    "source": 
    """
        {
        	"query": {
              "bool": {
                "must": {
                  "match": {
                    "name": "{{name}}"
                  }
                },
                "filter":{
                  {{#isLike}}
                    "range":{
                      "likes":{
                        {{#start}}
                          "gte":"{{start}}"
                          {{#end}},{{/end}}
                        {{/start}}
                        {{#end}}
                          "lte":"{{end}}"
                        {{/end}}
                      }
                    }
                  {{/isLike}}
                }
              }
            }
        }
    """
  }
}

使用mustache template查詢:

GET _search/template
{
    "id": "condition", 
    "params": {
      "name":"gymbo",
      "isLike":true,
      "start":1,
      "end":500
    }
}

以上是常用的幾種搜索模板介紹,如果在大型項目,並且配置了專門的Elasticsearch工程師,就經常會用一些通用的功能進行模板化,開發業務系統的童鞋只需要使用模板即可。

定製映射模板

ES有自己的規則對插入的數據進行類型映射,如10,會自動映射成long類型,"10"會自動映射成text,還會自帶一個keyword的內置field。方便是很方便,但有時候這些類型不是我們想要的,比如我們的整數值10,我們期望是這個integer類型,"10"我們希望是keyword類型,這時候我們可以預先定義一個模板,插入數據時,相關的field就按我們預先定義的規則進行匹配,決定這個field值的類型。

另外要聲明一下,實際工作中編碼規範一般嚴謹一些,所有的document都是預先定義好類型再執行數據插入的,哪怕是中途增加的field,也是先執行mapping命令,再插入數據的。

但自定義動態映射模板也需要了解一下。

默認的動態映射效果

試着插入一條數據:

PUT /test_index/type/1
{
  "test_string":"hello kitty",
  "test_number":10
}

查看mapping信息

GET /test_index/_mapping/type

響應如下:

{
  "test_index": {
    "mappings": {
      "type": {
        "properties": {
          "test_number": {
            "type": "long"
          },
          "test_string": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

默認的動態映射規則,可能不是我們想要的。

例如,我們希望數字類型的默認是integer類型,字符串默認是string類型,但是內置的field名字叫raw,不叫keyword,保留128個字符。

動態映射模板

有兩種方式:

  1. 根據新加入的field的默認的數據類型,來進行匹配,匹配某個預定義的模板
  2. 根據新加入的field的名字,去匹配預定義的名字,或者去匹配一個預定義的通配符,然後匹配上某個預定義的模板
根據數據類型進行匹配
PUT /test_index
{
  "mappings": {
    "type": {
      "dynamic_templates": [
        {
          "integers" : {
            "match_mapping_type": "long",
            "mapping": {
              "type":"integer"
            }
          }
        },
        {
          "strings" : {
            "match_mapping_type": "string",
            "mapping": {
              "type":"text",
              "fields": {
                "raw": {
                  "type": "keyword",
                  "ignore_above": 128
                }
              }
            }
          }
        }
      ]
    }
  }
}

刪除索引,重新插入數據,查看mapping信息如下:

{
  "test_index": {
    "mappings": {
      "type": {
        "dynamic_templates": [
          {
            "integers": {
              "match_mapping_type": "long",
              "mapping": {
                "type": "integer"
              }
            }
          },
          {
            "strings": {
              "match_mapping_type": "string",
              "mapping": {
                "fields": {
                  "raw": {
                    "ignore_above": 128,
                    "type": "keyword"
                  }
                },
                "type": "text"
              }
            }
          }
        ],
        "properties": {
          "test_number": {
            "type": "integer"
          },
          "test_string": {
            "type": "text",
            "fields": {
              "raw": {
                "type": "keyword",
                "ignore_above": 128
              }
            }
          }
        }
      }
    }
  }
}

以按預計類型進行映射,符合預期。

  • 按field名稱進行映射
  • "long_"開頭的field,並且原本是long類型的,轉換爲integer類型
  • "string_"開頭的field,並且原本是string類型的,轉換爲string.raw類型
    "_text"結尾的field,並且原本是string類型的,保持不變
PUT /test_index
{
  "mappings": {
    "type": {
      "dynamic_templates":[
       {
         "long_as_integer": {
	         "match_mapping_type":"long",
           "match": "long_*",
           "mapping":{
             "type":"integer"
           }
         }
       },
       {
         "string_as_raw": {
	         "match_mapping_type":"string",
           "match": "string_*",
           "unmatch":"*_text",
           "mapping": {
              "type":"text",
              "fields": {
                "raw": {
                  "type": "keyword",
                  "ignore_above": 128
                }
              }
            }
         }
       }
      ]
    }
  }
}

插入數據:

PUT /test_index/type/1
{
  "string_test":"hello kitty",
  "long_test": 10,
  "title_text":"Hello everyone"
}

查詢mapping信息

{
  "test_index": {
    "mappings": {
      "type": {
        "dynamic_templates": [
          {
            "long_as_integer": {
              "match": "long_*",
              "match_mapping_type": "long",
              "mapping": {
                "type": "integer"
              }
            }
          },
          {
            "string_as_raw": {
              "match": "string_*",
              "unmatch": "*_text",
              "match_mapping_type": "string",
              "mapping": {
                "fields": {
                  "raw": {
                    "ignore_above": 128,
                    "type": "keyword"
                  }
                },
                "type": "text"
              }
            }
          }
        ],
        "properties": {
          "long_test": {
            "type": "integer"
          },
          "string_test": {
            "type": "text",
            "fields": {
              "raw": {
                "type": "keyword",
                "ignore_above": 128
              }
            }
          },
          "title_text": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

結果符合預期。

在某些日誌管理的場景中,我們可以定義好type,每天按日期創建一個索引,這種索引的創建就可以用到映射模板,把我們定義的映射關係全部做進去。

高亮搜索

我們在瀏覽器上搜索文本時,發現我們輸入的關鍵字有高亮顯示,查看html源碼就知道,高亮的部分是加了<em>標籤的,ES也支持高亮搜索這種操作的,並且在返回的文檔中自動加了<em>標籤,兼容html5頁面。

highlight基本語法

我們還是以音樂網站爲案例,開始進行高亮搜索:

GET /music/children/_search 
{
  "query": {
    "match": {
      "content": "love"
    }
  },
  "highlight": {
    "fields": {
      "content": {}
    }
  }
}

highlight裏面的參數即爲高亮搜索的語法,指定高亮的字段爲content,我們可以看到命中的Love裏面帶了<em>高亮標籤,<em></em>表現在html上會變成紅色,所以說你的指定的field中,如果包含了那個搜索詞的話,就會在那個field的文本中,對搜索詞進行紅色的高亮顯示。

{
  "took": 35,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "music",
        "_type": "children",
        "_id": "5",
        "_score": 0.2876821,
        "_source": {
          "id": "1740e61c-63da-474f-9058-c2ab3c4f0b0a",
          "author_first_name": "Jean",
          "author_last_name": "Ritchie",
          "author": "Jean Ritchie",
          "name": "love somebody",
          "content": "love somebody, yes I do",
          "language": "english",
          "tags": "love",
          "length": 38,
          "likes": 3,
          "isRelease": true,
          "releaseDate": "2019-12-22"
        },
        "highlight": {
          "content": [
            "<em>love</em> somebody, yes I do"
          ]
        }
      }
    ]
  }
}

highlight下的字段可以指定多個,這樣就可以在多個字段命中的關鍵詞進行高亮顯示,例如:

GET /music/children/_search 
{
  "query": {
    "match": {
      "content": "love"
    }
  },
  "highlight": {
    "fields": {
      "name":{},
      "content": {}
    }
  }
}

三種高亮語法

有三種高亮的語法:

  1. plain highlight:使用standard Lucene highlighter,對簡單的查詢支持度非常好。
  2. unified highlight:默認的高亮語法,使用Lucene Unified Highlighter,將文本切分成句子,並對句子使用BM25計算詞條的score,支持精準查詢和模糊查詢。
  3. fast vector highlighter:使用Lucene Fast Vector highlighter,功能很強大,如果在mapping中對field開啓了term_vector,並設置了with_positions_offsets,就會使用該highlighter,對內容特別長的文本(大於1MB)有性能上的優勢。

例如:

PUT /music
{
  "mappings": {
    "children": {
      "properties": {
        "name": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "term_vector" : "with_positions_offsets"
        }
      }
    }
  }
}

一般情況下,用plain highlight也就足夠了,不需要做其他額外的設置
如果對高亮的性能要求很高,可以嘗試啓用unified highlight
如果field的值特別大,超過了1M,那麼可以用fast vector highlight

自定義高亮html標籤

我們知道高亮的默認標籤是<em>,這個標籤可以自己定義的,然後使用自己喜歡的樣式:

GET /music/children/_search 
{
  "query": {
    "match": {
      "content": "Love"
    }
  },
  "highlight": {
    "pre_tags": ["<tag1>"],
    "post_tags": ["</tag2>"], 
    "fields": {
      "content": {
        "type": "plain"
      }
    }
  }
}

高亮片段fragment的設置

針對一些很長的文本,我們不可能在頁面上完整顯示的,我們需要只顯示有關鍵詞的上下文即可,這裏設置fragment就行:

GET /_search
{
    "query" : {
        "match": { "content": "friend" }
    },
    "highlight" : {
        "fields" : {
            "content" : {"fragment_size" : 150, "number_of_fragments" : 3, "no_match_size": 150 }
        }
    }
}

fragment_size: 設置要顯示出來的fragment文本判斷的長度,默認是100。

number_of_fragments:你可能你的高亮的fragment文本片段有多個片段,你可以指定就顯示幾個片段。

地理位置

現在基於地理位置的app層出不窮,支持地理位置的組件也有不少,Elasticsearch也不例外,並且ES可以把地理位置、全文搜索、結構化搜索和分析結合到一起,我們來看一下。

geo point數據類型

Elasticsearch基於地理位置的搜索,有一個專門的對象geo_point存儲地理位置信息(經度,緯度),並且提供了一些基本的查詢方法,如geo_bounding_box。

建立geo_point類型的mapping

PUT /location
{
  "mappings": {
    "hotels": {
      "properties": {
        "location": {
          "type": "geo_point"
        },
        "content": {
          "type": "text"
        }
      }
    }
  }
}
插入數據

推薦使用如下插入數據方式:

#latitude:維度,longitude:經度
PUT /location/hotels/1
{
  "content":"7days hotel",
  "location": {
    "lon": 113.928619,
    "lat": 22.528091
  }
}

還有兩種插入數據的方式,但特別容易搞混經緯度的位置,所以不是很推薦:

# location中括號內,前一個是經度,後一個是緯度
PUT /location/hotels/2
{
  "content":"7days hotel ",
  "location": [113.923567,22.523988]
}

# location中,前一個是緯度,後一個是經度
PUT /location/hotels/3
{
  "text": "7days hotel Orient Sunseed Hotel",
  "location": "22.521184, 113.914578" 
}
查詢方法

geo_bounding_box查詢,查詢某個矩形的地理位置範圍內的座標點

GET /location/hotels/_search
{
  "query": {
     "geo_bounding_box": {
      "location": {
        "top_left":{
          "lon": 112,
          "lat": 23
        },
        "bottom_right":{
          "lon": 114,
          "lat": 21
        }
      }
    } 
  }
}

常見查詢場景

geo_bounding_box方式
GET /location/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_all": {}}
      ],
      "filter": {
        "geo_bounding_box": {
          "location": {
            "top_left":{
              "lon": 112,
              "lat": 23
            },
            "bottom_right":{
              "lon": 114,
              "lat": 21
            }
          }
        }
      }
    }
  }
}
geo_polygon方式,三個點組成的多邊形(三角形)區域

支持多邊形,只是這個過濾器使用代價很大,儘量少用。

GET /location/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_all": {}}
      ],
      "filter": {
        "geo_polygon": {
          "location": {
            "points": [
              {"lon": 115,"lat": 23},
              {"lon": 113,"lat": 25},
              {"lon": 112,"lat": 21}
            ]
          }
        }
      }
    }
  }
}
geo_distance方式

根據當前位置的距離進行搜索,非常實用

GET /location/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_all": {}}
      ],
      "filter": {
        "geo_distance": {
          "distance": 500, 
          "location": {
            "lon": 113.911231,
            "lat": 22.523375
          }
        }
      }
    }
  }
}
按距離排序

根據當前位置進行條件搜索,會指定一個距離的上限,2km或5km,並且符合條件查詢的結果顯示與當前位置的距離(可以指定單位),並且按從近到遠排序,這個是非常常用的場景。

請求示例:

GET /location/hotels/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_all": {}}
      ],
      "filter": {
        "geo_distance": {
          "distance": 2000, 
          "location": {
            "lon": 113.911231,
            "lat": 22.523375
          }
        }
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { 
          "lon": 113.911231,
          "lat": 22.523375
        },
        "order":         "asc",
        "unit":          "m", 
        "distance_type": "plane" 
      }
    }
  ]
}
  • filter.geo_distance.distance: 最大的距離,這裏是2000m
  • _geo_distance: 固定寫法,下面爲指定位置的經緯度
  • order: 排序方式,asc或desc
  • unit: 距離的單位,m/km都行
  • distance_type: 計算距離的方式,sloppy_arc (默認值), arc (精準的) and plane (最快速的)

響應如下:

"hits": [
      {
        "_index": "location",
        "_type": "hotels",
        "_id": "3",
        "_score": null,
        "_source": {
          "text": "7days hotel Orient Sunseed Hotel",
          "location": "22.521184, 113.914578"
        },
        "sort": [
          421.35435857277366
        ]
      },
      {
        "_index": "location",
        "_type": "hotels",
        "_id": "2",
        "_score": null,
        "_source": {
          "content": "7days hotel",
          "location": [
            113.923567,
            22.523988
          ]
        },
        "sort": [
          1268.8952707727062
        ]
      }

sort裏面的內容,就是與當前位置的地面距離,單位是m。

統計我當前位置幾個範圍內酒店的數量

unit表示距離單位,常用的是mi和km。

distance_type表示計算距離的方式,sloppy_arc (默認值), arc (精準的) and plane (最快速的)。

GET /location/hotels/_search
{
  "size": 0,
  "aggs": {
    "group_by_distance": {
      "geo_distance": {
        "field": "location",
        "origin": {
          "lon": 113.911231,
          "lat": 22.523375
        },
        "unit": "mi", 
        "distance_type": "arc", 
        "ranges": [
          {"from": 0,"to": 500},
          {"from": 500,"to": 1500},
          {"from": 150,"to": 2000}
        ]
      }
    }
  }
}

小結

本篇簡單介紹了一下搜索模板、映射模板、高亮搜索和地理位置的簡單玩法,有些ES相關的項目做得比較深的,搜索模板和映射模板用處還是很大的。高亮搜索一般體現在瀏覽器搜索引擎上,地理位置的應用挺有意思,也可以參與到基於Location的APP應用當中。

專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術
Java架構社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章