Permalink
Browse files

Clean up and apply geodistance patch (Jeremy Seitz, Mark Lane).

  • Loading branch information...
1 parent 8ebc11e commit 807632c8caa807b9826b475964bf3f97d2ac1f2c @evan committed Mar 24, 2008
Showing with 556 additions and 93 deletions.
  1. +2 −0 CHANGELOG
  2. +10 −2 Manifest
  3. +1 −0 README
  4. +2 −2 examples/default.base
  5. +46 −3 lib/ultrasphinx/search.rb
  6. +51 −5 lib/ultrasphinx/search/internals.rb
  7. +9 −9 test/integration/app/app/controllers/addresses_controller.rb
  8. +9 −9 test/integration/app/app/controllers/states_controller.rb
  9. +5 −0 test/integration/app/app/models/category.rb
  10. +1 −1 test/integration/app/app/models/geo/address.rb
  11. +2 −2 test/integration/app/app/views/addresses/edit.html.erb
  12. +2 −2 test/integration/app/app/views/addresses/index.html.erb
  13. +1 −1 test/integration/app/app/views/addresses/new.html.erb
  14. +2 −2 test/integration/app/app/views/states/edit.html.erb
  15. +2 −2 test/integration/app/app/views/states/index.html.erb
  16. +1 −1 test/integration/app/app/views/states/new.html.erb
  17. +3 −3 test/integration/app/app/views/users/index.html.erb
  18. +1 −0 test/integration/app/config/environment.rb
  19. +2 −2 test/integration/app/config/ultrasphinx/default.base
  20. +26 −10 test/integration/app/config/ultrasphinx/development.conf.canonical
  21. +3 −3 test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb
  22. +14 −0 test/integration/app/db/migrate/010_create_categories.rb
  23. +15 −0 test/integration/app/db/migrate/011_categories_sellers.rb
  24. +77 −6 test/integration/app/test/fixtures/addresses.yml
  25. +101 −0 test/integration/app/test/fixtures/categories.yml
  26. +29 −0 test/integration/app/test/fixtures/categories_sellers.yml
  27. +9 −4 test/integration/app/test/functional/addresses_controller_test.rb
  28. +9 −2 test/integration/app/test/functional/sellers_controller_test.rb
  29. +10 −4 test/integration/app/test/functional/states_controller_test.rb
  30. +7 −2 test/integration/app/test/functional/users_controller_test.rb
  31. +1 −1 test/integration/app/test/unit/address_test.rb
  32. +8 −0 test/integration/app/test/unit/category_test.rb
  33. +1 −1 test/integration/app/test/unit/country_test.rb
  34. +1 −1 test/integration/app/test/unit/state_test.rb
  35. +1 −1 test/integration/app/test/unit/user_test.rb
  36. +67 −6 test/integration/search_test.rb
  37. +14 −3 test/setup.rb
  38. +8 −0 test/teardown.rb
  39. +3 −3 test/test_helper.rb
View
@@ -1,4 +1,6 @@
+v1.9.2. Add geodistance support (Jeremy Seitz, Mark Lane).
+
v1.9.1. Add ultrasphinx:index:merge task for index merging, and a note in DEPLOYMENT_NOTES about how to use it.
v1.9. Delta indexing. ERb now supported in .base files. Allow setting the searched indexes at runtime.
View
@@ -37,6 +37,7 @@ test/integration/app/app/helpers/application_helper.rb
test/integration/app/app/helpers/sellers_helper.rb
test/integration/app/app/helpers/states_helper.rb
test/integration/app/app/helpers/users_helper.rb
+test/integration/app/app/models/category.rb
test/integration/app/app/models/geo/address.rb
test/integration/app/app/models/geo/country.rb
test/integration/app/app/models/geo/state.rb
@@ -71,7 +72,6 @@ test/integration/app/config/environments/test.rb
test/integration/app/config/locomotive.yml
test/integration/app/config/routes.rb
test/integration/app/config/ultrasphinx/default.base
-test/integration/app/config/ultrasphinx/development.conf
test/integration/app/config/ultrasphinx/development.conf.canonical
test/integration/app/db/migrate/001_create_users.rb
test/integration/app/db/migrate/002_create_sellers.rb
@@ -82,7 +82,8 @@ test/integration/app/db/migrate/006_add_deleted_to_user.rb
test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb
test/integration/app/db/migrate/008_add_mission_statement_to_seller.rb
test/integration/app/db/migrate/009_create_countries.rb
-test/integration/app/db/schema.rb
+test/integration/app/db/migrate/010_create_categories.rb
+test/integration/app/db/migrate/011_categories_sellers.rb
test/integration/app/doc/README_FOR_APP
test/integration/app/public/404.html
test/integration/app/public/500.html
@@ -115,6 +116,8 @@ test/integration/app/script/process/spawner
test/integration/app/script/runner
test/integration/app/script/server
test/integration/app/test/fixtures/addresses.yml
+test/integration/app/test/fixtures/categories.yml
+test/integration/app/test/fixtures/categories_sellers.yml
test/integration/app/test/fixtures/countries.yml
test/integration/app/test/fixtures/sellers.yml
test/integration/app/test/fixtures/states.yml
@@ -125,6 +128,7 @@ test/integration/app/test/functional/states_controller_test.rb
test/integration/app/test/functional/users_controller_test.rb
test/integration/app/test/test_helper.rb
test/integration/app/test/unit/address_test.rb
+test/integration/app/test/unit/category_test.rb
test/integration/app/test/unit/country_test.rb
test/integration/app/test/unit/seller_test.rb
test/integration/app/test/unit/state_test.rb
@@ -153,6 +157,7 @@ vendor/riddle/README
vendor/riddle/spec/fixtures/data/anchor.bin
vendor/riddle/spec/fixtures/data/any.bin
vendor/riddle/spec/fixtures/data/boolean.bin
+vendor/riddle/spec/fixtures/data/comment.bin
vendor/riddle/spec/fixtures/data/distinct.bin
vendor/riddle/spec/fixtures/data/field_weights.bin
vendor/riddle/spec/fixtures/data/filter.bin
@@ -166,6 +171,8 @@ vendor/riddle/spec/fixtures/data/filter_range_exclude.bin
vendor/riddle/spec/fixtures/data/group.bin
vendor/riddle/spec/fixtures/data/index.bin
vendor/riddle/spec/fixtures/data/index_weights.bin
+vendor/riddle/spec/fixtures/data/keywords_with_hits.bin
+vendor/riddle/spec/fixtures/data/keywords_without_hits.bin
vendor/riddle/spec/fixtures/data/phrase.bin
vendor/riddle/spec/fixtures/data/rank_mode.bin
vendor/riddle/spec/fixtures/data/simple.bin
@@ -179,6 +186,7 @@ vendor/riddle/spec/fixtures/sql/conf.example.yml
vendor/riddle/spec/fixtures/sql/data.sql
vendor/riddle/spec/fixtures/sql/structure.sql
vendor/riddle/spec/functional/excerpt_spec.rb
+vendor/riddle/spec/functional/keywords_spec.rb
vendor/riddle/spec/functional/search_spec.rb
vendor/riddle/spec/functional/update_spec.rb
vendor/riddle/spec/spec_helper.rb
View
1 README
@@ -32,6 +32,7 @@ Features include:
* spellcheck
* faceting on text, date, and numeric fields
* field weighting, merging, and aliases
+* sorting and filtering by geodistance
* <tt>belongs_to</tt> and <tt>has_many</tt> includes
* drop-in compatibility with will_paginate[http://err.lighthouseapp.com/projects/466/home]
* drop-in compatibility with Interlock[http://blog.evanweaver.com/files/doc/fauna/interlock/]
View
@@ -34,7 +34,7 @@ searchd
{
# What interface the search daemon should listen on and where to store its logs
address = 0.0.0.0
- port = 3312
+ port = 3313
seamless_rotate = 1
log = <%= path %>log/searchd.log
query_log = <%= path %>log/query.log
@@ -52,7 +52,7 @@ client
# How your application connects to the search daemon (not necessarily the same as above)
server_host = localhost
- server_port = 3312
+ server_port = 3313
}
# Individual SQL source options
View
@@ -42,18 +42,49 @@ module Ultrasphinx
<tt>:weights</tt>:: A hash. Text-field names and associated query weighting. The default weight for every field is 1.0. Example: <tt>:weights => {'title' => 2.0}</tt>
<tt>:filters</tt>:: A hash. Names of numeric or date fields and associated values. You can use a single value, an array of values, or a range. (See the bottom of the ActiveRecord::Base page for an example.)
<tt>:facets</tt>:: An array of fields for grouping/faceting. You can access the returned facet values and their result counts with the <tt>facets</tt> method.
+<tt>:location</tt>:: A hash. Specify the names of your latititude and longitude attributes as declared in your is_indexed calls. To sort the results by distance, set <tt>:sort_mode => 'extended'</tt> and <tt>:sort_by => 'distance asc'.</tt>
<tt>:indexes</tt>:: An array of indexes to search. Currently only <tt>Ultrasphinx::MAIN_INDEX</tt> and <tt>Ultrasphinx::DELTA_INDEX</tt> are available. Defaults to both; changing this is rarely needed.
+== Query Defaults
+
Note that you can set up your own query defaults in <tt>environment.rb</tt>:
self.class.query_defaults = HashWithIndifferentAccess.new({
- :per_page => 10,
+ :per_page => 10,c
:sort_mode => 'relevance',
:weights => {'title' => 2.0}
})
= Advanced features
+== Geographic distance
+
+If you pass a <tt>:location</tt> Hash, distance from the location in meters will be available in your result records via the <tt>distance</tt> accessor:
+
+ @search = Ultrasphinx::Search.new(:class_names => 'Point',
+ :query => 'pizza',
+ :sort_mode => 'extended',
+ :sort_by => 'distance',
+ :location => {
+ :lat => 40.3,
+ :long => -73.6
+ })
+
+ @search.run.first.distance #=> 1402.4
+
+Note that Sphinx expects lat/long to be indexed as radians. If you have degrees in your database, do the conversion in the <tt>is_indexed</tt> as so:
+
+ is_indexed 'fields' => [
+ 'name',
+ 'description',
+ {:field => 'lat', :function_sql => "RADIANS(?)"},
+ {:field => 'lng', :function_sql => "RADIANS(?)"}
+ ]
+
+Then, set <tt>Ultrasphinx::Search.client_options[:location][:units] = 'degrees'</tt>.
+
+The <tt>:double</tt> column type is recommended for storing location data.
+
== Interlock integration
Ultrasphinx uses the <tt>find_all_by_id</tt> method to instantiate records. If you set <tt>with_finders: true</tt> in {Interlock's}[http://blog.evanweaver.com/files/doc/fauna/interlock] <tt>config/memcached.yml</tt>, Interlock overrides <tt>find_all_by_id</tt> with a caching version.
@@ -108,7 +139,12 @@ class Search
:weights => {},
:class_names => [],
:filters => {},
- :facets => []
+ :facets => [],
+ :location => HashWithIndifferentAccess.new({
+ :lat_attribute_name => 'lat',
+ :long_attribute_name => 'lng',
+ :units => 'radians'
+ })
})
cattr_accessor :excerpting_options
@@ -262,12 +298,19 @@ def initialize opts = {}
opts = Hash[HashWithIndifferentAccess.new(opts._deep_dup._coerce_basic_types)]
unless self.class.query_defaults.instance_of? Hash
self.class.query_defaults = Hash[self.class.query_defaults]
+ self.class.query_defaults['location'] = Hash[self.class.query_defaults['location']]
+
self.class.client_options = Hash[self.class.client_options]
self.class.excerpting_options = Hash[self.class.excerpting_options]
self.class.excerpting_options['content_methods'].map! {|ary| ary.map {|m| m.to_s}}
end
+
+ # We need an annoying deep merge on the :location parameter
+ opts['location'].reverse_merge!(self.class.query_defaults['location']) if opts['location']
+
+ # Merge the rest of the defaults
+ @options = self.class.query_defaults.merge(opts)
- @options = self.class.query_defaults.merge(opts)
@options['query'] = @options['query'].to_s
@options['class_names'] = Array(@options['class_names'])
@options['facets'] = Array(@options['facets'])
@@ -2,6 +2,9 @@
module Ultrasphinx
class Search
module Internals
+
+ INFINITY = 1/0.0
+
include Associations
# These methods are kept stateless to ease debugging
@@ -11,6 +14,8 @@ module Internals
def build_request_with_options opts
request = Riddle::Client.new
+
+ # Basic options
request.instance_eval do
@server = Ultrasphinx::CLIENT_SETTINGS['server_host']
@port = Ultrasphinx::CLIENT_SETTINGS['server_port']
@@ -20,8 +25,29 @@ def build_request_with_options opts
@max_matches = [@offset + @limit + Ultrasphinx::Search.client_options['max_matches_offset'], MAX_MATCHES].min
end
+ # Geosearch location
+ loc = opts['location']
+ loc.stringify_keys!
+ lat, long = loc['lat'], loc['long']
+ if lat and long
+ # Convert degrees to radians, if requested
+ if loc['units'] == 'degrees'
+ lat = degrees_to_radians(lat)
+ long = degrees_to_radians(long)
+ end
+ # Set the location/anchor point
+ request.set_anchor(loc['lat_attribute_name'], lat, loc['long_attribute_name'], long)
+ end
+
# Sorting
sort_by = opts['sort_by']
+ if options['location']
+ case sort_by
+ when "distance asc", "distance" then sort_by = "@geodist asc"
+ when "distance desc" then sort_by = "@geodist desc"
+ end
+ end
+
# Use the additional sortable column if it is a text type
sort_by += "_sortable" if Fields.instance.types[sort_by] == "text"
@@ -65,13 +91,19 @@ def build_request_with_options opts
end
# Extract raw filters
- # XXX This is poorly done. We should coerce based on the Field types, not the value class
+ # XXX This is poorly done. We should coerce based on the Field types, not the value class.
+ # That would also allow us to move numeric filters from the query string into the hash.
Array(opts['filters']).each do |field, value|
- field = field.to_s
- type = Fields.instance.types[field]
- unless type
- raise UsageError, "field #{field.inspect} is invalid"
+
+ field = field.to_s
+ type = Fields.instance.types[field]
+
+ # Special derived attribute
+ if field == 'distance' and options['location']
+ field, type = '@geodist', 'float'
end
+
+ raise UsageError, "field #{field.inspect} is invalid" unless type
begin
case value
@@ -292,6 +324,16 @@ def reify_results(ids)
end
end
end
+
+ # Add an accessor for distance, if requested
+ if self.options['location']['lat'] and self.options['location']['long']
+ results.each_with_index do |result, index|
+ if result
+ distance = (response[:matches][index][:attributes]['@geodist'] or INFINITY)
+ result.instance_variable_get('@attributes')['distance'] = distance
+ end
+ end
+ end
results.compact!
@@ -335,6 +377,10 @@ def strip_query_commands(s)
# Also removes apostrophes in the middle of words so that they don't get split in two.
s.gsub(/(^|\s)(AND|OR|NOT|\@\w+)(\s|$)/i, "").gsub(/(\w)\'(\w)/, '\1\2')
end
+
+ def degrees_to_radians(value)
+ Math::PI * value / 180.0
+ end
end
end
@@ -2,7 +2,7 @@ class AddressesController < ApplicationController
# GET /addresses
# GET /addresses.xml
def index
- @addresses = Address.find(:all)
+ @addresses = Geo::Address.find(:all)
respond_to do |format|
format.html # index.html.erb
@@ -13,7 +13,7 @@ def index
# GET /addresses/1
# GET /addresses/1.xml
def show
- @address = Address.find(params[:id])
+ @address = Geo::Address.find(params[:id])
respond_to do |format|
format.html # show.html.erb
@@ -24,7 +24,7 @@ def show
# GET /addresses/new
# GET /addresses/new.xml
def new
- @address = Address.new
+ @address = Geo::Address.new
respond_to do |format|
format.html # new.html.erb
@@ -34,18 +34,18 @@ def new
# GET /addresses/1/edit
def edit
- @address = Address.find(params[:id])
+ @address = Geo::Address.find(params[:id])
end
# POST /addresses
# POST /addresses.xml
def create
- @address = Address.new(params[:address])
+ @address = Geo::Address.new(params[:address])
respond_to do |format|
if @address.save
flash[:notice] = 'Address was successfully created.'
- format.html { redirect_to(@address) }
+ format.html { redirect_to(address_path(@address)) }
format.xml { render :xml => @address, :status => :created, :location => @address }
else
format.html { render :action => "new" }
@@ -57,12 +57,12 @@ def create
# PUT /addresses/1
# PUT /addresses/1.xml
def update
- @address = Address.find(params[:id])
+ @address = Geo::Address.find(params[:id])
respond_to do |format|
if @address.update_attributes(params[:address])
flash[:notice] = 'Address was successfully updated.'
- format.html { redirect_to(@address) }
+ format.html { redirect_to(address_path(@address)) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
@@ -74,7 +74,7 @@ def update
# DELETE /addresses/1
# DELETE /addresses/1.xml
def destroy
- @address = Address.find(params[:id])
+ @address = Geo::Address.find(params[:id])
@address.destroy
respond_to do |format|
Oops, something went wrong.

0 comments on commit 807632c

Please sign in to comment.