はじめに
Snapmap は位置情報検索と全文検索の機能が備わっています。
位置情報検索や全文検索を機能実装する場合、通常は Elasticsearch などが候補に上がるかと思いますが、初期の要件などを検討した結果、今回は Mroonga を採用することにしました1。
mroonga_command
Mroonga は全文検索のレスポンスがとても速く検索条件の柔軟性も高いため、ほとんど困らないのですが、かなり複雑なことをしようとすると Groonga も直接呼び出したくなります。
その場合 mroonga_command を利用すればOKです。
例えば、こんな spots テーブルがあったとして
カラム | 型 | 長さ |
---|---|---|
id | int | 11 |
name | varchar | 256 |
location | geometry | - |
特定の位置から半径 1000m 以内に location が存在するデータを取得するにはこんな SQL を実行することになります。
SELECT mroonga_command(' select spots --filter \'geo_in_circle(lat_lon, \"35.6812x139.76624\", 1000)\' --sortby \'geo_distance(lat_lon, \"35.6812x139.76624\")\' --limit 100 ')
結果はちょっと変わっていて、こんなフォーマットで取得されます。
# refs: http://mroonga.org/ja/blog/2013/03/29/release.html
[
[
# 取得件数
[2],
# ヘッダー
[
["_id","UInt32"],
["_key","Int32"],
["id","Int32"],
["location","WGS84GeoPoint"],
["name","LongText"]
],
# 以下データ
[1,1,1,"128563247x503268584","Shop A"],
[2,2,2,"128331724x502961461","Shop B"]
]
]
ActiveRecordで扱う
mroonga_command を使えばいい感じに取得できるのですが、ActiveRecord で扱えるようにしたい。というわけでこんな感じのメソッドを作りました。
# ActiveSupport::Concernに生やすのでも可
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
class << self
def mroonga_command(sql)
# mroonga_commandは改行を入れてはいけないので注意
chunk = ActiveRecord::Base.connection.select_all(sql)
chunk = JSON.parse(chunk[0].values[0])[0]
row_count = chunk[0][0]
schema = chunk[1]
rows = chunk[2..-2]
return [] if row_count == 0
rows.each_with_object([]) do |row, records|
record = self.new
schema.each_with_index do |column, i|
attr_name = column[0]
attr_type = column[1]
val = row[i]
# ActiveRecordと同名のattributeがあれば格納
next unless method_defined?(attr_name)
record.send("#{attr_name}=", mr2ar(attr_type, val))
end
records << record
end
end
#
# Mroongaから送られた型に従いActiveRecordで使用可能な値に変換する
#
# Time: 1487840400 → 2017/02/23 18:00:00
# WGS84GeoPoint: 127606000x502738000 → POINT(35.44611111111111 139.6494444444444)
def mr2ar(attr_type, val)
case attr_type
when "Time"
Time.zone.at(val)
when "WGS84GeoPoint"
md = /(\d+)x(\d+)/.match(val)
return nil if md.size < 2
# 日本測地系(ミリ秒)を世界測地系(度)に変換
"POINT(#{md[2].to_f / 3_600_000} #{md[1].to_f / 3_600_000})"
else
val
end
end
end
end
これで mroonga_command を ActiveRecord として扱えるようになりました。
めでたしめでたし。(が、最終的には使いませんでした…)
[1] pry(main)> Spot.mroonga_command("SELECT mroonga_command(' select spots --filter \\'geo_in_circle(lat_lon, \"35.6812x139.76624\", 1000)\\' --sortby \\'geo_distance(lat_lon, \"35.6812x139.76624\")\\' --limit 100 ')")
(0.5ms) SELECT mroonga_command(' select spots --filter \'geo_in_circle(lat_lon, "35.6812x139.76624", 1000)\' --sortby \'geo_distance(lat_lon, "35.6812x139.76624")\' --limit 100 ')
=> [#<Spot:0x007f7944294ae0
id: 6519,
name: "KITTEガーデン",
location: #<RGeo::Cartesian::PointImpl:0x3fbcaecb3e28 "POINT (139.76487666666668 35.67979972222222)">>,