Elasticsearch地理位置總結


更多內容請參考 : https://www.felayman.com 
翻譯版本:https://es.xiaoleilu.com/310_Geopoints/00_Intro.html 
官方原文:https://www.elastic.co/guide/en/elasticsearch/guide/current/geoloc.html

本文只是針對這些內容通過具體的例子用java來實現其具體細節,如果只想看java實現部分,請直接往下面代碼實現部分看

地理坐標點(geo-point) 是指地球表面可以用經緯度描述的一個點。地理坐標點可以用來計算兩個坐標位置間的距離,或者判斷一個點是否在一個區域中。地理坐標點不能被動態映射(dynamic mapping)自動檢測,而是需要顯式聲明對應字段類型為 geo_point 。

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type": "geo_point"
        }
      }
    }
  }
}

如上例,location 被聲明為 geo_point 后,我們就可以索引包含了經緯度信息的文檔了。經緯度信息的形式可以是字符串,數組或者對象。

PUT /attractions/restaurant/1
{
  "name":     "Chipotle Mexican Grill",
  "location": "40.715, -74.011" <1>
}

PUT /attractions/restaurant/2
{
  "name":     "Pala Pizza",
  "location": { <2>
    "lat":     40.722,
    "lon":    -73.989
  }
}

PUT /attractions/restaurant/3
{
  "name":     "Mini Munchies Pizza",
  "location": [ -73.983, 40.719 ] <3>
}
  • <1> 以半角逗號分割的字符串形式 “lat,lon”;
  • <2> 明確以 lat 和 lon 作為屬性的對象;
  • <3> 數組形式表示 [lon,lat]。

注意 : 
可能所有人都至少踩過一次這個坑:地理坐標點用字符串形式表示時是緯度在前,經度在后(”latitude,longitude”),而數組形式表示時剛好相反,是經度在前,緯度在后([longitude,latitude])。其實,在 Elasticesearch 內部,不管字符串形式還是數組形式,都是緯度在前,經度在后。不過早期為了適配 GeoJSON 的格式規范,調整了數組形式的表示方式。因此,在使用地理位置(geolocation)的路上就出現了這么一個“捕熊器”,專坑那些不了解這個陷阱的使用者。

通過地理坐標點過濾

有四種地理坐標點相關的過濾方式可以用來選中或者排除文檔:

  • geo_bounding_box:: 
    找出落在指定矩形框中的坐標點
  • geo_distance:: 
    找出與指定位置在給定距離內的點
  • geo_distance_range:: 
    找出與指定點距離在給定最小距離和最大距離之間的點
  • geo_polygon:: 
    找出落在多邊形中的點。這個過濾器使用代價很大。當你覺得自己需要使用它,最好先看看 geo-shapes

所有這些過濾器的工作方式都相似: 把 索引中所有文檔(而不僅僅是查詢中匹配到的部分文檔,見 fielddata-intro)的經緯度信息都載入內存,然后每個過濾器執行一個輕量級的計算去判斷當前點是否落在指定區域。 
提示

地理坐標過濾器使用代價昂貴 —— 所以最好在文檔集合盡可能少的場景使用。 你可以先使用那些簡單快捷的過濾器,比如 term 或者 range,來過濾掉盡可能多的文檔,最后才交給地理坐標過濾器處理。

布爾型過濾器(bool filter)會自動幫你做這件事。 它會優先讓那些基於“bitset”的簡單過濾器(見 filter-caching)來過濾掉盡可能多的文檔,然后依次才是地理坐標過濾器或者腳本類的過濾器。

地理坐標盒模型過濾器

這是目前為止最有效的 地理坐標過濾器了,因為它計算起來非常簡單。 你指定一個矩形的 頂部(top), 底部(bottom), 左邊界(left), 和 右邊界(right), 然后它只需判斷坐標的經度是否在左右邊界之間,緯度是否在上下邊界之間。(譯注:原文似乎寫反了)

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "location": { <1>
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.7,
              "lon": -73.0
            }
          }
        }
      }
    }
  }
}
  • <1> 盒模型信息也可以用 bottom_left(左下方點)和 top_right(右上方點) 來表示。

優化盒模型

地理坐標盒模型過濾器不需要把所有坐標點都加載到內存里。 因為它要做的只是簡單判斷 緯度 和 經度 坐標數值是否在給定的范圍內,所以它可以用倒排索引來做一個范圍(range)過濾。 
要使用這種優化方式,需要把 geo_point 字段用 緯度(lat)和經度(lon)方式表示並分別索引。

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type":    "geo_point",
          "lat_lon": true <1>
        }
      }
    }
  }
}
  • <1> location.lat 和 location.lon 字段將被分別索引。它們可以被用於檢索,但是不會在檢索結果中返回。

然后,查詢時你需要告訴 Elasticesearch 使用已索引的 lat和lon。

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "type":    "indexed", <1>
          "location": {
            "top_left": {
              "lat":  40.8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.7,
              "lon":  -73.0
            }
          }
        }
      }
    }
  }
}

<1> 設置 type 參數為 indexed (默認為 memory) 來明確告訴 Elasticsearch 對這個過濾器使用倒排索引。 

注意:geo_point 類型可以包含多個地理坐標點,但是針對經度緯度分別索引的這種優化方式(lat_lon)只對單個坐標點的方式有效。

地理距離過濾器

地理距離過濾器(geo_distance)以給定位置為圓心畫一個圓,來找出那些位置落在其中的文檔:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance": "1km", <1>
          "location": { <2>
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  • <1> 找出所有與指定點距離在1公里(1km)內的 location 字段。訪問 Distance Units 查看所支持的距離表示單位

  • <2> 中心點可以表示為字符串,數組或者(如示例中的)對象。詳見 lat-lon-formats

地理距離過濾器計算代價昂貴。 為了優化性能,Elasticsearch 先畫一個矩形框(邊長為2倍距離)來圍住整個圓形, 這樣就可以用消耗較少的盒模型計算方式來排除掉那些不在盒子內(自然也不在圓形內)的文檔, 然后只對落在盒模型內的這部分點用地理坐標計算方式處理。

提示 
你需要判斷你的使用場景,是否需要如此精確的使用圓模型來做距離過濾? 通常使用矩形模型是更高效的方式,並且往往也能滿足應用需求。

更快的地理距離計算

兩點間的距離計算,有多種性能換精度的算法:

  • arc:: 
    最慢但是最精確是弧形(arc)計算方式,這種方式把世界當作是球體來處理。 不過這種方式精度還是有限,因為這個世界並不是完全的球體。
  • plane:: 
    平面(plane)計算方式,(((“plane distance calculation”)))把地球當成是平坦的。 這種方式快一些但是精度略遜;在赤道附近位置精度最好,而靠近兩極則變差。
  • sloppy_arc:: 
    如此命名,是因為它使用了 Lucene 的 SloppyMath 類。 這是一種用精度換取速度的計算方式,它使用 Haversine formula 來計算距離; 它比弧形(arc)計算方式快4~5倍, 並且距離精度達99.9%。這也是默認的計算方式。

你可以參考下例來指定不同的計算方式:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance":      "1km",
          "distance_type": "plane", <1>
          "location": {
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  • <1> 使用更快但精度稍差的平面(plane)計算方式。

提示: 你的用戶真的會在意一個賓館落在指定圓形區域數米之外了嗎? 一些地理位置相關的應用會有較高的精度要求;但大部分實際應用場景中,使用精度較低但響應更快的計算方式可能就挺好。

 

地理距離區間過濾器

 

地理距離過濾器(geo_distance)和地理距離區間過濾器(geo_distance_range)的唯一差別在於后者是一個環狀的,它會排除掉落在內圈中的那部分文檔。

 

指定到中心點的距離也可以換一種表示方式: 指定一個最小距離(使用 gt或者gte)和最大距離(使用lt或者lte),就像使用區間(range)過濾器一樣。

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance_range": {
          "gte":    "1km", <1>
          "lt":     "2km", <1>
          "location": {
            "lat":  40.715,
            "lon": -73.988
          }
        }
      }
    }
  }
}
  • 匹配那些距離中心點超過1公里而小於2公里的位置。

緩存地理位置過濾器###

因為如下兩個原因,地理位置過濾器默認是不被緩存的:

  • 地理位置過濾器通常是用於查找用戶當前位置附近的東西。但是用戶是在移動的,並且沒有兩個用戶的位置完全相同,因此緩存的過濾器基本不會被重復使用到。
  • 過濾器是被緩存為比特位集合來表示段(segment)內的文檔。假如我們的查詢排除了幾乎所有文檔,只剩一個保存在這個特別的段內。一個未緩存的地理位置過濾器只需要檢查這一個文檔就行了,但是一個緩存的地理位置過濾器則需要檢查所有在段內的文檔。

緩存對於地理位置過濾器也可以很有效。 假設你的索引里包含了所有美國的賓館。一個在紐約的用戶是不會對舊金山的賓館感興趣的。 所以我們可以認為紐約是一個熱點(hot spot),然后畫一個邊框把它和附近的區域圍起來。

如果這個地理盒模型過濾器(geo_bounding_box)被緩存起來,那么當有位於紐約市的用戶訪問時它就可以被重復使用了。 它可以直接排除國內其它區域的賓館。然后我們使用未緩存的,更加明確的地理盒模型過濾器(geo_bounding_box)或者地理距離過濾器(geo_distance)來在剩下的結果集中把范圍進一步縮小到用戶附近:

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "bool": {
          "must": [
            {
              "geo_bounding_box": {
                "type": "indexed",
                "_cache": true, <1>
                "location": {
                  "top_left": {
                    "lat":  40,8,
                    "lon": -74.1
                  },
                  "bottom_right": {
                    "lat":  40.4,
                    "lon": -73.7
                  }
                }
              }
            },
            {
              "geo_distance": { <2>
                "distance": "1km",
                "location": {
                  "lat":  40.715,
                  "lon": -73.988
                }
              }
            }
          ]
        }
      }
    }
  }
}
  • <1> 緩存的地理盒模型過濾器把結果集縮小到了紐約市。
  • <2> 代價更高的地理距離過濾器(geo_distance)讓結果集縮小到1km內的用戶。

減少內存占用

每一個 經緯度(lat/lon)組合需要占用16個字節的內存。要知道內存可是供不應求的。 使用這種占用16字節內存的方式可以得到非常精確的結果。不過就像之前提到的一樣,實際應用中幾乎都不需要這么精確。 
你可以通過這種方式來減少內存使用量: 設置一個壓縮的(compressed)數據字段格式並明確指定你的地理坐標點所需的精度。 即使只是將精度降低到1毫米(1mm)級別,也可以減少1/3的內存使用。 更實際的,將精度設置到3米(3m)內存占用可以減少62%,而設置到1公里(1km)則節省75%之多。

這個設置項可以通過 update-mapping API 來對實時索引進行調整:

POST /attractions/_mapping/restaurant
{
  "location": {
    "type": "geo_point",
    "fielddata": {
      "format":    "compressed",
      "precision": "1km" <1>
    }
  }
}
  • <1> 每一個經緯度(lat/lon)組合現在只需要4個字節,而不是16個。

另外,你還可以這樣做來避免把所有地理坐標點全部同時加載到內存中: 使用在優化盒模型(optimize-bounding-box)中提到的技術, 或者把地理坐標點當作文檔值(doc values)來存儲。

PUT /attractions
{
  "mappings": {
    "restaurant": {
      "properties": {
        "name": {
          "type": "string"
        },
        "location": {
          "type":       "geo_point",
          "doc_values": true <1>
        }
      }
    }
  }
}
  • <1> 地理坐標點現在不會被加載到內存,而是保存在磁盤中。

將地理坐標點映射為文檔值的方式只能是在這個字段第一次被創建時。 相比使用字段值,使用文檔值會有一些小的性能代價,不過考慮到它對內存的節省,這種方式通常是還值得的。

按距離排序

檢索結果可以按跟指定點的距離排序:

提示 當你可以(can)按距離排序時,按距離打分(scoring-by-distance)通常是一個更好的解決方案。

GET /attractions/restaurant/_search
{
  "query": {
    "filtered": {
      "filter": {
        "geo_bounding_box": {
          "type":       "indexed",
          "location": {
            "top_left": {
              "lat":  40,8,
              "lon": -74.0
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.0
            }
          }
        }
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { <1>
          "lat":  40.715,
          "lon": -73.998
        },
        "order":         "asc",
        "unit":          "km", <2>
        "distance_type": "plane" <3>
      }
    }
  ]
}
  • <1> 計算每個文檔中 location 字段與指定的 lat/lon 點間的距離。
  • <2> 以 公里(km)為單位,將距離設置到每個返回結果的 sort 鍵中。
  • <3> 使用快速但精度略差的平面(plane)計算方式。

你可能想問:為什么要制定距離的單位(unit)呢? 用於排序的話,我們並不關心比較距離的尺度是英里,公里還是光年。 原因是,這個用於排序的值會設置在每個返回結果的 sort 元素中。

...
  "hits": [
     {
        "_index": "attractions",
        "_type": "restaurant",
        "_id": "2",
        "_score": null,
        "_source": {
           "name": "New Malaysia",
           "location": {
              "lat": 40.715,
              "lon": -73.997
           }
        },
        "sort": [
           0.08425653647614346 <1>
        ]
     },
...
  • <1> 賓館距離我們的指定位置距離是 0.084km。
  • 你可以通過設置單位(unit)來讓返回值的形式跟你應用中想要的匹配。

提示 
地理距離排序可以對多個坐標點來使用,不管(這些坐標點)是在文檔中還是排序參數中。 使用 sort_mode 來指定是否需要使用位置集合的 最小(min),最大(max)或者平均(avg)距離。 這樣就可以返回離我的工作地和家最近的朋友這樣的結果了。

按距離打分

有可能距離只是決定返回結果排序的唯一重要因素,不過更常見的情況是距離會和其它因素, 比如全文檢索匹配度,流行程度或者價格一起決定排序結果。

遇到這種場景你需要在查詢分值計算(function_score query)中指定方式讓我們把這些因子處理得到一個綜合分。 decay-functions中有個一個例子就是地理距離影響排序得分的。

另外按距離排序還有個缺點就是性能:需要對每一個匹配到的文檔都進行距離計算。 而 function_score請求,在 rescore phase階段有可能只需要對前 n 個結果進行計算處理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM