I have a model that's backed by Mongodb and I'm trying to get Gmaps4Rails to be able to properly use a location indexed array field that's in my mongo document.
I'm failing to figure out how I should map this given that the latitude and longitude aren't stored as independent values in order to take advantage of the geoindexing on mongo:
class Site
include MongoMapper::Document
include Gmaps4rails::ActsAsGmappable
acts_as_gmappable :lat => ???,
:lon => ???,
:process_geocoding => false
key :name, String
key :location, Array
ensure_index [[:location, '2d']]
end
for now I'm just doing this:
class Site
include MongoMapper::Document
include Gmaps4rails::ActsAsGmappable
acts_as_gmappable :process_geocoding => false
key :name, String
key :location, Array
ensure_index [[:location, '2d']]
def lat
return latitude
end
def lon
return longitude
end
def latitude
return location[1]
end
def longitude
return location[0]
end
end
Related
At the moment I have a form in which the user can input price per person and/or duration and/or team_size. What I would like to accomplish is to retrieve all records from a table that match the user input and for this I set scope in the model:
scope :filter_by_team_size, -> (team_size) { where("team_size = ?", team_size) }
scope :filter_by_duration, -> (duration) { where("duration = ?", duration) }
scope :filter_by_price, -> (price) { where("price = ?", price) }
And then in the controller use that again to retrieve the records by doing so:
#experiences = policy_scope(Experience).order(team_size: :desc).geocoded.filter_by_team_size(params[:team_size]) if params[:team_size].present?
#experiences = policy_scope(Experience).order(duration: :desc).geocoded.filter_by_duration(params[:duration]) if params[:duration].present?
#experiences = policy_scope(Experience).order(price: :desc).geocoded.filter_by_price(params[:price]) if params[:price].present?
However, this only gives me only the records for which the first input value matches but ignores all other values. Additionally, when you are viewing the search results and use the filter again it should apply the filter only for the records it found already.
Any suggestion on how to solve this would be much appreciated!
One way to handle this is to use a virtual model that handles binding parameters to and from the form:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
end
You can then setup the form:
<%= form_with(model: (#search_query || SearchQuery.new), url: '/experiences', method: :get) %>
<div>
<%= f.label :team_size %>
<%= f.number_field :team_size %>
</div>
# ..
<% end %>
And then you can just bind the params to the model with ActionController::Parameters#permit just like you would with a normal ActiveRecord model:
class ExperiencesController
before_action :set_search_query, only: :index, if: ->{ params[:search_query].present? }
# ...
def index
#experiences = if #search_query
#search_query.build_scope(policy_scope(Experience))
else
policy_scope(Experience)
end.geocoded
end
private
def set_search_query
#search_query = SearchQuery.new(search_query_params)
end
def search_query_params
params.fetch(:search_query).permit(:team_size, :duration, :price)
end
end
This loopback will make the form stateful just like your normal CRUD forms. We have not actually implemented #build_scope yes so lets do so:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
def build_scope(base_scope)
compacted_attributes = attributes.reject { value.nil? || value.empty? }
compacted_attributes.each_with_object(base_scope) do |(k,v), base|
if base.respond_to? "filter_by_#{k}"
# lets you customize the logic with a scope
base.send("filter_by_#{k}", v) # the scope is responsible for ordering
else
# convention over configuration!
base.where(Hash[k,v]).order(Hash[k,:desc])
end
end
end
end
Since this uses convention over configuration you can get rid of those pointless scopes in your model.
The gem manual indicates Venue.near([40.71, -100.23], 20, :units => :km)
However this application's model objects have two geocoded co-ordinates variables in addition to the individual longitudes and latitudes. In order to test the various search options (postGIS, numerical indexes of lon and lat, or calling geocoder services) is there any way to access a specific model variable in the finding of objects near a given location?
The relevant schema data is:
t.decimal "origin_lon", precision: 15, scale: 10
t.decimal "origin_lat", precision: 15, scale: 10
t.spatial "origin_lonlat", limit: {:srid=>3857, :type=>"point"}
add_index "strappos", ["origin_lat"], :name => "index_strappos_on_origin_lat"
add_index "strappos", ["origin_lon"], :name => "index_strappos_on_origin_lon"
add_index "strappos", ["origin_lonlat"], :name => "index_strappos_on_origin_lonlat", :spatial => true
GeoCoder defaults to using latitude and longitude as their variable names. Since yours are different, you'll need to add an extra option in order to query origin_lon and origin_lat.
In your model, where you initialize geocoder for the model, you have the option of assigning a different name for the lat/lon attribute:
# YourModel.rb
geocoded_by :address,
latitude: :origin_lat,
longitude: :origin_lon
I placed it on multiple lines for clarity. As you can see, I'm telling geocoder that my lat/lon attribute names are different than the defaults.
Now, querying the model using near will look at those new attribute names as opposed to the defaults.
I am trying to search my postgresql db in rails. I followed the Railscasts #111 Advanced Search tutorial and it is working for the name and category of my items column in plain text. However, I want to set a min/max price on my search as well which is where I come into my problem. In my db my price is stored as a string in the format "AU $49.95". Can I convert this into a float on the fly in my scoped search? If so how? If not, what should I do?
Here is the code:
search.rb
class Search < ActiveRecord::Base
attr_accessible :keywords, :catagory, :minimum_price, :maximum_price
def items
#items ||= find_items
end
private
def find_items
scope = Item.scoped({})
scope = scope.scoped :conditions => ["to_tsvector('english', items.name) ## plainto_tsquery(?)", "%#{keywords}%"] unless keywords.blank?
scope = scope.scoped :conditions => ["items.price >= ?", "AU \$#{minimum_price.to_s}"] unless minimum_price.blank?
# scope = scope.scoped :conditions => ["items.price <= ?", "AU \$#{maximum_price.to_s}"] unless maximum_price.blank?
scope = scope.scoped :conditions => ["to_tsvector('english', items.catagory) ## ?", catagory] unless catagory.blank?
scope
end
end
searches_controller.rb
class SearchesController < ApplicationController
def new
#search = Search.new
end
def create
#search = Search.new(params[:search])
if #search.save
redirect_to #search, :notice => "Successfully created search."
else
render :action => 'new'
end
end
def show
#search = Search.find(params[:id])
end
end
Thanks for reading this far!
Use the data type numeric or money for exact numerical calculation without rounding errors - and sorting as a number (not as text).
Converting string literal to numeric should not be a performance problem at all.
I'm in the process of switching my app to use geocoder. In my places table I have columns for address, lat, lng, street_address, city & zip. Using geocoder I'm happily able to fill lat, lng & address columns after validation with with the following in my places model
attr_accessible :address, :lat, :lng
geocoded_by :address, :latitude => :lat, :longitude => :lng
after_validation :geocode, :if => :address_changed?
Is there a way to also have geocoder add the street name, city and zip to three other, separate columns?
I'm still newish to rails so I missed this at first, but hope this helps someone else.
in my model
geocoded_by :address do |obj,results|
if geo = results.first
obj.city = geo.city
obj.lat = geo.latitude
obj.lng = geo.longitude
obj.zip = geo.postal_code
obj.state = geo.state
obj.country = geo.country_code
end
end
and in my view
#tonic.address = params[:address]
I have a Fieldnote model in my app, which has_many :activities attached to it through a table called :fieldnote_activities. I then define a searchable index this way:
searchable :auto_index => true, :auto_remove => true do
integer :id
integer :user_id, :references => User
integer :activity_ids, :multiple => true do
activities.map(&:id)
end
text :observations
end
And then I have a Search model to store / update searches. The search model thus also has its own associations with activities. I then perform my searches like this:
#search = Search.find(params[:id])
#query = Fieldnote.search do |query|
query.keywords #search.terms
if #search.activities.map(&:id).empty? == false
query.with :activity_ids, #search.activities.map(&:id)
end
end
#fieldnotes = #query.results
Now this all works GREAT. The problem is that if I change which activities that are associated with a fieldnote, the search results do not change because it appears the indices for that fieldnote do not change. I was under the impression that the :auto_index => true and :auto_remove => true flags when I define the searchable index would keep track of new associations (or deleted associations), but this appears not to be the case. How do I fix this?
You're right that :auto_index and :auto_remove don't apply to associated objects, just the searchable object they are specified on.
When denormalizing, you should use after_save hooks on the associated objects to trigger a reindex where necessary. In this case, you want changes to the Activity model and the FieldnoteActivity join model to trigger a reindex of their associated Fieldnote objects when saved or destroyed.
class Fieldnote
has_many :fieldnote_activities
has_many :activities, :through => :fieldnote_activities
searchable do
# index denormalized data from activities
end
end
class FieldnoteActivity
has_many :fieldnotes
has_many :activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end
class Activity
has_many :fieldnote_activities
has_many :fieldnotes, :through => :fieldnote_activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end