• Home

  • Custom Ecommerce
  • Application Development
  • Database Consulting
  • Cloud Hosting
  • Systems Integration
  • Legacy Business Systems
  • Security & Compliance
  • GIS

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Blog
  • Careers

  • CasePointer

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    A Ruby on Rails Tag Cloud Tutorial with Spree

    Steph Skardal

    By Steph Skardal
    March 7, 2011

    A tag cloud from a recent End Point blog post.

    Tag clouds have become a fairly popular way to present data on the web. One of our Spree clients recently asked End Point to develop a tag cloud reporting user-submitted search terms in his Spree application. The steps described in this article can be applied to a generic Rails application with a few adjustments.

    Step 1: Determine Organization

    If you are running this as an extension on Spree pre-Rails 3.0 versions, you’ll create an extension to house the custom code. If you are running this as part of a Rails 3.0 application or Spree Rails 3.0 versions, you’ll want to consider creating a custom gem to house the custom code. In my case, I’m writing a Spree extension for an application running on Spree 0.11, so I create an extension with the command script/generate extension SearchTag.

    Step 2: Data Model & Migration

    First, the desired data model for the tag cloud data should be defined. Here’s what mine will look like in this tutorial:

    Next, a model and migration must be created to introduce the class, table and it’s fields. In Spree, I run script/generate extension_model SearchTag SearchRecord and update the migration file to contain the following:

    class CreateSearchRecords < ActiveRecord::Migration
      def self.up
        create_table :search_records do |t|
          t.string :term
          t.integer :count, :null => false, :default => 0
        end
      end
    
      def self.down
        drop_table :search_records
      end
    end
    

    I also add a filter method to my model to be used later:

    class SearchRecord < ActiveRecord::Base
      def self.filter(term)
        term.gsub(/\+/, ' ')
          .gsub(/\s+/, ' ')
          .gsub(/^\s+/, '')
          .gsub(/\s+$/, '')
          .downcase
          .gsub(/[^0-9a-z\s-]/, '')
      end
    end
    

    Step 3: Populating the Data

    After the migration has been applied, I’ll need to update the code to populate the data. I’m going to add an after filter on every user search. In the case of using Spree, I update search_tag_extension.rb to contain the following:

    def activate
      Spree::ProductsController.send(:include, Spree::SearchTagCloud::ProductsController)
    end
    

    And my custom module contains the following:

    module Spree::SearchTagCloud::ProductsController
      def self.included(controller)
        controller.class_eval do
          controller.append_after_filter :record_search, :only => :index
        end
      end
    
      def record_search
        if params[:keywords]
          term = SearchRecord.filter(params[:keywords])
          return if term == ''
          record = SearchRecord.find_or_initialize_by_term(term)
          record.update_attribute(:count, record.count+1)
        end
      end
    end
    

    The module appends an after filter to the products#index action. The after filter method cleans the search term and creates a record or increments the existing record’s count. If this is added directly into an existing Rails application, this bit of functionality may be added directly into one or more existing controller methods to record the search term.

    Step 4: Reporting the Data

    To present the data, I create a controller with script/generate extension_controller SearchTag Admin::SearchTagClouds first. I update config/routes.rb with a new action to reference the new controller:

    map.namespace :admin do |admin|
      admin.resources :search_tag_clouds, :only => [:index]
    end
    

    And I update my controller to calculate the search tag cloud data, shown below. The index method method retrieves all of the search records, sorts, and grabs the the top x results, where x is some configuration defined by the administrator. The method determines the linear solution for scaling the search_record.count to font sizes ranging from 8 pixels to 25 pixels. This order of terms is randomized (.shuffle) and linear equation applied. This linear shift can be applied to different types of data. For example, if a tag cloud is to show products with a certain tag, the totals per tag must be calculated and scaled linearly.

    class Admin::SearchTagCloudsController < Admin::BaseController
      def index
        search_records = SearchRecord.all
          .collect { |r| [r.count, r.term] }
          .sort
          .reverse[0..Spree::SearchTagCloud::Config[:count]]
        max = search_records.empty? ? 1 : search_records.first.first
    
        # solution is: a*x_factor - y_shift = font size
        # max font size is 25, min is 8
        x_factor = (Spree::SearchTagCloud::Config[:max] -
          Spree::SearchTagCloud::Config[:min]) / max.to_f
        y_shift = max.to_f*x_factor - Spree::SearchTagCloud::Config[:max]
    
        @results = search_records.shuffle.inject([]) do |a, b|
          a.push([b[0].to_f*x_factor - y_shift, b[1]])
          a
        end
      end
    end
    

    The data is presented to the user in the following view:

    <h3>Tag Cloud:</h3>
    <div id="tag_cloud">
    <% @results.each do |b| %>
    <span style="font-size:<%= b[0] %>px;"><%= b[1] %></span>
    <% end -%>
    </div>
    

    Step 5: Adding Flexibility

    In this project, I added configuration variables for the total number of terms displayed, and maximum and minimum font size using Spree’s preference architecture. In a generic Rails application, this may be a nice bit of functionality to include with the preferred configuration architecture.

    Example tag cloud from the extension. Additional modifications can be applied to change the overall styling or color of individual search terms.

    Conclusion

    These steps are pretty common for introducing new functionality into an existing application: data migration and model, manipulation on existing controllers, and presentation of results with a new or existing controller and view. Following MVC convention in Rails keeps the code organized and methods simple. In the case of Spree 0.11, this functionality has been packaged into a single extension that is abstracted from the Spree core. The code can be reviewed here, with a few minor differences.

    ecommerce rails spree


    Comments