Help us understand the problem. What is going on with this article?

MySQLのgeometry型で○km以内の場所を取得してみました

More than 1 year has passed since last update.

これはGaiax Engineers' Advent Calendar 2015の8日目の記事です。

Gaiaxで主にPerlを書いてます@mitaniです。
最近はTABICAという「地域の暮らしを旅する」をコンセプトにした、地元の人が開催する旅(ツアー)の予約サービスの開発を行っています。

TABICA(たびか)へのレコメンド機能追加

今回、特定の場所(過去の予約や、閲覧している旅など)から近くの旅をレコメンドする機能を実装する事になりました。

以下は、レコメンド機能の実装の際にやってみたまとめになります。
MySQLのgeometry型については、エンジニアネタとしては使い古された感じもありますが大目に見てもらえると幸いです。

既存のデータ

TABICAではGoogleマップを利用して旅の開催場所を表示するために、MySQL(5.6系)で各旅の位置(緯度経度)をgeometry型で保持しています。

以下のようなテーブルに

CREATE TABLE `spot` (
  `id` bigint(16) unsigned NOT NULL,
  `latlng` geometry NOT NULL,
  PRIMARY KEY (`htm_id`),
  SPATIAL KEY `latlng` (`latlng`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

以下のようなデータが入っています。

mysql> SELECT id,ASTEXT(latlng) FROM spot;
+----+---------------------------------------------+
| id | ASTEXT(latlng)                              |
+----+---------------------------------------------+
|  1 | POINT(139.7454299032688 35.65858405958573)  |
|  2 | POINT(139.8106947541237 35.71006652116823)  |
+----+---------------------------------------------+

idには各旅固有のidが、
latlngには該当の旅の場所の緯度経度が入っています。

ASTEXT
geometry型をそのまま取得するとバイナリが取得されて人には読めないので、文字列で結果を取得します。

POINT
座標から特定の場所(点)を表すデータ型。

近い順に旅を取得

上記のテーブルからまずは 特定の旅(id=1) の場所から近い順に旅のidを取得しようと思います。

SELECT
  s2.id,
  GLENGTH(GEOMFROMTEXT(
      CONCAT('LINESTRING(',
          X( s1.latlng ),' ',Y( s1.latlng ), ',',
          X( s2.latlng ),' ',Y( s2.latlng ),
      ')'
  ))
) AS distance
FROM spot AS s1
  INNER JOIN spot AS s2
WHERE s1.id = 1 AND s1.id != s2.id
ORDER BY distance;

上記で、特定の旅(id=1) から近い順に旅を取得する事ができました。
id=1の緯度経度から、id!=1の緯度経度まで線を引いて、線の長さが短い順に並べるという具合です。

GLENGTH
LINESTRINGの長さを取得。

LINESTRING
POINTとPOINTを繋いだ線を表すデータ型。

X
経度

Y
緯度

GEOMFROMTEXT
geometry型への変換。

CONCAT関数
これは説明不要だと思いますが、文字列結合です。

近い順??

上記のSQLでは、近い順に旅を取得することができましたが、このままでは1000kmも2000kmも離れてる旅がレコメンドされる事になってしまい、とても近いとは言えません。
今回は歩ける距離ということで、1km以内を近い旅としました。

そこで問題となるのが、GLENGTHで取得できる距離の単位が度数なこと。
まずは1kmが何度なのかを知る為に地球の外周が何kmなのかを把握する必要があります。

Google先生曰く、地球の外周は約40075kmとの事なので、
40075km / 360度 ≒ 111.3194km
1度は、約111.3194kmのようです。
(およその数字にしているのは、地球が完全な球体ではない為です)

1 / 111.3194 ≒ 0.0089831601679492
1kmは、約0.0089831601679492度ですね。
(すでに誤差があるので、もう1km≒0.009度にしてしまってもいいかもしれませんが)

先ほどのSQLに条件を追加して、特定の旅(id=1) から約1km以内の旅を近い順に取得してみます。

SELECT
  s2.id,
  GLENGTH(GEOMFROMTEXT(CONCAT(
      'LineString(',
        X( s1.latlng ),' ',Y( s1.latlng ), ',',
        X( s2.latlng ),' ',Y( s2.latlng ),
      ')'
  )))
  AS distance
FROM spot AS s1
  INNER JOIN spot s2
WHERE s1.id = 1 AND s1.id != s2.id
GROUP BY s2.id HAVING distance <= 0.0089831601679492
ORDER BY distance

上記のSQLでとある旅(id=1) から約1km以内の旅を近い順に取得することができました。
これで、条件が何km以内であっても取得できそうです。

明日(9日目)の記事は?

明日はGaiaxの図書委員長こと @kyrieleison さんです。
当記事より100倍は役立つと思いますのでお楽しみに。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした