2点間の距離検索をgeokit-railsからelasticsearchに切り替えのため、検索手法を調査した

### はじめに

Railsで緯度経度の2点間の距離を検索するために、geokit-railsを使っていたが、全ての検索をelasticsearchにしたいので、検索手法を調査した

### geokit-railsによる距離検索

geokit-railsによる距離検索はsphere_distance_sqlとflat_distance_sqlの2種類ある

```ruby
def distance_sql(origin, units=default_units, formula=default_formula)
  case formula
  when :sphere
    sql = sphere_distance_sql(origin, units)
  when :flat
    sql = flat_distance_sql(origin, units)
  end
  sql
end
```

https://github.com/geokit/geokit-rails/blob/cc5fd43ab4e69878fb31ebd1fc22918e2952b560/lib/geokit-rails/acts_as_mappable.rb#L213

デフォルトはGeokit::default_formulaが使われている

```ruby
self.default_formula = options[:default_formula] || Geokit::default_formula
```

https://github.com/geokit/geokit-rails/blob/cc5fd43ab4e69878fb31ebd1fc22918e2952b560/lib/geokit-rails/acts_as_mappable.rb#L37

Geokit::default_formulaの値は:sphereが設定されている

```ruby
Geokit::default_formula = :sphere
```

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/generators/templates/geokit_config.rb#L3

では、sphere_distance_sqlとflat_distance_sqlの違いは何か？？


### 大円距離（sphere_distance_sql）

![スクリーンショット 2020-08-17 15.56.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214984/dd94fa36-13fa-099d-0b7e-50f6d40f0125.png)

https://ja.wikipedia.org/wiki/%E5%A4%A7%E5%86%86%E8%B7%9D%E9%9B%A2

sphere_distance_sqlは大円距離という球面上の2点間の長さが最短となる距離で計算している

geokit-railsでの計算箇所

```ruby
def sphere_distance_sql(lat, lng, multiplier)
  %|
    (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
    COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
    SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
    |
end
```

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/geokit-rails/adapters/mysql.rb#L5

### ピタゴラスの定理（flat_distance_sql）

ピタゴラスの定理は、直角三角形の3辺の長さの関係を表す
2点を斜辺とする直角三角形から距離を計算している

geokit-railsでの計算箇所

```ruby
def flat_distance_sql(origin, lat_degree_units, lng_degree_units)
  %|
    SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
    POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
    |
end
```

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/geokit-rails/adapters/mysql.rb#L13

次に、elasticsearchはどの計算手法を使っているのか？？


### elasticsearchによる距離検索

distance_typeでarcかplaneを指定することができる
デフォルトではarcで計算される


> distance_type
> How to compute the distance. Can either be arc (default), or plane (faster, but > inaccurate on long distances and close to the poles).

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-query.html

### まとめ

- geokit-railsはデフォルトでは大円距離の計算手法
- Elasticsearchはデフォルトでarc


### 余談

もう少し、elasticsearchの計算分岐箇所を追ってみた

```java
public double calculate(double srcLat, double srcLon, double dstLat, double dstLon, DistanceUnit unit) {
    if (this == PLANE) {
        return DistanceUnit.convert(GeoUtils.planeDistance(srcLat, srcLon, dstLat, dstLon),
            DistanceUnit.METERS, unit);
    }
    return DistanceUnit.convert(GeoUtils.arcDistance(srcLat, srcLon, dstLat, dstLon), DistanceUnit.METERS, unit);
}
```

https://github.com/elastic/elasticsearch/blob/237650e9c054149fd08213b38a81a3666c1868e5/server/src/main/java/org/elasticsearch/common/geo/GeoDistance.java#L73

大円距離の方も見ていくと

```java
DistanceUnit.convert(GeoUtils.arcDistance(srcLat, srcLon, dstLat, dstLon), DistanceUnit.METERS, unit)
```

```java
/** Return the distance (in meters) between 2 lat,lon geo points using the haversine method implemented by lucene */
public static double arcDistance(double lat1, double lon1, double lat2, double lon2) {
    return SloppyMath.haversinMeters(lat1, lon1, lat2, lon2);
}
```

https://github.com/elastic/elasticsearch/blob/a01d2bd24b0e849af034b88b563897a692a87bd8/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java#L603

```java
public static double haversinMeters(double lat1, double lon1, double lat2, double lon2) {
  return haversinMeters(haversinSortKey(lat1, lon1, lat2, lon2));
}
```

https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/util/SloppyMath.java#L50


```java
public static double haversinMeters(double sortKey) {
  return TO_METERS * 2 * asin(Math.min(1, Math.sqrt(sortKey * 0.5)));
}
```

https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/util/SloppyMath.java#L59

ぱっと見、geokitのsphereの計算式と違うように見える、、時間ある時に調べよ、、