How can I combine record results in Rails? - ruby-on-rails

So I have two separate queries:
tagged_items = Item.tagged_with(params[:s], :on => :tags)
searched_items = Item.find(:all, :conditions => ["MATCH(title) AGAINST (? IN BOOLEAN MODE)", "*#{params[:s]}*"])
The first tagged_items is using the acts_as_taggable_on plugin to find all the items tagged with XYZ.
The second, searched_items, is used to search the items table for the search term.
So, how could I combine (and avoid duplicates) the results of these two?

Check out named_scope. The second query can be converted to named_scope easily, I'm not sure about the first one, but if you can rewrite it using find, you're home.
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html

items = (tagged_items + searched_items).unique
But it would be much better if you could fetch them with single query.

This approach...
#items = tagged_items | searched_items
...would make more sense if you're looking to use the results of these queries in a View, instead of working with an Array, and accomplishes the de-duplication as well.

Related

Combining queries into an ActiveRecord_AssociationRelation

Hi I'm working on a project and I need to take result of two database queries and combine them into one ActiveRecord_AssociationRelation, at the moment I have:
results.where(pos_or_neg: "neg").order("value DESC") + (results.where(pos_or_neg: "pos").order("value ASC"))
However this returns an array which doesn't work as I need to do more processing afterwards. I've tried:
results.where(pos_or_neg: "neg").order("value DESC").merge(results.where(pos_or_neg: "pos").order("value ASC"))
but this only seems to return the half of the results.
Thanks
results.order("pos_or_neg ASC,case when pos_or_neg="neg" then value else -1*value end DESC")
I believe using merge is the equivalent of an AND query in SQL. What you are looking for is an OR query.
Since Rails 5 this is one of the Active Record query methods that you can use!
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-or
Try to replace your .merge with .or and see if that works better

Multi parameter search via user input - ruby on rails & mongodb

I have a web page where a user can search through documents in a mongoDB collection.
I get the user's input through #q = params[:search].to_s
I then run a mongoid query:
#story = Story.any_of( { :Tags => /#{#q}/i}, {:Name => /#{#q}/i}, {:Genre => {/#{#q}/i}} )
This works fine if the user looks for something like 'humor' 'romantic comedy' or 'mystery'. But if looking for 'romance fiction', nothing comes up. Basically I'd like to add 'and' 'or' functionality to my search so that it will find documents in the database that are related to all strings that a user types into the input field.
How can this be done while still maintaining the substring search capabilties I currently have?Thanks in advance for help!
UPDATE:
Per Eugene's comment below...
I tried converting to case insensitive with #q.map! { |x| x="/#{x}/i"}. It does save it properly as ["/romantic/i","/comedy/i"]. But the query Story.any_of({:Tags.in => #q}, {:Story.in => #q})finds nothing.
When I change the array to be ["Romantic","Comedy"]. Then it does.
How can I properly make it case insensitive?
Final:
Removing the quotes worked.
However there is now no way to use an .and() search to find a book that has both words in all these fields.
to create an OR statement, you can convert the string into an array of strings, and then convert the array of strings into an array of regex and then use the '$in' option. So first, pick a delimeter - perhaps commas or space or you can set up a custom like ||. Let's say you do comma seperated. When user enters:
romantic, comedy
you split that into ['romantic', 'comedy'], then convert that to [/romantic/i, /comedy/i] then do
#story = Story.any_of( { :Tags.in => [/romantic/i, /comedy/i]}....
To create an AND query, it can get a little more complicated. There is an elemMatch function you could use.
I don't think you could do {:Tags => /romantic/i, :Tags => /comedy/i }
So my best thought would be to do sequential queries, even though there would be a performance hit, but if your DB isn't that big, it shouldn't be a big issue. So if you want Romantic AND Comedy you can do
query 1: find all collections that match /romantic/i
query 2: take results of query 1, find all collections that match /comedy/i
And so on by iterating through your array of selectors.

Chaining multiple instances of the same named scope

I've got a Rails app with models called Item, ItemTag, and Tag. Items have many Tags through ItemTags. The associations are working correctly.
I want to build a search interface for Items that allows filtering by multiple Tags. I have a bunch of named scopes for various conditions, and they chain without problems. I figure I'll build the query based on user input. So I made a scope that returns the set of Items that has a given tag:
scope :has_tag, -> (tag_id) { joins(:tags).where(tags: {id: tag_id}) }
It works!
> Item.has_tag(73).count
=> 6
> Item.has_tag(81).count
=> 5
But:
> Item.has_tag(73).has_tag(81).count
=> 0
There are two items that have both tags, but chaining the scopes together produces the following SQL, which is always going to return empty, for obvious reasons; it's looking for a tag that has two ids, rather than an item that has two tags.
SELECT [items].*
FROM [items]
INNER JOIN [item_tags] ON [item_tags].[item_id] = [items].[id]
INNER JOIN [tags] ON [tags].[id] = [item_tags].[tag_id]
WHERE [tags].[id] = 81 AND [tags].[id] = 73
I know I can get the intersection of the collections after they're returned, but this seems inefficient, so I'm wondering if there is a standard practice here. (It seems like a common task.)
> (Item.has_tag(73) & Item.has_tag(81)).count
=> 2
Is there a way to write the scope to make this work, or will this need to be done another way?
I think the input tags need to be processed as an array, not chained. With this statement, you're getting a subset under tag(83) of tag(71), which is not what you want. The query confirms this.
Item.has_tag(73).has_tag(81).count
You might try a scope that processes the tags as an array and then constructs the query using lambda
scope :has_tags, lambda { |tag_ids| includes(:tag_ids)
.where("tag_id IN (?)", tag_ids.collect { |tag_id| } )
.group('"items"."id"')
.having('COUNT(DISTINCT "tag_ids"."id") = ?', tag_ids.count) }
I don't have full knowledge of your model relationship setups, etc, and I'm the typo queen, so the snippet above may or may not work out of the box, but I think the information here is that yes, you can process a set of tags in a single scope and query once to get a collection.

Does ActiveRecord find_all_by_X preserve order? If not, what should be used instead?

Suppose I have a non-empty array ids of Thing object ids and I want to find the corresponding objects using things = Thing.find_all_by_id(ids). My impression is that things will not necessarily have an ordering analogous to that of ids.
Is my impression correct?
If so, what can I used instead of find_all_by_id that preserves order and doesn't hit the database unnecessarily many times?
Yes
Use Array#sort
Check it out:
Thing.where(:id => ids).sort! {|a, b| ids.index(a.id) <=> ids.index(b.id)}
where(:id => ids) will generate a query using an IN(). Then the sort! method will iterate through the query results and compare the positions of the id's in the ids array.
#tybro0103's answer will work, but gets inefficient for a large N of ids. In particular, Array#index is linear in N. Hashing works better for large N, as in
by_id = Hash[Thing.where(:id => ids).map{|thing| [thing.id, thing]}]
ids.map{|i| by_id[i]}
You can even use this technique to arbitrarily sort by any not-necessarily unique attribute, as in
by_att = Thing.where(:att => atts).group_by(&:att)
atts.flat_map{|a| by_att[a]}
find_all_by_id is deprecated in rails 4, which is why I use where here, but the behavior is the same.

how to find entries with no tags using acts-as-taggable-on?

What's the Right Way(tm) to find the entries that have no tags?
I tried using Entry.tagged_with(nil) but it just returns an empty hash.
I need it so I can list the entries I still need to tag.
Thanks.
The following seems to work for me:
Entry.includes("taggings").where("taggings.id is null")
This should support chaining as well. For example, the following should work:
Entry.includes("taggings").where("taggings.id is null").where(foo: "bar")
Here is my solution to find the tagged and untagged photos.
scope :with_taggings, where('id in (select taggable_id from taggings where taggable_type = ’Photo‘ and context = ’bibs‘)')
scope :without_taggings, where('id not in (select taggable_id from taggings where taggable_type = ’Photo‘ and context = ’bibs‘)')
But it works for the Photo model, but can not be chained with other scopes.
I realize this is ancient but I just came on a similar need today. I just did it with a simple multiple-query scope:
scope :untagged, lambda {
# first build a scope for the records of this type that *are* tagged
tagged_scope = Tagging.where(:taggable_type => base_class.to_s)
# only fetching the taggable ids
tagged_scope = tagged_scope.select('DISTINCT taggable_id')
# and use it to retrieve the set of ids you *don't* want
tagged_ids = connection.select_values(tagged_scope.to_sql)
# then query for the rest of the records, excluding what you've found
where arel_table[:id].not_in(tagged_ids)
}
It may not be efficient for huge datasets but it suits my purposes.
Without knowing the internals of acts-as-taggable-on I can't think of a neat way that doesn't involve looping queries or raw SQL. This is readable:
need_to_tag = []
Entry.all.each do |e|
need_to_tag << e if e.tag_list.blank?
end
need_to_tag then holds any untagged Entries.

Resources