Dùng Elasticsearch Geo Api để giải quyết các bài toán tìm kiếm địa điểm

1, Giới thiệu

Elasticsearch không chỉ là một tool hiệu quả giúp tìm kiếm các thông tin cơ bản trong Web Application mà nó cũng có khả năng giúp chúng ta giải quyết các bài toán tìm kiếm địa điểm dựa vào GEO API. Tôi giả sử rằng bạn nắm được các kiến thức cơ bản về Elasticsearch như cluster, node, shard, replicas, index, type, document, analysis, mapping, query, filter, aggregation,...Nếu chưa, tôi khuyên bạn tìm hiểu các kiến thức cơ bản về Elasticsearch trước khi đọc thêm về bài viết này. Trong bài viết này, tôi sẽ giới thiệu với các bạn cách dùng Elasticsearch Geo Api để tìm kiếm địa điểm mà cụ thể ở đây chính là tìm kiếm nhà hàng.

2, Modeling restaurant data

Trước tiên, chúng ta cần đưa tất cả thông tin về nhà hàng vào Elasticsearch. Để thực hiện điều này, chúng ta cần tạo index, setting cho index, thêm các restaurants vào index.

2.1, Tạo và setting index

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

Như trên, tôi đã tạo một index với tên là 'restaurants', và định nghĩa một analyzer mới 'flat' với tokenizer là keyword và các tokenizers là case insensitive.

2.2, Mapping type

curl -XPUT 'http://localhost:9200/restaurants/restaurant/_mapping' -d '{
  "restaurant": {
    "properties": {
      "name": {"type": "string"},
      "location" : { "type" : "geo_point"}
    }
  }
}'

Ở trên, tôi đã setting kiểu dữ liệu cho các fields của restaurant document, với name field kiểu string, location field kiểu geo_point và có độ chính xác là 1km. Chú ý rằng khi xác định location có kiểu là geo_point thì Elasticsearch sẽ tự tạo ra 2 field con là lat, lon.

2.3, Tạo documents cho index

Tạo một restaurant cho restaurants index:

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

3, Giải quyết các bài toán tìm kiếm nhà hàng

Với các bước settings ở trên, chúng ta đã có đầy đủ những thứ cần thiết để thực hiện việc tìm kiếm các nhà hàng thỏa mãn điều kiện xác định.

3.1, Tìm kiếm nhà hàng gần nhất

Giả sử, chúng ta đang ở một địa điểm có tọa độ là (1.231, 1.012), và muốn tìm kiếm nhà hàng gần nhất so với vị trí hiện tại. Để có được điều này, chúng ta sẽ sử dụng function_score query với decay (Gauss) function mà Elasticsearch cung cấp.

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

Kết quả:

"hits" : {
    "total" : 1,
    "max_score" : 0.0,
    "hits" : [ {
      "_index" : "restaurants",
      "_type" : "restaurant",
      "_id" : "AVn9Xtkz5dnZmSGyu5nj",
      "_score" : 0.0,
      "_source" : {
        "name" : "Santiago Restaurant",
        "location" : {
          "lat" : 1.1,
          "lon" : 1.54
        }
      }
    } ]
  }

Với query này, Elasticsearch sẽ trả về danh sách nhà hàng mà được sắp xếp theo khoảng cách so với địa điểm hiện tại, tức là nhà hàng nào gần thì sẽ được đánh score cao hơn.

3.2, Tìm kiếm các nhà hàng trong phạm vi nhất định

Giả sử, chúng ta muốn tìm tất cả các nhà hàng trong bán kính 10km so với vị trí hiện tại. Để có được những địa điểm này, chúng ta sẽ sử dụng geo distance filter của Elasticsearch

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

Kết quả:

"hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "restaurants",
      "_type" : "restaurant",
      "_id" : "AVn9Xtkz5dnZmSGyu5nj",
      "_score" : 1.0,
      "_source" : {
        "name" : "Santiago Restaurant",
        "location" : {
          "lat" : 1.1,
          "lon" : 1.54
        }
      }
    } ]
  }

Ở đây, chúng ta dùng filter chứ không phải là query bởi vì chúng ta không cần sắp xếp, và tính score cho kết quả trả về.

3.3, Tìm kiếm các nhà hàng thuộc phạm vi của một thành phố

Giả sử phạm vi của một thành phố được xác định bởi một hình chữ nhật, có tọa độ của các điểm top left và bottom right được xác định trước. Chúng ta muốn tìm tất cả các nhà hàng thuộc phạm vi thành phố này. Elasticsearch cung cấp bounding box filter cho chúng ta giải quyết bài toán này:

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}
            }
         }
       }
    }
  }
}'

Ở trên, chúng ta đã dùng query filter match_all để lấy ra tất cả documents trong index, sau đó dùng bounding box filter để lấy ra các nhà hàng thuộc phạm vi của thành phố được xác định bởi hình chữ nhật có tọa độ cho trước.

3.4, Tìm khoảng cách từ địa điểm hiện tại tới các nhà hàng

Giả sử chúng ta muốn tình khoảng cách từ vị trí hiện tại tới từng nhà hàng cụ thể, để thực hiện điều này chúng ta sử dụng scripts, tọa độ của vị trí hiện tại được pass tới script sau đó query để tìm khoảng với mỗi restaurant Giả sử vị trí hiện tại là (1, 2)

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

Kết quả:

"hits" : {
    "total" : 1,
    "max_score" : 0.19178301,
    "hits" : [ {
      "_index" : "restaurants",
      "_type" : "restaurant",
      "_id" : "AVn9Xtkz5dnZmSGyu5nj",
      "_score" : 0.19178301,
      "fields" : {
        "name" : [ "Santiago Restaurant" ],
        "distance" : [ 52.39453978846142 ]
      }
    } ]
  }

Ở trên, chúng ta đã sử dụng arcDistanceInKm function để tính toán khoảng cách của mỗi nhà hàng với địa điểm xác định. Sau đó, chúng ta cũng xác định các field bổ sung sẽ được trả về bởi Elasticsearch và cuối cùng là một match query để lấy ra các nhà hàng mà tên chứa token là vietnamese. Có một điểm cần chú ý rằng, các kết quả trả về là khoảng cách theo đường chim bay, không phải là khoảng cách đường bộ.

3.5, Tìm kiếm các nhà hàng bên ngoài phạm vi thành phố

Giả sử, chúng ta muốn tìm kiếm tất cả nhà hàng mà không thuộc phạm vi của thành phố hiện tại nhưng lại thuộc phạm vị của thành phố bên cạnh. Cụ thể hơn, chẳng hạn, chúng ta muốn tìm kiếm các nhà hàng mà cách trung tâm thành phố hiện tại ít nhất là 15km và nhiều nhất là 100km. Elasticsearch cung cấp cho chúng ta geo_distance_range filter để chúng ta giải quyết bài toán này:

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}
       }
      }
    }
  }'

Ở trên, chúng ta sử dụng geo_distance_range filter để tính toán khoảng cách các điểm và chọn ra các nhà hàng trong phạm vị của thành phố bên cạnh. Trong geo_distance_filter, chúng ta đã xác định 3 loại thông tin cần thiết là from, to (khoảng cách tối thiểu, khoảng cách tối đa so với trung tâm thành phố hiện tại), location (tọa độ của trung tâm thành phố)

3.6, Phân loại nhà hàng dựa trên khoảng cách

Trong thực tế, đôi khi, chúng ta cần phân chia các nhà hàng thành các nhóm dựa theo khoảng cách chẳng hạn như chia làm 3 nhóm là các nhà hàng gần, các nhà hàng xa bình thường, và các nhà hàng ở xa. Để giải quyết bài toán này, chúng ta sử dụng distance range aggregation của Elasticsearch

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}
         ]
       }
     }
  }
}'

Kết quả:

 "aggregations" : {
    "distanceRanges" : {
      "buckets" : [ {
        "key" : "Near by Locations",
        "from" : 0.0,
        "from_as_string" : "0.0",
        "to" : 200.0,
        "to_as_string" : "200.0",
        "doc_count" : 0
      }, {
        "key" : "Medium distance Locations",
        "from" : 200.0,
        "from_as_string" : "200.0",
        "to" : 2000.0,
        "to_as_string" : "2000.0",
        "doc_count" : 0
      }, {
        "key" : "Far Away Locations",
        "from" : 2000.0,
        "from_as_string" : "2000.0",
        "doc_count" : 1
      } ]
    }
  }

Ở trên, trong distance range aggregation chúng ta đã xác định các thông tin như: các fields sẽ trả về, tọa độ của vị trí hiện tại, đơn vị khoảng cách, và các ranges mà sẽ dùng để phân nhóm

4, Kết luận

Trong bài viết này, tôi đã giới thiệu tới bạn cách dùng Elasticsearch để giải quyết các bài toán liên quan tới geo point, và cũng đã giới thiệu với các bạn các geo-specific operators đa dạng để xử lý nhiều loại bài toán đáp ứng các yêu cầu khác nhau.

Với geo api của Elasticsearch chúng ta có thể giải quyết các bài toán tìm kiếm địa điểm với tốc độ tìm kiếm rất nhanh, kết quả tìm kiếm cũng có độ chính xác thật tuyệt vời. Tuy nhiên, nhược điểm của nó chính là sự phức tạp của cú pháp, cũng như cách để đưa nó vào ứng dụng thực tế.

Tài liệu tham khảo

https://www.elastic.co/blog/geo-location-and-search https://www.elastic.co/guide/en/elasticsearch/guide/current/geoloc.html http://www.elasticsearchtutorial.com/spatial-search-tutorial.html