Rails Thinking Sphinx Indexing Self Join Associations Tree Structure - ruby-on-rails

I have products which belong to a category. And categories make up a tree
structure by having a parent and children using self joins:
Associations:
class Category < ActiveRecord::Base
has_many :children, class_name: "Category", foreign_key: "parent_id"
belongs_to :parent, class_name: "Category"
end
class Product < ActiveRecord::Base
belongs_to :category
end
For example,
Fruits & Vegetables => "High" Category
Fresh Fruits => "Intermediate" Category
Citrus => "Low" Category
Limes Large => Product
I would like to use Thinking Sphinx to index both the "low" category name and
"high" category name for a product, and possibly even all category names in between in the tree hierarchy.
I had no trouble indexing the low category parent name as follows:
class Product < ActiveRecord::Base
indexes :name
indexes category.parent.name, as: :low_category
end
NOTE: The number of nodes between the "High" and "Low" categories are variable. I need a way to dynamically add the hierarchical names.
But how do I go about indexing category names further up in the tree? I know I can't use methods
in TS indexing, so how I do I setup the database?
Most importantly, how do I index the "high" category name?

Can you do this ?
class Product < ActiveRecord::Base
indexes :name
category = category.parent
indexes category.name, as: :low_category
while category.parent do
if category.parent
indexes category.name, as: :root_category
elsif category.parent
indexes category.name, as: :high_category
else
indexes category.name
end
category = category.parent
end
end

Related

Querying through four models in Rails

My relationships are set up like this:
A Project has_many documents
A Document has_one content_body
ContentBody has_many audits
I need to retrieve the Audits having only a project id in a descending order.
Assuming
class Audit
belongs_to :content_body
end
class ContentBody
belongs_to :document
end
class Document
belongs_to :project
end
Then
#audits = Audit.joins(content_body: { document: :project })
.where(projects: {id: some_id })
.order(created_at: :desc)
Or
#audits = Audit.joins(content_body: :document)
.where(documents: {project_id: some_id })
.order(created_at: :desc)
Should do the trick. As for the order you did not specify what column exactly descending should be based on so I assumed created_at

Ruby on Rails / get count on self nested model (has_many / belongs_to)

I have a self nested category model: which has_many and belongs_to it self
class Category < ActiveRecord::Base
has_many :subcategories, class_name: "Category", foreign_key: "parent_id", dependent: :destroy
belongs_to :parent_category, class_name: "Category", foreign_key: "parent_id"
end
In the view I want to display not only the #category.subcategories.count but the count of all nested subcategories
how would I get that?
~~~ UPDATE: ~~~
In the categories controller I get the current category from the parameters like:
def show
#category = Category.find(params[:id])
end
now I want use in the view (but the following example doesn't give me all nested subcategories back)
<div>
<%= #category.name %> has <%= #category.subcategories.count %> subcategories in total
</div>
create a recursive model method...
def deep_count
count = subcategories.count
subcategories.each { |subcategory| count += subcategory.deep_count }
count
end
If in your design it's possible for a child to be the parent of an ancestor
(e.g. "4x4" -> "Jeep" - > "SUV" -> "4x4" -> ...)
Then you could end up with a stack overflow. To avoid that you can track categories to ensure you don't deep_count them twice...
def deep_count(seen_ids=[])
seen_ids << id
count = subcategories.count
subcategories.where("id NOT IN (?)", seen_ids).each do |subcategory|
count += subcategory.deep_count(seen_ids)
end
count
end
As an addition to #SteveTurczyn's epic answer, you may wish to look at using one of the hierarchy gems (we use acts_as_tree).
Not only will this extract your has_many :subcategories association, but provides a myriad of functionality to allow you to better handle nested objects.
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
end
This will allow you to use the following:
#category = Category.find x
#category.children #-> collection of subcategories
#category.parent #-> Category record for "parent"
#category.children.create name: "Test" #-> creates new subcategory called "test"
Because acts_as_tree uses parent_id, you wouldn't have to change anything in your database.
--
You'd still be able to use the deep_count method:
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
def deep_count
count = children.count
children.each {|child| count += child.deep_count }
count
end
end
I'm sure there must be a way to count the "children" but I've not got any code at hand for it.
The main benefit of it is the recursion with displaying your categories. For example, if a Post has_many :categories:
#app/views/posts/index.html.erb
<%= render #post.categories %>
#app/views/categories/_category.html.erb
<%= category.name %>
Subcategories:
<%= render category.children if category.children.any? %>
--
Just seems a lot cleaner than two ActiveRecord associations.

Thinking Sphinx Rails Multiple Association

I have the following models
class Product < ActiveRecord::Base
belongs_to :sub_category
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_many :products
end
class Category < ActiveRecord::Base
has_many :sub_categories , -> { where("activate = 1") }
end
I need to index my products table.I need to search using category name(which is in category table) and subcategory name(in subcategories table)
ThinkingSphinx::Index.define :product , :with => :active_record do
indexes description
indexes name
indexes merchant_name
indexes sub_category(:sub_category) , :as => :sub_category_name
indexes category(:name) , :as => :cat_name
has sub_category_id
end
The category(:name) is failing.The subcategory is working fine.
Could somebody please help.I tried sub_category.category(:name) but thats also failing
Error Message
ERROR: index 'link_core': sql_range_query: You have an error in your
SQL syntax; check the manual that corresponds to your MySQL server
version for the right syntax to use near 'AS cat_name, products.id AS
sphinx_internal_id, 'Product' AS `sphinx_internal_' at line 1
(DSN=mysql://root:***#localhost:3306/xxxx_dev_phase4)
name should be passed as a chained method, not as an argument
indexes sub_category.category.name , :as => "category_name"
Thanks to the owner Pat for helping me out
concerned github thread

Creating a model that has a tree structure

I have categories that are in a tree structure. I am trying to link them together by defining a parent for each one. (I couldn't figure out how to call the property parent so it's just category for now, but it means the parent).
class Category < ActiveRecord::Base
has_one :category # the parent category
end
But the relationship ends up the wrong way around.
The getter function is on the child category (correctly) but the category_id is stored on the parent:
parent = Category.create(:name => "parent")
child = Category.create(:name => "child", :category => parent)
parent.id # 1
child.id # 2
child.category_id # nil
parent.category_id # 2
child.category.name # "parent" (!!)
The parent needs to be able to have multiple children so this isn't going to work.
What you're looking for is self joins. Check this section of the Rails guide out: http://guides.rubyonrails.org/association_basics.html#self-joins
class Category < ActiveRecord::Base
  has_many :children, class_name: "Category", foreign_key: "parent_id"
  belongs_to :parent, class_name: "Category"
end
Every Category will belong_to a parent, even your parent categories. You can create a single category parent that your highest level categories all belong to, then you can disregard that information in your application.
You can use acts_as_tree gem to achieve this, find below example and link.
https://github.com/amerine/acts_as_tree/tree/master
class Category < ActiveRecord::Base
include ActsAsTree
acts_as_tree order: "name"
end
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1
You should take a look at the ancestry gem: https://github.com/stefankroes/ancestry
It provides all the functionality you need and is able to get all descendants, siblings, parents, etc with a single SQL query by using a variant of materialized paths so it'll have better performance than the self-joins and acts_as_tree answers above.
Category should have many categories, and the foreign key of each category should be the parent_id. So, when you do parent.children it lists all the categories which have parent_id=parent.id.
Have you read on Single Table Inheritance?
Full Article - https://blog.francium.tech/best-practices-for-handling-hierarchical-data-structure-in-ruby-on-rails-b5830c5ea64d
A Simple table
Table Emp
id: Integer
name: String
parent_id: Integer
Associations
app/models/emp.rb
class Emp < ApplicationRecord
has_many :subs, class_name: 'Emp', foreign_key: :parent_id
belongs_to :superior, class_name: 'Emp', foreign_key: :parent_id
end
Scope Definition
class Emp < ApplicationRecord
----
----
scope :roots, -> { where(parent_id: nil) }
end
Fetching data
def tree_data
output = []
Emp.roots.each do |emp|
output << data(emp)
end
output.to_json
end
def data(employee)
subordinates = []
unless employee.subs.blank?
employee.subs.each do |emp|
subordinates << data(emp)
end
end
{name: employee.name, subordinates: subordinates}
end
Eager Loading
def tree_data
output = []
Emp.roots.includes(subs: {subs: {subs: subs}}}.each do |emp|
output << data(emp)
end
output.to_json
end

How To Get Additional Attributes From Has Many Through

I am using Rails 3 beta 4.
I have the following models:
class Player < ActiveRecord::Base
has_many :players_items, :dependent => :destroy
has_many :items, :through => :players_items
end
class PlayersItem < ActiveRecord::Base
belongs_to :player
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :players_items, :dependent => :destroy
has_many :players, :through => :players_items
end
In the players_controller
def items
#player = Player.find(params[:id])
#player_items = #player.items
end
I have the following attributes
--Items Model--
Item_id:Integer
Name:String
Cost:Integer
Description:Text
--PlayersItem Model--
Item_id:Integer
Player_id:Integer
Total:Integer
Traded:Integer
I am trying to print out all the items associated with a player and for each item print out the "Name", "Cost", "Description", "Total", and "Traded" values.
When I call #player_items in the items.html.erb, I can only access the attributes associated with the Item Model and not any of the attributes associated with PlayersItem model.
I am trying to access the attributes from both the items model and players_items model in the same "call" similar to SQL Join Statement like this
SELECT * FROM players_items INNER JOIN items ON players_items.item_id=items.id
WHERE players_items.player_id = "#player"
Is this possible?
#player = Player.order("created_at").last
#player.players_items.each do |item|
puts "#{item.player}: #{item.description} cost:#{item.cost}"
end
Has many through is a little weird. Think of it as a model whose name should (ideally) be descriptive of the relationship between the two other models. So maybe if equipment is being distributed to the players you could call the join model distributions or loans or something. The players_items is the naming convention for join tables which aren't going to be addressed directly.
I hope that helps!
Used in the controller
#player = Player.find(params[:id], :include => [:items,:players_items])
And in the view
#player.players_items.each do |player|
puts "#{player.name}: #{player.player.item.description} cost:#{player.item.cost}"
end

Resources