Advanced routing setup
I recently watched the look into routing video. I have a very complex routing scenario I have been asked to set up for work. I was hoping I could lay it out and get some help.
Currently we have:
- products
- technologies
- industries
I would like to be able to use the following url structure
- products
- products/:slug
- products/:technology_name - show all products belonging to that technology
- products/:industry_name - show products belonging to that industry
The first two parts of these are understood and completed. I currently have a product listing page as well as use friendly_id so I can have clear product urls mapping to detail pages.
I have a basic concept of how I would code the controller to find the technology id
by the technology name
, return results from the products_technologies relationship table with the correct technology id.
My confusion is how to handle this from a routing perspective. This also conflicts with friendly_id, and I assume would conflict with industries as well. I would really appreciate any help or information you could provide. Thanks!
So the problem with this (as you've already noticed) is that you can no longer assume that the slug references a Product anymore. It might be a product, or it might be a Technology, or it might be an Industry. My personal suggestion would be to use the routes like this instead:
/products
/products/:slug
/industries
/industries/:slug
/industries/:slug/products
/technologies
/technologies/:slug
/technologies/:slug/products
That would let you create nice restful routes for each of the industries and technologies plus the products inside them. The way to think about this is that you start with the most generic thing (Industry or Technology) and then you narrow down into Products inside them.
Now I know while that may be ideal, it might not be a practical solution, so here's how you can merge those into one controller like you are asking.
Normally you have a before_action
that sets the product. In this case, you actually want the ones that don't match a product to render the index for a technology or industry. Your controller will first look up the product by name and if it doesn't find anything, you need to try your alternatives.
My solution would basically be this:
- Try to find the product. Continue as normal if found.
- If no product matches, look up a Technology or Industry.
- Once we have a Technology or Industry, let's render a view for the products. This is going to look just like the regular products index at the simplest, but you may want to render different templates for Industry and Technology results instead.
class ProductsController < ApplicationController
before_action :set_product
before_action :find_products_if_not_found
def index
@products = Product.all
end
def show
end
private
def set_product
@product = Product.friendly.find(params[:id])
end
def find_products_if_not_found
# @product gets set in the before_action
if @product.nil?
@parent = Technology.friendly.find(params[:id]) || Industry.friendly.find(params[:id])
@products = @parent.products
# Render the index template if we found a parent for products
# Since we are in the before_action this should halt rendering of the show action
render action: :index
end
end
end
Basically everything comes in the same route and you start checking each type. If one fails, try the next. Continue doing that until you run out of things to match and then you can let the view fail if there are no matches at all.
Let me know if that helps!
This basic concept helped me get setup however I am having a bit more trouble.
declaring find_products_if_not_found as a before filter never gets called as the set_product
throws a record not found exception. To solve this I removed this before filter and added a rescue to the set_product filter.
def set_product
@product = Product.friendly.find(params[:id])
rescue ActiveRecord::RecordNotFound
find_products_if_not_found
end
If I pass in an id associated with a technology this works correctly, however industries won't work. The OR clause is never executed as again you get a record not found exception. Any ideas? Thanks!
Yeah, the find will throw an exception. I think this will work instead because find_by
doesn't throw an exception when the record doesn't exist.
before_action :set_product
before_action :find_products_if_not_found, if: ->{ @product.blank? }
private
def set_product
@product = Product.friendly.find_by(id: params[:id])
end
def find_products_if_not_found
# @product gets set in the before_action
if @product.nil?
@parent = Technology.friendly.find_by(id: params[:id]) || Industry.friendly.find_by(id: params[:id])
# Since this could be nil, we need to redirect or render a 404 if not found
if @parent.present?
@products = @parent.products
# Render the index template if we found a parent for products
# Since we are in the before_action this should halt rendering of the show action
render action: :index
else
redirect_to root_path
end
end
end
So you might hate me... this worked, but I needed to make a few changes. The products section of our site receives the majority of our traffic, so I am making an effort to make it as fast as possible. With this is mind, I moved to elasticsearch searches over active record in an attempt to increase speed and flexibility of this page.
I needed to add the ability to search for products that fit into various Categories, Benefits, Collections, and Families as well as the initial Industries and Technologies requirements. We need clean urls to contain dash separated keywords which needed to be unique across previously mentioned list of models.
To solve I made a polymorphic model containing a string :identity and included in all above models. Now I can verify uniqueness of the slug across models. I can also get results by searching the polymorphic model. I can currently pull the identities from my url, process them to figure out which industries, collections, families, etc apply.
I am just having trouble submitting to ElasticSearch to get results. Every tutorial I find mentions the Tire gem which is no more. ElasticSearch-rails gem has a few examples, but the jump from the basic example to the expert example is so great that I am having trouble following. I can currently search my products for matching collections or families as product contains a foreign key for these. My trouble is when I try to search for Products with related Benefits.
Sorry, I think this is a lot to ask and I am not sure I did a great job laying out my specific confusion, but any help or direction you could provide would be appreciated!
ElasticSearch definitely lost a good gem with Tire. It really seems like the new ElasticSearch-rails gem isn't anywhere near as friendly. There were a few other good ones that I can't seem to find.
From my reasonably limited understanding of search, you'll want to index Products and include the benefits inside of the Product record. That way when you do a search, the match on Benefit will return the Product.
Not sure if that helps you at all, but maybe it's a start?
Yes, that helps. Yes the lack of tire is really frustrating as I have to ignore about 85% of what I find as it includes tire gem.
I confirmed that benefits is in fact included in in the product record. Using the syntax below in case it may be useful to anyone else.
curl -XGET 'http://127.0.0.1:9200/products/_search?pretty=1'
I get a listing of products like the example included below:
{
"id":4,
"name":"product name"
"family_id":16
"collection_id":6
"created_at":"2015-04-13T12:49:42.000Z"
"updated_at":"2015-04-13T12:49:42.000Z"
"benefits":[
{"id":2,"name":"my benefit 2"},
{"id":6,"name":"my benefit 6"},
{"id":7,"name":"my benefit 7"}
],
"categories":[
{"id":2,"name":"category 2"}
]}
},
{...}
Now I think I might just have a syntax issue. I need to figure out how to search for products with benefits with ids 2, 6, AND 7 in ElasticSearch if I wanted the above example product.
For nested queries like that, I think you can use benefits.id
as the field. This might be of some help: http://www.elastic.co/guide/en/elasticsearch/reference/1.4/mapping-nested-type.html