I created a scope in my book model and want to include the author relation. Unfortunately, the relation isn't loaded with the following code:
scope :search, ->(title) {
quoted_title = ActiveRecord::Base.connection.quote_string(title)
includes(:author).where("title % :title", title: title).
order(Arel.sql("similarity(title, '#{quoted_title}') DESC"))
}
I tried several tweaks such as using joins(:author).merge() but the relation is still not loaded. Any idea how to load the relation within a scope? Thanks.
Here is the controller with the method I called through Ajax to render search results:
def search
results_books = Book.search(search_params[:q]).first(5)
results_authors = Author.search(search_params[:q]).first(5)
results = results_books + results_authors
render json: { results: results }, status: :ok
end
For the search function in your scope, if I understand correctly, you are trying to pick out books that matches the searched params according to title field. If so, may I suggest a shorter version like this:
scope :search, lambda { |title|
where('title like ?', "%#{title}%")
}
As for including the associated authors into the json output. We usually use JBuilder when returning JSON objects to the front-end. If you insist in doing it using basic RoR then check out this answer by Substanstial https://stackoverflow.com/a/26800097/9972821
This isn't tested so let me know how well it goes. The rest of the post I shared also touches on JBuilder as the preferred alternative.
Related
I am trying to create a custom ActiveAdmin filter that takes date_range as a parameter. Every solution I've found has been for excessively simple model methods.
Is there a way to pass both parameters into the model ransacker method, and/or at the very least to control the order in which these parameters are passed as well as to know which one is being passed? (end_date vs. start_date -- start_date is passed first, whereas I might be able to work around this is end_date were sent first). Any alternative solution, which would not break all other filters in the application (ie, overwriting activeadmin filters to use scopes - this is one filter out of hundreds in the application) welcome as well.
Thank you!
admin/model.rb
filter :model_method_in, as: :date_range
models/model.rb
ransacker :model_method, :formatter => proc { |start_date, end_date|
Model.complicated_method(start_date, end_date)
} do |parent|
parent.table[:id]
end
...
def method_for_base_queries(end_date)
Model.long_complicated_sql_call_using_end_date
end
def complicated_method(start_date, end_date)
model_instances = method_for_base_queries(end_date)
model_instances.logic_too_complex_for_sql_using_start_date
end
Similar question, but filter/model logic was simple enough for an alternative solution that didn't require both parameters to be passed in: Custom ActiveAdmin filter for Date Range
This might help. Given your index filter
filter :model_method, as: :date_range
you can write the following in your model:
scope :model_method_gteq_datetime, -> (start_date) {
self.where('users.your_date_column >= ?', start_date)
}
scope :model_method_lteq_datetime, -> (end_date) {
# added one day since apparently the '=' is not being counted in the query,
# otherwise it will return 0 results for a query on the same day (as "greater than")
self.where('users.your_date_column <= ?', (Time.parse(end_date) + 1.day).to_date.to_s)
}
def self.ransackable_scopes(auth_object = nil)
[model_method_gteq_datetime, :model_method_lteq_datetime]
end
..._gteq_datetime and ..._lteq_datetime is how Activeadmin interprets the two dates in a custom date_range index filter (see also the corresponding url generated after adding the filter).
I've written a sample query that fits my case (given that users is a model related to the current one), since I don't know the complexity of yours.
I'm using:
Ruby 2.3.1
Rails 5.0.7
Activeadmin 1.3.0
I finally got my filterrific get working and its a great gem, if not a little complex for a noob like me.
My original index page was filtering the active records based on those nearby to the user like this:
def index
location_ids = Location.near([session[:latitude], session[:longitude]], 50, order: '').pluck(:id)
#vendor_locations = VendorLocation.includes(:location).where(location_id: location_ids)
#appointments = Appointment.includes(:vendor).
where(vendor_id: #vendor_locations.select(:vendor_id))
end
So this pulls in all of the Appointments with Vendors in the area, but how do I pass this over to the Filterrific search:
#filterrific = initialize_filterrific(
params[:filterrific],
select_options:{ sorted_by: Appointment.options_for_sorted_by, with_service_id: Service.options_for_select },
) or return
#appointments = #filterrific.find.page(params[:page])
respond_to do |format|
format.html
format.js
end
It seems like the Filterrerrific is loading ALL of the appointments by default, but I want to limit to the ones nearby. What am I missing?
What you appear to be missing is a param default_filter_params to filterrific macro in the model. (Your question didn't mention that you made any adjustments to the VendorLocation model, since that is the object that you want to filter, that's where the macro should be called. Maybe you just omitted it from your question...)
From the model docs:
filterrific(
default_filter_params: { sorted_by: 'created_at_desc' },
available_filters: [
:sorted_by,
:search_query,
:with_country_id,
:with_created_at_gte
]
)
You probably found this already, it was on the first page of the documentation, but there's more important stuff in the example application that you need (I ran into this too, when I was just recently using Filterrific for the first time.)
The information on the start page is not enough to really get you started at all.
You have to read a bit further to see the other ways you may need to change your models, model accesses, and views in order to support Filterrific.
The part that makes the default filter setting effective is this default_filter_params hash (NOT select_options, which provides the options for "select" aka dropdown boxes. That's not what you want at all, unless you're doing a dropdown filter.) This hash holds a list of the scopes that need to be applied by default (the hash keys) and the scope parameter is used as the hash value.
That default_filter_params hash may not be the only thing you are missing... You also must define those ActiveRecord scopes for each filter that you want to use in the model, and name these in available_filters as above to make them available to filterrific:
scope :with_created_at_gte, lambda { |ref_date|
where('created_at >= ?', ref_date)
end
It's important that these scopes all take an argument (the value comes from the value of the filter field on the view page, you must add these to your view even if you want to keep them hidden from the user). It's also important that they always return ActiveRecord associations.
This is more like what you want:
scope :location_near, lambda { |location_string|
l = Location.near(location_string).pluck(:id)
where(location_id: l)
end
The problem with this approach is that in your case, there is no location_string or any single location variable, you have multiple coordinates for your location parameters. But you are not the first person to have this problem at all!
This issue describes almost exactly the problem you set out to solve. The author of Filterrific recommended embedding the location fields into hidden form fields in a nested fields_for, so that the form can still pass a single argument into the scope (as in with_distance_fields):
<%= f.fields_for :with_distance do |with_distance_fields| %>
<%= with_distance_fields.hidden_field :lat, value: current_user.lat %>
<%= with_distance_fields.hidden_field :lng, value: current_user.lng %>
<%= with_distance_fields.select :distance_in_meters,
#filterrific.select_options[:with_distance] %>
<% end %>
... make that change in your view, and add a matching scope that looks something like (copied from the linked GitHub issue):
scope :with_distance, -> (with_distance_attrs) {
['lng' => '-123', 'lat' => '49', 'distance_in_meters' => '2000']
where(%{
ST_DWithin(
ST_GeographyFromText(
'SRID=4326;POINT(' || courses.lng || ' ' || courses.lat || ')'
),
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
%d
)
} % [with_distance_attrs['lng'], with_distance_attrs['lat'], with_distance_attrs['distance_in_meters']])
}
So, your :with_distance scope should go onto the VendorLocation model and it should probably look like this:
scope :with_distance, -> (with_distance_attrs) {
lat = with_distance_attrs['lat']
lng = with_distance_attrs['lng']
dist = with_distance_attrs['distance']
location_ids = Location.near([lat, lng], dist, order: '').pluck(:id)
where(location_id: location_ids)
end
Last but not least, you probably noticed that I removed your call to includes(:location) — I know you put it there on purpose, and I didn't find it very clear in the documentation, but you can still get eager loading and have ActiveRecord optimize into a single query before passing off the filter work to Filterrific by defining your controller's index method in this way:
def index
#appointments = Appointment.includes(:vendor).
filterrific_find(#filterrific).page(params[:page])
end
Hope this helps!
I'm just moving over from the Tire gem to the official elasticsearch Ruby wrapper and am working on implementing better search functionality.
I have a model InventoryItem and a model Store. Store has_many :inventory_items. I have a model scope on Store called local
scope :local, lambda{|user| near([user.latitude, user.longitude], user.distance_preference, :order => :distance)}
I want the search to only return results from this scope so I tried: InventoryItem.local(user).search.... but it searches the entire index, not the scope. After doing some research, it looks like filter's are a good way to achieve this, but I'm unsure how to implement. I'm open to other ways of achieving this as well. My ultimate goal is be able to search a subset of the InventoryItem model based on store location.
Another thing you can do is to send the list of valid ids right to elastic, so it will filter records out by itself and then perform a search on ones that left. We were not doing tests whether it is faster yet, but I think it should, because elastic is a search engine after all.
I'll try to compose an example using you classes + variables and our experience with that:
def search
# retrieve ids you want to perform search within
#store_ids = Store.local(current_user).select(:id).pluck(:id)
# you could also check whether there are any ids available
# if there is none - no need to request elastic to search
#response = InventoryItem.search_by_store_ids('whatever', #store_ids)
end
And a model:
class InventoryItem
# ...
# search elastic only for passed store ids
def self.search_by_store_ids(query, store_ids, options = {})
# use method below
# also you can use it separately when you don't need any id filtering
self.search_all(query, options.deep_merge({
query: {
filtered: {
filter: {
terms: {
store_id: store_ids
}
}
}
}
}))
end
# search elastic for all inventory items
def self.search_all(query, options = {})
self.__elasticsearch__.search(
{
query: {
filtered: {
query: {
# use your fields you want to search, our's was 'text'
match: { text: query },
},
filter: {},
strategy: 'leap_frog_filter_first' # do the filter first
}
}
}.deep_merge(options)
# merge options from self.search_by_store_ids if calling from there
# so the ids filter will be applied
)
end
# ...
end
That way you also have to index store_id.
You can read more about filters here.
I'll leave this answer without accepting until the bounty is over - feel free to add an answer if you think you've found a better solution that one below.
The answer to this ended up being fairly simple after some digging.
Using the named scope:
scope :for_stores, lambda{ |stores| where(store_id: stores.map(&:id)) }
My controller method:
def search
#stores = Store.local(current_user) # get local stores
response = InventoryItem.search 'whatever' # execute the search
#inventory_items = response.records.for_stores(#stores) # call records
end
On elasticsearch responses, you can either call records or results. Calling just results will simply yield the results from the index that you can display etc. Calling records actually pulls the AR records which allows you to chain methods like I did above. Cool! More info in the docs obviously.
I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking
So I am developing a rails app, and I am working on paginating the feed. While I was doing it I wondered if I was doing it the right way because my load times were over 1500ms. My code was:
stories = Story.feed
#stories = Kaminari.paginate_array(stories).page(params[:page]).per(params[:pageSize])
I have a few questions about this:
Should I be paginating Story.feed, or is there some sort of method
that only returns some the stories I need?
Is this load time normal?
What are other things I can be doing to optimize this
(Also, Story.feed returns an array of story objects. The code for that is here:
def self.feed
rawStories = Story.includes([:likes, :viewers, :user, :storyblocks]).all
newFeaturedStories = rawStories.where(:featured => true).where(:updated_at.gte => (Date.today - 3)).desc(:created_at).entries
normalStories = rawStories.not_in(:featured => true, :or => [:updated_at.gte => (Date.today - 3)]).desc(:created_at).entries
newFeaturedStories.entries.concat(normalStories.entries)
end
I am using mongoid and mongodb
The issue is that you get all feeds from db in an array and this takes long time.
I suggest you use the any_of query from this great gem.
From there, do:
def self.feed_stories
newFeaturedStories = Story.where(:featured => true).where(:updated_at.gte => (Date.today - 3.days))
normalStories = Story.not_in(:featured => true, :or => [:updated_at.gte => (Date.today - 3.days)])
Story.includes([:likes, :viewers, :user, :storyblocks]).any_of(newFeaturedStories, normalStories).desc(:created_at)
end
Then paginate this:
selected_stories = Story.feed_stories.per(page_size).page(page)
Dont really understand what are your entries but get them at this moment.
To sum up: the idea s to make a unique paginated db query.
I suspect that when you call Kaminari.paginate_array on an ActiveRecord::Relation, it causes the whole result set to be fetched from DB and loaded in memory similar to calling Model.all.to_a.
To avoid this, I'd first find a way to turn Story.feed into a scope, rather than a class method. Superficially they'll seem the same—the differences are subtle but deep. See Active Record scopes vs class methods.
Next, ditch paginate_array in favor of chain Kaminari's page() and per() scopes.
For example (simplified version of yours):
class Article < ActiveRecord::Base
scope :featured, -> { where(featured: true) }
scope :last_3_days, -> { where(:updated_at.gte => (Date.today - 3)).desc(:created_at) }
scope :feed, -> { featured.last_3_days }
And then paginate simply by going:
Article.feed.per(page_size).page(page)
The biggest advantage of this is that Kaminari can chain into the generated SQL inserting the proper LIMIT and OFFSET clauses thereby reducing the size of the result set returned to only what needs to be displayed, as opposed to returning every matching record.
I think Will Paginate will help you out here -> mislav/will_paginate.
From there you can simply give your controller action .per_page(20) for example and after 20 objects (you can define the objects, see the wiki) there will be pagination