Skip to content

Commit

Permalink
Working but basic implementation of facets
Browse files Browse the repository at this point in the history
  • Loading branch information
pat committed Jan 17, 2009
1 parent 6531f28 commit a62a2c4
Show file tree
Hide file tree
Showing 18 changed files with 191 additions and 38 deletions.
2 changes: 1 addition & 1 deletion cucumber.yml
@@ -1 +1 @@
default: "--require features/support/env.rb --require features/support/db/mysql.rb --require features/support/db/active_record.rb --require features/support/post_database.rb --require features/step_definitions/alpha_steps.rb --require features/step_definitions/beta_steps.rb --require features/step_definitions/cat_steps.rb --require features/step_definitions/common_steps.rb --require features/step_definitions/datetime_delta_steps.rb --require features/step_definitions/delayed_delta_indexing_steps.rb --require features/step_definitions/find_arguments_steps.rb --require features/step_definitions/gamma_steps.rb --require features/step_definitions/search_steps.rb --require features/step_definitions/sphinx_steps.rb"
default: "--require features/support/env.rb --require features/support/db/mysql.rb --require features/support/db/active_record.rb --require features/support/post_database.rb --require features/step_definitions/alpha_steps.rb --require features/step_definitions/beta_steps.rb --require features/step_definitions/cat_steps.rb --require features/step_definitions/common_steps.rb --require features/step_definitions/datetime_delta_steps.rb --require features/step_definitions/delayed_delta_indexing_steps.rb --require features/step_definitions/facet_steps.rb --require features/step_definitions/find_arguments_steps.rb --require features/step_definitions/gamma_steps.rb --require features/step_definitions/search_steps.rb --require features/step_definitions/sphinx_steps.rb"
2 changes: 1 addition & 1 deletion features/datetime_deltas.feature
Expand Up @@ -51,5 +51,5 @@ Feature: Datetime Delta Indexing
And I search for thirteen
Then I should get 1 result

When I search for the specific id of 107 in the theta_core index
When I search for the document id of theta thirteen in the theta_core index
Then it should exist
7 changes: 7 additions & 0 deletions features/facets.feature
@@ -0,0 +1,7 @@
Feature: Search and browse models by their defined facets

Scenario: Test
Given Sphinx is running
And I am searching on developers
When I am requesting facet results
Then I should see facet results
7 changes: 1 addition & 6 deletions features/searching_across_models.feature
Expand Up @@ -11,14 +11,9 @@ Feature: Searching across multiple model

Scenario: Confirming existance of a document id in a given index
Given Sphinx is running
When I search for the specific id of 51 in the person_core index
When I search for the document id of alpha one in the alpha_core index
Then it should exist

Scenario: Unsuccessfully confirming existance of a document id in a given index
Given Sphinx is running
When I search for the specific id of 52 in the person_core index
Then it should not exist

Scenario: Retrieving results from multiple models
Given Sphinx is running
When I search for ten
Expand Down
8 changes: 8 additions & 0 deletions features/step_definitions/facet_steps.rb
@@ -0,0 +1,8 @@
When "I am requesting facet results" do
@method = :facets
end

Then "I should see facet results" do
results.should be_kind_of(Hash)
results.values.each { |value| value.should be_kind_of(Hash) }
end
4 changes: 2 additions & 2 deletions features/step_definitions/search_steps.rb
Expand Up @@ -32,10 +32,10 @@
end

Then /^I can iterate by result and group and count$/ do
results.each_with_group_and_count do |result, group, count|
results.each_with_groupby_and_count do |result, group, count|
result.should be_kind_of(@model)
count.should be_kind_of(Integer)
group.should be_kind_of(Integer) unless group.nil?
group.should be_kind_of(Integer)
end
end

Expand Down
39 changes: 39 additions & 0 deletions features/support/db/migrations/create_developers.rb
@@ -0,0 +1,39 @@
require 'faker'

ActiveRecord::Base.connection.create_table :developers, :force => true do |t|
t.column :name, :string, :null => false
t.column :city, :string
t.column :state, :string
t.column :country, :string
t.column :age, :integer
end

Developer.create :name => "Pat Allan", :city => "Melbourne", :state => "Victoria", :country => "Australia", :age => 26

2.times do
Developer.create :name => Faker::Name.name, :city => "Melbourne", :state => "Victoria", :country => "Australia", :age => 30
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Sydney", :state => "New South Wales", :country => "Australia", :age => 28
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Adelaide", :state => "South Australia", :country => "Australia", :age => 32
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Bendigo", :state => "Victoria", :country => "Australia", :age => 30
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Goulburn", :state => "New South Wales", :country => "Australia", :age => 28
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Auckland", :state => "North Island", :country => "New Zealand", :age => 32
end

2.times do
Developer.create :name => Faker::Name.name, :city => "Christchurch", :state => "South Island", :country => "New Zealand", :age => 30
end
7 changes: 7 additions & 0 deletions features/support/models/developer.rb
@@ -0,0 +1,7 @@
class Developer < ActiveRecord::Base
define_index do
indexes country, :facet => true
indexes state, :facet => true
has age, :facet => true
end
end
1 change: 1 addition & 0 deletions lib/thinking_sphinx.rb
Expand Up @@ -12,6 +12,7 @@
require 'thinking_sphinx/attribute'
require 'thinking_sphinx/collection'
require 'thinking_sphinx/configuration'
require 'thinking_sphinx/facet'
require 'thinking_sphinx/field'
require 'thinking_sphinx/index'
require 'thinking_sphinx/rails_additions'
Expand Down
2 changes: 1 addition & 1 deletion lib/thinking_sphinx/active_record.rb
Expand Up @@ -10,7 +10,7 @@ module ThinkingSphinx
module ActiveRecord
def self.included(base)
base.class_eval do
class_inheritable_array :sphinx_indexes
class_inheritable_array :sphinx_indexes, :sphinx_facets
class << self
# Allows creation of indexes for Sphinx. If you don't do this, there
# isn't much point trying to search (or using this plugin at all,
Expand Down
7 changes: 7 additions & 0 deletions lib/thinking_sphinx/active_record/search.rb
Expand Up @@ -42,6 +42,13 @@ def search_for_id(*args)
args << options
ThinkingSphinx::Search.search_for_id(*args)
end

def facets(*args)
options = args.extract_options!
options[:class] = self
args << options
ThinkingSphinx::Search.facets(*args)
end
end
end
end
Expand Down
43 changes: 25 additions & 18 deletions lib/thinking_sphinx/attribute.rb
Expand Up @@ -9,7 +9,7 @@ module ThinkingSphinx
# associations. Which can get messy. Use Index.link!, it really helps.
#
class Attribute
attr_accessor :alias, :columns, :associations, :model
attr_accessor :alias, :columns, :associations, :model, :faceted

# To create a new attribute, you'll need to pass in either a single Column
# or an array of them, and some (optional) options.
Expand Down Expand Up @@ -59,8 +59,9 @@ def initialize(columns, options = {})

raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }

@alias = options[:as]
@type = options[:type]
@alias = options[:as]
@type = options[:type]
@faceted = options[:facet]
end

# Get the part of the SELECT clause related to this attribute. Don't forget
Expand Down Expand Up @@ -133,6 +134,27 @@ def unique_name
end
end

# Returns the type of the column. If that's not already set, it returns
# :multi if there's the possibility of more than one value, :string if
# there's more than one association, otherwise it figures out what the
# actual column's datatype is and returns that.
def type
@type ||= case
when is_many?
:multi
when @associations.values.flatten.length > 1
:string
else
translated_type_from_database
end
end

def to_facet
return nil unless @faceted

ThinkingSphinx::Facet.new(unique_name, @columns, self)
end

private

def adapter
Expand Down Expand Up @@ -190,21 +212,6 @@ def is_string?
columns.all? { |col| col.is_string? }
end

# Returns the type of the column. If that's not already set, it returns
# :multi if there's the possibility of more than one value, :string if
# there's more than one association, otherwise it figures out what the
# actual column's datatype is and returns that.
def type
@type ||= case
when is_many?
:multi
when @associations.values.flatten.length > 1
:string
else
translated_type_from_database
end
end

def all_ints?
@columns.all? { |col|
klasses = @associations[col].empty? ? [@model] :
Expand Down
4 changes: 2 additions & 2 deletions lib/thinking_sphinx/collection.rb
Expand Up @@ -113,9 +113,9 @@ def method_missing(method, *args, &block)
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
end

def each_with_group_and_count(&block)
def each_with_groupby_and_count(&block)
results[:matches].each_with_index do |match, index|
yield self[index], match[:attributes]["@group"], match[:attributes]["@count"]
yield self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
end
end

Expand Down
49 changes: 49 additions & 0 deletions lib/thinking_sphinx/facet.rb
@@ -0,0 +1,49 @@
module ThinkingSphinx
class Facet
attr_reader :name, :column, :reference

def initialize(name, columns, reference)
@name = name
@columns = columns
@reference = reference
end

def attribute_name
@attribute_name ||= case @reference
when Attribute
@reference.unique_name.to_s
when Field
@reference.unique_name.to_s + "_sort"
end
end

def value(object, attribute_value)
return translate(object, attribute_value) if @reference.is_a?(Field)

case @reference.type
when :string, :multi
translate(object, attribute_value)
when :datetime
Time.at(attribute_value)
when :boolean
attribute_value > 0
else
attribute_value
end
end

private

def translate(object, attribute_value)
if @columns.length > 1
raise "Can't translate Facets on multiple-column field or attribute"
end

column = @columns.first
column.__stack.each { |method|
object = object.send(method)
}
object.send(column.__name)
end
end
end
18 changes: 13 additions & 5 deletions lib/thinking_sphinx/field.rb
Expand Up @@ -8,7 +8,8 @@ module ThinkingSphinx
# associations. Which can get messy. Use Index.link!, it really helps.
#
class Field
attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes, :prefixes
attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
:prefixes, :faceted

# To create a new field, you'll need to pass in either a single Column
# or an array of them, and some (optional) options. The columns are
Expand Down Expand Up @@ -58,10 +59,11 @@ def initialize(columns, options = {})

raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }

@alias = options[:as]
@sortable = options[:sortable] || false
@infixes = options[:infixes] || false
@prefixes = options[:prefixes] || false
@alias = options[:as]
@sortable = options[:sortable] || false
@infixes = options[:infixes] || false
@prefixes = options[:prefixes] || false
@faceted = options[:facet] || false
end

# Get the part of the SELECT clause related to this field. Don't forget
Expand Down Expand Up @@ -110,6 +112,12 @@ def unique_name
end
end

def to_facet
return nil unless @faceted

ThinkingSphinx::Facet.new(unique_name, @columns, self)
end

private

def adapter
Expand Down
8 changes: 8 additions & 0 deletions lib/thinking_sphinx/index.rb
Expand Up @@ -167,6 +167,14 @@ def initialize_from_builder(&block)
@delta_object = ThinkingSphinx::Deltas.parse self, builder.properties
@options = builder.properties

@model.sphinx_facets ||= []
@fields.select { |field| field.faceted }.each { |field|
@model.sphinx_facets << field.to_facet
}
@attributes.select { |attrib| attrib.faceted }.each { |attrib|
@model.sphinx_facets << attrib.to_facet
}

# We want to make sure that if the database doesn't exist, then Thinking
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
# and db:migrate). It's a bit hacky, but I can't think of a better way.
Expand Down
4 changes: 2 additions & 2 deletions lib/thinking_sphinx/index/builder.rb
Expand Up @@ -89,13 +89,13 @@ def indexes(*args)
args.each do |columns|
fields << Field.new(FauxColumn.coerce(columns), options)

if fields.last.sortable
if fields.last.sortable || fields.last.faceted
attributes << Attribute.new(
fields.last.columns.collect { |col| col.clone },
options.merge(
:type => :string,
:as => fields.last.unique_name.to_s.concat("_sort").to_sym
)
).except(:facet)
)
end
end
Expand Down
17 changes: 17 additions & 0 deletions lib/thinking_sphinx/search.rb
Expand Up @@ -352,6 +352,23 @@ def search_for_id(*args)
end
end

def facets(*args)
options = args.extract_options!.merge! :group_function => :attr

options[:class].sphinx_facets.inject({}) do |hash, facet|
facet_result = {}
options[:group_by] = facet.attribute_name

results = search *(args + [options])
results.each_with_groupby_and_count do |result, group, count|
facet_result[facet.value(result, group)] = count
end
hash[facet.name] = facet_result

hash
end
end

private

# This method handles the common search functionality, and returns both
Expand Down

0 comments on commit a62a2c4

Please sign in to comment.