is_taggable
===============================================================================
This plugin is almost entirely based on acts-as-taggable-on by Michael Bleigh +
contributors.
is_taggable supports the awesome contextual tagging of acts-as-taggable-on, but
with a couple substantial changes to the underlying architecture.
Architecture Differences
===============================================================================
We had a couple key issues with the underlying architecture in
acts-as-taggable-on which we felt deserved a forking into a new direction.
There are two main architectural differences between is_taggable and
acts-as-taggable-on:
1) is_taggable does *not* use a normalized data model -- there is one table
"taggings" and not two tables "taggings" and "tags". This means that tags are
duplicated in the taggings table. The reason behind this was that we could not
find a reason justifying the join that couldn't be easily overcome with unique
indexing and grouped selects.
2) (This is the big one) -- is_taggable skips validations and callbacks on the
Tagging model when saving. This totally breaks the normal AR behavior and the
behavior used by all other AR tagging plugins. The reason for this is that the
taggings are updated with a multi-insert -- which also means this plugin can
only be used on databases which support multi-insert (MySQL, Postgres, Oracle,
...). The reason we use a multi-insert is because we ran into massive problems
with large numbers of writes on taggings. Take this scenario:
I upload a photo and fill in my tags when I upload it. I want people to find it
so I tag it with about 20 different keywords. In order to save the tags on my
photo the original plugin would have to (at least):
2) do 20 SELECTs on the taggings table for validates_uniqueness_of
4) do some number (at most 20) of INSERTs on the tags table to save the tags
3) do 20 INSERTs on the taggings table to save the taggings
So best case 20 INSERTs, 20 SELECTs -- worst case 40 INSERTs, 20 SELECTs.
Now get a few users adding lots of tags on things concurrently and you can see
writes quickly becoming a problem, and as the number of tags I'm adding grows,
the problem gets worse. Individual INSERTs are fast, but once you have
concurrency you have lock waits and the the problem gets massively compounded.
By using a multi-insert, a non-normalized table, and a "manual" validation (do
all the validates_uniqueness_of checks at once) we can get it down to:
2) do 1 SELECT to check for duplicated taggings
3) do 1 multi-INSERT to INSERT all the taggings
Furthermore, no matter how many tags your inserting, it's always 1 SELECT and 1
INSERT.
Another thing to consider is the impact of updating tags. In the original
plugin this was done as a loop causing multiple DELETEs followed by multiple
INSERTs. We've taken this down to 1 DELETE followed by 1 multi-INSERT.
Compatibility
===============================================================================
is_taggable requires that your underlying database support multi-INSERT
statements (i.e. INSERT INTO taggings (tag) VALUES ('foo', 'bar', 'baz')). Most
"major" databases do -- including MySQL, PostgreSQL and Oracle.
It has only been tested with Rails 2.1+ and makes use of named_scope (introduced
in Rails 2.1).
Installation
===============================================================================
GemPlugin
-------------------------------------------------------------------------------
Rails 2.1+ introduces gem dependencies, to use them add this line to
environment.rb:
config.gem "citrusbyte-is_taggable", :source => "http://gems.github.com", :lib => "is_taggable"
Then run "rake gems:install" to install the gem.
Plugin
-------------------------------------------------------------------------------
script/plugin install git://github.com/citrusbyte/is_taggable.git
Gem
-------------------------------------------------------------------------------
gem install citrusbyte-is_taggable --source http://gems.github.com
Post Installation
-------------------------------------------------------------------------------
1. script/generate is_taggable_migration
2. rake db/migrate
Testing
===============================================================================
is_taggable uses RSpec for its test coverage, if you're using RSpec type:
rake spec:plugins
Examples (all stolen from acts-as-taggable-on docs)
===============================================================================
class User < ActiveRecord::Base
is_taggable :tags, :skills, :interests
end
@user = User.new(:name => "Bobby")
@user.tag_list = "awesome, slick, hefty" # this should be familiar
@user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
@user.skill_list # => ["joking","clowning","boxing"] as TagList
@user.save
@user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
@user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
# The old way
User.find_tagged_with("awesome", :on => :tags) # => [@user]
User.find_tagged_with("awesome", :on => :skills) # => []
# The better way (utilizes named_scope)
User.tagged_with("awesome", :on => :tags) # => [@user]
User.tagged_with("awesome", :on => :skills) # => []
@frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
@frankie.skill_counts
Finding Tagged Objects
======================
is_taggable utilizes Rails 2.1's named_scope to create an association
for tags. This way you can mix and match to filter down your results, and it
also improves compatibility with the will_paginate gem:
class User < ActiveRecord::Base
is_taggable :tags
named_scope :by_join_date, :order => "created_at DESC"
end
User.tagged_with("awesome").by_date
User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
Relationships
=============
You can find objects of the same type based on similar tags on certain contexts.
Also, objects will be returned in descending order based on the total number of
matched tags.
@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]
@frankie = User.find_by_name("Frankie")
@frankie.skill_list # => ["hacking"]
@tom = User.find_by_name("Tom")
@tom.skill_list # => ["hacking", "jogging", "diving"]
@tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
@bobby.find_related_skills # => [<User name="Tom">]
@frankie.find_related_skills # => [<User name="Tom">]
Dynamic Tag Contexts
====================
In addition to the generated tag contexts in the definition, it is also possible
to allow for dynamic tag contexts (this could be user generated tag contexts!)
@user = User.new(:name => "Bobby")
@user.set_tag_list_on(:customs, "same, as, tag, list")
@user.tag_list_on(:customs) # => ["same","as","tag","list"]
@user.save
@user.tags_on(:customs) # => [<Tag name='same'>,...]
@user.tag_counts_on(:customs)
User.find_tagged_with("same", :on => :customs) # => [@user]
Tag Ownership
=============
Tags can have owners:
class User < ActiveRecord::Base
is_tagger
end
class Photo < ActiveRecord::Base
is_taggable :locations
end
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
@some_photo.locations_from(@some_user)
Caveats
===============================================================================
1) Your underlying database *must* support multi-INSERT
2) You probably need Rails 2.1+, but seriously named_scope is so cool you need
it anyways.
3) You cannot use callbacks/validations on the Tagging model (you can still use
them like normal on your Taggables and Taggers...)
Contributors
===============================================================================
is_taggable:
* Ben Alavi & Michel Martens - Ruthless hackers of acts-as-taggable-on
acts-as-taggable-on:
* Michael Bleigh - Original Author
* Brendan Lim - Related Objects
* Pradeep Elankumaran - Taggers
* Sinclair Bain - Patch King
Patch Contributors
-------------------------------------------------------------------------------
acts-as-taggable-on:
* tristanzdunn - Related objects of other classes
* azabaj - Fixed migrate down
* Peter Cooper - named_scope fix
* slainer68 - STI fix
* harrylove - migration instructions and fix-ups
* lawrencepit - cached tag work
Resources
===============================================================================
* GitHub - http://github.com/citrusbyte/is_taggable
* Lighthouse - http://citrusbyte.lighthouseapp.com/projects/
is_taggable:
Copyright (c) 2008 Citrusbyte, LLC, released under the MIT license
acts-as-taggable-on:
Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license