利用GeoHash優化查詢附近門店功能

1.背景

有一個營銷砍價活動,當用戶報名完成後,需要選擇去領取活動商品的門店,可選的門店列表需要按照距離當前用戶經緯度最近的3家門店進行距離排序,假設候選門店有10000家門店。具體demo如下圖:

在這裏插入圖片描述

2. 方案

2.1 思路

  • 思路一:暴力解法。維持一個只有3個結點的小根堆,遍歷10000家門店即可,比如使用java的PriorityQueue隊列可實現。缺點是比較耗時,需要遍歷所有門店進行距離計算,然後進行排序。

  • 思路二:使用GeoHash算法(這裏不做重點講解)來優化,首先在活動綁定門店表(包含經緯度信息)中新加一列表示當前的geohash值。然後從精度依次範圍查詢,比如精度爲GeoHash精度5位,即5KM(實際4.9KM)以內的門店,若匹配到3家門店立即返回,否則匹配精度4位的,直到匹配到精度爲1位的(這裏同一精度的門店任選3家即可,然後進行3家門店排序,舉個例子:比如匹配到5KM內的門店有20家,只取任意3家,且只對這3家進行排序即可)。

    • 匹配geohash精度5位,即5KM(實際4.9KM)以內的門店
    • 匹配geohash精度4位,即20KM(實際19.5KM)以內的門店
    • 匹配geohash精度3位,即156M(實際156KM)以內的門店
    • 匹配geohash精度2位,即624KM(實際624.1KM)以內的門店
    • 匹配geohash精度1位,即5000KM(實際19.5KM)以內的門店

2.2 GeoHash實踐

2.2.1 活動綁定門店表增加geohash字段
id(自增,bigint) shop_id(門店ID,bigint) lon(經度,String) lat(緯度,String) geohash(String)
23 234232 119.466602 29.214044 wthynw2b
2.2.2 構造查詢SQL
SELECT * FROM t_cmc_act_child_shop
WHERE is_deleted = 0
AND act_id =990
AND geohash LIKE CONCAT(#{geoCode,jdbcType=VARCHAR}, '%');
limit 3
2.3 代碼
  • 在每次創建活動時需要構造活動綁定門店表的geohash字段,構造過程代碼如下:

    public static String getGeohashStringWithBitLen(String longitude, String latitude, int bitLen) {
      if(Strings.isNullOrEmpty(longitude) || Strings.isNullOrEmpty(latitude)) {
        return "";
      }
      double longitudeValue, latitudeValue;
      try {
        longitudeValue = Double.valueOf(longitude);
        latitudeValue = Double.valueOf(latitude);
        return GeohashUtils.encodeLatLon(latitudeValue, longitudeValue, bitLen);
      } catch (Exception e) {
        log.error("[GeoHashUtils-getGeohashStringWithBitLen] 轉換 geohash 失敗", e);
      }
      return "";
    }
    
  • 依次根據精度進行查詢,直接選擇出最近3家門店即可返回,代碼如下:

    private List<ActJoinChildShopDTO> doGetNearbyShopList(Long actId, String userLongitude, String userLatitude) {
      // 校驗省略
      List<ActJoinChildShopDTO> allActJoinChildShopDs = Lists.newArrayList();
      int precision = 5;
      for(; precision >= 1; precision--) {
        List<ActJoinChildShopDTO> tempActJoinChildShopDTOS = getNearbyShopList(precision, userLongitude, userLatitude, actIds);
        allActJoinChildShopDs.addAll(tempActJoinChildShopDTOS);
        if(allActJoinChildShopDs.size() >= 3) {
          // 按距離進行排序
          allActJoinChildShopDs.sort(Comparator.comparing(ActJoinChildShopDTO::getShopDistance));
          return allActJoinChildShopDs.subList(0, 3);
        }
      }
      return Lists.newArrayList();
    }
    
    private List<ActJoinChildShopDTO> getNearbyShopList(int bitLength, String userLongitude, String userLatitude, Long actId) {
      String geohash = GeoHashUtils.getGeohashStringWithBitLen(userLongitude, userLatitude, bitLength);
      if(Strings.isNullOrEmpty(geohash)) {
        return Collections.emptyList();
      }
      // 數據庫sql查詢
      List<ActChildShopDTO> actChildShopDTOS = actChildShopRepository.getNearbyActChildShops(geohash, actIds);
      // 轉換結果並返回(其中需要根據經緯度計算距離)
      ......
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章