Advanced search with many to many in Rails 4 - ruby-on-rails

I have a list of real estates, which each real estate has some features. Then, I have 2 tables:
real_estates, to store all real estates add by users.
re_home_features, to store all default features, added by admin, like pool, closet, office, garden and a lot of features that the real estate can to have
The same real_estate can to have many features AND the same feature can to have many real estates. I created these models:
real_estate.rb
class RealEstate < ActiveRecord::Base
has_many :real_estate_home_features
has_many :re_home_features, as: :home_features, through: :real_estate_home_features, dependent: :destroy
end
re_home_features.rb
class ReHomeFeature < ActiveRecord::Base
has_many :real_estate_home_features
has_many :real_estates, through: :real_estate_home_features
end
real_estate_home_feature.rb
class RealEstateHomeFeature < ActiveRecord::Base
belongs_to :real_estate
belongs_to :re_home_feature
end
With this, the relation many to many is working fine.
I have a search to real estates with some parameters, like:
Number of living rooms
Number of bathrooms
Sell price (min to max)
Area total (min to max)
Real estate code
And a lot of other params
My search is like that:
real_estates_controller.rb
def search
r = real_estates
r = r.where(code: params['code']) if params['code'].present?
r = r.where(city_name: params['city']) if params['city'].present?
r = r.where(garage: params['garage']) if params['garage'].present?
r = r.where(restrooms: params['restrooms']) if params['restrooms'].present?
r = r.paginate(:page => params[:page], :per_page => 10)
r
end
This search is working fine too. No problems with this, because all parameters are within the same table real_estates.
But now, the search is a little bit more complex. I have to search real estates with specific features. Example: I want all real estates, which has 4 restrooms, 2 cars in garage AND has pool.
In my example, a search in real estates with 4 restrooms and 2 cars returned to me 50 real estates, but only 15 of these real estates have pool.
How can I filter these 50 real estates to show only the records associated with the 'pool feature'?
I can't to verify in the view, because causes a wrong number per page. I think the filter must occur in the database query moment, just before the paginate.
I appreciate any help!
environment
rails -v: Rails 4.2.1
ruby -v: ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
so: Ubuntu 16.04.5 LTS
localhost database: MySQL (development)
server database: Postgres (production)
EDIT 1
Based on #jvillian answer, I will change my controller, adding a JOIN in the query.
if params['home_features'].present? && params['home_features'].any?
r = r.joins(:re_home_features).where(re_home_features: {id: params['home_features']}).distinct(:id)
end
In my tests, I had:
params['home_features'] = [1 , 2, 3]
I have 1 real estate which has these 3 features. But, in the view, I got the same site showed 3 times. If I added the distinct, the real estate is showed only once. BUT... If I change the params to:
params['home_features'] = [1 , 2, 3, 500]
I have no real estates with these 4 features, but the results are the same.
Without distinct, the real estate are showed 3 times. With the dinstict, the real estate is showed once. The expected result is zero results, because I want real estates with all the selected features.
But I think we are almost there! I will provide some information about my models:
table real_estates
id | title | description | code | city_name | garage | ...
7 | Your House | Awesome... | 1234 | Rio de Janeiro | 4
table re_home_features
id | name
1 | Pool
2 | Garden
3 | Tenis court
4 | Closet
table real_estate_home_features - association many to many
id | real_estate_id | re_home_feature_id
1 | 7 | 1
2 | 7 | 2
3 | 7 | 3
If I run:
r = r.joins(:re_home_features).where(re_home_features: {id: [1,2,3,500]}).distinct(:id)
I got these query (rails console):
SELECT DISTINCT `real_estates`.* FROM `real_estates` INNER JOIN `real_estate_home_features` ON `real_estate_home_features`.`real_estate_id` = `real_estates`.`id` INNER JOIN `re_home_features` ON `re_home_features`.`id` = `real_estate_home_features`.`re_home_feature_id` WHERE `re_home_features`.`id` IN (1, 2, 3, 500)
And it returns 1 result. The real estate id 7. If I remove the distinct, I have 3 results: the same real estate, 3 times.
The expected is zero results. If params['home_features'] = [1 , 2, 3] I expect 1 result.
EDIT 2
This join method works, but it returns like "OR" query. In my case, I need a join query with "AND". The query must be: "Return all real estates which has features 1 AND 2 AND 3".
Tks!

I'm not sure this is an "advanced search". Searching on joined models is covered in the guide under 12.1.4 Specifying Conditions on the Joined Tables. It would look something like:
real_estates.joins(:re_home_features).where(re_home_features: {feature_name: 'pool'})
Naturally, that's probably not going to exactly work because you don't tell us much about how to find the 'pool feature'. But, it should give you the right direction.
BTW, the reason this:
params['home_features'] = [1 , 2, 3, 500]
returns records that have any of the home_features is because it employs an 'or'. If you want real_estates with all home_features, then you want an 'and'. You can google around on that.
I think I would try something like:
def search
r = real_estates.joins(:re_home_features)
%i(
code
city_name
garage
restrooms
).each do |attribute|
r = r.where(attribute => params[attribute]) unless params[attribute].blank?
end
params[:home_features].each do |home_feature_id|
r = r.where(re_home_features: {id: home_feature_id})
end unless params[:home_features].blank?
r = r.uniq
r = r.paginate(:page => params[:page], :per_page => 10)
r
end
NOTE: I changed params[:city] to params[:city_name] to make that first each iterator cleaner. You will need to change your view if you want to do it this way. If you don't want to do it this way, then you can go back to the non-iterator approach you already have.
This is not tested, so I'm not confident that it will work exactly as presented.

Related

Limit query by sum of attributes of objects in Rails and Postgresql

I have a Lead model that has a pre-calculated float column called "sms_price". I want to allow users to send text messages to those leads, and to assign a budget to their campaigns (something similar to what you can find on fb ads).
I need a scope that can limit the number of leads by the total price of those leads. So for example if the user has defined a total budget of 200
id: 1 | sms_price: 0.5 | total_price: 0.5
id: 2 | sms_price: 1.2 | total_price: 1.7
id: 3 | sms_price: 0.9 | total_price: 2.6
...
id: 94 | sms_price: 0.8 | total_price: 199.4 <--- stop query here
id: 95 | sms_price: 0.7 | total_price: 200.1
So I need two things in my scope:
Calculate the total price recursively
Get only the leads that have a total price lower than the desired budget
So far I have only managed to do the first task (Calculate the total price recursively) using this scope:
scope :limit_by_total_price, ->{select('"leads".*, sum(sms_price) OVER(ORDER BY id) AS total_price')}
This works and if I do Lead.limit_by_total_price.last.total_price I get 38039.7499999615
Now what I need is a way to retrieve only the leads that have a total price lower than the budget:
scope :limit_by_total_price, ->(budget){select('"leads".*, sum(sms_price) OVER(ORDER BY id) AS total_price').where('total_price < ?', budget)}
But it doesn't recognise the total_price attribute:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column "total_price" does not exist
Why does it recognise the total_price attribute in a single object and not in the scope ?
The problem is that the columns calculated in a SELECT clause are not available to the WHERE clause in the same statement. To do what you want, you need a subquery.
You can do this and yet stay in the Rails universe using ActiveRecord's from method. The technique is nicely illustrated in this Hashrocket blog post.
In your case it might look something like this (because of the complexity, I would use a class method rather than a scope):
def self.limit_by_total_price(budget)
subquery = select('leads.*, sum(leads.sms_price) over(order by leads.id) as total_price')
from(subquery, :leads).where('total_price < ?', budget)
end

How to make Rails/ActiveRecord return unique objects using join table's boolean column

I have a Rails 4 app using ActiveRecord and Postgresql with two tables: stores and open_hours. a store has many open_hours:
stores:
Column |
--------------------+
id |
name |
open_hours:
Column |
-----------------+
id |
open_time |
close_time |
store_id |
The open_time and close_time columns represent the number of seconds since midnight of Sunday (i.e. beginning of the week).
I would like to get list of store objects ordered by whether the store is open or not, so stores that are open will be ranked ahead of the stores that are closed. This is my query in Rails:
Store.joins(:open_hours).order("#{current_time} > open_time AND #{current_time} < close_time desc")
Notes that current_time is in number of seconds since midnight on the previous Sunday.
This gives me a list of stores with the currently open stores ranked ahead of the closed ones. However, I'm getting a lot of duplicates in the result.
I tried using the distinct, uniq and group methods, but none of them work:
Store.joins(:open_hours).group("stores.id").group("open_hours.open_time").group("open_hours.close_time").order("#{current_time} > open_time AND #{current_time} < close_time desc")
I've read a lot of the questions/answers already on Stackoverflow but most of them don't address the order method. This question seems to be the most relevant one but the MAX aggregate function does not work on booleans.
Would appreciate any help! Thanks.
Here is what I did to solve the issue:
In Rails:
is_open = "bool_or(#{current_time} > open_time AND #{current_time} < close_time)"
Store.select("stores.*, CASE WHEN #{is_open} THEN 1 WHEN #{is_open} IS NULL THEN 2 ELSE 3 END AS open").group("stores.id").joins("LEFT JOIN open_hours ON open_hours.store_id = stores.id").uniq.order("open asc")
Explanation:
The is_open variable is just there to shorten the select statement.
The bool_or aggregate function is needed here to group the open_hours records. Otherwise there likely will be two results for each store (one open and one closed), which is why using the uniq method alone doesn't eliminate the duplicate issues
LEFT JOIN is used instead of INNER JOIN so we can include the stores that don't have any open_hours objects
The store can be open (i.e. true), closed (i.e. false) or not determined (i.e. nil), so the CASE WHEN statement is needed here: if a store is open, then it's 1, 2 if not determined and 3 if closed
Ordering the results ASC will show open stores first, then the not determined ones, then the closed stores.
This solution works but doesn't feel very elegant. Please post your answer if you have a better solution. Thanks a lot!
Have you tried uniq method, just append it at the end
Store.joins(:open_hours).order("#{current_time} > open_time AND #{current_time} < close_time desc").uniq

Sorting by rank and total where multiple entries may exist

Ruby 2.1.5
Rails 4.2.1
My model is contributions, with the following fields:
event, contributor, date, amount
The table would have something like this:
earth_day, joe, 2014-04-14, 400
earth_day, joe, 2015-05-19, 400
lung_day, joe, 2015-05-20, 800
earth_day, john, 2015-05-19, 600
lung_day, john, 2014-04-18, 900
lung_day, john, 2015-05-21, 900
I have built an index view that shows all these fields and I implemented code to sort (and reverse order) by clicking on the column titles in the Index view.
What I would to do is have the Index view displayed like this:
Event Contributor Total Rank
Where event is only listed once per contributor and the total is sum of all contributions for this event by the contributor and rank is how this contributor ranks relative to everyone else for this particular event.
I am toying with having a separate table where only a running tally is kept for each event/contributor and a piece of code to compute rank and re-insert it in the table, then use that table to drive views.
Can you think of a better approach?
Keeping a running tally is a fine option. Writes will slow down, but reads will be fast.
Another way is to create a database view, if you are using postgresql, something like:
-- Your table structure and data
create table whatever_table (event text, contributor text, amount int);
insert into whatever_table values ('e1', 'joe', 1);
insert into whatever_table values ('e2', 'joe', 1);
insert into whatever_table values ('e1', 'jim', 0);
insert into whatever_table values ('e1', 'joe', 1);
insert into whatever_table values ('e1', 'bob', 1);
-- Your view
create view event_summary as (
select
event,
contributor,
sum(amount) as total,
rank() over (order by sum(amount) desc) as rank
from whatever_table
group by event, contributor
);
-- Using the view
select * from event_summary order by rank;
event | contributor | total | rank
-------+-------------+-------+------
e1 | joe | 2 | 1
e1 | bob | 1 | 2
e2 | joe | 1 | 2
e1 | jim | 0 | 4
(4 rows)
Then you have an ActiveRecord class like:
class EventSummary < ActiveRecord::Base
self.table_name = :event_summary
end
and you can do stuff like EventSummary.order(rank: :desc) and so on. This won't slow down writes, but reads will be a little slower, depending on how much data you are working with.
Postgresql also has support for materialized views, which could give you the best of both worlds, assuming you can have a little bit of lag between when the data is entered and when the summary table is updated.

Fastest way to order by matching has many through association?

When using a has many association to manage a serious of tags, what is the most efficient way to order/sort the collection by the number of tags selected.
For example:
Product can have many tags through ProductTags
When a user selects the tags, I would like to order the products by the number of the selected tags each product has.
Is it possible to use a cache_counter or something similar in this case? I'm not convinced using sort is the best option. Am I correct in thinking that using order on the actual database is generally faster than sort?
Clarification/update
Sorry if the above is confusing. Basically what I'm after is closer to ordering by relevancy. For example a user might select tag 1, 2, and 4. If an product has all tree tags associated with it, I want that product listed first. The second product might only have tags 1 & 4. And so on. I'm almost certain that this will have to use sort versus order, but was wondering if anyone has found a more efficient way of doing this.
Ordering by relevance within the database is both possible and far more efficient than using the sort method in Ruby. Assuming the following model structure and an appropriate underlying SQL table structure:
class Product < ActiveRecord::Base
has_many :product_taggings
has_many :product_tags, :through => :product_taggings
end
class ProductTags < ActiveRecord::Base
has_many :product_taggings
has_many :products, :through => :product_taggings
end
class ProductTaggings < ActiveRecord::Base
belongs_to :product
belongs_to :product_tags
end
Querying for relevance in MySQL would look something like:
SELECT
`product_id`
,COUNT(*) AS relevance
FROM
`product_taggings` AS ptj
LEFT JOIN
`products` AS p
ON p.`id` = ptj.`product_id`
LEFT JOIN
`product_tags` AS pt
ON pt.`id` = ptj.`product_tag_id`
WHERE
pt.`name` IN ('Tag 1', 'Tag 2')
GROUP BY
`product_id`
If I have the following products and related tags:
Product 1 -> Tag 3
Product 2 -> Tag 1, Tag 2
Product 3 -> Tag 1, Tag 3
Then the WHERE clause from above should net me:
product_id | relevance
----------------------
2 | 2
3 | 1
* Product 1 is not included since there were no matches.
Given that the user is performing a filtered search,
this behavior is probably fine. There's a way to get
Product 1 into the results with 0 relevance if
necessary.
What you've done is create a nice little result set that can act as a sort of inline join table. In order to stick a relevance score onto each row of a query from your products table, use this query as a subquery as follows:
SELECT *
FROM
`products` AS p
,(SELECT
`product_id`
,COUNT(*) AS relevance
FROM
`product_taggings` AS ptj
LEFT JOIN
`products` AS p
ON p.`id` = ptj.`product_id`
LEFT JOIN
`product_tags` AS pt
ON pt.`id` = ptj.`product_tag_id`
WHERE
pt.`name` IN ('Tag 1', 'Tag 2')
GROUP BY `product_id`
) AS r
WHERE
p.`id` = r.`product_id`
ORDER BY
r.`relevance` DESC
What you'll have is a result set containing the fields from your products table and an additional relevance column at the end that will then be used in the ORDER BY clause.
You'll need to write up a method that will in-fill this query with your desired pt.name IN list. Be certain to sanitize that list before plugging it into the query or you'll open yourself up to possible SQL injection.
Take the result of your query assembling method and run it through Product.find_by_sql(my_relevance_sql) to get your models pre-sorted by relevance directly from the DB.
The obvious down-side is that you introduce a DBMS-specific dependency into your Rails code (and risk SQL injection if you're not careful). If you're not using MySQL, the syntax might need to be adapted. However, it should perform much faster, especially on a huge result set, than using a Ruby sort on the results. Furthermore, adding a LIMIT clause will give you pagination support if needed.
Building on Ryan's excellent answer, I wanted a method that could be used acts-as-taggable-on and similar plug-ins (tables called tags/taggings), and ended up with this:
def Product.find_by_tag_list(tag_list)
tag_list_sql = "'" + tag_list.join("','") + "'"
Product.find_by_sql("SELECT * FROM products, (SELECT taggable_id, COUNT(*) AS relevance FROM taggings LEFT JOIN tags ON tags.id = taggings.tag_id WHERE tags.name IN (" + tag_list_sql + ") GROUP BY taggable_id) AS r WHERE products.id = r.taggable_id ORDER BY r.relevance DESC;")
end
To get a list of related products ordered by relevance, I then can do:
Product.find_by_tag_list(my_product.tag_list)

Complex queries using Rails query language

I have a query used for statistical purposes. It breaks down the number of users that have logged-in a given number of times. User has_many installations and installation has a login_count.
select total_login as 'logins', count(*) as `users`
from (select u.user_id, sum(login_count) as total_login
from user u
inner join installation i on u.user_id = i.user_id
group by u.user_id) g
group by total_login;
+--------+-------+
| logins | users |
+--------+-------+
| 2 | 3 |
| 6 | 7 |
| 10 | 2 |
| 19 | 1 |
+--------+-------+
Is there some elegant ActiveRecord style find to obtain this same information? Ideally as a hash collection of logins and users: { 2=>3, 6=>7, ...
I know I can use sql directly but wanted to know how this could be solved in rails 3.
# Our relation variables(RelVars)
U =Table(:user, :as => 'U')
I =Table(:installation, :as => 'I')
# perform operations on relations
G =U.join(I) #(implicit) will reference final joined relationship
#(explicit) predicate = Arel::Predicates::Equality.new U[:user_id], I[:user_id]
G =U.join(I).on( U[:user_id].eq(I[:user_id] )
# Keep in mind you MUST PROJECT for this to make sense
G.project(U[:user_id], I[:login_count].sum.as('total_login'))
# Now you can group
G=G.group(U[:user_id])
#from this group you can project and group again (or group and project)
# for the final relation
TL=G.project(G[:total_login].as('logins') G[:id].count.as('users')).group(G[:total_login])
Keep in mind this is VERY verbose because I wanted to show you the order of operations not just the "Here is the code". The code can actually be written with half the code.
The hairy part is Count()
As a rule, any attribute in the SELECT that is not used in an aggregate should appear in the GROUP BY so be careful with count()
Why would you group by the total_login count?
At the end of the day I would simply ask why don't you just do a count of the total logins of all installations since the user information is made irrelevant by the outer most count grouping.
I don't think you'll find anything as efficient as having the db do the work. Remember that you don't want to have to retrieve the rows from the db, you want the db itself to compute the answer by grouping the data.
If you want to push the SQL further into the database, you can create the query as a view in the database and then use a Rails ActiveRecord class to retrieve the results.
In the end imo the SQL syntax is way more readable. This arel stuff is just slowing me down all the time when I only need just a tiny bit more complexity. It's just another syntax you have learn, not worth it imo. I'd stick to SQL in these cases.

Resources