bloodhound 0.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "bloodhound"
3
- s.version = "0.3"
4
- s.date = "2010-02-07"
3
+ s.version = "0.3.1"
4
+ s.date = "2010-03-01"
5
5
 
6
6
  s.description = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
7
7
  s.summary = "Map strings like 'user:foca age:23' to ActiveRecord named_scopes"
@@ -2,7 +2,7 @@ require "chronic"
2
2
  require "active_record"
3
3
 
4
4
  class Bloodhound
5
- VERSION = "0.3"
5
+ VERSION = "0.3.1"
6
6
  ATTRIBUTE_RE = /\s*(?:(?:(\S+):)?(?:"([^"]*)"|'([^']*)'|(\S+)))\s*/.freeze
7
7
 
8
8
  attr_reader :fields
@@ -14,17 +14,28 @@ class Bloodhound
14
14
 
15
15
  def field(name, options={}, &mapping)
16
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),
20
- :mapping => mapping || default_mapping
17
+ :attribute => options.fetch(:attribute, name).to_s,
18
+ :type => options.fetch(:type, :string).to_sym,
19
+ :case_sensitive => options.fetch(:case_sensitive, true),
20
+ :match_substring => options.fetch(:match_substring, false),
21
+ :options => options.except(:attribute, :type, :case_sensitive, :match_substring),
22
+ :mapping => mapping || default_mapping
21
23
  }
22
24
 
25
+ define_field(name, field)
26
+ end
27
+
28
+ def define_field(name, field)
23
29
  define_scope(name) do |value|
24
- value = field[:mapping].call(cast_value(value, field[:type]))
25
- field[:options].merge(:conditions => { field[:attribute] => value })
30
+ field[:options].merge(:conditions => setup_conditions(field, value))
26
31
  end
27
32
  end
33
+ private :define_field
34
+
35
+ def alias_field(name, other_field)
36
+ @fields[name.to_sym] = @fields[other_field.to_sym]
37
+ define_field(name, @fields[name.to_sym])
38
+ end
28
39
 
29
40
  def text_search(*fields)
30
41
  options = fields.extract_options!
@@ -75,14 +86,10 @@ class Bloodhound
75
86
  "false" => false,
76
87
  "no" => false,
77
88
  "n" => false }.fetch(value.downcase, true)
78
- when :integer
79
- # Kernel#Float is a bit more lax about parsing numbers, like
80
- # Integer(0.0) fails, when we just want it interpreted as a zero
81
- Float(value).to_i
82
- when :float, :decimal
83
- Float(value)
89
+ when :float, :decimal, :integer
90
+ NumericExpression.new(value, type)
84
91
  when :date
85
- Date.parse(Chronic.parse(value).to_s)
92
+ Date.parse(Chronic.parse(value, :context => :past).to_s)
86
93
  when :time, :datetime
87
94
  Chronic.parse(value)
88
95
  else
@@ -91,6 +98,38 @@ class Bloodhound
91
98
  end
92
99
  private :cast_value
93
100
 
101
+ def setup_conditions(field, value)
102
+ value = field[:mapping].call(cast_value(value, field[:type]))
103
+ case field[:type]
104
+ when :string
105
+ conditions_for_string_search(field, value)
106
+ when :float, :decimal, :integer
107
+ conditions_for_numeric(field, value)
108
+ else
109
+ { field[:attribute] => value }
110
+ end
111
+ end
112
+ private :setup_conditions
113
+
114
+ def conditions_for_numeric(field, value)
115
+ [ "#{field[:attribute]} #{value.condition} ?", value.to_s ]
116
+ end
117
+
118
+ def conditions_for_string_search(field, value)
119
+ if field[:case_sensitive]
120
+ field_to_search = field[:attribute]
121
+ value_to_search = value
122
+ else
123
+ field_to_search = "lower(#{field[:attribute]})"
124
+ value_to_search = value.downcase
125
+ end
126
+ if field[:match_substring]
127
+ value_to_search = "%#{value_to_search}%"
128
+ end
129
+ [ "#{field_to_search} like ?", value_to_search ]
130
+ end
131
+ private :conditions_for_string_search
132
+
94
133
  def self.tokenize(query)
95
134
  query.scan(ATTRIBUTE_RE).map do |(key,*value)|
96
135
  value = value.compact.first
@@ -108,4 +147,34 @@ class Bloodhound
108
147
  bloodhound.search(query)
109
148
  end
110
149
  end
150
+
151
+ class NumericExpression
152
+ attr_reader :value, :condition
153
+
154
+ def initialize(value, type)
155
+ parts = value.scan(/(?:[=<>]+|(?:\d|\.)+)/)[0,2]
156
+ # Kernel#Float is a bit more lax about parsing numbers, like
157
+ # Integer(0.0) fails, when we just want it interpreted as a zero
158
+ @value = Float(parts.last)
159
+ @value = @value.to_i if type == :integer
160
+ @condition = sanitize_condition(parts.first)
161
+ end
162
+
163
+ def sanitize_condition(cond)
164
+ valid = %w(= == > < <= >= <>)
165
+ valid.include?(cond) ? cond : "="
166
+ end
167
+
168
+ def to_i
169
+ value.to_i
170
+ end
171
+
172
+ def to_f
173
+ value.to_f
174
+ end
175
+
176
+ def to_s
177
+ value.to_s
178
+ end
179
+ end
111
180
  end
@@ -7,7 +7,10 @@ class User < ActiveRecord::Base
7
7
 
8
8
  bloodhound do |search|
9
9
  search.field :first_name
10
+ search.alias_field :name, :first_name
10
11
  search.field :last_name
12
+ search.field :insensitive_last_name, :attribute => "last_name", :case_sensitive => false
13
+ search.field :substringed_last_name, :attribute => "last_name", :match_substring => true
11
14
  search.field :available_product, :type => :boolean,
12
15
  :attribute => "products.available",
13
16
  :include => :products
@@ -25,7 +28,7 @@ class Product < ActiveRecord::Base
25
28
  search.text_search "products.name", "products.value"
26
29
 
27
30
  search.field :name
28
- search.field :value
31
+ search.field :value, :type => :decimal
29
32
  search.field :available, :type => :boolean
30
33
 
31
34
  search.keyword :sort do |value|
@@ -45,6 +48,10 @@ describe Bloodhound do
45
48
  User.scopes_for_query("first_name:John").should include(@john)
46
49
  end
47
50
 
51
+ it "finds a user by first_name, using an alias" do
52
+ User.scopes_for_query("name:John").should include(@john)
53
+ end
54
+
48
55
  it "finds by text search" do
49
56
  Product.scopes_for_query("duck rubber").should include(@ducky)
50
57
  end
@@ -54,6 +61,16 @@ describe Bloodhound do
54
61
  User.scopes_for_query("last_name:Doe").should have(1).element
55
62
  end
56
63
 
64
+ it "can find using case insensitive search" do
65
+ User.scopes_for_query("insensitive_last_name:doe").should include(@john)
66
+ User.scopes_for_query("insensitive_last_name:doe").should have(1).element
67
+ end
68
+
69
+ it "can find matching substrings" do
70
+ User.scopes_for_query("substringed_last_name:oe").should include(@john)
71
+ User.scopes_for_query("substringed_last_name:oe").should have(1).element
72
+ end
73
+
57
74
  it "can find using key:value pairs for attribuets that define a type" do
58
75
  User.scopes_for_query("available_product:yes").should include(@john)
59
76
  User.scopes_for_query("available_product:no").should_not include(@john)
@@ -62,4 +79,18 @@ describe Bloodhound do
62
79
  it "allows defining arbitrary keywords to create scopes" do
63
80
  @john.products.scopes_for_query("sort:name").all.should == [@tv, @ducky]
64
81
  end
82
+
83
+ it "allows to search by numeric fields with greater than or lower than modifiers" do
84
+ @john.products.scopes_for_query("value:=10").should include(@ducky)
85
+ @john.products.scopes_for_query("value:=10").should have(1).element
86
+ @john.products.scopes_for_query("value:10").should include(@ducky)
87
+ @john.products.scopes_for_query("value:10").should have(1).element
88
+ @john.products.scopes_for_query("value:>100").should include(@tv)
89
+ @john.products.scopes_for_query("value:>100").should have(1).element
90
+ @john.products.scopes_for_query("value:<100").should include(@ducky)
91
+ @john.products.scopes_for_query("value:<100").should have(1).element
92
+ @john.products.scopes_for_query("value:>200").should have(0).elements
93
+ @john.products.scopes_for_query("value:>=200").should include(@tv)
94
+ @john.products.scopes_for_query("value:>=200").should have(1).element
95
+ end
65
96
  end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bloodhound
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.3"
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 1
9
+ version: 0.3.1
5
10
  platform: ruby
6
11
  authors:
7
12
  - "Nicol\xC3\xA1s Sanguinetti"
@@ -9,7 +14,7 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2010-02-07 00:00:00 -02:00
17
+ date: 2010-03-01 00:00:00 -02:00
13
18
  default_executable:
14
19
  dependencies: []
15
20
 
@@ -41,18 +46,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
46
  requirements:
42
47
  - - ">="
43
48
  - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
44
51
  version: "0"
45
- version:
46
52
  required_rubygems_version: !ruby/object:Gem::Requirement
47
53
  requirements:
48
54
  - - ">="
49
55
  - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
50
58
  version: "0"
51
- version:
52
59
  requirements: []
53
60
 
54
61
  rubyforge_project:
55
- rubygems_version: 1.3.5
62
+ rubygems_version: 1.3.6
56
63
  signing_key:
57
64
  specification_version: 3
58
65
  summary: Map strings like 'user:foca age:23' to ActiveRecord named_scopes