2

I have struggle with understanding how to accomplish this, and there seems to be a lot of people asking this question with no answers. I have a users table with their zip code. I created a zips table with every zip code with latitude/longitude in the United States.

What I would like to do is connect the two so that users can search for other users. I have Thinking Sphinx and I would prefer to continue using it. I want to provide users a checkbox for the distance to search (5, 10, 25, 50, 100, 500 miles). The results should always return the closest users.

I don't think code from the controller or model is required for this, however if needed please ask and I will provide.

search form:

<%= form_tag searches_path, method: :get do %>
<p>
        <%= text_field_tag :search, params[:search] %>
        <%= button_tag "Search", name: nil %>
        </p>
<% end %>

<P><%= link_to "Advanced Search", new_search_path %><p>

<%= form_tag users_path, method: :get do %>
<%= label :zip_code, "Enter zip code: " %>
<%= text_field_tag :zip_code, params[:zip_code] %>
<% end %>

/indices/user_index.rb:

     ThinkingSphinx::Index.define :user, :with => :active_record do
  # fields
  indexes name, :as => :user, :sortable => true
  indexes religion, zip_code, about_me, career, sexuality, children, user_smoke, user_drink, gender, ethnicity, education


  # attributes
  has id, created_at, updated_at
  has zips.city, :as => :zip_city

  has "RADIANS(zips.lat)",  :as => :latitude,  :type => :float
  has "RADIANS(zips.lon)", :as => :longitude, :type => :float


end

User model:

  has_and_belongs_to_many :zips

Zip model:

class Zip < ActiveRecord::Base
  attr_accessible :city, :lat, :lon, :code, :zipcode
  has_and_belongs_to_many :users

  validates :code, uniqueness: true

    self.primary_key = 'code'      

  def self.code(code)
    find_by(:code => code)
  end


end

User table has the following columns: zip_code.

The zip codes table has the following columns: code, city, state, lat, lon

Cornelius Wilson
  • 2,844
  • 4
  • 21
  • 41
  • Where are you stuck at? http://stackoverflow.com/questions/1078386/see-if-lat-long-falls-within-a-polygon-using-mysql – Marcus Adams Nov 08 '13 at 21:00
  • I am stuck at the beginning. So I have the index file setup. As you can see it is pulling from the User table. Should I create another one named zip_index.rb that will be for the zips table that contains all the zip codes with lat/lon in United States? What kind of work do I need to do in the user model (or do I need to create a search model). There's a lot of information on Sphinx, and I have set it up for other parts of the app. There's just no information really on the geo searching aspect. – Cornelius Wilson Nov 08 '13 at 21:44
  • I think you'll need to implement the [Haversive formula](http://en.wikipedia.org/wiki/Haversine_formula) unless Thinking Sphinx has a function which does it for you. The formula calculates the distance between two points on a sphere. The function is just some basic trig; a MySQL version of it would be `(6378.10 * ACOS(COS(RADIANS(lat1)) * COS(RADIANS(lat2)) * COS(RADIANS(long1) - RADIANS(long2)) + SIN(RADIANS(lat1)) * SIN(RADIANS(lat2))))` – madebydavid Nov 10 '13 at 20:03
  • Is it solved? That is it `locations` on `User` model ? – Dmitry Dedov Nov 16 '13 at 21:05

4 Answers4

1

The first step is to create an association between the User and its Location, so that the ActiveRecord for the Location can be referenced from that of the User.

class User < ActiveRecord::Base
  belongs_to :location
  attr_accessible :name
end

class Location < ActiveRecord::Base
  attr_accessible :city, :latitude, :longitude, :zipcode
end

Next, use the association in your index.

You have to create an alias for a field on the Location model, to make sure the location table gets joined. And you must add attributes for the location's latitude and longitude:

ThinkingSphinx::Index.define :user, :with => :active_record do
  # fields
  indexes name

  # attributes
  has created_at, updated_at
  has location.city, :as => :location_city
  has "RADIANS(locations.latitude)",  :as => :latitude,  :type => :float
  has "RADIANS(locations.longitude)", :as => :longitude, :type => :float

end

As others have already mentioned, since the earth is not flat, you'll need to account for that when you compute the distance between locations. The Haversine function is good for that. Thinking Sphinx has it built-in and you can filter and sort on it using :geo .

Then for example, to find all users within 200 kilometers of lat / lng parameters in degrees, ordered by nearest first:

class DistanceController < ApplicationController
  def search
    @lat = params[:lat].to_f * Math::PI / 180
    @lng = params[:lng].to_f * Math::PI / 180
    @users = User.search :geo => [@lat, @lng], :with => {:geodist => 0.0..200_000.0}, :order => "geodist ASC"
  end
end

For debugging, it's nice to know that in the view you can refer to the computed distance too:

  <% @users.each do |user| %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.location.zipcode %></td>
      <td><%= user.location.city %></td>
      <td><%= user.distance %></td>
    </tr>

EDIT: added more detail about my working version, for completeness' sake also adding the table definitions. (MySQL, generated using db:migrate, these are the create scripts as MySQL Workbench generates them):

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `location_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_users_on_location_id` (`location_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;


CREATE TABLE `locations` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `zipcode` varchar(255) DEFAULT NULL,
  `latitude` float DEFAULT NULL,
  `longitude` float DEFAULT NULL,
  `city` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
flup
  • 26,937
  • 7
  • 52
  • 74
  • I get a `undefined method `active_record'` when creating the association unless I need a foreign key. The user model I added `has_many :locations`, though belongs_to sounds better since the user has one location. I created a Location model and added `has_many :users`. I am not referencing the city and state atm. Users will only be able to search by zip code. I am going to use city/state for the users profile location. Ex as shown on profile: User A from Las Vegas, NV – Cornelius Wilson Nov 18 '13 at 16:29
  • 1
    Yes, I'd expect you need a foreign key on the zip code in the user table. The user belongs to exactly one location, no? So I agree that `belongs_to` sounds best. – flup Nov 18 '13 at 21:19
  • I think I am setting foreign key wrong. I get the following error when rebuilding ts `index 'user_core': sql_range_query: Unknown column 'user.location.latitude' in 'field list'` I have updated my code in posting – Cornelius Wilson Nov 19 '13 at 16:30
  • 1
    I got it to work over here! The trick was a) to add one attribute from the location model to the index to make sure the table gets joined, and b) to use the locations table name in the computation of the latitude and longitude attributes. Updated the answer. – flup Nov 20 '13 at 01:23
  • I am getting a `undefined method `klass' for nil:NilClass` when I rebuild index. Not sure as it only happens when I add-on to the user_index.rb. I changed `locations` to `zips` since that's the name of the table inside my database that has the latitude, longitude, and zip codes. I have been trying to correct this error this entire morning. Perhaps my model is wrong. – Cornelius Wilson Nov 20 '13 at 20:05
  • 1
    I have an ID column on locations table, is the only difference I can think of. And I've generated the table using db:migrate, based on the active record definitions. – flup Nov 20 '13 at 20:17
  • sorry I fixed it. I didn't setup the index file properly, as I have to list two separate models inside of it. I just updated how the code looks. Let me add the search form so I can test it and I will let you know if it works soon. – Cornelius Wilson Nov 20 '13 at 20:20
  • I am having all users returned back. How does your search form look? I added mines to the original. – Cornelius Wilson Nov 20 '13 at 20:53
  • 1
    You have created two separate indexes, so searching for Users will not take Locations into account. The User index should talk about locations, and for this query it is the only one you need. You wish to join the locations table to the users table. – flup Nov 20 '13 at 21:22
  • I had to make separate indexes as I was receiving errors if I didn't. As it was not recogonizing to go into the Zips table. I will change it back and continue to work out the error. Will keep you posted. – Cornelius Wilson Nov 20 '13 at 21:35
  • I am still working on these. Each time I clear one error I get another! Though I think the one I am now is the final. With my code as updated, I get this error `ERROR: source 'user_core_0': expected attr type ('uint' or 'timestamp' or 'bigint') in sql_attr_multi, got 'string zip_city from field'.`. If I remove the ` has zips.city, :as => :zip_city` line my error is then `ndex 'user_core': sql_range_query: Unknown column 'zips.lat' in 'field list' (DSN=mysql://root:***@localhost:3306/dating_development).` – Cornelius Wilson Nov 21 '13 at 18:53
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/41659/discussion-between-flup-and-cornelius-wilson) – flup Nov 21 '13 at 19:52
  • Ok I have it indexing now. Next I am going to work on the view so it can output the information properly. Will post when finished. – Cornelius Wilson Nov 22 '13 at 19:27
  • Thanks for taking the time to assist me. I did get it to work and there were additional changes needed but you definitely helped me get off the ground. I appreciate the amount of time you put into this question. – Cornelius Wilson Jan 27 '14 at 14:50
1

Sounds like your having an issue mapping users to locations, the original question seems to be misleading.

Instead of mapping locations to users the locations table should probably be a reference table of all locations based on zip's. Then you should add a scope to the Location to enable you to search for locations by zip code.

Something like this:

# Add scope to /models/location.rb
scope :by_zip_code, ->(code) { where('code = ?', code) }

Then in the user class, instead of saying a user has_many location, add a method, to retrieve all locations based on the users zip code.

Example:

# in /models/user.rb

 def location

    if Location.by_zip_code(self.zip_code.to_s).any?
        # you can return all here if you want more than one
        # for testing just returning the first one
        return Location.by_zip_code(self.zip_code.to_s).first
    else
        return nil
    end
end

Then when you have a user object, you can use the new instance method:

user = User.first
user.location
jmulca
  • 94
  • 3
0

You can do all this (and much more) with the geocoder gem. The gem is customizable, you can specify whatever maps API you want (default is google maps). Here's a railscasts episode on geocoder with a detailed explanation on usage.

rb512
  • 6,880
  • 3
  • 36
  • 55
  • I wouldn't want to use a API, as I don't see the need for one when I have the information inside my database already. I will take a look at Geocoder to see if it can fit this project. Though I preferred TS since I am using it for all my search options already. Thanks for the tip – Cornelius Wilson Nov 12 '13 at 14:27
  • geocoder doesn't care where you get the data from and works on a set of input : zip, address, co-ordinates to get you the desired output. – rb512 Nov 12 '13 at 17:52
  • Ok I will apply geocoded and try to create a relationship between it from the users and zips tables. – Cornelius Wilson Nov 12 '13 at 18:45
-1

You can compute the distance between two points utilizing the Haversine formula.

Landon Cox has gone through the trouble of creating the Haversine class for you already at this location: http://www.esawdust.com/blog/gps/files/HaversineFormulaInRuby.html

I have used this class in a previous project and it works pretty nicely

All you need to do is put in the logic to compare the distance of each user from your current location or selected user. Since you already have the lat and long of everything this should be pretty simple.

Hope this helps.

jmulca
  • 94
  • 3