vlambda博客
学习文章列表

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

Chapter 7. Spicing Up a Search Using Geo

地理点是指地球上某个点的纬度和经度。它上面的每个位置都有自己的唯一的纬度和经度。 Elasticsearch 知道基于地理的点,并允许您在其上执行各种操作。在许多情况下,还需要考虑地理位置组件来获得各种功能。例如,假设您需要搜索附近所有提供中餐的餐馆,或者我需要找到最近的免费出租车。在其他一些情况下,我需要找到特定地理位置所在的状态,以了解我当前所处的位置。

本章的建模使得所有提到的例子都与现实生活中的场景、餐厅搜索相关,以便更好地理解。在这里,我们以根据地理偏好对餐厅进行排序为例。本章涵盖了从简单的案例(例如查找最近的餐馆)到更复杂的案例(例如基于距离对餐馆进行分类)的许多案例。

Note

Elasticsearch 独特而强大的原因在于,您可以将地理操作与任何其他常规搜索查询结合起来,以产生包含位置数据和查询数据的结果。

Restaurant search


让我们考虑 为餐馆创建一个搜索门户。以下是 其要求:

  • 查找最近的中国菜餐厅,其名称中有ChingYang

  • 降低城市范围以外所有餐厅的重要性。

  • 查找每个先前餐厅匹配的餐厅与当前点之间的距离。

  • 查找此人是否在特定城市的范围内。

  • 聚合10公里范围内的所有餐厅。也就是说,对于前 10 公里的半径,我们必须计算餐馆的数量。对于接下来的 10 公里,我们 需要计算餐馆的数量 等等上。

Data modeling for restaurants


首先,我们需要查看数据的各个方面并围绕 JSON 文档对其进行建模,以便 Elasticsearch 理解数据。餐厅有名称、位置信息和评级。为了存储位置信息,Elasticsearch 提供了了解纬度和经度信息的功能,并具有基于它进行搜索的功能。因此,最好使用此功能。

让我们看看如何做到这一点。

首先,让我们看看我们的文档应该是什么样子:

{
  "name" : "Tamarind restaurant",
  "location" : {
      "lat" : 1.10,
      "lon" : 1.54
  }
}

现在,让我们定义相同的模式:

curl -X PUT "http://$hostname:9200/restaurants" -d '{
    "index": {
        "number_of_shards": 1,
        "number_of_replicas": 1
    },
    "analysis":{     
        "analyzer":{         
            "flat" : {
                "type" : "custom",
                "tokenizer" : "keyword",
                "filter" : "lowercase"
            }
        }
    }
}'

echo
curl -X PUT "http://$hostname:9200/restaurants/restaurant/_mapping" -d '{
    "restaurant" : {
    "properties" : {
        "name" : { "type" : "string"  },
        "location" : { "type" : "geo_point", "accuracy" : "1km" }
    }}

}'

现在让我们 索引索引中的一些文档。上一节中显示的 Tamarind restaurant 数据就是一个例子。我们可以如下索引数据:

curl -XPOST 'http://localhost:9200/restaurants/restaurant' -d '{
    "name": "Tamarind restaurant",
    "location": {
        "lat": 1.1,
        "lon": 1.54
    }
}'

同样,我们可以索引任意数量的文档。为方便起见,本章我们只索引了总共五家餐厅。

纬度和经度应该是这种格式。 Elasticsearch 还接受其他两种格式(geohashlat_lon),但让我们坚持使用这一种。当我们将字段位置映射到类型 geo_point 时,Elasticsearch 知道这些信息的含义以及如何对其采取行动。

The nearest hotel problem


假设我们处于纬度为 1.234,经度为 2.132 的特定点。我们需要找到离这个位置最近的餐馆。

为此,function_score 查询是最佳选择。我们可以使用decayGauss)功能 的函数分数查询来实现这个:

curl -XPOST 'http://localhost:9200/restaurants/_search' -d '{
  "query": {
    "function_score": {
      "functions": [
        {
          "gauss": {
            "location": {
              "scale": "1km",
              "origin": [
                1.231,
                1.012
              ]
            }
          }
        }
      ]
    }
  }
}'

在这里,我们 告诉 Elasticsearch 为我们给出的推荐点附近的餐厅提供更高的分数。距离越近,重要性越高。

The maximum distance covered


现在,让我们继续另一个例子,寻找距离我当前 位置 10 公里以内的餐馆。那些超过 10 公里的我不感兴趣。所以,它从我现在的位置几乎组成了一个半径为 10 公里的圆,如下图所示:

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

我们最好的选择是使用地理距离过滤器。它可以按如下方式使用:

curl -XPOST 'http://localhost:9200/restaurants/_search' -d '{
  "query": {
    "filtered": {
      "filter": {
        "geo_distance": {
          "distance": "100km",
          "location": {
            "lat": 1.232,
            "lon": 1.112
          }
        }
      }
    }
  }
}'

Inside the city limits


接下来,我只需要考虑那些在特定城市范围内的餐厅;其余的对我不感兴趣。由于下图所示的城市本质上是矩形,这使我的工作更容易:

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

现在,要查看地理点是否在矩形内,我们可以使用边界框过滤器。当您输入左上角和右下角时,会标记一个矩形。

假设城市位于以下矩形内,左上角为 XY 和右下角的点为 AB :

curl -XPOST 'http://localhost:9200/restaurants/_search' -d '{
  "query": {
    "filtered": {
      "query": {
        "match_all": {}
      },
      "filter": {
        "geo_bounding_box": {
          "location": {
            "top_left": {
              "lat": 2,
              "lon": 0
            },
            "bottom_right": {
              "lat": 0,
              "lon": 2
            }
          }
        }
      }
    }
  }
}'

Distance values between the current point and each restaurant


现在,考虑需要查找用户位置与每家餐厅之间的距离的场景。我们怎样才能达到这个要求?我们可以使用脚本;当前地理坐标被传递给脚本,然后运行查询以查找每个餐厅之间的距离,如以下代码所示。在这里,当前位置以 (1, 2) 的形式给出:

curl -XPOST 'http://localhost:9200/restaurants/_search?pretty' -d '{
  "script_fields": {
    "distance": {
      "script": "doc['"'"'location'"'"'].arcDistanceInKm(1, 2)"
    }
  },
  "fields": [
    "name"
  ],
  "query": {
    "match": {
      "name": "chinese"
    }
  }
}'

我们在前面的查询中使用了名为 arcDistanceInKm 的函数,它接受地理坐标,然后返回该点与位置之间的距离 对查询感到满意。请注意,计算的距离单位是 公里 (km )。您可能已经注意到前面提到的脚本中 location 前后有一长串引号和双引号。这是标准格式,如果我们不使用它,将导致在处理时返回格式错误。

计算从当前点到过滤后的酒店的距离,并在响应的distance字段中返回,如下代码所示:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.7554128,
    "hits" : [ {
      "_index" : "restaurants",
      "_type" : "restaurant",
      "_id" : "AU08uZX6QQuJvMORdWRK",
      "_score" : 0.7554128,
      "fields" : {
        "distance" : [ 112.92927483176413 ],
        "name" : [ "Great chinese restaurant" ]
      }
    }, {
      "_index" : "restaurants",
      "_type" : "restaurant",
      "_id" : "AU08uZaZQQuJvMORdWRM",
      "_score" : 0.7554128,
      "fields" : {
        "distance" : [ 137.61635969665923 ],
        "name" : [ "Great chinese restaurant" ]
      }
    } ]
  }
}

请注意,从当前点到酒店的距离是直接距离,而不是 道路距离。

Restaurants out of city limits

我的一个朋友打电话给我,让我和他一起去下一个城市。当我们 离开城市时,他特别想在城市范围外的某家餐厅用餐,但要在下一个城市之外。为此,该要求适用于距市中心至少 15 公里、最大 100 公里的任何餐厅。因此,我们有类似甜甜圈的东西,我们必须在其中进行搜索,如下图所示:

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

甜甜圈里面的区域是匹配的,但外面的区域不是。对于这个甜甜圈面积计算,我们有 geo_distance_range 过滤器来拯救我们。在这里,我们可以应用 fromto 字段中的最小距离和最大距离来填充结果,如以下代码:

curl -XPOST 'http://localhost:9200/restaurants/_search' -d '{
  "query": {
    "filtered": {
      "query": {
        "match_all": {}
      },
      "filter": {
        "geo_distance_range": {
          "from": "15km",
          "to": "100km",
          "location": {
            "lat": 1.232,
            "lon": 1.112
          }
        }
      }
    }
  }
}'

Restaurant categorization based on distance


在电子商务解决方案中,要搜索餐厅,您需要增加应用程序的可搜索特性。这意味着,如果我们能够提供前 10 个结果以外的结果的快照,它将增加搜索的可搜索特征。例如,如果我们能够显示有多少餐厅供应印度、泰国或其他美食,它实际上会帮助用户更好地了解结果集。

类似地,如果我们能告诉他们餐厅是在附近、在中距离还是在远处,我们真的可以在餐厅搜索用户体验中拉动和弦,如下图所示:

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

实现这一点并不难,因为我们有一个叫做距离范围聚合的东西。在这种 聚合类型中,我们可以手工制作我们感兴趣的距离范围,并为每个距离创建一个桶。我们也可以定义我们需要的键名,如下代码所示:

curl -XPOST 'http://localhost:9200/restaurants/_search' -d '{
  "aggs": {
    "distanceRanges": {
      "geo_distance": {
        "field": "location",
        "origin": "1.231, 1.012",
        "unit": "meters",
        "ranges": [
          {
            "key": "Near by Locations",
            "to": 200
          },
          {
            "key": "Medium distance Locations",
            "from": 200,
            "to": 2000
          },
          {
            "key": "Far Away Locations",
            "from": 2000
          }
        ]
      }
    }
  }
}'

前面的代码中,我们将餐厅分为三个距离范围,即附近酒店(200米以内)、中距离酒店(200米以内)到 2,000 米)和较远的(大于 2,000 米)。使用 which 将此逻辑转换为 Elasticsearch 查询,我们收到的结果如下:

{
  "took": 44,
  "timed_out": false,  
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 5,
    "max_score": 0,
    "hits": [
      
    ]
  },
  "aggregations": {
    "distanceRanges": {
      "buckets": [
        {
          "key": "Near by Locations",
          "from": 0,
          "to": 200,
          "doc_count": 1
        },
        {
          "key": "Medium distance Locations",
          "from": 200,
          "to": 2000,
          "doc_count": 0
        },
        {
          "key": "Far Away Locations",
          "from": 2000,
          "doc_count": 4
        }
      ]
    }
  }
}

结果中,我们收到了 doc_count 字段指示的每个距离范围内有多少家餐馆。

Aggregating restaurants based on their nearness


在前面的 示例中,我们看到了基于餐厅从当前点到三个不同类别的距离的聚合。现在,我们可以考虑另一种情况,根据餐馆所属的 geohash 网格对餐馆进行分类。如果用户想要了解餐馆分布的地理情况,这种分类可能是有利的。

以下是基于 geohash 的餐厅聚合代码:

curl -XPOST 'http://localhost:9200/restaurants/_search?pretty' -d '{ 
  "size": 0,
  "aggs": {
    "DifferentGrids": {
      "geohash_grid": {
        "field": "location",
        "precision": 6
      },
      "aggs": {
        "restaurants": {
          "top_hits": {}
        }
      }
    }
  }
}'

你可以从前面的代码中看到我们使用了geohash聚合,它被命名为DifferentGrids和这里的精度,设置为6precision 字段值可以在 112 的范围内变化, 1 是最低的,12 是最高的精度参考。

此外,我们在 DifferentGrids 聚合中使用了另一个名为 restaurants 的聚合。 restaurant 聚合使用 top_hits 查询从 DifferentGrids 聚合,否则只会返回 keydoc_count 值。

因此,运行前面的代码会给我们以下结果:

{
   "took":5,
   "timed_out":false,
   "_shards":{
      "total":1,
      "successful":1,
      "failed":0
   },
   "hits":{
      "total":5,
      "max_score":0,
      "hits":[

      ]
   },
   "aggregations":{
      "DifferentGrids":{
         "buckets":[
            {
               "key":"s009",
               "doc_count":2,
               "restaurants":{... }
            },
            {
               "key":"s01n",
               "doc_count":1,
               "restaurants":{... }
            },
            {
               "key":"s00x",
               "doc_count":1,
               "restaurants":{... }
            },
            {
               "key":"s00p",
               "doc_count":1,
               "restaurants":{... }
            }
         ]
      }
   }
}

正如我们从响应中看到的,有四个带有key值的桶,分别是s009s01ns00xs00p 。这些键值代表餐厅所属的不同 geohash 网格。从前面的结果中,我们可以很明显的说 s009 网格中包含两个餐厅,而所有其他网格每个包含一个。

先前聚合的图形表示将如下图所示:

读书笔记《elasticsearch-blueprints》使用Geo增强搜索的趣味性

Summary


我们发现 Elasticsearch 可以处理地理点和各种特定于地理的操作。我们在本章中介绍的一些地理特定和地理点操作是搜索附近的餐馆(圆圈内的餐馆),搜索范围内的餐馆(同心圆内的餐馆),搜索城市内的餐馆(矩形内的餐馆) ,在多边形内搜索餐馆,并按邻近度对餐馆进行分类。除此之外,我们还可以使用 Elasticsearch 提供的灵活强大的可视化工具 Kibana,用于基于地理的操作。