SolrのSpatial Searchを試してみた
前書き
10代の頃は(ゴースト)ライターという職業に憧れていた時期もありました@yutakikuchi_です。
Geospatial Indexes and Queries ― MongoDB Manual 2.4.9
MySQL :: MySQL 4.1 リファレンスマニュアル :: 10.6.1 空間インデックスの作成
位置情報IndexをMongoDBで管理する手法については前に調査済みで、mysqlにもSpatialindexはあまり普及していない印象、ということで...今日は検索SolrのSpatial Searchについて調べてみます。最終的にはFessやNutchでWebPageをCrawlingして得た住所データをGeocodingでLat/Lngデータに変換して自前のServerにIndexingしていく事を考えており、その前段階の作業です。Solrを選ぶ理由ですがSpatial Search以外にもTermVectorでの類似度を算出してくれるMoreLikeThisという機能があり、Lat/Lngデータの掛け合わせでコンテンツを面白くSuggestすることを考えています。MoreLikeThisについても調査したら書きますね。
Solr設定
java, tomcat6, Solr
javaとtomcat、Solr本体が必要なので以下の手順でInstallです。Solrは2014.3.15現在で最新のV4.7.0を取ってきます。僕が3年程前にSolrを使っていた時はV1.*とかだったので、もう過去の記憶や記録は役立たなさそうですね...
$ sudo yum install java-1.7.0-openjdk tomcat6 --enablerepo=remi $ wget "ftp://ftp.riken.jp/net/apache/lucene/solr/4.7.0/solr-4.7.0.tgz" $ tar xf solr-4.7.0.tgzPortfowarding
Solrのadminツールに接続する為の設定です。僕の場合はMacでVirtualBoxを立ち上げ、HostOSからGuestOS(CentOS)に接続してSolrを使っているのでVirtualBox内のPortfowardingとGuestOS側のFireWallの設定をします。VirtualBoxでは設定=>ネットワーク=>ポートフォワーディングで以下の画面に辿れます。Solrのdefaultportである8983を指定しておきます。※GuestOS側のIPアドレスをifconfigで調べて設定してください。
Firewall
下はHostOS側のFirewall設定です。
$ sudo vi /etc/sysconfig/iptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT #追加 -A INPUT -m state --state NEW -m tcp -p tcp --dport 8983 -j ACCEPT $ sudo service iptables restartSolr-exampleの起動
Solrのexampleにある管理画面を表示してみます。tarで展開したディレクトリ以下にstart.jarファイルがあるのでそれを実行します。下のコマンドを実行してhttp://localhost:8983/solrにアクセスします。start.jarの実行時もそうですが、色々とエラーがでていますが取りあえずは画面を見る事ができます。
$ cd solr-4.7.0/example $ java -jar start.jar 0 [main] INFO org.eclipse.jetty.server.Server – jetty-8.1.10.v20130312 41 [main] INFO org.eclipse.jetty.deploy.providers.ScanningAppProvider – Deployment monitor /home/yuta/work/src/solr/solr-4.7.0/example/contexts at interval 0 50 [main] INFO org.eclipse.jetty.deploy.DeploymentManager – Deployable added: /home/yuta/work/src/solr/solr-4.7.0/example/contexts/solr-jetty-context.xml 1872 [main] INFO org.eclipse.jetty.webapp.StandardDescriptorProcessor – NO JSP Support for /solr, did not find org.apache.jasper.servlet.JspServlet (略)exampledocsの追加
展開したSolrのディレクトリにexampledocsがあるのでSolrに追加してみます。xmlの中身を見てみるとPCパーツや電化製品の情報みたいです。post.shを実行するとcollection1という新しいコレクションが生成され、32個のドキュメントが追加されます。indexのデータはsolr-4.7.0/example/solr/collection1/data/indexに生成されます。
$ cd solr-4.7.0/example/exampledocs $ cat vidcard.xml <add> <doc> <field name="id">EN7800GTX/2DHTV/256M</field> <field name="name">ASUS Extreme N7800GTX/2DHTV (256 MB)</field> <!-- Denormalized --> <field name="manu">ASUS Computer Inc.</field> <!-- Join --> <field name="manu_id_s">asus</field> <field name="cat">electronics</field> <field name="cat">graphics card</field> <field name="features">NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz</field> <field name="features">256MB GDDR3 Memory clocked at 1.35GHz</field> <field name="features">PCI Express x16</field> <field name="features">Dual DVI connectors, HDTV out, video input</field> <field name="features">OpenGL 2.0, DirectX 9.0</field> <field name="weight">16</field> <field name="price">479.95</field> <field name="popularity">7</field> <field name="store">40.7143,-74.006</field> <field name="inStock">false</field> <field name="manufacturedate_dt">2006-02-13T15:26:37Z/DAY</field> </doc> $ ./post.sh *.xml Posting file gb18030-example.xml to http://localhost:8983/solr/update <?xml version="1.0" encoding="UTF-8"?> <response> <lst name="responseHeader"><int name="status">0</int><int name="QTime">164</int></lst> </response> (略) $ ls solr-4.7.0/example/solr/collection1/data/index -rw-rw-r--. 1 yuta yuta 4928 3月 16 00:55 2014 _0.fdt -rw-rw-r--. 1 yuta yuta 45 3月 16 00:55 2014 _0.fdx -rw-rw-r--. 1 yuta yuta 2300 3月 16 00:55 2014 _0.fnm -rw-rw-r--. 1 yuta yuta 250 3月 16 00:55 2014 _0.nvd -rw-rw-r--. 1 yuta yuta 112 3月 16 00:55 2014 _0.nvm -rw-rw-r--. 1 yuta yuta 395 3月 16 00:55 2014 _0.si -rw-rw-r--. 1 yuta yuta 144 3月 16 00:55 2014 _0.tvd -rw-rw-r--. 1 yuta yuta 45 3月 16 00:55 2014 _0.tvx -rw-rw-r--. 1 yuta yuta 1047 3月 16 00:55 2014 _0_Lucene41_0.doc -rw-rw-r--. 1 yuta yuta 34 3月 16 00:55 2014 _0_Lucene41_0.pay -rw-rw-r--. 1 yuta yuta 2204 3月 16 00:55 2014 _0_Lucene41_0.pos -rw-rw-r--. 1 yuta yuta 14134 3月 16 00:55 2014 _0_Lucene41_0.tim -rw-rw-r--. 1 yuta yuta 712 3月 16 00:55 2014 _0_Lucene41_0.tip -rw-rw-r--. 1 yuta yuta 20 3月 16 00:55 2014 segments.gen -rw-rw-r--. 1 yuta yuta 110 3月 16 00:55 2014 segments_2 -rw-rw-r--. 1 yuta yuta 0 3月 16 00:25 2014 write.lockquery実行
SolrはREST形式なのでUPDATEやSELECTも基本的にはcurlにて行います。solr-4.7.0/example/exampledocsにはtest_utf8.shというテストスクリプトがあるのでこれを参考に実行してみます。test_utf8.shの実行ではERROR:と表示されなければ問題ないと判断できます。test_utf8.sh中に記載されているcurlのスクリプトを真似てqueryをASUS、outputをjsonとしてSolrからのresponseを確認します。
$ ./test_utf8.sh Solr server is up. HTTP GET is accepting UTF-8 HTTP POST is accepting UTF-8 HTTP POST defaults to UTF-8 HTTP GET is accepting UTF-8 beyond the basic multilingual plane HTTP POST is accepting UTF-8 beyond the basic multilingual plane HTTP POST + URL params is accepting UTF-8 beyond the basic multilingual plane Response correctly returns UTF-8 beyond the basic multilingual plane $ curl "http://localhost:8983/solr/select?q=ASUS¶ms=explicit&wt=json" | python -mjson.tool { "response": { "docs": [ { "_version_": 1462657512030339072, "cat": [ "electronics", "graphics card" ], "features": [ "NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz", "256MB GDDR3 Memory clocked at 1.35GHz", "PCI Express x16", "Dual DVI connectors, HDTV out, video input", "OpenGL 2.0, DirectX 9.0" ], "id": "EN7800GTX/2DHTV/256M", "inStock": false, "manu": "ASUS Computer Inc.", "manu_id_s": "asus", "manufacturedate_dt": "2006-02-13T00:00:00Z", "name": "ASUS Extreme N7800GTX/2DHTV (256 MB)", "popularity": 7, "price": 479.94999999999999, "price_c": "479.95,USD", "store": "40.7143,-74.006", "weight": 16.0 } ], "numFound": 1, "start": 0 }, "responseHeader": { "QTime": 1, "params": { "params": "explicit", "q": "ASUS", "wt": "json" }, "status": 0 } }設定ファイル
solr-4.7.0/example/solr/collection1/conf/solrconfig.xmlに色々な記述が書かれているのでより深くexampleでのREST仕様を理解したい人は目を通してみると良いかもしれません。solr/selectにアクセスするとsolr.SearchHandlerClassを呼び出すなどの定義が書かれています。その他indexのschemaについてはsolr-4.7.0/example/solr/collection1/conf/schema.xmlで定義されています。bi-gram、tokenizer、filter等の指定がされています。
$ vi solr-4.7.0/example/solr/collection1/conf/solrconfig.xml <requestHandler name="/select" class="solr.SearchHandler"> <!-- default values for query parameters can be specified, these will be overridden by parameters in the request --> <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int> <str name="df">text</str> </lst> (略) $ vi solr-4.7.0/example/solr/collection1/conf/schema.xml <!-- CJK bigram (see text_ja for a Japanese configuration using morphological analysis) --> <fieldType name="text_cjk" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="solr.StandardTokenizerFactory"/> <!-- normalize width before bigram, as e.g. half-width dakuten combine --> <filter class="solr.CJKWidthFilterFactory"/> <!-- for any non-CJK --> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.CJKBigramFilterFactory"/> </analyzer> </fieldType>solrpy
solrpy - Python Solr Module - Google Project Hosting
Reference ― solrpy v0.9.2 documentation
SolrがREST形式ならclient言語は別に何だっていいんですが、僕はJavaよりPythonが好きなのでおしゃべりする言語に使用します。solrpyというライブラリがありますが日本語ドキュメントは無いので、英語版を見ましょう。solrpyには2種類のclassがあってclass solr.Solr(url)、class solr.SolrConnection(url)になります。SolrConnectionの方が記述が分かり易いのですが、Solrの方が新しいという事で以下ではclass solr.Solrを使っています。
Solr-exampleのschema.xmlを少し書き換えて配列形式のauthorを登録するような処理を書いてみます。複数のauthorが登録できるようにmultiValued="true"にします。設定しないとsolrpyのスクリプト実行時にsolr.core.SolrException: HTTP code=400, reason=Bad Requestとerrorが出ます。
$ sudo easy_install solrpy (略) Installed /usr/lib/python2.6/site-packages/solrpy-0.9.6-py2.6.egg Processing dependencies for solrpy Finished processing dependencies for solrpy # 下のtest_solrpy.pyを実行 $ python test_solrpy.py (略) solr.core.SolrException: HTTP code=400, reason=Bad Requestとerror # errorを回避する為に修正 $ vi solr-4.7.0/example/solr/collection1/conf/schema.xml <!-- multiValued属性を"true"に設定 --> <field name="author" type="text_general" indexed="true" stored="true" multiValued="true"/> <!-- author_sを追加 --> <field name="author_s" type="text_general" indexed="true" stored="true" multiValued="true"/> # solrのrestart $ java -jar start.jar # 再度実行 $ python test_solrpy.py 1 [u'Lucene in Action'] [u'Erik Hatcher', u'Otis Gospodneti\u0107']#!/usr/bin/env python #coding:utf-8 #solrpyのテスト import solr # add a document to the index con = solr.Solr('http://localhost:8983/solr') doc ={'id':1, 'title':u'Lucene in Action', 'author':[u'Erik Hatcher', u'Otis Gospodneti〓']} con.add(doc,commit=True) # do a search response = con.select('title:lucene') for hit in response.results: print hit['id'],hit['title'],hit['author']
Spatial Search
設定
SpatialSearch - Solr Wiki
SpatialSearchについては上のwikiに詳しく載っています。内容に書かれているschema.xmlのfield name="store"、fieldType name="location"は既にexampleでは定義済みなので追記は不要だと思います。$ vi solr-4.7.0/example/solr/collection1/conf/schema.xml # 以下はexampleのfieldTypeにdefaultで記載済み <field name="store" type="location" indexed="true" stored="true"/> <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>テスト
post.shにてindex化したデータに対してSpatial Searchを掛けてみると問題なく対象となるデータが抽出出来ている事が確認できます。ちょいとSpatial Searchとして指定するパラメータが複雑ですが纏めると下の表のようになります。
parameter meaning fl 結果として抽出するfield q query (下では*:*を指定) fq queryのfilter(下では{!geofilt}を指定) sfield Spatial Searchの対象field pt 検索位置のlatlng d ptからの検索対象距離を指定 wt 出力形式
$ curl "http://localhost:8983/solr/select?fl=name,store&q=*%3A*&fq=%7B%21geofilt%7D&sfield=store&pt=45.15,-93.85&d=5&wt=json" | python -mjson.tool { "response": { "docs": [ { "name": "Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300", "store": "45.17614,-93.87341" }, { "name": "Belkin Mobile Power Cord for iPod w/ Dock", "store": "45.18014,-93.87741" }, { "name": "A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM", "store": "45.18414,-93.88141" } ], "numFound": 3, "start": 0 }, "responseHeader": { "QTime": 0, "params": { "d": "5", "fl": "name,store", "fq": "{!geofilt}", "pt": "45.15,-93.85", "q": "*:*", "sfield": "store", "wt": "json" }, "status": 0 } }実用データでテスト
ソニア - 新橋/お好み焼き [食べログ]
広島風お好み焼きがとても美味しい上のお店のデータでテストしてみます。冒頭の写真はお店のお好み焼き(シングルというメニュー)です。新橋/汐留近辺ではとても有名なお店だと思うので近い方は一度行ってみてください。さて、YahooのGeocoderAPIを使ってお店の住所からlatlngを出し、SolrのIndexに格納しSpatialSearchをやってみます。
食べログの案内に汐留駅から372mと書かれており、パラメータd=0.3では抽出不可、d=0.4=抽出可能という点から正確にSpatialSearchが出来たと思います。#!/usr/bin/env python #coding:utf-8 #住所データからSpatial Indexを作成 #latlon.pyとして保存する import urllib,urllib2,json,solr opener = urllib2.build_opener() ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/ 534.51.22' referer = 'http://www.yahoo.co.jp/' opener.addheaders = [('User-Agent', ua),('Referer', referer)] appid = 'Yahoo!' # yahooapiのappidをここに定義 address = u'東京都港区新橋5-15-1' try: url = 'http://geo.search.olp.yahooapis.jp/OpenLocalPlatform/V1/geoCoder?appid=%s&query=%s&output=%s' % (appid, urllib.quote_plus(address.encode('utf-8')), 'json') res = json.loads(opener.open(url).read()) if (res['ResultInfo']['Status'] == 200 and res['ResultInfo']['Count'] > 0 ): (lng,lat) = res['Feature'][0]['Geometry']['Coordinates'].split(',') latlng = lat + ',' + lng except urllib2.URLError: print "Error: API" try: con = solr.Solr('http://localhost:8983/solr') doc ={'id':'sonia_latlon', 'name':u'ソニア', 'store':latlng} con.add(doc,commit=True) #汐留駅からの0.5KMの距離で計算 response = con.select('*:*', 'id,name,store', '', 'true', 'id', 'desc', fq='{!geofilt}', sfield='store', d='0.4', pt='35.662800,139.760000') for hit in response.results: print hit['id'], hit['name'], hit['store'] except solr.SolrException: print "Error: Solr"$ python latlon.py sonia_latlon ソニア 35.66223082,139.75596233