diff --git a/README.md b/README.md index 81b4dda4e1ebd0e0dc742deae2907927397aaa6f..7e7b60c69d346868b24d6df9a3c5bde1632cf1f6 100644 --- a/README.md +++ b/README.md @@ -527,6 +527,15 @@ Calling `obj.coordinates` directly returns the internal representation of the co For consistency with the rest of Geocoder, always use the `to_coordinates` method instead. +Optimisation of Distance Queries +-------------------------------- + +In MySQL and Postgres the finding of objects near a given point is speeded up by using a bounding box to limit the number of points over which a full distance calculation needs to be done. + +To take advantage of this optimisation you need to add a composite index on latitude and longitude. In your Rails migration: + + add_index :table, [:latitude, :longitude] + Distance Queries in SQLite -------------------------- diff --git a/lib/geocoder/stores/active_record.rb b/lib/geocoder/stores/active_record.rb index b49a4a97c891bf7af4a98f1df35d8eaff3448802..b088089f0a445bbecb85a158c8da1798d9062a56 100644 --- a/lib/geocoder/stores/active_record.rb +++ b/lib/geocoder/stores/active_record.rb @@ -102,15 +102,18 @@ module Geocoder::Store options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units) bearing = bearing_sql(latitude, longitude, options) distance = distance_sql(latitude, longitude, options) + + b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options) + args = b + [ + full_column_name(geocoder_options[:latitude]), + full_column_name(geocoder_options[:longitude]) + ] + bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args) + if using_sqlite? - b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options) - args = b + [ - full_column_name(geocoder_options[:latitude]), - full_column_name(geocoder_options[:longitude]) - ] - conditions = Geocoder::Sql.within_bounding_box(*args) + conditions = bounding_box_conditions else - conditions = ["#{distance} <= ?", radius] + conditions = [bounding_box_conditions + " AND #{distance} <= ?", radius] end { :select => select_clause(options[:select], distance, bearing), diff --git a/test/near_test.rb b/test/near_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..359f2011423d0b38d3b83cbbb10eb2bfeed49abe --- /dev/null +++ b/test/near_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' + +class NearTest < Test::Unit::TestCase + + def test_near_scope_options_without_sqlite_includes_bounding_box_condition + result = Event.send(:near_scope_options, 1.0, 2.0, 5) + + assert_match /test_table_name.latitude BETWEEN 0.9276\d* AND 1.0723\d* AND test_table_name.longitude BETWEEN 1.9276\d* AND 2.0723\d* AND /, + result[:conditions][0] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index e390f0dbae5af55a5ca3419af546204cf7ebed94..1f592ecf259edd4d72efd06d4a19d38474579545 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,12 @@ require 'test/unit' $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +class MysqlConnection + def adapter_name + "mysql" + end +end + ## # Simulate enough of ActiveRecord::Base that objects can be used for testing. # @@ -28,6 +34,10 @@ module ActiveRecord def self.scope(*args); end + def self.connection + MysqlConnection.new + end + def method_missing(name, *args, &block) if name.to_s[-1..-1] == "=" write_attribute name.to_s[0...-1], *args