Skip to content

Instantly share code, notes, and snippets.

@svs
Created September 20, 2012 21:06
Show Gist options
  • Select an option

  • Save svs/3758355 to your computer and use it in GitHub Desktop.

Select an option

Save svs/3758355 to your computer and use it in GitHub Desktop.
Easy Tag Search Using Sequel
class Search
ALLOWED_MODELS = [:receipt, :business_card, :other_document]
SEARCH_COLS = {
:receipt => [:title,:organisation],
:business_card => [:first_name,:last_name,:organisation,:designation,:locality,:city,:pincode, :email, :twitter, :mobile, :notes],
:other_document => [:title,:notes]
}
NON_SEARCH_KEYS = [:tags, :q, :model, :all, :only, :from, :to, :by, :page]
attr_accessor :options, :result, :model_id_column
# options is a hash with the following keys
# model -> the name of the model to search. will search all models if omitted
# q -> the text to search. will also search tags and categories
# t -> to explicitly search tags and categories, pass them in this parameter. |See #parse_params for info on what this parameter looks like
# from -> the date to search from
# to -> the date to search uptil
# by -> paginate by this column. i.e. sometimes we want to paginate by first name or last name.
# page -> if paginating, whichth page to bring. i.e. you can say {:by => "last_name", :page => 'd'} will return all whose last name starts with d
# any other keys get passed on as where clauses
def initialize(options)
@options = parse_options(options)
end
# Public: Returns the ids of the objects matching the search criteria
def do
@result = join_table.select(model_id_column.as(:model_id))
add_additional_where_clauses
search_for_text
paginate
filter_dates
filter_tags
{model_name.camelcase => @result.distinct.all.map{|h| h.values}.flatten}
end
private
# Private: returns a Sequel#function equivalent to i.e. CONCAT('title',' ','organisation') for Receipt and so on
# this is the value against which the search term is compared
# model -> the name of the model table to query
# returns a Sequel CONCAT function
def self.search_function_for(model)
concat_cols = search_columns_for(model)
c = concat_cols.zip((2..concat_cols.count).map{" "}).flatten.compact
Sequel.function(*([:concat] + c))
end
def self.search_columns_for(model)
SEARCH_COLS[model.to_sym] + [:frozen_category_list, :frozen_tag_list]
end
# Private:
def parse_options(options)
options.tap do
tags = options.delete(:t)
options[:tags] = {:must => [], :must_not => [], :any => [], :only => []}
if tags
tags = tags.split(",").map(&:strip) if tags.class == String
only = tags.delete("!")
tags.each do |t|
key = {"+" => :must, "-" => :must_not}.fetch(t.strip[0],:any)
options[:tags][key] << t.gsub(/[+-]/,"").strip
end
options[:tags] = options[:tags].select{|k,v| !v.empty?}
options[:tags][:only] = options[:tags].delete(:any) if only
end
end
end
# Returns a Sequel representation of the model table joined with the taggings table joined with the tags table
def join_table
DB[model_table].left_join(:taggings,:taggable_id => :id, :taggable_type => model_name.camelcase).left_join(:tags, :id => :tag_id)
end
def add_additional_where_clauses
@result = @result.where(@options.except(*NON_SEARCH_KEYS).symbolize_keys)
end
def search_for_text
if @options[:q]
@result = @result.where(Sequel.ilike(Search.search_function_for(model_name), "%#{@options[:q]}%"))
end
end
def paginate
if options[:by] and options[:page]
@result = @result.where(Sequel.ilike(options[:by].to_sym, "#{@options[:page]}%"))
end
end
def filter_dates
if @options[:from]
from_date = Date.parse(@options[:from].to_s)
if from_date.year == 0
@result = @result.where(:date => nil)
else
@result = @result.where('date >= ?', from_date) if @options[:from]
end
end
if @options[:to]
to_date = Date.parse(@options[:to].to_s) rescue nil
@result = @result.where('date <= ?', Date.parse(@options[:to].to_s)) if to_date and to_date.year > 0
end
end
def filter_tags
if @options[:tags]
filter_for_any_clause
filter_for_must_have_clause
filter_for_only_clause
filter_for_must_not_clause
end
end
def filter_for_any_clause
unless @options[:tags][:any].blank?
@result = @result.where(:name => @options[:tags][:any])
end
end
def filter_for_must_have_clause
if !@options[:tags][:must].blank?
count_must_have_tags = join_table.select(model_id_column.as(:model_id),Sequel.function(:count, model_id_column).as(:count_must)).where(:name => @options[:tags][:must]).group_by(model_id_column)
num = @options[:tags][:must].count
@result = @result.left_join(count_must_have_tags, :model_id => model_id_column).where{count_must >= num}
end
end
def filter_for_only_clause
if !@options[:tags][:only].blank?
count_only_tags = join_table.select(model_id_column.as(:model_id),Sequel.function(:count, model_id_column).as(:count_only)).group_by(model_id_column)
num = @options[:tags][:only].count
@result = @result.left_join(count_only_tags, :model_id => model_id_column).where(:count_only => num, :name => @options[:tags][:only])
end
end
def filter_for_must_not_clause
if !@options[:tags][:must_not].blank?
mid = model_id_column
count_must_not_have_tags = join_table.select(model_id_column.as(:model_id)).where(:name => @options[:tags][:must_not]).group_by(model_id_column).having{Sequel.function(:count, mid) > 0}
@result = @result.exclude(:receipts__id => count_must_not_have_tags)
end
end
# i.e. "receipt"
def model_name
@options[:model].to_s
end
# i.e. :receipts
def model_table
model_name.pluralize.to_sym
end
# i.e. :receipts__id
def model_id_column
"#{model_name.pluralize}__id".to_sym
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment