bloodhound 0.2.1 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "bloodhound"
3
- s.version = "0.2.1"
4
- s.date = "2010-02-04"
3
+ s.version = "0.3"
4
+ s.date = "2010-02-07"
5
5
 
6
- s.description = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
7
- s.summary = "Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }"
6
+ s.description = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
7
+ s.summary = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
8
8
  s.homepage = "http://github.com/foca/bloodhound"
9
9
 
10
10
  s.authors = ["Nicolás Sanguinetti"]
@@ -1,82 +1,65 @@
1
1
  require "chronic"
2
+ require "active_record"
2
3
 
3
4
  class Bloodhound
4
- VERSION = "0.2.1"
5
+ VERSION = "0.3"
5
6
  ATTRIBUTE_RE = /\s*(?:(?:(\S+):)?(?:"([^"]*)"|'([^']*)'|(\S+)))\s*/.freeze
6
7
 
7
- def initialize
8
- @search_fields = {}
9
- @keywords = {}
10
- end
8
+ attr_reader :fields
11
9
 
12
- def fields
13
- @search_fields
10
+ def initialize(model)
11
+ @model = model
12
+ @fields = {}
14
13
  end
15
14
 
16
15
  def field(name, options={}, &mapping)
17
- attribute = options.delete(:attribute) || name
18
- fuzzy = options.delete(:fuzzy)
19
- type = options.delete(:type).try(:to_sym)
20
-
21
- fields[name.to_sym] = {
22
- :attribute => attribute.to_s,
23
- :type => type,
24
- :fuzzy => fuzzy.nil? ? true : fuzzy,
25
- :options => options,
16
+ @fields[name.to_sym] = field = {
17
+ :attribute => options.fetch(:attribute, name).to_s,
18
+ :type => options.fetch(:type, :string).to_sym,
19
+ :options => options.except(:attribute, :type),
26
20
  :mapping => mapping || default_mapping
27
21
  }
28
- end
29
22
 
30
- def keyword(name, &block)
31
- @keywords[name.to_sym] = block
23
+ define_scope(name) do |value|
24
+ value = field[:mapping].call(cast_value(value, field[:type]))
25
+ field[:options].merge(:conditions => { field[:attribute] => value })
26
+ end
32
27
  end
33
28
 
34
- def search_scope(model, query)
35
- tokenize(query).inject(model) do |model, (key,value)|
36
- if value.nil?
37
- text_search_for(model, key)
38
- elsif has_keyword?(key)
39
- keyword_search_for(model, key, value)
40
- else
41
- attribute_search_for(model, key, value)
42
- end
29
+ def text_search(*fields)
30
+ options = fields.extract_options!
31
+ fields = fields.map {|f| "LOWER(#{f}) LIKE :value" }.join(" OR ")
32
+
33
+ define_scope "text_search" do |value|
34
+ options.merge(:conditions => [fields, { :value => "%#{value.downcase}%" }])
43
35
  end
44
36
  end
45
37
 
46
- def text_search_for(model, value)
47
- return model if fields.empty?
38
+ def keyword(name, &block)
39
+ define_scope(name, &block)
40
+ end
48
41
 
49
- model = scoped_by_text_search_options(model)
42
+ def search(query)
43
+ self.class.tokenize(query).inject(@model) do |model, (key,value)|
44
+ key, value = "text_search", key if value.nil?
50
45
 
51
- text_search_conditions = fields.inject([[], {}]) do |conditions, (name,properties)|
52
- if properties[:fuzzy]
53
- conditions[0] << "#{properties[:attribute]} LIKE :#{name}"
54
- conditions[1].update(name => properties[:mapping].call("%#{value}%"))
55
- conditions
46
+ if model.respond_to?(scope_name_for(key))
47
+ model.send(scope_name_for(key), value)
56
48
  else
57
- conditions
49
+ model
58
50
  end
59
51
  end
60
-
61
- model = model.scoped(:conditions => [text_search_conditions[0].join(" OR "),
62
- text_search_conditions[1]])
63
52
  end
64
- private :text_search_for
65
-
66
- def attribute_search_for(model, name, value)
67
- properties = fields.fetch(name.to_sym, {})
68
53
 
69
- return model if properties[:type].nil?
70
-
71
- value = properties[:mapping].call(cast_value(value, properties[:type]))
72
- model.scoped(properties[:options].merge(:conditions => { properties[:attribute] => value }))
54
+ def define_scope(name, &scope)
55
+ @model.send(:named_scope, scope_name_for(name), scope)
73
56
  end
74
- private :attribute_search_for
57
+ private :define_scope
75
58
 
76
- def keyword_search_for(model, keyword, value)
77
- model.scoped(@keywords[keyword.to_sym].call(value))
59
+ def scope_name_for(key)
60
+ "bloodhound_for_#{key}"
78
61
  end
79
- private :keyword_search_for
62
+ private :scope_name_for
80
63
 
81
64
  def default_mapping
82
65
  lambda {|value| value }
@@ -108,33 +91,21 @@ class Bloodhound
108
91
  end
109
92
  private :cast_value
110
93
 
111
- def scoped_by_text_search_options(model)
112
- fields.inject(model) do |model, (name,properties)|
113
- properties[:fuzzy] ? model.scoped(properties[:options]) : model
114
- end
115
- end
116
- private :scoped_by_text_search_options
117
-
118
- def tokenize(query)
94
+ def self.tokenize(query)
119
95
  query.scan(ATTRIBUTE_RE).map do |(key,*value)|
120
96
  value = value.compact.first
121
97
  [key || value, key && value]
122
98
  end
123
99
  end
124
- private :tokenize
125
-
126
- def has_keyword?(name)
127
- @keywords.has_key?(name.to_sym)
128
- end
129
100
 
130
101
  module Searchable
131
102
  def bloodhound(&block)
132
- @bloodhound ||= Bloodhound.new
103
+ @bloodhound ||= Bloodhound.new(self)
133
104
  block ? @bloodhound.tap(&block) : @bloodhound
134
105
  end
135
106
 
136
- def scoped_search(query)
137
- bloodhound.search_scope(self, query)
107
+ def scopes_for_query(query)
108
+ bloodhound.search(query)
138
109
  end
139
110
  end
140
111
  end
@@ -6,14 +6,13 @@ class User < ActiveRecord::Base
6
6
  has_many :products
7
7
 
8
8
  bloodhound do |search|
9
- search.field(:first_name, :attribute => "lower(users.first_name)") {|value| value.downcase }
9
+ search.field :first_name
10
10
  search.field :last_name
11
- search.field :product, :attribute => "products.name", :joins => :products, :group => column_names.join(",")
12
11
  search.field :available_product, :type => :boolean,
13
12
  :attribute => "products.available",
14
- :fuzzy => false,
15
- :joins => :products,
16
- :group => column_names.join(",")
13
+ :include => :products
14
+
15
+ search.text_search "first_name", "last_name", "products.name", :include => :products
17
16
  end
18
17
  end
19
18
 
@@ -23,9 +22,11 @@ class Product < ActiveRecord::Base
23
22
  belongs_to :user
24
23
 
25
24
  bloodhound do |search|
25
+ search.text_search "products.name", "products.value"
26
+
26
27
  search.field :name
27
28
  search.field :value
28
- search.field :available, :type => :boolean, :fuzzy => false
29
+ search.field :available, :type => :boolean
29
30
 
30
31
  search.keyword :sort do |value|
31
32
  { :order => "#{search.fields[value.to_sym][:attribute]} DESC" }
@@ -36,51 +37,29 @@ end
36
37
  describe Bloodhound do
37
38
  before :all do
38
39
  @john = User.create(:first_name => "John", :last_name => "Doe")
39
- @tv = @john.products.create(:name => "Rubber Duck", :value => 10)
40
- @ducky = @john.products.create(:name => "TV", :value => 200)
40
+ @ducky = @john.products.create(:name => "Rubber Duck", :value => 10)
41
+ @tv = @john.products.create(:name => "TV", :value => 200)
41
42
  end
42
43
 
43
- it "finds a user by first_name, in a case insensitive manner" do
44
- User.scoped_search("John").should include(@john)
45
- User.scoped_search("joHN").should include(@john)
44
+ it "finds a user by first_name" do
45
+ User.scopes_for_query("first_name:John").should include(@john)
46
46
  end
47
47
 
48
- it "uses LIKE '%query%' for fuzzy searches" do
49
- User.scoped_search("oh").should include(@john)
48
+ it "finds by text search" do
49
+ Product.scopes_for_query("duck rubber").should include(@ducky)
50
50
  end
51
51
 
52
52
  it "finds a user by last_name, but only in a case sensitive manner" do
53
- User.scoped_search("Doe").should include(@john)
54
- User.scoped_search("Doe").should have(1).element
55
- User.scoped_search("Smith").should_not include(@john)
53
+ User.scopes_for_query("last_name:Doe").should include(@john)
54
+ User.scopes_for_query("last_name:Doe").should have(1).element
56
55
  end
57
56
 
58
57
  it "can find using key:value pairs for attribuets that define a type" do
59
- User.scoped_search("available_product:yes").should include(@john)
60
- User.scoped_search("available_product:no").should_not include(@john)
58
+ User.scopes_for_query("available_product:yes").should include(@john)
59
+ User.scopes_for_query("available_product:no").should_not include(@john)
61
60
  end
62
61
 
63
62
  it "allows defining arbitrary keywords to create scopes" do
64
- @john.products.scoped_search("order:name").all.should == [@tv, @ducky]
65
- end
66
-
67
- it "exposes the fields via bloodhound#attributes" do
68
- last_name = User.bloodhound.fields[:last_name]
69
- last_name[:attribute].should == "last_name"
70
- last_name[:type].should be_nil
71
- last_name[:fuzzy].should be_true
72
- last_name[:options].should == {}
73
-
74
- first_name = User.bloodhound.fields[:first_name]
75
- first_name[:attribute].should == "lower(users.first_name)"
76
- first_name[:type].should be_nil
77
- first_name[:fuzzy].should be_true
78
- first_name[:options].should == {}
79
-
80
- availability = Product.bloodhound.fields[:available]
81
- availability[:attribute].should == "available"
82
- availability[:type].should == :boolean
83
- availability[:fuzzy].should be_false
84
- availability[:options].should == {}
63
+ @john.products.scopes_for_query("sort:name").all.should == [@tv, @ducky]
85
64
  end
86
65
  end
@@ -27,18 +27,3 @@ ActiveRecord::Schema.define(:version => 1) do
27
27
  t.belongs_to :user
28
28
  end
29
29
  end
30
-
31
- Spec::Runner.configure do |config|
32
- module CustomMatchers
33
- def be_included_in_extracted_attributes_from(attribute_string)
34
- simple_matcher :other_set_of_attributes do |given|
35
- expected = subject.tokenize(attribute_string)
36
- given.each do |key, value|
37
- expected.fetch(key).should == value
38
- end
39
- end
40
- end
41
- end
42
-
43
- config.include CustomMatchers
44
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bloodhound
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: "0.3"
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Nicol\xC3\xA1s Sanguinetti"
@@ -9,11 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-04 00:00:00 -02:00
12
+ date: 2010-02-07 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
16
+ description: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
17
17
  email: contacto@nicolassanguinetti.info
18
18
  executables: []
19
19
 
@@ -55,6 +55,6 @@ rubyforge_project:
55
55
  rubygems_version: 1.3.5
56
56
  signing_key:
57
57
  specification_version: 3
58
- summary: Convert strings like 'user:foca age:23' to { 'user' => 'foca' => 'age' => 23 }
58
+ summary: Map strings like 'user:foca age:23' to ActiveRecord named_scopes
59
59
  test_files: []
60
60