I'm building an application where I have products and categories. Category has_many properties and each property has a list of possible values. After a category is set to the product all properties show up in the form and the user can set that property to one of the properties possible values.
My question is:
Is it possible for Thinking Sphinx to filter the products through a property and property value ex:
:with => {:property_id => property_value}
If it's possible, what is the best way to implement this? If not is there any other library out there to solve this problem?
Thanks
/ Ola
One approach is to store the property_id as multi-value attribute.
class Product < ActivRecord::Base
has_one :category
has_many :properties, :through => :category
KVP = "###"
define_index do
has properties("CONCAT(`properties`.`key`, \"%s\", `properties`.`value`)" %
KVP, :as => :category_key_value
end
def search_with_properties keys, with_attr={}, p={}
wp = (with_attr||{}).dup
values = p.map{|k, v| "#{k}#{KVP}#{v}"} unless p.empty?
wp = wp.merge({:category_key_value => values}) unless values.empty?
search keys, :with => wp
end
end
class Category < ActivRecord::Base
belongs_to :product
has_many :properties
end
class Property < ActivRecord::Base
belongs_to :Category
#key E.g: region
#value E.g: South West
end
Now you can issue following search commands:
Product.search_with_properties("XYZ", nil, :region => "South West")
Try this:
Add the following to your define_index:
has properties(:id), :as => :property_ids
Then you can use :with / :without like:
:with => {:property_ids => property_value}
Does this answer your question:
https://github.com/freelancing-god/thinking-sphinx/issues/356
Related
I'm trying to model items that has_many tags. Items can have multiple tags, but are required to have at least 3 predefined ones.
Here's what I have so far:
class Item < ActiveRecord::Base
has_one :type, :through => :item_tags, :source => :tag
has_one :material, :through => :item_tags, :source => :tag
has_one :use, :through => :item_tags, :source => :tag
has_many :tag, :through => :item_tags
has_many :item_tags
end
This is giving me an ActiveRecord::HasOneThroughCantAssociateThroughCollection when I try to do Item.find(1).type.
I'm not sure how to do this. Can anyone help?
EDIT: I also want to be able to find the three predefined tags by doing item.type and item.use etc.
It's easier to consider this first by seeing how you want your database set up. You want:
Table: Tag
id
tag_name
Table: ItemTag
id
item_id
tag_id
Table: Item
id
type_id
material_id
use_id
So, your model would be more like:
class Item < ActiveRecord::Base
belongs_to :type, :class_name => 'Tag'
belongs_to :material, :class_name => 'Tag'
belongs_to :use, :class_name => 'Tag'
# Require these tags
validates_presence_of :type, :material, :use
has_many :item_tags
has_many :tags, :through => :item_tags
def list_tags
[type, material, use] + tags
end
end
So, your database will have three columns directly in the item table, which link to the tag table. These are required via validations, but you can also set up in your migrations to make these columns not null as well if you wish. The other optional tags keep their same relationship.
You want belongs_to and not has_one, because that pushes the relationship to the Item, where you want it. Has_one puts an item_id column in the Tag table, which is not what you want.
To get the three required tags to appear with the rest of the tags via this method, I'd recommend adding a function just for this use, defined as list_tags above.
Hope that helps!
I think you may want to use custom validations to check that Item.tags includes the ones you require, then use either scopes and class methods to get item.use, item.type, etc. to work as you want.
Item model:
class Item < ActiveRecord::Base
has_many :tags, :class_name => 'ItemTag'
validate :has_type, :has_use, :has_material
# Access methods
def types
self.tags.types
end
def uses
self.tags.uses
end
def materials
self.tags.materials
end
private
# Custom validation methods
def has_type
unless tags.present? and tags.include?(ItemTag.types)
errors.add("Tags must include a type.")
end
end
def has_material
unless tags.present? and tags.include?(ItemTag.materials)
errors.add("Tags must include a material.")
end
end
def has_use
unless tags.present? and tags.include?(ItemTag.use)
errors.add("Tags must include a use.")
end
end
end
ItemTag model:
class ItemTag < ActiveRecord::Base
scope :types, lambda { where(...) }
scope :materials, lambda { where(...) }
scope :uses, lambda { where(...) }
end
You could grab single occurances if preferred using .first in the access methods. You'll need to adjust the where(...) queries based on how you are determining what constitutes a type/material/use.
I have this relation in my Product model:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
So I can do Product.features
which works fine. But I want to be able to filter that by fields in the feature table, when and if necessary. For example in pseudo code:
find all product features where feature is comparable
compare is a bool field on the feature.
I have been trying for 2 hours solid and cannot figure it out (without writing a new query completely). I can't figure out how to access the feature table's fields from the Product.features relation, as it seems it can only filter on product_features fields.
This is what I have come up with so far:
def features_compare
features.feature.where(:compare => true)
end
But it just says feature is not a valid method, which I understand.
Edit
I have updated my model so the relationships are clearer:
product.rb:
class Product < ActiveRecord::Base
belongs_to :company
belongs_to :insurance_type
has_many :product_features
has_many :reviews
attr_accessible :description, :name, :company
end
product_feature.rb:
class ProductFeature < ActiveRecord::Base
belongs_to :product
belongs_to :feature
delegate :name, :to => :feature
attr_accessible :value
end
feature.rb
class Feature < ActiveRecord::Base
attr_accessible :name, :compare
end
I want to be able to query the product_features that belong to a product and feature where Feature.compare is true. Something like this:
product.rb
def features_compare
product_features.where(:compare => true)
end
This throws an error because compare in in the Feature model, not ProductFeature. I have tried the following in product_feature.rb:
delegate :compare, :to => :feature
but I didn't help.
I will adding a bounty to this in a few hours so please please help me!
find all product features where feature is comparable is just
ProductFeature.joins(:feature).where(:feature => {:compare => true})
You can make that a bit more reusable by introducing a scope:
#in product_feature.rb
scope :with_feature_like, lambda do |filter|
joins(:feature).where(:feature => filter)
end
#elsewhere
ProductFeature.with_feature_like(:compare => true)
#all the product features of a certain product with at comparable features
some_product.product_features.with_feature_like(:compare => true)
Finally, if you want all products with product features with comparable features, you want something like:
Product.joins(:product_features => :feature).where(:feature => {:compare => true})
which of course you can also turn into a scope on Product.
This seems like a has_many :through relationship. Try changing this:
has_many :features, :class_name => 'ProductFeature', :source => :product_feature, :include => :feature
to this:
has_many :product_features
has_many :features, :through => :product_features
As long as your ProductFeature model has this:
belongs_to :product
belongs_to :feature
And you have the appropriate columns on product_features (product_id, feature_id), then you should be able to access that product's features and all the attributes on both Product and ProductFeature.
See here:
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
EDIT: Here's how to filter by feature fields.
Product.joins(:features).where(:features => {:name => "Size"})
#product.each |p| { p.features.where(:comparable => true) } is probably your best bet here, but I'm open to being enlightened.
Currently, I can add the creator_id like this in my controller:
#entry = Entry.new(params[:entry].merge(:creator => current_user._id))
If this is my model:
class Entry
include Mongoid::Document
belongs_to :User
field :creator, :type => String
field :title, :type => String
field :content, :type => String
field :scorea, :type => Integer
field :scoreb, :type => Integer
field :scorec, :type => Integer
end
Is there a better way to do this?
Your model doesn't looks very good, do you really want to store the user_id in a string field?
I suggest you change your models to following:
class Entry
include Mongoid::Document
belongs_to :creator, :class_name => 'User', :inverse_of => :entries
# field definitions
end
class User
include Mongoid::Document
has_many :entries, :inverse_of => :creator
end
Once you change the models you can continue using what you are now or alternatively:
#entry = current_user.entries.build(params[:entry])
Update:
The method to initialize entry is not much different in the way I did it. It is just more towards the rails way of doing things. The main difference is that you were not using the associations. From your model definitions it is clear that you want a one-to-many association between user and entries and this is how you create such associations. Associations has a lot of goodies attached to them, like you can do following things:
user.entries << entry # add a entry to users, will automatically change entry.creator_id
entry.creator = user # sets creato_id = user_id
entry.creator # returns associated user. no need to do User.find(entry.creator_id)
user.entries # returns all entries for use <=> Entry.where(creator_id: user.id)
for more details go to http://mongoid.org/docs/relations.html
There are no better way to do. Or you can do it in 2 line :
#entry = Entry.new(params[:entry])
#entry.creator = current_user._id
this second case allow you to add creator field in attr_protected
You can do it in this manner
#entery = Entery.new(params[:entry])
#entery.creator = current_user
or
#entery.creator_id = current_user.id
hence you can assigned id of creator to this entery.
I've not seen this feature as a plug in and was wondering how to achieve it, hopefully using rails.
The feature I'm after is the ability to rate one object (a film) by various attributes such as plot, entertainment, originality etc etc on one page/form.
Can anyone help?
I don't think you need a plugin to do just that... you could do the following with AR
class Film < ActiveRecord::Base
has_many :ratings, :as => :rateable
def rating_for(field)
ratings.find_by_name(field).value
end
def rating_for=(field, value)
rating = nil
begin
rating = ratigns.find_by_name(field)
if rating.nil?
rating = Rating.create!(:rateable => self, :name => field, :value => value)
else
rating.update_attributes!(:value => value)
end
rescue ActiveRecord::RecordInvalid
self.errors.add_to_base(rating.errors.full_messages.join("\n"))
end
end
end
class Rating < ActiveRecord::Base
# Has the following field:
# column :rateable_id, Integer
# column :name, String
# column :value, Integer
belongs_to :rateable, :polymorphic => true
validates_inclusion_of :value, :in => (1..5)
validates_uniqueness_of :name, :scope => :rateable_id
end
Of course, with this approach you would have a replication in the Rating name, something that is not that bad (normalize tables just for one field doesn't cut it).
You can also use a plugin ajaxfull-rating
Here's another, possibly more robust rating plugin...it's been around for a while and has been revised to work with Rails 2
http://agilewebdevelopment.com/plugins/acts_as_rateable
I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end