Skip to content

Commit

Permalink
modifications to acts_as_solr in order to provide date faceting facil…
Browse files Browse the repository at this point in the history
…ities, includes tests
  • Loading branch information
Mitchell Hatter authored and roidrage committed Feb 28, 2009
1 parent d2220b1 commit 8ea5714
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 2 deletions.
47 changes: 47 additions & 0 deletions lib/class_methods.rb
Expand Up @@ -41,6 +41,35 @@ module ClassMethods
# sort:: Sorts the faceted resuls by highest to lowest count. (true|false)
# browse:: This is where the 'drill-down' of the facets work. Accepts an array of
# fields in the format "facet_field:term"
# mincount:: Replacement for zeros (it has been deprecated in Solr). Specifies the
# minimum count necessary for a facet field to be returned. (Solr's
# facet.mincount) Overrides :zeros if it is specified. Default is 0.
#
# dates:: Run date faceted queries using the following arguments:
# fields:: The fields to be included in the faceted date search (Solr's facet.date).
# It may be either a String/Symbol or Hash. If it's a hash the options are the
# same as date_facets minus the fields option (i.e., :start:, :end, :gap, :other,
# :between). These options if provided will override the base options.
# (Solr's f.<field_name>.date.<key>=<value>).
# start:: The lower bound for the first date range for all Date Faceting. Required if
# :fields is present
# end:: The upper bound for the last date range for all Date Faceting. Required if
# :fields is prsent
# gap:: The size of each date range expressed as an interval to be added to the lower
# bound using the DateMathParser syntax. Required if :fields is prsent
# hardend:: A Boolean parameter instructing Solr what do do in the event that
# facet.date.gap does not divide evenly between facet.date.start and facet.date.end.
# other:: This param indicates that in addition to the counts for each date range
# constraint between facet.date.start and facet.date.end, other counds should be
# calculated. May specify more then one in an Array. The possible options are:
# before:: - all records with lower bound less than start
# after:: - all records with upper bound greater than end
# between:: - all records with field values between start and end
# none:: - compute no other bounds (useful in per field assignment)
# all:: - shortcut for before, after, and between
# filter:: Similar to :query option provided by :facets, in that accepts an array of
# of date queries to limit results. Can not be used as a part of a :field hash.
# This is the only option that can be used if :fields is not present.
#
# Example:
#
Expand All @@ -51,6 +80,24 @@ module ClassMethods
# :fields => [:category, :manufacturer],
# :browse => ["category:Memory","manufacturer:Someone"]}
#
#
# Examples of date faceting:
#
# basic:
# Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at, :created_at],
# :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :gap => '+2YEARS', :other => :before}}
#
# advanced:
# Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at,
# {:created_at => {:start => 'NOW-20YEARS/DAY', :end => 'NOW-10YEARS/DAY', :other => [:before, :after]}
# }], :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :other => :before, :filter =>
# ["created_at:[NOW-10YEARS/DAY TO NOW/DAY]", "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"]}}
#
# filter only:
# Electronic.find_by_solr "memory", :facets => {:dates => {:filter => "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"}}
#
#
#
# scores:: If set to true this will return the score as a 'solr_score' attribute
# for each one of the instances found. Does not currently work with find_id_by_solr
#
Expand Down
41 changes: 41 additions & 0 deletions lib/parser_methods.rb
Expand Up @@ -23,9 +23,42 @@ def parse_query(query=nil, options={}, models=nil)
query_options[:facets][:sort] = :count if options[:facets][:sort]
query_options[:facets][:mincount] = 0
query_options[:facets][:mincount] = 1 if options[:facets][:zeros] == false
# override the :zeros (it's deprecated anyway) if :mincount exists
query_options[:facets][:mincount] = options[:facets][:mincount] if options[:facets][:mincount]
query_options[:facets][:fields] = options[:facets][:fields].collect{|k| "#{k}_facet"} if options[:facets][:fields]
query_options[:filter_queries] = replace_types([*options[:facets][:browse]].collect{|k| "#{k.sub!(/ *: */,"_facet:")}"}) if options[:facets][:browse]
query_options[:facets][:queries] = replace_types(options[:facets][:query].collect{|k| "#{k.sub!(/ *: */,"_t:")}"}) if options[:facets][:query]


if options[:facets][:dates]
query_options[:date_facets] = {}
# if options[:facets][:dates][:fields] exists then :start, :end, and :gap must be there
if options[:facets][:dates][:fields]
[:start, :end, :gap].each { |k| raise "#{k} must be present in faceted date query" unless options[:facets][:dates].include?(k) }
query_options[:date_facets][:fields] = []
options[:facets][:dates][:fields].each { |f|
if f.kind_of? Hash
key = f.keys[0]
query_options[:date_facets][:fields] << {"#{key}_d" => f[key]}
validate_date_facet_other_options(f[key][:other]) if f[key][:other]
else
query_options[:date_facets][:fields] << "#{f}_d"
end
}
end

query_options[:date_facets][:start] = options[:facets][:dates][:start] if options[:facets][:dates][:start]
query_options[:date_facets][:end] = options[:facets][:dates][:end] if options[:facets][:dates][:end]
query_options[:date_facets][:gap] = options[:facets][:dates][:gap] if options[:facets][:dates][:gap]
query_options[:date_facets][:hardend] = options[:facets][:dates][:hardend] if options[:facets][:dates][:hardend]
query_options[:date_facets][:filter] = replace_types([*options[:facets][:dates][:filter]].collect{|k| "#{k.sub!(/ *:(?!\d) */,"_d:")}"}) if options[:facets][:dates][:filter]

if options[:facets][:dates][:other]
validate_date_facet_other_options(options[:facets][:dates][:other])
query_options[:date_facets][:other] = options[:facets][:dates][:other]
end

end
end

if models.nil?
Expand Down Expand Up @@ -156,5 +189,13 @@ def scorable_record?(record, doc)
record_id(record).to_s == doc_id.to_s
end
end

def validate_date_facet_other_options(options)
valid_other_options = [:after, :all, :before, :between, :none]
options = [options] unless options.kind_of? Array
bad_options = options.map {|x| x.to_sym} - valid_other_options
raise "Invalid option#{'s' if bad_options.size > 1} for faceted date's other param: #{bad_options.join(', ')}. May only be one of :after, :all, :before, :between, :none" if bad_options.size > 0
end

end
end
31 changes: 30 additions & 1 deletion lib/solr/request/standard.rb
Expand Up @@ -12,7 +12,7 @@

class Solr::Request::Standard < Solr::Request::Select

VALID_PARAMS = [:query, :sort, :default_field, :operator, :start, :rows, :shards,
VALID_PARAMS = [:query, :sort, :default_field, :operator, :start, :rows, :shards, :date_facets,
:filter_queries, :field_list, :debug_query, :explain_other, :facets, :highlighting, :mlt]

def initialize(params)
Expand Down Expand Up @@ -92,6 +92,35 @@ def to_hash
end
end
end

if @params[:date_facets]
hash["facet.date"] = []
if @params[:date_facets][:fields]
@params[:date_facets][:fields].each do |f|
if f.kind_of? Hash
key = f.keys[0]
hash["facet.date"] << key
f[key].each { |k, v|
hash["f.#{key}.facet.date.#{k}"] = v
}
else
hash["facet.date"] << f
end
end
end
hash["facet.date.start"] = @params[:date_facets][:start]
hash["facet.date.end"] = @params[:date_facets][:end]
hash["facet.date.gap"] = @params[:date_facets][:gap]
hash["facet.date.other"] = @params[:date_facets][:other]
hash["facet.date.hardend"] = @params[:date_facets][:hardend]
if @params[:date_facets][:filter]
if hash[:fq]
hash[:fq] << @params[:date_facets][:filter]
else
hash[:fq] = @params[:date_facets][:filter]
end
end
end
end

# highlighting parameter processing - http://wiki.apache.org/solr/HighlightingParameters
Expand Down
1 change: 1 addition & 0 deletions test/db/migrate/004_create_electronics.rb
Expand Up @@ -6,6 +6,7 @@ def self.up
t.column :features, :string
t.column :category, :string
t.column :price, :string
t.timestamps
end
end

Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/electronics.yml
Expand Up @@ -5,6 +5,8 @@ ipod_video:
features: iTunes, Podcasts, Audiobooks
category: Electronics
price: 599.00
created_at: <%= Date.today - 1.year %>
updated_at: <%= Date.today - 1.month %>

dell_monitor:
id: 2
Expand All @@ -13,6 +15,8 @@ dell_monitor:
features: 30" TFT active matrix LCD, 2560 x 1600, .25mm dot pitch, 700:1 contrast
category: Electronics
price: 750.00
created_at: <%= Date.today - 1.year %>
updated_at: <%= Date.today - 1.month %>

samsung_hd:
id: 3
Expand All @@ -21,6 +25,8 @@ samsung_hd:
features: 7200RPM, 8MB cache, IDE Ultra ATA-133
category: Hard Drive
price: 319.00
created_at: <%= Date.today - 2.years %>
updated_at: <%= Date.today - 2.months %>

corsair_ram:
id: 4
Expand All @@ -29,6 +35,8 @@ corsair_ram:
features: CAS latency 2, 2-3-3-6 timing, 2.75v, unbuffered, heat-spreader
category: Memory
price: 155.00
created_at: <%= Date.today - 6.years %>
updated_at: <%= Date.today - 3.months %>

a_data_ram:
id: 5
Expand All @@ -37,3 +45,5 @@ a_data_ram:
features: CAS latency 3, 2.7v
category: Memory
price: 65.79
created_at: <%= Date.today - 9.years %>
updated_at: <%= Date.today - 4.months %>
41 changes: 41 additions & 0 deletions test/functional/faceted_search_test.rb
Expand Up @@ -119,4 +119,45 @@ def test_faceted_search_with_drill_down
assert_equal({"category_facet"=>{"Memory"=>2}}, records.facets['facet_fields'])
end

def test_faceted_search_with_dates
records = Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:created_at, :updated_at],
:start => (Date.today - 7.years).strftime("%Y-%m-%dT%H:%M:%SZ"), :end => Date.today.strftime("%Y-%m-%dT%H:%M:%SZ"), :gap => '+1YEAR', :other => :all}}

assert_equal 4, records.docs.size

assert_equal 0, records.facets["facet_dates"]["created_at_d"]["after"]
assert_equal 1, records.facets["facet_dates"]["created_at_d"]["before"]
assert_equal 3, records.facets["facet_dates"]["created_at_d"]["between"]

assert_equal 0, records.facets["facet_dates"]["updated_at_d"]["after"]
assert_equal 0, records.facets["facet_dates"]["updated_at_d"]["before"]
assert_equal 4, records.facets["facet_dates"]["updated_at_d"]["between"]
end

def test_faceted_search_with_dates_filter
records = Electronic.find_by_solr "memory", :facets => {:dates => {:filter => ["updated_at:[#{(Date.today - 3.months).strftime("%Y-%m-%dT%H:%M:%SZ")} TO NOW-1MONTH/DAY]"]}}

assert_equal 2, records.docs.size

records.docs.each { |r|
assert r.updated_at >= (Date.today - 3.month)
assert r.updated_at <= (Date.today - 1.month)
}
end

def test_faceted_search_with_dates_filter_and_facets
# this is a very contrived example but gives us data to validate
records = Electronic.find_by_solr "memory", :facets => {:dates => {:filter => ["updated_at:[#{(Date.today - 3.months).strftime("%Y-%m-%dT%H:%M:%SZ")} TO NOW-1MONTH/DAY]"],
:fields => [:created_at, :updated_at], :start => 'NOW-2MONTHS/DAY', :end => 'NOW-1MONTH/DAY', :gap => '+1MONTH', :other => :all}}

assert_equal 2, records.docs.size

assert_equal 0, records.facets["facet_dates"]["created_at_d"]["after"]
assert_equal 2, records.facets["facet_dates"]["created_at_d"]["before"]
assert_equal 0, records.facets["facet_dates"]["created_at_d"]["between"]

assert_equal 0, records.facets["facet_dates"]["updated_at_d"]["after"]
assert_equal 1, records.facets["facet_dates"]["updated_at_d"]["before"]
assert_equal 1, records.facets["facet_dates"]["updated_at_d"]["between"]
end
end
3 changes: 2 additions & 1 deletion test/models/electronic.rb
Expand Up @@ -5,10 +5,11 @@
# - features
# - category
# - price
# - created_on

class Electronic < ActiveRecord::Base
acts_as_solr :facets => [:category, :manufacturer],
:fields => [:name, :manufacturer, :features, :category, {:price => {:type => :range_float, :boost => 10.0}}],
:fields => [:name, :manufacturer, :features, :category, {:created_at => :date}, {:updated_at => :date}, {:price => {:type => :range_float, :boost => 10.0}}],
:boost => 5.0,
:exclude_fields => [:features]

Expand Down

0 comments on commit 8ea5714

Please sign in to comment.