SQL - Order records by the closest value - ruby-on-rails

I have created one API for product details
In this API I also need to attach similar products in response.
So for similar products scenario are like following
1) Price wise (+10 and -10)
2) Then after category wise (From same category)
e.g.
I have product with id #30 and price with $30 and category with "beer"
SO the similar products listing will be like following
First show all products beer category products which are come between range +10 and -10 of $30 (I mean b/w range 20 to 40)
Then after attach other products which are belongs from same category "beer" with closest price of $30
Suppose products price with same category are following
$10, $17, $45, $42, $50
so the products will sort as following as closet to $30
$42 ($42 - $30 = 12), $17 ($30 - $17 = 13), $45 ($45 - $30 = $15), $10 ($30 - $10 = $20), $50 ($50 - $30 = $20)
for similar products in +10 -10 range i created below query
similar_price = Product.includes(:sub_category, :brand, :product_type, :category).in_stock.where("(actual_price BETWEEN ? AND ? AND category_id = ? ) ", min_price, max_price, self.category_id)
now i need to order products with closest price.
How can fix this issue with postgres query in rails ?

Yes you can perform this operation by using just simple order query as like following
`Product.where("product.category_id = ?", "category_id").order("abs(products.price - #{your_selected_product_price})")`

The previous answer is dangerous to use if the price comes from untrusted user input, since it allows SQL injection. It also breaks if price can be nil.
The solution is to properly quote the price for the given column:
class Product < ActiveRecord::Base
scope :closest_by_price, ->(price) {
quoted_price = connection.quote(price, columns_hash["price"])
order("abs(products.price - #{quoted_price})")
}
end
Product.closest_by_price(50.0)

Related

Cypher : book recommendation

I have 3 nodes:
Users (id, age).
Ratings (isbn, id, rating (this has a value of 0 to 10)).
Books (isbn, title, ...)
And the relationships:
Users - [GIVE_RATINGS]-Ratings -[BELONGS_TO]- Books
I need to create a recommendation where the input will be one or more books the reader liked, and the output will be books that users who rated positively also rated books the reader has already read.
I tried to create such a query, but it doesn't work.
MATCH (u:Users{id:'11676'})-[:GIVE_RATING]->(book)<-[:GIVE_RATING]-(person), (person)-[:GIVE_RATING]->(book2)<-[:GIVE_RATING]-(r:Ratings{rating:'9'})
WHERE NOT EXIST (book2)-[:GIVE_RATING]->(u)
RETURN book2.isbn,person.id
you probably want to store your ratings as integers or floats, not strings, better to use [not] exists { pattern } in newer versions
A common recommendation statement would look like this:
MATCH (u:Users{id:$user})-[:GIVE_RATING]->(rating)
<-[:GIVE_RATING]-(person)-[:GIVE_RATING]->(rating2)
<-[:GIVE_RATING]-(rating3)
WHERE abs(rating2.rating - rating.rating) <= 2 // similar ratings
AND rating3.rating >= 9
AND NOT EXIST { (rating3)<-[:GIVE_RATING]-(u) }
WITH rating3, count(*) as freq
RETURN rating3.isbn,person.id
ORDER BY freq DESC LIMIT 5
You could also represent your rating information on the relationship between user and book, no need for the extra Node.

Active Record query Distinct Group by

I have a query, where I am trying to find min score of a user in a grade, in a grade, there are users with the same min score
Example: User A has a score of 2 and user B has a score of 2, so my expectation is to get both the users grouped by grade.
However, I am only getting one user. The query is :
users = Users.all
#user_score = users
.where.not(score: [ nil, 0 ])
.select('DISTINCT ON ("users"."grade") grade, "users".*')
.order('"users"."grade" ASC, "users"."score" ASC')
.group_by(&:grade)
Please if some can guide me what am i doing wrong here.
DISTINCT will cut off all non uniq values in the result, so there is no way to get multiple users with same min score in your query.
I think you can achieve the desired result with window function:
SELECT * FROM
(SELECT *, rank() OVER (PARTITION BY grade ORDER BY score) AS places
FROM users
WHERE score IS NOT NULL AND score != 0
) AS ranked_by_score
WHERE places = 1;

Get the average of the most recent records within groups with ActiveRecord

I have the following query, which calculates the average number of impressions across all teams for a given name and league:
#all_team_avg = NielsenData
.where('name = ? and league = ?', name, league)
.average('impressions')
.to_i
However, there can be multiple entries for each name/league/team combination. I need to modify the query to only average the most recent records by created_at.
With the help of this answer I came up with a query which gets the result that I need (I would replace the hard-coded WHERE clause with name and league in the application), but it seems excessively complicated and I have no idea how to translate it nicely into ActiveRecord:
SELECT avg(sub.impressions)
FROM (
WITH summary AS (
SELECT n.team,
n.name,
n.league,
n.impressions,
n.created_at,
ROW_NUMBER() OVER(PARTITION BY n.team
ORDER BY n.created_at DESC) AS rowcount
FROM nielsen_data n
WHERE n.name = 'Social Media - Twitter Followers'
AND n.league = 'National Football League'
)
SELECT s.*
FROM summary s
WHERE s.rowcount = 1) sub;
How can I rewrite this query using ActiveRecord or achieve the same result in a simpler way?
When all you have is a hammer, everything looks like a nail.
Sometimes, raw SQL is the best choice. You can do something like:
#all_team_avg = NielsenData.find_by_sql("...your_sql_statement_here...")

Ranking position

I have a Rails application with the following models:
User
Bet
User has many_bets and Bets belongs_to User. Every Bet has a Profitloss value, which states how much the User has won/lost on that Bet.
So to calculate how much a specific User has won overall I cycle through his bets in the following way:
User.bets.sum(:profitloss)
I would like to show the User his ranking compared to all the other Users, which could look something like this:
"Your overall ranking: 37th place"
To do so I need to sum up the overall winnings per User, and find out in which position the current user is.
How do I do that and how to do it, so it don't overload the server :)
Thanks!
You can try something similar to
User.join(:bets).
select("users.id, sum(bets.profitloss) as accumulated").
group("users.id").
order("accumulated DESC")
and then search in the resulting list of "users" (not real users, they have only two meaningful attributes, their ID and a accumulated attribute with the sum), for the one corresponding to the current one.
In any case to get a single user's position, you have to calculate all users' accumulated, but at least this is only one query. Even better, you can store in the user model the accumulated value, and query just it for ranking.
If you have a large number of Users and Bets, you won't be able to compute and sort the global profitloss of each user "on demand", so I suggest that you use a rake task that you schedule regularly (once a day, every hour, etc...)
Add a column position in the User model, get the list of all Users, compute their global profitloss, sort the list of Users with their profitloss, and finally update the position attribute of each User with their position in the list.
Best way to do it is to keep a pre calculated total in your database either on user model itself or on a separate model that has 1:1 relation to user. If you don't do this, you will have to calculate sum for all users at all times in order to get their rating, which means full table operation on bets table. This said, this query will give you desired results, if more than 1 person has the same total, it will count both as rating X:
select id, (select count(h.id) from users u inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b2
on b2.user_id = u.id, (select id from users) h inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b
on b.user_id = h.id where u.id = 1 and (b.total > b2.total))
as `rating` from users where id = 1;
You will need to plug user.id into query in where id = X
if you add a column to user table to keep track of their total, query is a little simpler, in this example column name is total_profit_loss:
select id, total_profit_loss, (select count(h.username)+1 from users u,
(select username, score from users) h
where id = 1 and (h.total_profit_loss > u.total_profit_loss))
as `rating` from users where id = 1;

Logic and code help

Here are my models and associations:
User has many Awards
Award belongs to User
Prize has many Awards
Award belongs to Prize
Let's pretend that there are four Prizes (captured as records):
Pony
Toy
Gum
AwesomeStatus
Every day a User can be awarded one or more of these Prizes. But the User can only receive each Prize once per day. If the User wins AwesomeStatus, for ex, a record is added to the Awards table with a fk to User and Prize. Obviously, if the User doesn't win the AwesomeStatus for the day, no record is added.
At the end of the day (before midnight, let's say), I want to return a list of Users who lost their AwesomeStatus. (Of course, to lose your AwesomeStatus, you had to have the day before.) Unfortunately, in my case, I don't think observers will work and will have to rely on a script. Regardless, how would you go about determining which Users lost their AwesomeStatus? Note: don't make your solution overly dependent on the period of time -- in this case a day. I want to maintain flexibility in how many times per whatever period Users have an opportunity to win the prize (and to also lose it).
I would probably do something like this:
The class Award should also have a column awarded_at which contains the day the prize was awarded. So when it is time to create the award it can be done like this:
# This will make sure that no award will be created if it already exists for the current date
#user.awards.find_or_create_by_prize_id_and_awarded_at(#prize.id, Time.now.strftime("%Y-%m-%d"))
And then we can have a scope to load all users with an award that will expire today and no active awards for the supplied prize.
# user.rb
scope :are_losing_award, lambda { |prize_id, expires_after|
joins("INNER JOIN awards AS expired_awards ON users.id = expired_awards.user_id AND expired_awards.awarded_at = '#{(Time.now - expires_after.days).strftime("%Y-%m-%d")}'
LEFT OUTER JOIN awards AS active_awards ON users.id = active_awards.user_id AND active_awards.awarded_at > '(Time.now - expires_after.days).strftime("%Y-%m-%d")}' AND active_awards.prize_id = #{prize_id}").
where("expired_awards.prize_id = ? AND active_awards.id IS NULL", prize_id)
}
So then we can call it like this:
# Give me all users who got the prize three days ago and has not gotten it again since
User.are_losing_award(#prize.id, 3)
There might be some ways to write the scope better with ARel queries or something, I'm no expert with that yet, but this way should work until then :)
I'd add an integer "time period" field to awards, which stands for a given period of time (day, week, 5 hour period, whatever you want).
Now, you can search the awards table for users who have the award status at t-1, but not at t:
SELECT prev.user_id
FROM awards prev
OUTER JOIN awards current ON prev.user_id = current.user_id
AND prev.prize_id = current.prize_id
AND current.time_period = 1000
WHERE prev.prize_id = 1
AND current.prize_id IS NULL
AND prev.time_period = 999
Just use updated_at, or add an awarded_at like suggested above and use it like this:
scope :awarded, proc {|date| where(["updated_at <= ?", date])}
In your Award model. Print it like this, maybe:
awesome_status = Prize.find_by_name('AwesomeStatus')
p "Users who do not have AwesomeStatus anymore:"
User.all.each {|user| p user.username if user.awards.awarded(1.day.ago).collect(&:id).include?(awesome_status)}
If you want it to be dynamic, displayed somewhere, etc. throw a 'lasts_for' into Prize and compare against it and simply write a maintenance cronjob that sets an 'active' boolean on Award to false instead of deleting the association.

Resources