人生ずっと勉強

人生ずっと勉強ですね。 https://twitter.com/KiyotakaGoto

AWS EC2 に mroonga を入れて、全文検索と位置情報検索を試してみる。

データの準備

あるお店リストの名前の全文検索と、位置情報検索ができるようにしたいと思います。
3.3.1. ストレージモード — mroonga v3.03 documentation を参考に create table 文を作ります。

CREATE TABLE shops (
       id INT PRIMARY KEY AUTO_INCREMENT,
       name VARCHAR(255),
       location POINT NOT NULL,
       FULLTEXT INDEX (name),
       SPATIAL INDEX (location)
) ENGINE = mroonga DEFAULT CHARSET utf8;
mysql> create database mroonga_test;
mysql> use mroonga_test;
mysql> source /path/to/create_table.sql;

次に、テスト用のデータを入れます。
今回は以下のたい焼き店データをサンプルとして利用しました。
groongaで高速な位置情報検索 - ククログ(2011-09-13)

位置情報データの挿入には、3.3.1.4. 位置情報検索の利用方法 にあるように、
GeomFromText()関数 を使います。

INSERT INTO shops VALUES (null, 'Nezu\'s Taiyaki', GeomFromText('POINT(139.762573 35.720253)'));

POINT 型は「経度、緯度」の順番に位置情報を指定することに注意。

たい焼きデータを以下の perl コードで sql に直しました(使い捨てなので汚いですが・・・)

use strict;
use warnings;

my $taiyaki = << "TAIYAKI";
["根津のたいやき", "35.720253,139.762573"],
["たい焼 カタオカ", "35.712521,139.715591"],
["そばたいやき空", "35.683712,139.659088"],
["車", "35.721516,139.706207"],
["広瀬屋", "35.714844,139.685608"],
["さざれ", "35.714653,139.685043"],
["おめで鯛焼き本舗錦糸町東急店", "35.700516,139.817154"],
["尾長屋 錦糸町店", "35.698254,139.81105"],
["たいやき工房白家 阿佐ヶ谷店", "35.705517,139.638611"],
["たいやき本舗 藤家 阿佐ヶ谷店", "35.703938,139.637115"],
["みよし", "35.644539,139.537323"],
["寿々屋 菓子", "35.628922,139.695755"],
["たい焼き / たつみや", "35.665501,139.638657"],
["たい焼き鉄次 大丸東京店", "35.680912,139.76857"],
["吾妻屋", "35.700817,139.647598"],
["ほんま門", "35.722736,139.652573"],
["浪花家", "35.730061,139.796234"],
["代官山たい焼き黒鯛", "35.650345,139.704834"],
["たいやき神田達磨 八重洲店", "35.681461,139.770599"],
["柳屋 たい焼き", "35.685341,139.783981"],
["たい焼き写楽", "35.716969,139.794846"],
["たかね 和菓子", "35.698601,139.560913"],
["たい焼き ちよだ", "35.642601,139.652817"],
["ダ・カーポ", "35.627346,139.727356"],
["松島屋", "35.640556,139.737381"],
["銀座 かずや", "35.673508,139.760895"],
["ふるや古賀音庵 和菓子", "35.680603,139.676071"],
["蜂の家 自由が丘本店", "35.608021,139.668106"],
["薄皮たい焼き あづきちゃん", "35.64151,139.673203"],
["横浜 くりこ庵 浅草店", "35.712013,139.796829"],
["夢ある街のたいやき屋さん戸越銀座店", "35.616199,139.712524"],
["何故屋", "35.609039,139.665833"],
["築地 さのきや", "35.66592,139.770721"],
["しげ田", "35.672626,139.780273"],
["にしみや 甘味処", "35.671825,139.774628"],
["たいやきひいらぎ", "35.647701,139.711517"],
TAIYAKI

my @shops = split "\n", $taiyaki;
for my $shop ( @shops ) {
    $shop =~ s/\[//;
    $shop =~ s/\],//;
    $shop =~ s/"//g;

    my ($name, $lat, $long) = split /,/, $shop;
    print "INSERT INTO shops VALUES (null, '$name', GeomFromText('POINT($long $lat)'));\n";
}

挿入した位置情報データをそのまま select するとひどいことになるので、
AsText() をかませます。

mysql> SELECT id, name, AsText(location) FROM shops limit 1;
+----+-----------------------+------------------------------------------+
| id | name                  | AsText(location)                         |
+----+-----------------------+------------------------------------------+
|  1 | 根津のたいやき        | POINT(139.762573055556 35.7202530555556) |
+----+-----------------------+------------------------------------------+
1 row in set (0.00 sec)

ちゃんと入ってるようです。

検索してみる

店名の全文検索

mysql> SELECT id, name, AsText(location) FROM shops WHERE MATCH(name) AGAINST("りこ");
+----+-------------------------------+------------------------------------------+
| id | name                          | AsText(location)                         |
+----+-------------------------------+------------------------------------------+
| 30 | 横浜 くりこ庵 浅草店          | POINT(139.796828888889 35.7120130555556) |
+----+-------------------------------+------------------------------------------+
1 row in set (0.00 sec)

検索できています。

指定した矩形エリアの検索

「この四角の範囲の中から何かを探す」というやつです。
LineString() の第1引数に左上の点、第2引数に右下の点を与えます。ここでも、それぞれの位置情報は「経度、緯度」の順番になるので注意。
今回は、google マップから適当に JR 浅草駅周辺の位置情報を取ってきて、左上・右上の点としました。

mysql> SELECT id, name, AsText(location) FROM shops WHERE MBRContains(GeomFromText('LineString(139.796411 35.712478, 139.79772 35.711171)'), location);
+----+-------------------------------+------------------------------------------+
| id | name                          | AsText(location)                         |
+----+-------------------------------+------------------------------------------+
| 30 | 横浜 くりこ庵 浅草店          | POINT(139.796828888889 35.7120130555556) |
+----+-------------------------------+------------------------------------------+
1 row in set (0.00 sec)

指定矩形範囲にあるのはくりこ庵だけのようです(正確には、わざとくりこ庵のみが引っかかる範囲を検索範囲としているだけですが・・・)。

指定した点を中心として指定半径内の円を検索

「この点を中心として半径いくつの範囲で何かを探す」というやつです。
これを sql のみで実現しようとすると、いろいろと計算をしないといけないようです
(参考:なんとなく始めてみた、プログラマー雑記 » データベース内の緯度・経度を利用して半径500m以内を検索する方法

なので、mroonga が用意している mroonga_command() 関数 を使います。
この関数を使えば sql の中で好きな groonga コマンドを実行できるようです。
(参考:3.3.1. ストレージモード — mroonga v3.03 documentation

指定円内の検索には、groonga の select の filter の検索条件に、geo_in_circle() という関数の結果を使います。
8.3.22. select — groonga v3.0.3ドキュメント
8.11.3. geo_in_circle — groonga v3.0.3ドキュメント

filter と geo_in_circle() を組み合わせることで、指定円内にあるスポットだけを取り出すことができます。

mysql> SELECT mroonga_command( 'select shops --filter \'geo_in_circle(location, "35.7119,139.7983", 500)\' ') AS result;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| result                                                                                                                                                                        |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[[1],[["_id","UInt32"],["_key","Int32"],["id","Int32"],["location","WGS84GeoPoint"],["name","ShortText"]],[30,30,30,"128563247x503268584","横浜 くりこ庵 浅草店"]]]          |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.02 sec)

mroonga_command の結果は json として返ってくるので、アプリケーション側でこれを parse しないといけないようです。
また、groonga の select はデフォルトでは 10 件しか返さないみたいです。
このあたりは --limit や --offset あたりを使って柔軟に。
ちなみに --limit -1 など、マイナスの値にすると全件取得になるっぽいです。

感想

けっこうカンタンにできちゃいますね。