Rails complicated selection with order in mongoid - ruby-on-rails

I have:
gem 'rails', '~> 5.1.1'
gem 'mongoid', '~> 6.1.0'
I have 3 models, related to this question.
class Book
include Mongoid::Document
field :name, type: String
field :author, type: String
field :description, type: String
field :status, type: String
field :image, type: String
has_many :histories, dependent: :destroy
has_many :likes, dependent: :destroy
has_many :comments, dependent: :destroy
def ordered_histories
histories.order(taken_in: :desc)
end
def book_rating
rating_array = likes.map { |like| like.rate.to_i }
return 0 if rating_array.empty?
(rating_array.sum / rating_array.size).round
end
end
class Like
include Mongoid::Document
field :rate, type: Integer
belongs_to :book
belongs_to :user
end
class History
include Mongoid::Document
field :taken_in, type: DateTime
field :returned_in, type: DateTime
belongs_to :book
belongs_to :user
end
I need to get top 5 books which have the most count of likes and histories.
As I know - mongoid not support joins.
I thought about storeing likes_count and histories_count in Book model. But I think that it is not best way. Please help me to find better solution to get top 5 books from mongodb.

You should use the aggregation framework.
Try the following code to get the ids of the top 5 books with most histories count. The same approach can be used for the Like class.
History.collection.aggregate([
{"$group" => {
"_id" => "$book_id",
"histories" => {"$sum" => 1}
}},
{"$sort" => { "histories" => -1}},
{"$limit" => 5 }
])

I found another way to make it works.
I have add counter_cash to model Like and History:
class Like
include Mongoid::Document
field :rate, type: Integer
belongs_to :book, counter_cache: :likes_count
belongs_to :user
end
class History
include Mongoid::Document
field :taken_in, type: DateTime
field :returned_in, type: DateTime
belongs_to :book, counter_cache: :histories_count
belongs_to :user
end
After that I use one query to get top 5 books which have the most count of likes and histories:
#top_books = Book.all.order_by(likes_count: :desc, histories_count: :desc).limit(5)
I think it is one of possible way to do this. May be it help somebody. Tell me please, if I made any mistakes.

Related

Rails: Elasticsearch :through association mapping

I'm trying to index a model when I have a has_many, :through association, but no results are being displayed.
class Business < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
def self.search(params)
tire.search(load: true) do
query { string params[:q]} if params[:q].present?
end
end
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :services do
indexes :service
indexes :description
end
end
def to_indexed_json #returns json data that should index (the model that should be searched)
to_json(methods: [:service_name, :service_description], include: { services: [:service, :description]})
end
def service_name
services.map(&:service)
end
def service_description
services.map(&:description)
end
has_many :professionals
has_many :services, :through => :professionals
end
Then this is Service model
class Service < ActiveRecord::Base
attr_accessible :service, :user_id, :description
belongs_to :professional
belongs_to :servicable, polymorphic: true
end
I've also reindex using this:
rake environment tire:import CLASS=Business FORCE=true
I can search for the items in Business, but when I tried to search something in Service, I get an empty result.
After struggling with mapping, I created a gem to make search a bit easier. https://github.com/ankane/searchkick
You can use the search_data method to accomplish this:
class Business < ActiveRecord::Base
searchkick
def search_data
{
service_name: services.map(&:name),
service_description: services.map(&:description)
}
end
end
I do not believe there is a way to do mapping on associations with Tire. What you will want to do instead is define easily searchable fields with the :as method and a proc. This way you can also get rid of the to_indexed_json method (you will actually need to)
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :service_name, type: 'string', :as => proc{service_name}
indexes :service_description, type: 'string', :as => proc{service_description}
end
Tire can associate with associations, I've used it to index on has_many association but have not tried has_many, :through yet. Try index on object?
mapping do
indexes :service_name
indexes :service_description
indexes :latitude
indexes :longitude
indexes :services, type: 'object',
properties: {
service: {type: 'string'}
description: {type: 'string'}
}
end
Also, it might be good to have a touch method :
class Service < ActiveRecord::Base
attr_accessible :service, :user_id, :description
belongs_to :professional, touch: true
belongs_to :servicable, polymorphic: true
end
and after_touch callback to update the index.

Changing Mongoid class name mid-production

Is this even possible?
I have a mongoid class named Magazine, with some associations as well, that I would like to re-name to Publication. Problem is that I already have a bunch of users who have already made magazines, issues and articles.
Original Magazine model:
class Magazine
# 1. Include mongoid stuff
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Slug
# 2. Define fields
field :title, type: String
field :description, type: String
field :live, type: Boolean, default: false
field :show_walkthrough, type: Boolean, default: true
# 3. Set attributes accesible
attr_accessible :title, :description, :live, :show_walkthrough, :cover_image_attributes, :logo_image_attributes
# 4. Set slug
slug :title
# 5. Set associations
belongs_to :user
has_many :issues, dependent: :delete, autosave: true
has_one :foreword, :as => :articleable, :class_name => 'Article', dependent: :delete, autosave: true
embeds_one :cover_image, :as => :imageable, :class_name => 'Image', cascade_callbacks: true, autobuild: true
embeds_one :logo_image, :as => :imageable, :class_name => 'Image', cascade_callbacks: true, autobuild: true
# 6. Accepting nested attributes
accepts_nested_attributes_for :cover_image, :allow_destroy => true
accepts_nested_attributes_for :logo_image, :allow_destroy => true
# 7. Set validations
validates_presence_of :title, :description, :cover_image, :logo_image
end
I know I can change the class-name to Publication and then do db.magazines.renameCollection( "publications" ) on the mongodb, but the associations doesn't follow along.
Any suggestions?
I looks like you have association fields in your Issue and Foreword models that probably refer to Magazine. So if you are happy enough to change the name of the class and underlying collection then renaming these association fields is your main problem. You may have something like:
class Issue
belongs_to :magazine
end
You could redefine this association as belongs_to :publication. Assuming that are happy to fix all the references to Issue#magazine in your code then your remaining problem is that your issues collection will be full of documents that have a magazine_id field instead of publication_field. You have two options to fix the database mapping.
First option is to rename the field in the database. See mongoDB : renaming column name in collection
The second option is to declare the association so that it maps to the old database field by overriding the 'foreign key' name:
belongs_to :publication, foreign_key: :magazine_id
You will have to repeat this for the Foreword model and any others that reference Magazine.
Just a heads up for polymorphism and class inheritance.
Mongoid handles inheritance and polymorphic associations by storing the class name as a document attribute.
On the class itself, this is stored as the "_type" attribute
For polymorphic associations like belongs_to :polymorphic_class mongoid adds an attribute "polymorphic_class_type", so that the class can be resolved (with Rails' .constantize) when browsing polymorphic associations.
So if you decide to change the class name, and you have inheritance or polymorphic associations, well you'll have to also rewrite all those attributes !

Mongoid: search in has_many relation

I'm using mongoid, and have the following code:
class Users::User
include Mongoid::Document
field :username, type: String
has_many :emails, class_name: "Users::Email"
end
class Users::Email
include Mongoid::Document
field :email, type: String
belongs_to :user, class_name: "Users::User", inverse_of: :emails
end
database:
#users collection
{
"_id" : ObjectId("5162de8a359f10cbf700000c"),
"username" : "bilbo"
}
#emails collection
{
"_id" : ObjectId("5162de8a359f10cbf700000b"),
"email" : "bilbo#jenkins.com",
"user_id" : ObjectId("5162de8a359f10cbf700000c"),
}
I'm trying to find with the following query:
Users::User.includes(:emails).any_of({username: login},{"emails.email"=> login}).first
and I don't know why, but this query ignoring search in emails relation.
When login = "bilbo" => true, but when login = "bilbo#jenkins.com" => nil
So, what I'm doing wrong?
You need a join to do what you're trying to do and Mongoid doesn't have joins. If you only need to access the emails through a user, you could denormalize them and embed them in Users::User.
class Users::User
include Mongoid::Document
field :username, type: String
embeds_many :emails, class_name: "Users::Email"
end
class Users::Email
include Mongoid::Document
field :email, type: String
embedded_in :user, class_name: "Users::User", inverse_of: :emails
end
That way, you can query on a user's emails:
irb(main):011:0> login = "bilbo#jenkins.com"
=> "bilbo#jenkins.com"
irb(main):012:0> Users::User.any_of({username: login},{"emails.email"=> login}).first
=> #<Users::User _id: 5163ee96e44f7b0301000001, username: "bilbo">
If Users::Email's only property is email you could go even a step further and omit the model completely and store the string in an array:
class Users::User
include Mongoid::Document
field :username, type: String
field :emails, type: Array
end
Querying gets even easier:
Users::User.any_of({username: login},{"emails"=> login}).first
=> #<Users::User _id: 5163ef95e44f7b6254000001, username: "bilbo", emails: ["bilbo#jenkins.com"]>

Sorting a Mongoid Object via its Associated Model's Attribute

I'm having some problems trying to understand how Mongoid does its sorting. I have 2 models, Gig and Venue, both of which are associated by a belong_to has_many relationship.
I'm trying to sort objects from Gig by the attribute 'name' of the Venue Object to no avail.
I'm hoping someone out there would be able to point me to the right direction.
Below are a truncated model description.
My Query is also below:
# Gig Model
class Gig
include Mongoid::Document
include Mongoid::Paperclip
include SearchMagic
belongs_to :owner, :class_name => "User", :inverse_of => :owns
belongs_to :venue
has_and_belongs_to_many :attenders, :class_name => "User", :inverse_of => :attending
has_and_belongs_to_many :artistes
<snip>
end
# Venue Model
class Venue
include Mongoid::Document
include Mongoid::Paperclip
include SearchMagic
has_many :gigs
field :foursquare_id, type: String
embeds_one :address
embeds_many :user_ratings
field :facebook, type: String
field :twitter, type: String
field :website, type: String
field :name, type: String
field :postal, type: String
field :tel, type: String
field :venue_type, type: String
field :description, type: String
field :rating, type: Float, default: 0.0
<snip>
end
# Console
>> Gig.desc('venue.name').map{|f| f.venue.name}
=> ["*SCAPE", "Velvet Underground", "Blujaz Lounge", "Velvet Underground", "Home Club", "Wh
ite House, Emily Hill", "Zouk", "Zouk", "The Pigeonhole", "Home Club", "Home Club", "Home C
lub"]
# sorting doesn't work
You can't join in mongo. If you need joins, use a relational database. A "feature" of non-relational databases is that you can't do joins.
You have basically two choices:
a before_save callback, which will inject the name of the venue into the gig as an additional field (see for instance https://github.com/rewritten/timebank/blob/master/lib/mongoid/denormalize.rb)
a map-reduce task, which after any modification of any venue or gig, will update the venue name into the gig as an additional field.
In the end, you need a field in the Gig collection to order it.

Why doesn't mongoid add my nested attributes on new()?

I can't seem to figure out why Mongoid won't set the nested attributes for a child object when I create a new parent. I want to create a new Folio, add one child Feature, then push it on the Folios array on Profile.
I have a Profile, which embed many Folios, which embed many Features:
class Profile
include Mongoid::Document
include Mongoid::Timestamps::Updated
#regular fields here; removed for brevity
embeds_many :folios, class_name: "Folio"
end
class Folio
include Mongoid::Document
include Mongoid::Timestamps::Updated
accepts_nested_attributes_for :features
embedded_in :profile
field :name
field :desc
field :order, type: Integer, default:0
embeds_many :features
attr_accessible :name, :desc, :order
end
class Feature
include Mongoid::Document
include Mongoid::Timestamps::Updated
embedded_in :folio
belongs_to :project
field :content_type, type: Integer #ContentType
field :content_id
field :txt, type: String
field :order, type: Integer, default:0
attr_accessible :project_id, :content_type, :content_id, :txt, :order
end
Controller:
def new
#folio = Folio.new
#folio.features.build
end
def create
#folio = Folio.new(params[:folio])
##folio.features is still empty here.
#profile.folios << #folio
#profile.save
render "create_or_update.js"
end
In create, the param hash looks good:
{"folio"=>{"id"=>"new", "name"=>"new name", "desc"=>"new description", "features_attributes"=>{"0"=>{"project_id"=>"4ea0b68e291ebb44a100000a", "content_type"=>"1", "content_id"=>"4ea0b68e291ebb44a100000d", "txt"=>"note here"}}}, "commit"=>"Save", "action"=>"create", "controller"=>"folios"}
But #folio.features is still empty.
This worked fine with AR, if I remember. Strangely, there is no features_attributes=() method on Folio. I thought that was required for the nested attributes to work? What am I missing?
This is on Rails 3.1 with Mongoid 2.2.3.
have you tried enabling AutoSave true for features in Folio document
class Folio
include Mongoid::Document
include Mongoid::Timestamps::Updated
accepts_nested_attributes_for :features , :autosave => true
embedded_in :profile
end

Resources