gsolr_ext 0.12.3

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.
@@ -0,0 +1,58 @@
1
+ module GSolr::Ext::Response::Facets
2
+
3
+ # represents a facet value; which is a field value and its hit count
4
+ class FacetItem
5
+ attr_reader :value, :hits
6
+ def initialize value, hits
7
+ @value, @hits = value, hits
8
+ end
9
+ end
10
+
11
+ # represents a facet; which is a field and its values
12
+ class FacetField
13
+ attr_reader :name, :items
14
+ def initialize name, items
15
+ @name, @items = name, items
16
+ end
17
+ end
18
+
19
+ # @response.facets.each do |facet|
20
+ # facet.name
21
+ # facet.items
22
+ # end
23
+ # "caches" the result in the @facets instance var
24
+ def facets
25
+ @facets ||= (
26
+ facet_fields.map do |(facet_field_name,values_and_hits)|
27
+ items = []
28
+ (values_and_hits.size/2).times do |index|
29
+ items << FacetItem.new(values_and_hits[index*2], values_and_hits[index*2+1])
30
+ end
31
+ FacetField.new(facet_field_name, items)
32
+ end
33
+ )
34
+ end
35
+
36
+ # pass in a facet field name and get back a Facet instance
37
+ def facet_by_field_name(name)
38
+ @facets_by_field_name ||= {}
39
+ @facets_by_field_name[name] ||= (
40
+ facets.detect{|facet|facet.name.to_s == name.to_s}
41
+ )
42
+ end
43
+
44
+ def facet_counts
45
+ @facet_counts ||= self['facet_counts'] || {}
46
+ end
47
+
48
+ # Returns the hash of all the facet_fields (ie: {'instock_b' => ['true', 123, 'false', 20]}
49
+ def facet_fields
50
+ @facet_fields ||= facet_counts['facet_fields'] || {}
51
+ end
52
+
53
+ # Returns all of the facet queries
54
+ def facet_queries
55
+ @facet_queries ||= facet_counts['facet_queries'] || {}
56
+ end
57
+
58
+ end # end Facets
@@ -0,0 +1,92 @@
1
+ # A mixin for making access to the spellcheck component data easy.
2
+ #
3
+ # response.spelling.words
4
+ #
5
+ module GSolr::Ext::Response::Spelling
6
+
7
+ def spelling
8
+ @spelling ||= Base.new(self)
9
+ end
10
+
11
+ class Base
12
+
13
+ attr :response
14
+
15
+ def initialize(response)
16
+ @response = response
17
+ end
18
+
19
+ # returns an array of spelling suggestion for specific query words,
20
+ # as provided in the solr response. Only includes words with higher
21
+ # frequency of occurrence than word in original query.
22
+ # can't do a full query suggestion because we only get info for each word;
23
+ # combination of words may not have results.
24
+ # Thanks to Naomi Dushay!
25
+ def words
26
+ @words ||= (
27
+ word_suggestions = []
28
+ spellcheck = self.response[:spellcheck]
29
+ if spellcheck && spellcheck[:suggestions]
30
+ suggestions = spellcheck[:suggestions]
31
+ unless suggestions.nil?
32
+ # suggestions is an array:
33
+ # (query term)
34
+ # (hash of term info and term suggestion)
35
+ # ...
36
+ # (query term)
37
+ # (hash of term info and term suggestion)
38
+ # 'correctlySpelled'
39
+ # true/false
40
+ # collation
41
+ # (suggestion for collation)
42
+ if suggestions.index("correctlySpelled") #if extended results
43
+ i_stop = suggestions.index("correctlySpelled")
44
+ elsif suggestions.index("collation")
45
+ i_stop = suggestions.index("collation")
46
+ else
47
+ i_stop = suggestions.length
48
+ end
49
+ # step through array in 2s to get info for each term
50
+ 0.step(i_stop-1, 2) do |i|
51
+ term = suggestions[i]
52
+ term_info = suggestions[i+1]
53
+ # term_info is a hash:
54
+ # numFound =>
55
+ # startOffset =>
56
+ # endOffset =>
57
+ # origFreq =>
58
+ # suggestion => [{ frequency =>, word => }] # for extended results
59
+ # suggestion => ['word'] # for non-extended results
60
+ origFreq = term_info['origFreq']
61
+ if suggestions.index("correctlySpelled")
62
+ word_suggestions << term_info['suggestion'].map do |suggestion|
63
+ suggestion['word'] if suggestion['freq'] > origFreq
64
+ end
65
+ else
66
+ # only extended suggestions have frequency so we just return all suggestions
67
+ word_suggestions << term_info['suggestion']
68
+ end
69
+ end
70
+ end
71
+ end
72
+ word_suggestions.flatten.compact.uniq
73
+ )
74
+ end
75
+
76
+ def collation
77
+ # FIXME: DRY up with words
78
+ spellcheck = self.response[:spellcheck]
79
+ if spellcheck && spellcheck[:suggestions]
80
+ suggestions = spellcheck[:suggestions]
81
+ unless suggestions.nil?
82
+ if suggestions.index("collation")
83
+ suggestions[suggestions.index("collation") + 1]
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
data/lib/mash.rb ADDED
@@ -0,0 +1,143 @@
1
+ # This class has dubious semantics and we only have it so that people can write
2
+ # params[:key] instead of params['key'].
3
+ class Mash < Hash
4
+
5
+ # @param constructor<Object>
6
+ # The default value for the mash. Defaults to an empty hash.
7
+ #
8
+ # @details [Alternatives]
9
+ # If constructor is a Hash, a new mash will be created based on the keys of
10
+ # the hash and no default value will be set.
11
+ def initialize(constructor = {})
12
+ if constructor.is_a?(Hash)
13
+ super()
14
+ update(constructor)
15
+ else
16
+ super(constructor)
17
+ end
18
+ end
19
+
20
+ # @param key<Object> The default value for the mash. Defaults to nil.
21
+ #
22
+ # @details [Alternatives]
23
+ # If key is a Symbol and it is a key in the mash, then the default value will
24
+ # be set to the value matching the key.
25
+ def default(key = nil)
26
+ if key.is_a?(Symbol) && include?(key = key.to_s)
27
+ self[key]
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
34
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
35
+
36
+ # @param key<Object> The key to set.
37
+ # @param value<Object>
38
+ # The value to set the key to.
39
+ #
40
+ # @see Mash#convert_key
41
+ # @see Mash#convert_value
42
+ def []=(key, value)
43
+ regular_writer(convert_key(key), convert_value(value))
44
+ end
45
+
46
+ # @param other_hash<Hash>
47
+ # A hash to update values in the mash with. The keys and the values will be
48
+ # converted to Mash format.
49
+ #
50
+ # @return <Mash> The updated mash.
51
+ def update(other_hash)
52
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
53
+ self
54
+ end
55
+
56
+ alias_method :merge!, :update
57
+
58
+ # @param key<Object> The key to check for. This will be run through convert_key.
59
+ #
60
+ # @return <TrueClass, FalseClass> True if the key exists in the mash.
61
+ def key?(key)
62
+ super(convert_key(key))
63
+ end
64
+
65
+ # def include? def has_key? def member?
66
+ alias_method :include?, :key?
67
+ alias_method :has_key?, :key?
68
+ alias_method :member?, :key?
69
+
70
+ # @param key<Object> The key to fetch. This will be run through convert_key.
71
+ # @param *extras<Array> Default value.
72
+ #
73
+ # @return <Object> The value at key or the default value.
74
+ def fetch(key, *extras)
75
+ super(convert_key(key), *extras)
76
+ end
77
+
78
+ # @param *indices<Array>
79
+ # The keys to retrieve values for. These will be run through +convert_key+.
80
+ #
81
+ # @return <Array> The values at each of the provided keys
82
+ def values_at(*indices)
83
+ indices.collect {|key| self[convert_key(key)]}
84
+ end
85
+
86
+ # @return <Mash> A duplicate of this mash.
87
+ def dup
88
+ Mash.new(self)
89
+ end
90
+
91
+ # @param hash<Hash> The hash to merge with the mash.
92
+ #
93
+ # @return <Mash> A new mash with the hash values merged in.
94
+ def merge(hash)
95
+ self.dup.update(hash)
96
+ end
97
+
98
+ # @param key<Object>
99
+ # The key to delete from the mash.\
100
+ def delete(key)
101
+ super(convert_key(key))
102
+ end
103
+
104
+ # Used to provide the same interface as Hash.
105
+ #
106
+ # @return <Mash> This mash unchanged.
107
+ def stringify_keys!; self end
108
+
109
+ # @return <Hash> The mash as a Hash with string keys.
110
+ def to_hash
111
+ Hash.new(default).merge(self)
112
+ end
113
+
114
+ protected
115
+ # @param key<Object> The key to convert.
116
+ #
117
+ # @param <Object>
118
+ # The converted key. If the key was a symbol, it will be converted to a
119
+ # string.
120
+ #
121
+ # @api private
122
+ def convert_key(key)
123
+ key.kind_of?(Symbol) ? key.to_s : key
124
+ end
125
+
126
+ # @param value<Object> The value to convert.
127
+ #
128
+ # @return <Object>
129
+ # The converted value. A Hash or an Array of hashes, will be converted to
130
+ # their Mash equivalents.
131
+ #
132
+ # @api private
133
+ def convert_value(value)
134
+ case value
135
+ when Hash
136
+ value.to_mash
137
+ when Array
138
+ value.collect { |e| convert_value(e) }
139
+ else
140
+ value
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,276 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe GSolr::Ext do
4
+
5
+ context GSolr::Client do
6
+
7
+ let(:client){GSolr.connect}
8
+
9
+ it 'should now have a #find method' do
10
+ client.should respond_to(:find)
11
+ end
12
+
13
+ it 'should produce results from the #find method' do
14
+ c = client
15
+ c.should_receive(:request).
16
+ with('/select', {:rows=>10, :start=>20, :q=>"*:*"}).
17
+ and_return({'response'=>{'docs' => []}, 'responseHeader' => {}})
18
+ response = c.find :page=>3, :per_page=>10, :q=>'*:*'#, :page=>1, :per_page=>10
19
+ response.should be_a(Mash)
20
+ end
21
+
22
+ it 'should call the #find method with a custom request handler' do
23
+ c = client
24
+ expected_response = {'response'=>{'docs' => []}, 'responseHeader' => {}}
25
+ # ok this is hacky... the raw method needs to go into a mixin dude
26
+ def expected_response.raw
27
+ {:path => '/select'}
28
+ end
29
+ c.should_receive(:request).
30
+ with('/select', {:q=>'*:*'}).
31
+ and_return(expected_response)
32
+ response = c.find '/select', :q=>'*:*'
33
+ response.raw[:path].should match(/\/select/)
34
+ end
35
+
36
+ it 'should be ok' do
37
+ c = client
38
+ c.should_receive(:request).
39
+ with('/select', :q=>'*:*').
40
+ and_return({'response'=>{'docs' => []}, 'responseHeader' => {'status'=>0}})
41
+ response = c.find :q=>'*:*'
42
+ response.should respond_to(:ok?)
43
+ response.ok?.should == true
44
+ end
45
+
46
+ it 'should call the #luke method' do
47
+ c = client
48
+ c.should_receive(:request).
49
+ with('/admin/luke', {"numTerms"=>0}).
50
+ and_return({"fields"=>nil, "index"=>nil, "info" => nil})
51
+ info = c.luke
52
+ info.should be_a(Mash)
53
+ info.should have_key('fields')
54
+ info.should have_key('index')
55
+ info.should have_key('info')
56
+ end
57
+
58
+ it 'should forwad #ping? calls to the connection' do
59
+ client.connection.should_receive(:request).
60
+ with('/admin/ping', :wt => :ruby ).
61
+ and_return( :params => { :wt => :ruby },
62
+ :status_code => 200,
63
+ :body => "{'responseHeader'=>{'status'=>0,'QTime'=>44,'params'=>{'echoParams'=>'all','echoParams'=>'all','q'=>'solrpingquery','qt'=>'standard','wt'=>'ruby'}},'status'=>'OK'}" )
64
+ client.ping?
65
+ end
66
+
67
+ it 'should raise an error if the ping service is not available' do
68
+ client.connection.should_receive(:request).
69
+ with('/admin/ping', :wt => :ruby ).
70
+ # the first part of the what the message would really be
71
+ and_raise( GSolr::RequestError.new("Solr Response: pingQuery_not_configured_consider_registering_PingRequestHandler_with_the_name_adminping_instead__") )
72
+ lambda { client.ping? }.should raise_error( GSolr::RequestError )
73
+ end
74
+
75
+ end
76
+
77
+ context 'requests' do
78
+
79
+ it 'should create a valid request' do
80
+ solr_params = GSolr::Ext::Request.map(
81
+ :page=>'2',
82
+ :per_page=>'10',
83
+ :phrases=>{:name=>'This is a phrase'},
84
+ :filters=>['test', {:price=>(1..10)}],
85
+ :phrase_filters=>{:manu=>['Apple']},
86
+ :queries=>'ipod',
87
+ :facets=>{:fields=>['cat', 'blah']},
88
+ :spellcheck => true
89
+ )
90
+ ["test", "price:[1 TO 10]", "manu:\"Apple\""].should == solr_params[:fq]
91
+ solr_params[:start].should == 10
92
+ solr_params[:rows].should == 10
93
+ solr_params[:q].should == "ipod name:\"This is a phrase\""
94
+ solr_params['facet.field'].should == ['cat', 'blah']
95
+ solr_params[:facet].should == true
96
+ end
97
+
98
+ it 'should map fq using the phrase_filters mapping' do
99
+ solr_params = GSolr::Ext::Request.map(
100
+ :phrase_filters=>{:manu=>['Apple', 'ASG'], :color=>['red', 'blue']}
101
+ )
102
+
103
+ solr_params[:fq].size.should == 4
104
+ solr_params[:fq].should include("color:\"red\"")
105
+ solr_params[:fq].should include("color:\"blue\"")
106
+ solr_params[:fq].should include("manu:\"Apple\"")
107
+ solr_params[:fq].should include("manu:\"ASG\"")
108
+
109
+ end
110
+
111
+ it 'should map :filters and :phrase_filters while keeping an existing :fq' do
112
+ solr_params = GSolr::Ext::Request.map(
113
+ :fq => 'blah blah',
114
+ :phrase_filters=>{:manu=>['Apple', 'ASG'], :color=>['red', 'blue']}
115
+ )
116
+
117
+ solr_params[:fq].size.should == 5
118
+ solr_params[:fq].should include("blah blah")
119
+ solr_params[:fq].should include("color:\"red\"")
120
+ solr_params[:fq].should include("color:\"blue\"")
121
+ solr_params[:fq].should include("manu:\"Apple\"")
122
+ solr_params[:fq].should include("manu:\"ASG\"")
123
+ end
124
+
125
+ it 'should map arrays of ranges in :phrase_filters' do
126
+ solr_params = GSolr::Ext::Request.map(
127
+ :phrase_filters=>{:range=>[1940..2020]}
128
+ )
129
+
130
+ solr_params[:fq].size.should == 1
131
+ solr_params[:fq].should include("range:[1940 TO 2020]")
132
+ end
133
+
134
+ end
135
+
136
+ context 'response' do
137
+
138
+ def create_response
139
+ raw_response = eval(mock_query_response)
140
+ GSolr::Ext::Response::Base.new(raw_response, '/select', raw_response['params'])
141
+ end
142
+
143
+ it 'should create a valid response' do
144
+ r = create_response
145
+ r.should respond_to(:header)
146
+ r.ok?.should == true
147
+ end
148
+
149
+ it 'should have accurate pagination numbers' do
150
+ r = create_response
151
+ r.rows.should == 11
152
+ r.total.should == 26
153
+ r.start.should == 0
154
+ r.docs.per_page.should == 11
155
+ end
156
+
157
+ it 'should create a valid response class' do
158
+ r = create_response
159
+
160
+ r.should respond_to(:response)
161
+ r.ok?.should == true
162
+ r.docs.size.should == 11
163
+ r.params[:echoParams].should == 'EXPLICIT'
164
+ r.docs.previous_page.should == 1
165
+ r.docs.next_page.should == 2
166
+ #
167
+ r.should be_a(GSolr::Ext::Response::Docs)
168
+ r.should be_a(GSolr::Ext::Response::Facets)
169
+ end
170
+
171
+ it 'should create a doc with gsolr_ext methods' do
172
+ r = create_response
173
+
174
+ doc = r.docs.first
175
+ doc.has?(:cat, /^elec/).should == true
176
+ doc.has?(:cat, 'elec').should_not == true
177
+ doc.has?(:cat, 'electronics').should == true
178
+
179
+ doc.get(:cat).should == 'electronics, hard drive'
180
+ doc.get(:xyz).should == nil
181
+ doc.get(:xyz, :default=>'def').should == 'def'
182
+ end
183
+
184
+ it 'should provide facet helpers' do
185
+ r = create_response
186
+ r.facets.size.should == 2
187
+
188
+ field_names = r.facets.collect{|facet|facet.name}
189
+ field_names.include?('cat').should == true
190
+ field_names.include?('manu').should == true
191
+
192
+ first_facet = r.facets.first
193
+ first_facet.name.should == 'cat'
194
+
195
+ first_facet.items.size.should == 10
196
+
197
+ expected = "electronics - 14, memory - 3, card - 2, connector - 2, drive - 2, graphics - 2, hard - 2, monitor - 2, search - 2, software - 2"
198
+ received = first_facet.items.collect do |item|
199
+ item.value + ' - ' + item.hits.to_s
200
+ end.join(', ')
201
+
202
+ expected.should == received
203
+
204
+ r.facets.each do |facet|
205
+ facet.respond_to?(:name).should == true
206
+ facet.items.each do |item|
207
+ item.respond_to?(:value).should == true
208
+ item.respond_to?(:hits).should == true
209
+ end
210
+ end
211
+
212
+ end
213
+
214
+ it 'should return the correct value when calling facet_by_field_name' do
215
+ r = create_response
216
+ facet = r.facet_by_field_name('cat')
217
+ facet.name.should == 'cat'
218
+ end
219
+
220
+ it 'should provide the responseHeader params' do
221
+ raw_response = eval(mock_query_response)
222
+ raw_response['responseHeader']['params']['test'] = :test
223
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', raw_response['params'])
224
+ r.params['test'].should == :test
225
+ end
226
+
227
+ it 'should provide the solr-returned params and "rows" should be 11' do
228
+ raw_response = eval(mock_query_response)
229
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
230
+ r.params[:rows].to_s.should == '11'
231
+ end
232
+
233
+ it 'should provide the ruby request params if responseHeader["params"] does not exist' do
234
+ raw_response = eval(mock_query_response)
235
+ raw_response.delete 'responseHeader'
236
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', :rows => 999)
237
+ r.params[:rows].to_s.should == '999'
238
+ end
239
+
240
+ it 'should provide spelling suggestions for regular spellcheck results' do
241
+ raw_response = eval(mock_response_with_spellcheck)
242
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
243
+ r.spelling.words.should include("dell")
244
+ r.spelling.words.should include("ultrasharp")
245
+ end
246
+
247
+ it 'should provide spelling suggestions for extended spellcheck results' do
248
+ raw_response = eval(mock_response_with_spellcheck_extended)
249
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
250
+ r.spelling.words.should include("dell")
251
+ r.spelling.words.should include("ultrasharp")
252
+ end
253
+
254
+ it 'should provide no spelling suggestions when extended results and suggestion frequency is the same as original query frequency' do
255
+ raw_response = eval(mock_response_with_spellcheck_same_frequency)
256
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
257
+ r.spelling.words.should == []
258
+ end
259
+
260
+ it 'should provide spelling suggestions for a regular spellcheck results with a collation' do
261
+ raw_response = eval(mock_response_with_spellcheck_collation)
262
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
263
+ r.spelling.words.should include("dell")
264
+ r.spelling.words.should include("ultrasharp")
265
+ end
266
+
267
+ it 'should provide spelling suggestion collation' do
268
+ raw_response = eval(mock_response_with_spellcheck_collation)
269
+ r = GSolr::Ext::Response::Base.new(raw_response, '/catalog', {})
270
+ r.spelling.collation.should == 'dell ultrasharp'
271
+ end
272
+
273
+ end
274
+
275
+ end
276
+