Searching through tag using Ransack - ruby-on-rails

I'm using Ransack to perform fairly complex searches through some models. One of these models holds free text and uses the acts_as_taggable gem to tag the words.
I'm trying to create a collection selector of these words so that ransack can find any of the full text records from a subset of the tags that the user can define.
This gets me nearly there, but if I try to choose more than one word, it doesn't return any results!
= f.select :note_in, #freetexts.tag_counts_on(:tags), {}, {:multiple => true}

Ransack is not really oriented to complex searchs. It is very probable that if you stress ransack enough you end with a harder problem that if you where doing a complex select.
For complex search I would recomment Sequel, from the same author of ransack and much better oriented to complex searchs.
Moreover, according to thes thread you are on a dead end:
Ransack and acts-as-taggable-on issues

I'm not an expert at all, but this non-ransack solution could work for those that need to filter by tags with the acts-as-taggable-on gem:
#search = MyModel.ransack(params[:q])
#result = #search.result(distinct: true).includes(:related_model)
#result = #result.tagged_with(params[:tags].split(/\s*,\s*/)) if params[:tags].present?
#result = #result.paginate(page: params[:page], per_page: 20)
This expects a new :tags param that is out of the scope of Ransack. You can use to filter out the results that Ransack gives you.

Related

How to paginate multiple models with the Pagy gem in Rails?

I'm using Searchkick with the Pagy gem to paginate my search results and it works great if I'm only searching and paginating on one model, but I can't figure out how to do this with Pagy when I need to combine multiple models into one set of Searchkick search results.
I tried using Pagy's "array extra" to combine all the the individual Searchkick model queries into an array and then loop through the results in the view, but that did not return any search results.
Here's the controller code that works for a single model:
query = params[:q].presence || "*"
results = BlogPost.pagy_search(query, suggest: true, per_page: 20)
#pagy, #results = pagy_searchkick(results, items: 10)
And in the view:
<%= pagy_nav(#pagy).html_safe %>
The following works as a combined Searchkick query across multiple models, but it's not paginated:
#search_results = Searchkick.search(query, models: [PressRelease, BlogPost, User])
How am I supposed to paginate across multiple models, then? The docs for Pagy's "Array Extra" warns:
if the data in the array comes from some DB or other persisted storage
(i.e. not some in-memory storage), then you should definitely review
your code
That's exactly what I'm trying to do. Using Pagy to paginate Searchkick results from multiple models seems like something that should be possible. If you're not supposed to use a Pagy array to do this, then how are you supposed to do this?
You can put this in the pagy initializer:
Searchkick.extend Pagy::Searchkick
Then you can use it as usual:
results = Searchkick.pagy_search(query, models: [PressRelease, BlogPost, User])
#pagy, #results = pagy_searchkick(results, items: 10)
It is not documented, so you should probably create a Documentation Issue for the missing documentation.

ActiveRecord Fuzzy Search

I'm looking to perform a search to find an object similar to this:
Object(id: 1, example: "abc")
by using a search like this:
params[:search] = "abcdef"
Object.where("example LIKE ?", "#{params[:search]%")
but am only able to use the above example if my search has less characters than my object, not more.
I think it should be
params[:search] = "abcdef"
Object.where("example LIKE ?", "%#{params[:search]}%")
Also might want to use ilike for case insensitive search (if you're using postgres)
Note: the fuzzily gem does not work with Rails 6. This solution has been deprecated.
The fuzzily gem allows you to do fuzzy searching of ActiveRecord models that you've instrumented appropriately.
Fuzzily finds misspelled, prefix, or partial needles in a haystack of strings. It's a fast, trigram-based, database-backed fuzzy string search/match engine for Rails.
Once you've installed the gem, you can instrument your model as follows:
class MyStuff < ActiveRecord::Base
# assuming my_stuffs has a 'name' attribute
fuzzily_searchable :name
end
You can then perform fuzzy searches as follows:
MyStuff.find_by_fuzzy_name('Some Name', :limit => 10)
# => records

Activeadmin: how to filter for strings that match two or more search terms

Let's say I've got User class with an :email field. And let's say I'm using activeadmin to manage Users.
Making a filter that returns emails that match one string, e.g. "smith", is very simple. In admin/user.rb, I just include the line
filter :email
This gives me a filter widget that does the job.
However, this filter doesn't let me search for the intersection of multiple terms. I can search for emails containing "smith", but not for emails containing both "smith" AND ".edu".
Google tells me that activerecord uses Ransack under the hood, and the Ransack demo has an 'advanced' mode that permits multiple term searches.
What's the easiest way to get a multiple term search widget into activeadmin?
Ideally, I'd like a widget that would allow me to enter smith .edu or smith AND .edu to filter for emails containing both terms.
there is simple solution using ranasckable scopes
So put something like this in your model
class User < ActiveRecord::Base
....
scope :email_includes, ->(search) {
current_scope = self
search.split.uniq.each do |word|
current_scope = current_scope.where('user.email ILIKE ?', "%#{word}%")
end
current_scope
}
def self.ransackable_scopes(auth_object = nil)
[ :email_includes]
end
end
After this you can add filter with AA DSL
Like
filter :email_includes, as: :string, label: "Email"
UPD
should work if change email_contains_any to email_includes
I've figured out a solution but it's not pretty.
The good news is that Ransack has no trouble with multiple terms searches. These searches use the 'predicate' cont_all. The following line works for finding emails containing 'smith' and '.edu'.
User.ransack(email_cont_all: ['smith','.edu'] ).result
Since these searches are easy in Ransack, they're probably straightforward in Activeadmin, right? Wrong! To get them working, I needed to do three things.
I put a custom ransack method (a.k.a. ransacker) into User.rb. I named the ransacker email_multiple_terms.
class User < ActiveRecord::Base
# ...
ransacker :email_multiple_terms do |parent|
parent.table[:path]
end
I declared a filter in my activeadmin dashboard, and associated it with the ransacker. Note that the search predicate cont_all is appended to the ransacker name.
admin/User.rb:
ActiveAdmin.register User do
# ...
filter :email_multiple_terms_cont_all, label: "Email", as: :string
This line creates the filter widget in Activeadmin. We're nearly there. One problem left: Activeadmin sends search queries to ransack as a single string (e.g. "smith .edu"), whereas our ransacker wants the search terms as an array. Somewhere, we need to convert the single string into an array of search terms.
I modified activeadmin to split the search string under certain conditions. The logic is in a method that I added to lib/active_admin/resource_controller/data_access.rb.
def split_search_params(params)
params.keys.each do |key|
if key.ends_with? "_any" or key.ends_with? "_all"
params[key] = params[key].split # turn into array
end
end
params
end
I then called this method inside apply_filtering.
def apply_filtering(chain)
#search = chain.ransack split_search_params clean_search_params params[:q]
#search.result
end
This code is live in my own fork of activeadmin, here: https://github.com/d-H-/activeadmin
So, to get multiple term search working, follow steps 1 and 2 above, and include my fork of A.A. in your Gemfile:
gem 'activeadmin', :git => 'git://github.com/d-H-/activeadmin.git'
HTH.
If anyone's got a simpler method, please share!
Just add three filters to your model:
filter :email_cont
filter :email_start
filter :email_end
It gives you a flexible way to manage your search.
This filter executes next sql code:
SELECT "admin_users".* FROM "admin_users"
WHERE ("admin_users"."email" ILIKE '%smith%' AND
"admin_users"."email" ILIKE '%\.edu')
ORDER BY "admin_users"."id" desc LIMIT 30 OFFSET 0
I expect that exactly what you're looking for.

How to filter results by multiple fields?

I am working on a survey application in ruby on rails and on the results page I want to let users filter the answers by a bunch of demographic questions I asked at the start of the survey.
For example I asked users what their gender and career was. So I was thinking of having dropdowns for gender and career. Both dropdowns would default to all but if a user selected female and marketer then my results page would so only answers from female marketers.
I think the right way of doing this is to use named_scopes where I have a named_scope for every one of my demographic questions, in this example gender and career, which would take in a sanitized value from the dropdown to use at the conditional but i'm unsure on how to dynamically create the named_scope chain since I have like 5 demographic questions and presumably some of them are going to be set to all.
You can chain named scopes together:
def index
#results = Results.scoped
#results = #results.gender(params[:gender]) unless params[:gender].blank?
#results = #results.career(params[:career]) unless params[:career].blank?
end
I prefer however to use the has_scope gem:
has_scope :gender
has_scope :career
def index
#results = apply_scopes(Results).all
end
If you use has_scope with inherited_resources, you don't even need to define the index action.
named_scope :gender,lambda { |*args|
unless args.first.blank?
{ :conditions => [ "gender = ?", args.first] }
end
}
If you write named scopes in this way, you can have all them chained, and if one of your params will be blank wont breaks.
Result.gender("Male") will return male results.
Result.gender("") will return male and female too.
And you can chain all of your methods like this. Finally as a filtering you can have like:
Result.age(16).gender("male").career("beginer")
Result.age(nil).gender("").career("advanced") - will return results with advanced career, etc.
Try some like this:
VistaFact.where( if active then {:field => :vista2} else {} end)
Or like this:
VistaFact.where(!data.blank? ? {:field=>data.strip} : {}).where(some? ? {:field2 => :data2} : {}).where ...
That work for me very nice!

Using will_paginate with multiple models (Rails)

Pretty sure that I'm missing something really simple here:
I'm trying to display a series of pages that contain instances of two different models - Profiles and Groups. I need them ordering by their name attribute. I could select all of the instances for each model, then sort and paginate them, but this feels sloppy and inefficient.
I'm using mislav-will_paginate, and was wondering if there is any better way of achieving this? Something like:
[Profile, Group].paginate(...)
would be ideal!
Good question, I ran into the same problem a couple of times. Each time, I ended it up by writing my own sql query based on sql unions (it works fine with sqlite and mysql). Then, you may use will paginate by passing the results (http://www.pathf.com/blogs/2008/06/how-to-use-will_paginate-with-non-activerecord-collectionarray/). Do not forget to perform the query to count all the rows.
Some lines of code (not tested)
my_query = "(select posts.title from posts) UNIONS (select profiles.name from profiles)"
total_entries = ActiveRecord::Base.connection.execute("select count(*) as count from (#{my_query})").first['count'].to_i
results = ActiveRecord::Base.connection.select_rows("select * from (#{my_query}) limit #{limit} offset #{offset}")
Is it overkilled ? Maybe but you've got the minimal number of queries and results are consistent.
Hope it helps.
Note: If you get the offset value from a http param, you should use sanitize_sql_for_conditions (ie: sql injection ....)
You can get close doing something like:
#profiles, #groups = [Profile, Group].map do |clazz|
clazz.paginate(:page => params[clazz.to_s.downcase + "_page"], :order => 'name')
end
That will then paginate using page parameters profile_page and group_page. You can get the will_paginate call in the view to use the correct page using:
<%= will_paginate #profiles, :page_param => 'profile_page' %>
....
<%= will_paginate #groups, :page_param => 'group_page' %>
Still, I'm not sure there's a huge benefit over setting up #groups and #profiles individually.
in my last project i stuck into a problem, i had to paginate multiple models with single pagination in my search functionality.
it should work in a way that the first model should appear first when the results of the first model a second model should continue the results and the third and so on as one single search feed, just like facebook feeds.
this is the function i created to do this functionality
def multi_paginate(models, page, per_page)
WillPaginate::Collection.create(page, per_page) do |pager|
# set total entries
pager.total_entries = 0
counts = [0]
offsets = []
for model in models
pager.total_entries += model.count
counts << model.count
offset = pager.offset-(offsets[-1] || 0)
offset = offset>model.count ? model.count : offset
offsets << (offset<0 ? 0 : offset)
end
result = []
for i in 0...models.count
result += models[i].limit(pager.per_page-result.length).offset(offsets[i]).to_a
end
pager.replace(result)
end
end
try it and let me know if you have any problem with it, i also posted it as an issue to will_paginate repository, if everyone confirmed that it works correctly i'll fork and commit it to the library.
https://github.com/mislav/will_paginate/issues/351
Have you tried displaying two different sets of results with their own paginators and update them via AJAX? It is not exactly what you want, but the result is similar.

Resources