バウンディングボックスで地理的クエリを使う

以前のニューヨークの犯罪に関するブログでは、GridDBのジオメトリ(幾何学)機能を使った地理空間データのクエリについて紹介しました。

ジオメトリとWKT(よく知られている幾何学のテキスト表現)のクエリはうまく機能しますが、数値比較よりも計算量が多いこと、CとJavaのAPIでしか利用できないこと、という2つの欠点があります。PythonやJDBCを使いたい場合は、別の方法を使う必要があります。このブログでは、同じデータセットに単純なバウンディングボックスを使ったクエリを紹介します。また、地理空間データを複数のコンテナに分割する方法についても紹介します。

パーティションデータ

GridDBのキーコンテナアーキテクチャを利用するために、データを異なるコンテナに分割します。これにより、データを均等に分散させながら、バイナリ検索ツリーを非常に短くすることができます。

これにはいくつかの方法がありますが、今回は最もシンプルな、緯度と経度を四捨五入する方法を紹介します。

JavaではDecimalFormatで実現していますが、Pythonではmath.ceil()とifを使っています。

DecimalFormat df = new DecimalFormat("#");
df.setRoundingMode(RoundingMode.CEILING);
df.setNegativePrefix("n");
String cName = "geo_"+df.format(c.Latitude*100)+"_"+df.format(c.Longitude*100);
cname = "geo_"+re.sub('-','n', str(math.ceil(lat*100)))+"_"+re.sub('-','n',str(math.ceil(lon*100)))))

最初のブログで紹介したIngest.javaを修正し、ジオメトリデータタイプを使ってデータを照会しました。

DecimalFormat df = new DecimalFormat("#");
df.setRoundingMode(RoundingMode.CEILING);
df.setNegativePrefix("n");
for (CSVRecord record : records) {
    try {
        if (i++ % 1000 == 0) System.out.println("Ingesting "+ i);
        Complaint c = parseCsvRecord(record);
        if(c != null) {
            String cName = "geo_"+df.format(c.Latitude*100)+"_"+df.format(c.Longitude*100);
            System.out.println("adding "+c+" to "+cName); 
            Collection<String, Complaint> col = store.putCollection(cName, Complaint.class);
            col.setAutoCommit(false);
            col.put(c);
            col.commit();
        }
    } catch(Exception e) {
        System.out.println("Failed to ingest "+i);
        System.out.println(e);
    }
}

これにより、以下の出力が得られます。

adding 927207428: Fri Jan 03 13:30:00 GMT 2014 @ (40.745241809, -73.894253382) to geo_4075_n7389
adding 492142357: Wed Apr 13 00:00:00 BST 2016 @ (40.810351863, -73.924942326) to geo_4082_n7392
adding 572616350: Mon Aug 18 14:30:00 BST 2014 @ (40.683659778, -73.85154207) to geo_4069_n7385
... snip ...

バウンディングボックス

GridDBにデータが保存されたので、照会することができるようになりました。まずは、境界を見つけてみましょう。Pythonでは、geopy distanceモジュールを使うことができます。

import geopy
from geopy import distance

d = geopy.distance.geodesic(meters=radius)
start = geopy.Point(lat, lon)

north = d.destination(point=start, bearing=0)[0]
south = d.destination(point=start, bearing=180)[0]
east = d.destination(point=start, bearing=90)[1]
west = d.destination(point=start, bearing=270)[1]

データが分割されているので、次のステップでは、クエリが必要となる可能性のあるコンテナのリストを生成します。

クエリエリアの距離がパーティションサイズよりも小さい場合は、コンテナを1つのセットとして定義することができます。

containers = {  re.sub('-', 'n', re.sub('\.', '', "geo_%d_%d" %(math.ceil(north*100), math.ceil(east*100)))),
                re.sub('-', 'n', re.sub('\.', '', "geo_%d_%d" %(math.ceil(north*100), math.ceil(west*100)))),
                re.sub('-', 'n', re.sub('\.', '', "geo_%d_%d" %(math.ceil(south*100), math.ceil(east*100)))),
                re.sub('-', 'n', re.sub('\.', '', "geo_%d_%d" %(math.ceil(south*100), math.ceil(west*100))))};

しかし、クエリ領域がパーティションサイズよりも大きい場合は、可能なすべてのコンテナを繰り返し処理する必要があります。

containers=[]
clat = math.ceil(south*100)
while clat <= math.ceil(north*100):
    clon = math.ceil(west*100)
    while clon <= math.ceil(east*100):
        containers.append("geo_"+re.sub('-','n', str(clat))+"_"+re.sub('-','n',str(clon)))
        clon+=1
    clat+=1

コンテナのリストができたので、バウンディングボックス内のポイントを検索するシンプルなTQL文でコンテナを照会することができるようになりました。

tql = "select * where Latitude > %f and Latitude < %f and Longitude < %f and Longitude > %f" %(south, north, east, west)


for container in containers:
    ts = gridstore.get_container(container)
    try:
        query = ts.query(tql) 
        rs = query.fetch(False) 

        while rs.has_next():
            complaints.append(rs.next()
    except:
        pass

このような検索を実行する際には、ループ内で例外を捕まえておくことが重要です。最初のコンテナには結果がなかった場合に、後でクエリされたコンテナに結果があるのにも関わらず、例外が返ってきてしまう可能性があるからです。

まとめ

このように、バウンディングボックスを使うと、指定した場所の近くにあるデータを簡単に見つけることができます。このブログのソースコードは、 GridDB.net GitHubリポジトリで公開しています。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *