ããã«ã¡ã¯ãããªãã¼æ ªå¼ä¼ç¤¾ã®å è¤ã§ããHoliday ( https://haveagood.holiday/ ) ã¨ãããµã¼ãã¹ã®éçºãè¡ã£ã¦ãã¾ãã
å æ¥éå¬ãã Cookpad TechConf 2016 ã§ã¯ãããã§ããã¹ãããæ¤ç´¢ã®ããããã - Holiday ãæ¯ããæ¤ç´¢æè¡ãã¨ããé¡ã§çºè¡¨ãè¡ãã¾ããã
ãã®çºè¡¨ã§ã¯ã
- ãã§ããã¹ãããã®æ¤ç´¢ã§ã¯ãå ¨ææ¤ç´¢ã ãã§ã¯æºè¶³ã®ããçµæã¯å¾ãããªã
- å°ç空éæ¤ç´¢ã«æ¡å¼µãããã¨ã§ããããæ¤ç´¢ä½é¨ãä½ããã¨ãå¯è½
- ãããå®ç¾ããããã® Elasticsearch ã®æ©è½ãç´¹ä»
ã¨ãããããªå 容ãç´¹ä»ãã¾ããã
ä¾ãã°ãæã ããä¸ç®é»ããæãæµ®ãã¹ãæã«ã¤ã¡ã¼ã¸ããã¨ãªã¢å ã®ä½æã«ã¯ããä¸ç®é»ãã¨ããæååãå«ã¾ãã¦ãã¾ããã ã¾ããã奥æ¸è°·ãã®ããã«ä½æã¨ãã¦ã¯ç¹å®ã§ããªããã ããããã®è¾ºããã¨ãããããªã¨ãªã¢ãåå¨ãã¾ãã ãããã¯ããä¸ç®é»é§ ãã¨èª¿ã¹ãã¦ã¼ã¶ã¯ããé§ ããã®è·é¢ãèæ ®ãã並ã³é ã§çµæãè¦ãããã¨èãã¦ãããã ã¨æ³åã§ãããã®ã®ãæååããã¯è·é¢ãè¨æ¸¬ãããã¨ã¯ã§ãã¾ããã
ãã®ããã«ãæ¤ç´¢ã¯ã¨ãªãæååã¨ãã¦æ±ãã ãã§ã¯ãæ¤ç´¢ã¯ã¨ãªã«è¾¼ããããã¦ã¼ã¶ã®æå³ãæ±²ã¿ã¨ã£ãæ¤ç´¢çµæãè¿ããã¨ãé£ãããªãã¾ãã
以åã®è¨äº ã§ãç´¹ä»ããããã«ãElasticsearch ã«ã¯ä½ç½®æ å ±ãå©ç¨ããæ¤ç´¢æ©è½ãç¨æããã¦ãã¾ãã ãããã®æ©è½ãæ´»ç¨ãããã¨ã§ãå ¨ææ¤ç´¢ã¨å°ç空éæ¤ç´¢ãçµã¿åããã¦ãããæºè¶³åº¦ã®é«ãæ¤ç´¢ä½é¨ãä½ããã¨ãã§ãã¾ãã
ãã ããããå°ç空éæ¤ç´¢æ©è½ã¯ãå ¬å¼ã¬ã¤ã ãThe Definitive Guideã ã§ãè¨åããã¦ããããã«ãæååããããªã©ã®æ¤ç´¢æ¡ä»¶ã«æ¯ã¹ã¦è¨ç®ã³ã¹ãã®é«ãå¦çã«ãªãã¾ãã
Geo-filters are expensiveâââthey should be used on as few documents as possible. First remove as many documents as you can with cheaper filters, like term or range filters, and apply the geo-filters last.
ãã®ãããªé«ã³ã¹ããªå¦çãæ¤ç´¢æã«æ¯åè¡ãã®ã¯ãã¾ãæã¾ããããã¾ããã ã§ãããªãã°é«è² è·ãªå¦çã¯äºåã«æ¸ã¾ãã¦ãããæ¤ç´¢æã«ã¯ä½ã³ã¹ããªå¦çã®ã¿ãè¡ãããã¨ããæããããã¾ãã
ä»åã¯ããã®ãã¼ãºãæºããããã«ãElasticsearch ã«ç¨æããã¦ãã Percolator ã¨ããæ©è½ã使ã£ã¦ã¿ããã¨æãã¾ãã
Percolator ã¨ã¯
é常ã®æ¤ç´¢æã«ã¯ããããããç»é²ããããã¥ã¡ã³ãã«å¯¾ãã¦æ¤ç´¢ã¯ã¨ãªãçºè¡ãããã¨ã§ãç®çã®æ å ±ãæ¢ãã¾ãã ä¸æ¹ã§ Percolator ã¯ããã¨ã¯å ¨ãéã§ããããããç»é²ãã¦ãããæ¤ç´¢ã¯ã¨ãªã«å¯¾ãã¦ããã¥ã¡ã³ããå½ã¦ããã¨ã§ãåè´ããæ¤ç´¢æ¡ä»¶ããªãããå¤å®ãã¾ãã
ãããã使ãæ¹ã¨ãã¦ã¯ãä¾ãã°ECãµã¤ãã«ããã¦ãããååã«èå³ãããã¦ã¼ã¶ã«å¯¾ãã¦ããã®ååãå ¥è·ãããæã«ãç¥ãããéãã¨ãããããªã±ã¼ã¹ãããåãä¸ãããã¾ãã
ä»åã¯ä¸è¨ã®ãããªä½¿ãæ¹ã§ã¯ãªããããã¹ãããããã©ããã£ãã¨ãªã¢ã«å±ããã®ããããã©ã®é§ ã®å¨è¾ºã«ä½ç½®ãã¦ããã®ããã¨ãã£ãå±æ§æ å ±ããPercolator ã®ä»çµã¿ã使ã£ã¦åå¾ãã¦ã¿ããã¨æãã¾ãã
ãªããå®è¡ç°å¢ã¯ä»¥ä¸ã®ã¨ããã§ãã
- Elasticsearch 2.1 *1
- Ruby 2.2.4
- Rails 4.2.6
- ã¯ã©ã¤ã¢ã³ãã«ã¯
elasticsearch-rails
gem ã使ç¨
- ã¯ã©ã¤ã¢ã³ãã«ã¯
æ¬ç¨¿ã§ä½¿ç¨ãããµã³ãã«ã³ã¼ãã¯ãGitHub ä¸ã§å ¬éãã¦ãã¾ãã
ãã¼ã¿ãæºåãã
ã¾ãã¯å¿ è¦ãªãã¼ã¿ãæºåãã¾ãã
ä»åã¯ã以ä¸ã®ãããªã¹ãã¼ãã使ãã¾ãã
ActiveRecord::Schema.define(version: 20160314093426) do create_table "areas", force: :cascade do |t| t.string "name" t.text "coordinates" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "spots", force: :cascade do |t| t.string "name" t.string "address" t.decimal "lat", precision: 9, scale: 6 t.decimal "lon", precision: 9, scale: 6 t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "stations", force: :cascade do |t| t.string "name" t.decimal "lat", precision: 9, scale: 6 t.decimal "lon", precision: 9, scale: 6 t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
ãµã³ãã«ãã¼ã¿ã¯ db/seeds.rb
å
ã«ç¨æãã¦ããã®ã§ã以ä¸ã®ã³ãã³ããå®è¡ããã°å¿
è¦ãªãã¼ã¿ãä½ãããã¯ãã§ãã
$ bundle exec rake db:create $ bundle exec rake db:migrate $ bundle exec rake db:seed
ãã§ããã¹ãããã®æ å ±ã¯ Elasticsearch ä¸ã§ã¯æ¬¡ã®ãããªã¹ãã¼ãã§æ ¼ç´ãããã¨ã«ãã¾ãã
class Spot < ActiveRecord::Base include Elasticsearch::Model index_name "#{Rails.env}-#{Rails.application.class.to_s.downcase}-#{self.name.downcase}" mapping do indexes :id, type: 'string', index: 'not_analyzed' indexes :name, type: 'string', analyzer: 'kuromoji' indexes :address, type: 'string', analyzer: 'kuromoji' indexes :location, type: 'geo_point' end def as_indexed_json(options = {}) { 'id' => id, 'name' => name, 'address' => address, 'location' => "#{lat},#{lon}", } end end
Elasticsearch ã«ããã¥ã¡ã³ããç»é²ããããã以ä¸ã®ã¿ã¹ã¯ãå®è¡ãã¾ãã
$ bundle exec rake environment elasticsearch:import:model CLASS='Spot' FORCE=y
ããã§ãäºåæºåã¯å®äºã§ãã
ããã¹ããããå«ãã§ããã¨ãªã¢ãåå¾ãã
ã§ã¯ãããã¹ããããå«ãã§ããã¨ãªã¢ãåå¾ãã¦ã¿ã¾ãã
ä»åããä¸ç®é»ã¨ãªã¢ãã以ä¸ã®éæ ã§å²ã¾ããç¯å²ã¨è¨å®ãã¾ããã
]
ãã®å¤è§å½¢ã®ã¨ãªã¢ãã¼ã¿ã¯ä»¥ä¸ã®ããã«è¡¨ç¾ããã¾ãã
areas = [ { name: "ä¸ç®é»ã¨ãªã¢", coordinates: [ [139.69300746917725, 35.64788092832728], # æåã¨æå¾ã¯åãå¤ãæå®ãã [139.6939516067505, 35.644114489675275], [139.69064712524414, 35.64059201137997], [139.69892978668213, 35.63846449878154], [139.70317840576172, 35.643033349486984], [139.70073223114014, 35.64721832699104], [139.69300746917725, 35.64788092832728] # æåã¨æå¾ã¯åãå¤ãæå®ãã ] } ]
Area ã«ã¯å称 name
ã¨å¤è§å½¢ã®é ç¹ã表ã座æ¨ãã¼ã¿ coordinates
ãæã¡ã¾ãã
ãã® coordinates
㯠[çµåº¦, 緯度] ãããªãé
åã§ãDBã«ã¯ã·ãªã¢ã©ã¤ãºãããç¶æ
ã§æ ¼ç´ããã¾ãã
class Area < ActiveRecord::Base serialize :coordinates end
æ¤ç´¢ã¯ã¨ãªãã¤ã³ããã¯ã¹ãã
ãã座æ¨ãç¹å®ã®å¤è§å½¢ã«å«ã¾ãããã©ãããæ¤ç´¢ããã«ã¯ãGeo Polygon Query ã使ãã¾ãã
elasticsearch-rails
gem ã使ã£ãå ´åãPercolator ã®ç»é²ã¯æ¬¡ã®ããã«è¡ãã¾ãã
# app/models/spot.rb class Spot < ActiveRecord::Base ... # id: æ¤ç´¢ã¯ã¨ãªæ¯ã«ã¦ãã¼ã¯ãªID # body: æ¤ç´¢æ¡ä»¶ def self.index_percolator(id, body) args = { index: self.__elasticsearch__.index_name, type: '.percolator', id: id, body: body, } self.__elasticsearch__.client.index(args) end end
ãã®ããã«ãPercolator ã®ç»é²å¦çã¯ãé常ã®ã¤ã³ããã¯ã¹å¦çã¨ã»ã¨ãã©å¤ããããéã㯠.percolator
ã¨ããç¹å¥ãª type åãç¨ããã¨ãããã¨ã ãã§ãã
å¼ã³åºãå´ã®è¨è¿°ã¯ä»¥ä¸ã®ããã«ãªãã¾ãã
class Area < ActiveRecord::Base ... def self.create_polygon_percolators Area.all.each do |area| id = "area-polygon-#{area.id}" # e.g. `area-polygon-1` body = { query: { filtered: { query: { match_all: {} }, filter: { geo_polygon: { location: { points: area.coordinates } } } } } } Spot.index_percolator(id, body) end end end
ã§ã¯ãrails console ãèµ·åãããã®ã¡ã½ãããå®è¡ãã¾ãã
$ rails console
Area.create_polygon_percolators
ãã㧠Elasticsearch ä¸ã® Spot ã¤ã³ããã¯ã¹ã«å¯¾ãã¦ãGeo Polygon Query ã®æ¤ç´¢æ¡ä»¶ãç»é²ããã¾ããã
å®è¡ãã
ã§ã¯ããã¹ã¿ã¼ããã¯ã¹ã³ã¼ãã¼ä¸ç®é»é§ ååºãã¨ããã¹ãããããä¸ç®é»ã¨ãªã¢ãã«å±ãã¦ããããå®éã«ç¢ºããã¦ã¿ã¾ãã ãã®ã¹ãããã¯ä¸ç®é»ã²ã¼ãã¿ã¦ã³ã¿ã¯ã¼å ã«ä½ç½®ãããããæ£ããåä½ãã¦ããã°ãå«ã¾ãããã¨å¤å®ãããã¯ãã§ãã
elasticsearch-rails
gem ã使ã£ãå ´åãPercolator ã¯ã¨ãªãå®è¡ããå¦çã¯ä»¥ä¸ã®ããã«è¨è¿°ãã¾ãã
class Spot < ActiveRecord::Base ... def percolate Spot.__elasticsearch__.client.percolate( index: Spot.__elasticsearch__.index_name, type: Spot.__elasticsearch__.document_type, body: { doc: { location: { lat: lat, lon: lon, } } } ) end
ã§ã¯ãrails console ãèµ·åãã以ä¸ã®å¦çãå®è¡ãã¦ã¿ã¾ãã
$ rails console spot = Spot.find_by(name: 'ã¹ã¿ã¼ããã¯ã¹ã³ã¼ãã¼ä¸ç®é»é§ ååº') spot.latlon => [35.643602, 139.699077] spot.percolate => { "took" => 1, "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 }, "total" => 1, "matches" => [ { "_index" => "development-espercolatorsample::application-spot", "_id" => "area-polygon-1" } ] }
ãã®ããã«ã ã¬ã¹ãã³ã¹å
ã® matches
ãã£ã¼ã«ãã«å
ã»ã©æå®ããIDãå«ã¾ãã¦ãããã¨ããããã¾ãã
matches
ã«ã¯ããã®ããã¥ã¡ã³ãã解ã¨ããæ¤ç´¢æ¡ä»¶ã®IDãå«ã¾ãã¾ãã
ã§ã¯ããæµæ¯å¯¿ã¬ã¼ãã³ãã¬ã¤ã¹ãããä¸ç®é»ã¨ãªã¢ãã«å«ã¾ãã¦ãããã©ããã確ããã¦ã¿ã¾ãããã
spot = Spot.find_by(name: 'æµæ¯å¯¿ã¬ã¼ãã³ãã¬ã¤ã¹') spot.latlon => [35.642186, 139.713309] spot.percolate => { "took" => 1, "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 }, "total" => 0, "matches" => [] }
å½ç¶ããã¯ãå«ã¾ãã¦ããªããã¨å¤æããã¾ãã
以ä¸ã®ããã«ãäºåã«ç»é²ãã¦ããã Geo Polygon Query ã«å¯¾ãã¦ã¹ãããæ å ±ï¼ç·¯åº¦çµåº¦ï¼ãæãããã¨ã§ããã®ã¹ãããã解ã¨ããæ¤ç´¢æ¡ä»¶ãåå¾ãããã¨ãã§ãã¾ããã ãã¨ã¯ãæ¤ç´¢æ¡ä»¶ã®IDããã¨ãªã¢IDãæ½åºããã°ãããã¹ããããã©ã®ã¨ãªã¢ã«å«ã¾ããã®ããç¥ããã¨ãã§ãã¾ãã
å¨è¾ºã®é§ ãç¹å®ãã
次ã«ãããã¹ããããã©ã®é§ ã®å¨è¾ºã«ããã®ããå¤å¥ãã¾ãã
æ¤ç´¢ã¯ã¨ãªãã¤ã³ããã¯ã¹ãã
ããä¸å¿ç¹ããã®ä¸å®è·é¢å ã«å«ã¾ããå°ç¹ãæ¤ç´¢ããã®ã«ã¯ã Geo Distance Query ãç¨ãã¾ãã
class Station < ActiveRecord::Base def self.create_distance_percolators(radius: 1000) Station.all.each do |station| id = "station-distance-#{station.id}-#{radius}" # e.g. `station-distance-1-1000` body = { query: { filtered: { filter: { geo_distance: { location: { lat: station.lat, lon: station.lon, }, distance: "#{radius}meters", } } } } } Spot.index_percolator(id, body) end end end
å ã»ã©ã¨åæ§ã«ãrails console ãèµ·åãããã®ã¡ã½ãããå®è¡ãã¾ãã
$ rails console
Area.create_polygon_percolators
ãã㧠Geo Distance Query
ã®æ¤ç´¢æ¡ä»¶ããPercolator ã¨ãã¦ç»é²ãããã¨ãã§ãã¾ããã
å®è¡ãã
ã§ã¯ããã¹ã¿ã¼ããã¯ã¹ã³ã¼ãã¼ä¸ç®é»é§ ååºãã¨ããã¹ãããããã©ã®é§ ã®å¨è¾ºï¼åå¾1000m以å ï¼ã«ããã®ãã調ã¹ã¦ã¿ã¾ãã
spot = Spot.find_by(name: 'ã¹ã¿ã¼ããã¯ã¹ã³ã¼ãã¼ä¸ç®é»é§ ååº') spot.latlon => [35.643602, 139.699077] spot.percolate => { "took" => 1, "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 }, "total" => 2, "matches" =>[ { "_index" => "development-espercolatorsample::application-spot", "_id" => "station-distance-1-1000" # station_id: 1 => ä¸ç®é»é§ }, { "_index" => "development-espercolatorsample::application-spot", "_id" => "station-distance-3-1000" # station_id: 3 => 代å®å±±é§ }, { "_index" => "development-espercolatorsample::application-spot", "_id" => "area-polygon-1" } ] }
ãã®ããã«ãä¸ç®é»é§ ï¼ID: 1ï¼ã¨ä»£å®å±±é§ ï¼ID: 3ï¼ãã¬ã¹ãã³ã¹ã«å«ã¾ãã¦ãã¾ãã ããã¯ããã®ã¹ãããããããã®é§ ããåå¾1000m以å ã®ä½ç½®ã«åå¨ãã¦ãããã¨ã示ãã¦ãã¾ãã
ã¾ããå®è¡çµæãããè¦ã¦ã¿ãã¨ãå ã®æ®µè½ã§ä½æãã Geo Polygon Query ã«ã¤ãã¦ãããããããæ¡ä»¶ã«å«ã¾ãã¦ãããã¨ãåããã¾ãã
Percolator ã使ãã¨ãããããã¥ã¡ã³ããä¸åº¦æããã ãã§ããã®ããã¥ã¡ã³ãããæ¤ç´¢æ¡ä»¶ã«åè´ãã¦ãããã¨ã¿ãªããã¹ã¦ã®æ¤ç´¢ã¯ã¨ãªãä¸æ°ã«åå¾ãããã¨ãã§ããã®ã§ãã
ã¾ã¨ã
Percolator ã§ã¯ãããã¾ã§æ¤ç´¢æã«ä½¿ã£ã¦ããã¯ã¨ãªã .percolator
ã¨ãã type åã§ã¤ã³ããã¯ã¹ããã ãã§ä½¿ããããã«ãªãã¾ãã
å¾ã¯ã#percolate
ã¡ã½ãããå¼ã³åºãã ãã§ããã®ããã¥ã¡ã³ãã®å±æ§æ
å ±ãåå¾ãããã¨ãå¯è½ã§ãã
以ä¸ã®ããã«ãPercolator ãç¨ããã¨ã³ã³ãã³ãã®å±æ§æ å ±ãå¾ãã³ã¼ããæ°è¦ã«æ¸ãã Elasticsearch å ã«å ±éåã§ããã®ã§ãã·ã³ãã«ãªå½¢ã§å®ç¾ãããã¨ãã§ãã¾ãã
ä»åç´¹ä»ãã使ãæ¹ä»¥å¤ã«ããä¸é©åãªã¯ã¼ããå«ãã³ã³ãã³ãã®æ稿ãæ¤ç¥ããããæ稿å 容ãããã¼ã¯ã¼ããæ½åºãã¦èªåçã«ã¿ã®ã³ã°ããã¨ãããããªãã¨ãå¯è½ã§ãã
ãã®è¨äºããPercolator ã試ãã¦ã¿ããããªã¨ããæ¹ã«ã¨ã£ã¦å°ãã§ãåèã«ãªãã°å¹¸ãã§ãã