rsolr-ext 0.9.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.rdoc +119 -0
- data/lib/mash.rb +143 -0
- data/lib/rsolr-ext/connection.rb +41 -0
- data/lib/rsolr-ext/doc.rb +44 -0
- data/lib/rsolr-ext/model.rb +99 -0
- data/lib/rsolr-ext/request.rb +101 -0
- data/lib/rsolr-ext/response/docs.rb +56 -0
- data/lib/rsolr-ext/response/facets.rb +62 -0
- data/lib/rsolr-ext/response/spelling.rb +65 -0
- data/lib/rsolr-ext/response.rb +51 -0
- data/lib/rsolr-ext.rb +42 -0
- data/rsolr-ext.gemspec +39 -0
- data/test/connection_test.rb +40 -0
- data/test/helper.rb +3 -0
- data/test/request_test.rb +33 -0
- data/test/response_test.rb +105 -0
- data/test/test_unit_test_case.rb +18 -0
- metadata +81 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2008-2009 Matt Mitchell
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
=RSolr::Ext
|
2
|
+
A set of helper methods/modules to assist in building Solr queries and handling responses when using the RSolr library.
|
3
|
+
|
4
|
+
==Related Resources & Projects
|
5
|
+
* {RSolr}[http://github.com/mwmitchell/rsolr]
|
6
|
+
|
7
|
+
==Requests
|
8
|
+
To use the RSolr::Ext connection instead of the normal RSolr connection:
|
9
|
+
solr = RSolr::Ext.connect
|
10
|
+
|
11
|
+
RSolr::Ext adds a #find and a #luke_admin method to the connection object.
|
12
|
+
|
13
|
+
===#luke
|
14
|
+
The #luke method returns a Hash/Mash result of a /admin/luke?numTerms=0 request:
|
15
|
+
luke_response = solr.luke
|
16
|
+
luke_response['index']
|
17
|
+
luke_response['fields']
|
18
|
+
luke_response['info']
|
19
|
+
|
20
|
+
|
21
|
+
===#find
|
22
|
+
The #find method listens for certain keys. All other keys are ignored, allowing the ability to mix-and-match special keys with normal Solr param keys. The recognized keys are describe below.
|
23
|
+
|
24
|
+
|
25
|
+
:page - This maps to the Solr "start" param. The current "page" in the results set. RSolr::Ext handles the page-to-rows math for you.
|
26
|
+
|
27
|
+
|
28
|
+
:per_page - This maps to the Solr "rows" param. How many "pages" in the result.
|
29
|
+
|
30
|
+
|
31
|
+
:queries - This key maps to the Solr "q" param. Accepts a string, array or hash. When an array is used, each value is joined by a space. When a hash is used, the keys are used as Solr fields.
|
32
|
+
|
33
|
+
* :queries => 'normal' BECOMES ?q=normal
|
34
|
+
* :queries => ['one', 'two'] BECOMES ?q=one two
|
35
|
+
* :queries => {:title=>'one'} BECOMES ?q=title:(one)
|
36
|
+
* :queries => ['red', {:title=>'one'}] BECOMES ?q=red title:(one)
|
37
|
+
|
38
|
+
|
39
|
+
:phrases - This value is mapped to the Solr "q" param. When this key is used, the value will become double-quoted, creating a Solr "phrase" based query.
|
40
|
+
|
41
|
+
* :phrases => 'normal' BECOMES ?q="normal"
|
42
|
+
* :phrases => ['one', 'two'] BECOMES ?q="one" "two"
|
43
|
+
* :phrases => {:title=>'one'} BECOMES ?q=title:("one")
|
44
|
+
* :phrases => ['red', {:title=>'one'}] BECOMES ?q="red" title:("one")
|
45
|
+
|
46
|
+
|
47
|
+
:filters - The :filters key maps to the Solr :fq param. This has the same behavior as the :queries key, except it's for the :fq param.
|
48
|
+
|
49
|
+
* :filters => 'normal' BECOMES ?fq=normal
|
50
|
+
* :filters => ['one', 'two'] BECOMES ?fq=one two
|
51
|
+
* :filters => {:title=>'one'} BECOMES ?fq=title:(one)
|
52
|
+
* :filters => ['red', {:title=>'one'}] BECOMES ?fq=red title:(one)
|
53
|
+
|
54
|
+
|
55
|
+
:phrase_filters - The :phrase_filters key maps to the Solr :fq param. This has the same behavior as the :phrases key, except it's for the :fq param.
|
56
|
+
|
57
|
+
* :phrase_filters => 'normal' BECOMES ?fq="normal"
|
58
|
+
* :phrase_filters => ['one', 'two'] BECOMES ?fq="one" "two"
|
59
|
+
* :phrase_filters => {:title=>'one'} BECOMES ?fq=title:("one")
|
60
|
+
* :phrase_filters => ['red', {:title=>'one'}] BECOMES ?fq="red" title:("one")
|
61
|
+
|
62
|
+
|
63
|
+
:facets - The :facets does a few different things. First, it sets the Solr param facet=true. It accepts a hash with a single key called :fields. This should be an array of field names to facet on.
|
64
|
+
|
65
|
+
* :facets=>{:fields=>['cat', 'blah']} BECOMES ?facet=true&facet.field=cat&facet.field=blah
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
==Request Example
|
70
|
+
solr = RSolr::Ext.connect
|
71
|
+
solr_params = {
|
72
|
+
:page=>2,
|
73
|
+
:per_page=>10,
|
74
|
+
:phrases=>{:name=>'This is a phrase'},
|
75
|
+
:filters=>['test', {:price=>(1..10)}],
|
76
|
+
:phrase_filters=>{:manu=>['Apple']},
|
77
|
+
:queries=>'ipod',
|
78
|
+
:facets=>{:fields=>['cat', 'blah']},
|
79
|
+
:echoParams => 'EXPLICIT'
|
80
|
+
}
|
81
|
+
response = rsolr.find solr_params
|
82
|
+
|
83
|
+
==Responses
|
84
|
+
RSolr::Ext decorates the normal output hash from RSolr and adds some helpful methods.
|
85
|
+
|
86
|
+
solr = RSolr::Ext.connect
|
87
|
+
|
88
|
+
response = solr.find :q=>'*:*'
|
89
|
+
|
90
|
+
response.ok?
|
91
|
+
response.params
|
92
|
+
response.docs
|
93
|
+
response.docs.previous_page
|
94
|
+
response.docs.next_page
|
95
|
+
response.facets.each do |facet|
|
96
|
+
puts facet.name
|
97
|
+
facet.items.each do |item|
|
98
|
+
puts "#{facet.name}::#{item.value} (#{item.hits})"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
You can access values in the response hash using symbols or strings.
|
103
|
+
|
104
|
+
===Documents/Pagination
|
105
|
+
If you wanna paginate, just throw the collection into the WillPaginate view helper.
|
106
|
+
<%= will_paginate response.docs %>
|
107
|
+
|
108
|
+
==The "Model" Module
|
109
|
+
You can create your own <read-only> "models" using RSolr::Ext::Model
|
110
|
+
|
111
|
+
class Book
|
112
|
+
include RSolr::Ext::Model
|
113
|
+
def self.find_by_author(author)
|
114
|
+
find(:fq=>'object_type:"book"', :rows=>10, :phrase_filters=>{:author=>author})
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
all_books = Book.find('*:*')
|
119
|
+
hawk_books = Book.find_by_author('hawk')
|
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,41 @@
|
|
1
|
+
module RSolr::Ext::Connection
|
2
|
+
|
3
|
+
# TWO modes of arguments:
|
4
|
+
#
|
5
|
+
# <request-handler-path>, <solr-params-hash>
|
6
|
+
# OR
|
7
|
+
# <solr-params-hash>
|
8
|
+
#
|
9
|
+
# The default request-handler-path is /select
|
10
|
+
#
|
11
|
+
# If a hash is used for solr params, all of the normal RSolr::Ext::Request
|
12
|
+
# mappings are available (everything else gets passed to solr).
|
13
|
+
# Returns a new RSolr::Ext::Response::Base object.
|
14
|
+
def find *args
|
15
|
+
# remove the handler arg - the first, if it is a string OR set default
|
16
|
+
path = args.first.is_a?(String) ? args.shift : '/select'
|
17
|
+
# remove the params - the first, if it is a Hash OR set default
|
18
|
+
params = args.first.kind_of?(Hash) ? args.shift : {}
|
19
|
+
# send path, map params and send the rest of the args along
|
20
|
+
response = self.request path, RSolr::Ext::Request.map(params), *args
|
21
|
+
RSolr::Ext::Response::Base.new(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
# TWO modes of arguments:
|
25
|
+
#
|
26
|
+
# <request-handler-path>, <solr-params-hash>
|
27
|
+
# OR
|
28
|
+
# <solr-params-hash>
|
29
|
+
#
|
30
|
+
# The default request-handler-path is /admin/luke
|
31
|
+
# The default params are numTerms=0
|
32
|
+
#
|
33
|
+
# Returns a new Mash object.
|
34
|
+
def luke *args
|
35
|
+
path = args.first.is_a?(String) ? args.shift : '/admin/luke'
|
36
|
+
params = args.pop || {}
|
37
|
+
params['numTerms'] ||= 0
|
38
|
+
self.request(path, params).to_mash
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RSolr::Ext::Doc
|
2
|
+
|
3
|
+
# for easy access to the solr id (route helpers etc..)
|
4
|
+
def id
|
5
|
+
self['id']
|
6
|
+
end
|
7
|
+
|
8
|
+
# Helper method to check if value/multi-values exist for a given key.
|
9
|
+
# The value can be a string, or a RegExp
|
10
|
+
# Multiple "values" can be given; only one needs to match.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# doc.has?(:location_facet)
|
14
|
+
# doc.has?(:location_facet, 'Clemons')
|
15
|
+
# doc.has?(:id, 'h009', /^u/i)
|
16
|
+
def has?(k, *values)
|
17
|
+
return if self[k].nil?
|
18
|
+
return true if self.key?(k) and values.empty?
|
19
|
+
target = self[k]
|
20
|
+
if target.is_a?(Array)
|
21
|
+
values.each do |val|
|
22
|
+
return target.any?{|tv| val.is_a?(Regexp) ? (tv =~ val) : (tv==val)}
|
23
|
+
end
|
24
|
+
else
|
25
|
+
return values.any? {|val| val.is_a?(Regexp) ? (target =~ val) : (target == val)}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# helper
|
30
|
+
# key is the name of the field
|
31
|
+
# opts is a hash with the following valid keys:
|
32
|
+
# - :sep - a string used for joining multivalued field values
|
33
|
+
# - :default - a value to return when the key doesn't exist
|
34
|
+
# if :sep is nil and the field is a multivalued field, the array is returned
|
35
|
+
def get key, opts={:sep=>', ', :default=>nil}
|
36
|
+
if self.key? key
|
37
|
+
val = self[key]
|
38
|
+
(val.is_a?(Array) and opts[:sep]) ? val.join(opts[:sep]) : val
|
39
|
+
else
|
40
|
+
opts[:default]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# include this module into a plain ruby class:
|
2
|
+
# class Book
|
3
|
+
# include RSolr::Ext::Model
|
4
|
+
# connection = RSolr::Ext.connect
|
5
|
+
# default_params = {:phrase_filters=>'type:book'}
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# Then:
|
9
|
+
# number_10 = Book.find_by_id(10)
|
10
|
+
#
|
11
|
+
module RSolr::Ext::Model
|
12
|
+
|
13
|
+
# Class level methods for altering object instances
|
14
|
+
module Callbacks
|
15
|
+
|
16
|
+
# method that only accepts a block
|
17
|
+
# The block is executed when an object is created via #new -> SolrDoc.new
|
18
|
+
# The blocks scope is the instance of the object.
|
19
|
+
def after_initialize(&blk)
|
20
|
+
hooks << blk
|
21
|
+
end
|
22
|
+
|
23
|
+
# Removes the current set of after_initialize blocks.
|
24
|
+
# You would use this if you wanted to open a class back up,
|
25
|
+
# but clear out the previously defined blocks.
|
26
|
+
def clear_after_initialize_blocks!
|
27
|
+
@hooks = []
|
28
|
+
end
|
29
|
+
|
30
|
+
# creates the @hooks container ("hooks" are blocks or procs).
|
31
|
+
# returns an array
|
32
|
+
def hooks
|
33
|
+
@hooks ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Findable is a module that gets mixed into the SolrDocument class object.
|
40
|
+
# These methods will be available through the class like: SolrDocument.find and SolrDocument.find_by_id
|
41
|
+
#
|
42
|
+
module Findable
|
43
|
+
|
44
|
+
attr_accessor :connection, :default_params
|
45
|
+
|
46
|
+
def connection
|
47
|
+
@connection ||= RSolr::Ext.connect
|
48
|
+
end
|
49
|
+
|
50
|
+
# this method decorates the connection find method
|
51
|
+
# and then creates new instance of the class that uses this module.
|
52
|
+
def find(*args)
|
53
|
+
decorate_response_docs connection.find(*args)
|
54
|
+
end
|
55
|
+
|
56
|
+
# this method decorates the connection find_by_id method
|
57
|
+
# and then creates new instance of the class that uses this module.
|
58
|
+
def find_by_id(id, solr_params={}, opts={})
|
59
|
+
decorate_response_docs connection.find_by_id(id, solr_params, opts)
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def decorate_response_docs response
|
65
|
+
response['response']['docs'].map!{|d| self.new d }
|
66
|
+
response
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# Called by Ruby Module API
|
72
|
+
# extends this *class* object
|
73
|
+
def self.included(base)
|
74
|
+
base.extend Callbacks
|
75
|
+
base.extend Findable
|
76
|
+
base.send :include, RSolr::Ext::Doc
|
77
|
+
end
|
78
|
+
|
79
|
+
# The original object passed in to the #new method
|
80
|
+
attr :_source
|
81
|
+
|
82
|
+
# Constructor **for the class that is getting this module included**
|
83
|
+
# source_doc should be a hash or something similar
|
84
|
+
# calls each of after_initialize blocks
|
85
|
+
def initialize(source_doc={})
|
86
|
+
@_source = source_doc.to_mash
|
87
|
+
self.class.hooks.each do |h|
|
88
|
+
instance_eval &h
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# the wrapper method to the @_source object.
|
93
|
+
# If a method is missing, it gets sent to @_source
|
94
|
+
# with all of the original params and block
|
95
|
+
def method_missing(m, *args, &b)
|
96
|
+
@_source.send(m, *args, &b)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module RSolr::Ext::Request
|
2
|
+
|
3
|
+
module Params
|
4
|
+
|
5
|
+
def map input
|
6
|
+
output = {}
|
7
|
+
if input[:per_page]
|
8
|
+
output[:rows] = input.delete :per_page
|
9
|
+
end
|
10
|
+
|
11
|
+
if page = input.delete(:page)
|
12
|
+
raise ':per_page must be set when using :page' unless output[:rows]
|
13
|
+
page = page.to_s.to_i-1
|
14
|
+
page = page < 1 ? 0 : page
|
15
|
+
output[:start] = page * output[:rows]
|
16
|
+
end
|
17
|
+
|
18
|
+
if queries = input.delete(:queries)
|
19
|
+
output[:q] = append_to_param output[:q], build_query(queries, false)
|
20
|
+
end
|
21
|
+
if phrases = input.delete(:phrases)
|
22
|
+
output[:q] = append_to_param output[:q], build_query(phrases, true)
|
23
|
+
end
|
24
|
+
if filters = input.delete(:filters)
|
25
|
+
output[:fq] = append_to_param output[:fq], build_query(filters), false
|
26
|
+
end
|
27
|
+
if phrase_filters = input.delete(:phrase_filters)
|
28
|
+
output[:fq] = append_to_param output[:fq], build_query(phrase_filters, true), false
|
29
|
+
end
|
30
|
+
if facets = input.delete(:facets)
|
31
|
+
output[:facet] = true
|
32
|
+
output['facet.field'] = append_to_param output['facet.field'], build_query(facets.values), false
|
33
|
+
end
|
34
|
+
output.merge input
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
module QueryHelpers
|
40
|
+
|
41
|
+
# Wraps a string around double quotes
|
42
|
+
def quote(value)
|
43
|
+
%("#{value}")
|
44
|
+
end
|
45
|
+
|
46
|
+
# builds a solr range query from a Range object
|
47
|
+
def build_range(r)
|
48
|
+
"[#{r.min} TO #{r.max}]"
|
49
|
+
end
|
50
|
+
|
51
|
+
# builds a solr query fragment
|
52
|
+
# if "quote_string" is true, the values will be quoted.
|
53
|
+
# if "value" is a string/symbol, the #to_s method is called
|
54
|
+
# if the "value" is an array, each item in the array is
|
55
|
+
# send to build_query (recursive)
|
56
|
+
# if the "value" is a Hash, a fielded query is built
|
57
|
+
# where the keys are used as the field names and
|
58
|
+
# the values are either processed as a Range or
|
59
|
+
# passed back into build_query (recursive)
|
60
|
+
def build_query(value, quote_string=false)
|
61
|
+
case value
|
62
|
+
when String,Symbol
|
63
|
+
quote_string ? quote(value.to_s) : value.to_s
|
64
|
+
when Array
|
65
|
+
value.collect do |v|
|
66
|
+
build_query(v, quote_string)
|
67
|
+
end.flatten
|
68
|
+
when Hash
|
69
|
+
return value.collect do |(k,v)|
|
70
|
+
if v.is_a?(Range)
|
71
|
+
"#{k}:#{build_range(v)}"
|
72
|
+
# If the value is an array, we want the same param, multiple times (not a query join)
|
73
|
+
elsif v.is_a?(Array)
|
74
|
+
v.collect do |vv|
|
75
|
+
"#{k}:#{build_query(vv, quote_string)}"
|
76
|
+
end
|
77
|
+
else
|
78
|
+
"#{k}:#{build_query(v, quote_string)}"
|
79
|
+
end
|
80
|
+
end.flatten
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# creates an array where the "existing_value" param is first
|
85
|
+
# and the "new_value" is the last.
|
86
|
+
# All empty/nil items are removed.
|
87
|
+
# the return result is either the result of the
|
88
|
+
# array being joined on a space, or the array itself.
|
89
|
+
# "auto_join" should be true or false.
|
90
|
+
def append_to_param(existing_value, new_value, auto_join=true)
|
91
|
+
values = [existing_value, new_value]
|
92
|
+
values.delete_if{|v|v.nil?}
|
93
|
+
auto_join ? values.join(' ') : values.flatten
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
extend QueryHelpers
|
99
|
+
extend Params
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module RSolr::Ext::Response::Docs
|
2
|
+
|
3
|
+
module Pageable
|
4
|
+
|
5
|
+
attr_accessor :start, :per_page, :total
|
6
|
+
|
7
|
+
# Returns the current page calculated from 'rows' and 'start'
|
8
|
+
# WillPaginate hook
|
9
|
+
def current_page
|
10
|
+
return 1 if start < 1
|
11
|
+
per_page_normalized = per_page < 1 ? 1 : per_page
|
12
|
+
@current_page ||= (start / per_page_normalized).ceil + 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# Calcuates the total pages from 'numFound' and 'rows'
|
16
|
+
# WillPaginate hook
|
17
|
+
def total_pages
|
18
|
+
@total_pages ||= per_page > 0 ? (total / per_page.to_f).ceil : 1
|
19
|
+
end
|
20
|
+
|
21
|
+
# returns the previous page number or 1
|
22
|
+
# WillPaginate hook
|
23
|
+
def previous_page
|
24
|
+
@previous_page ||= (current_page > 1) ? current_page - 1 : 1
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns the next page number or the last
|
28
|
+
# WillPaginate hook
|
29
|
+
def next_page
|
30
|
+
@next_page ||= (current_page == total_pages) ? total_pages : current_page+1
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_next?
|
34
|
+
current_page < total_pages
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_previous?
|
38
|
+
current_page > 1
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.extended(base)
|
44
|
+
d = base['response']['docs']
|
45
|
+
d.each{|doc| doc.extend RSolr::Ext::Doc }
|
46
|
+
d.extend Pageable
|
47
|
+
d.per_page = base['responseHeader']['params']['rows'].to_s.to_i
|
48
|
+
d.start = base['response']['start'].to_s.to_i
|
49
|
+
d.total = base['response']['numFound'].to_s.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def docs
|
53
|
+
response['docs']
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module RSolr::Ext::Response::Facets
|
2
|
+
|
3
|
+
# represents a facet value; which is a field value and its hit count
|
4
|
+
FacetItem = Struct.new :value,:hits
|
5
|
+
|
6
|
+
# represents a facet; which is a field and its values
|
7
|
+
FacetField = Struct.new :name, :items do
|
8
|
+
def items; @items ||= [] end
|
9
|
+
end
|
10
|
+
|
11
|
+
# @response.facets.each do |facet|
|
12
|
+
# facet.field
|
13
|
+
# end
|
14
|
+
# "caches" the result in the @facets instance var
|
15
|
+
def facets
|
16
|
+
# memoize!
|
17
|
+
@facets ||= (
|
18
|
+
all = facet_fields.collect do |(facet_field_name,values_and_hits_list)|
|
19
|
+
facet = FacetField.new(facet_field_name)
|
20
|
+
# the values_and_hits_list is an array where a value is immediately followed by it's hit count
|
21
|
+
# so we shift off an item (the value)
|
22
|
+
while value = values_and_hits_list.shift
|
23
|
+
# and then shift off the next to get the hit value
|
24
|
+
facet.items << FacetItem.new(value, values_and_hits_list.shift)
|
25
|
+
# repeat until there are no more pairs in the values_and_hits_list array
|
26
|
+
end
|
27
|
+
facet
|
28
|
+
end
|
29
|
+
#all.extend RSolr::Ext::Response::Docs::Pageable
|
30
|
+
#all.start = header['params']['facet.offset'].to_s.to_i
|
31
|
+
#all.per_page = header['params']['facet.limit'].to_s.to_i - 1
|
32
|
+
#all.total = -1
|
33
|
+
## override the has_next? method -- when paging through facets,
|
34
|
+
## it's not possible to know how many "pages" there are
|
35
|
+
#all.instance_eval "def has_next?; #{all.size == all.per_page+1} end"
|
36
|
+
all
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# pass in a facet field name and get back a Facet instance
|
41
|
+
def facet_by_field_name(name)
|
42
|
+
@facets_by_field_name ||= {}
|
43
|
+
@facets_by_field_name[name] ||= (
|
44
|
+
facets.detect{|facet|facet.name.to_s == name.to_s}
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def facet_counts
|
49
|
+
@facet_counts ||= self['facet_counts'] || {}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the hash of all the facet_fields (ie: {'instock_b' => ['true', 123, 'false', 20]}
|
53
|
+
def facet_fields
|
54
|
+
@facet_fields ||= facet_counts['facet_fields'] || {}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns all of the facet queries
|
58
|
+
def facet_queries
|
59
|
+
@facet_queries ||= facet_counts['facet_queries'] || {}
|
60
|
+
end
|
61
|
+
|
62
|
+
end # end Facets
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# A mixin for making access to the spellcheck component data easy.
|
2
|
+
#
|
3
|
+
# response.spelling.words
|
4
|
+
#
|
5
|
+
module RSolr::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
|
+
i_stop = suggestions.index("correctlySpelled")
|
43
|
+
# step through array in 2s to get info for each term
|
44
|
+
0.step(i_stop-1, 2) do |i|
|
45
|
+
term = suggestions[i]
|
46
|
+
term_info = suggestions[i+1]
|
47
|
+
# term_info is a hash:
|
48
|
+
# numFound =>
|
49
|
+
# startOffset =>
|
50
|
+
# endOffset =>
|
51
|
+
# origFreq =>
|
52
|
+
# suggestion => { frequency =>, word => }
|
53
|
+
origFreq = term_info['origFreq']
|
54
|
+
suggFreq = term_info['suggestion']['frequency']
|
55
|
+
word_suggestions << term_info['suggestion']['word'] if suggFreq > origFreq
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
word_suggestions.uniq
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module RSolr::Ext::Response
|
2
|
+
|
3
|
+
autoload :Docs, 'rsolr-ext/response/docs'
|
4
|
+
autoload :Facets, 'rsolr-ext/response/facets'
|
5
|
+
autoload :Spelling, 'rsolr-ext/response/spelling'
|
6
|
+
|
7
|
+
class Base < Mash
|
8
|
+
|
9
|
+
attr :original_hash
|
10
|
+
|
11
|
+
def initialize hash
|
12
|
+
super hash
|
13
|
+
@original_hash = hash
|
14
|
+
extend Response# if self['response']
|
15
|
+
extend Docs# if self['response'] and self['response']['docs']
|
16
|
+
extend Facets# if self['facet_counts']
|
17
|
+
extend Spelling# if self['spellcheck']
|
18
|
+
end
|
19
|
+
|
20
|
+
def header
|
21
|
+
self['responseHeader']
|
22
|
+
end
|
23
|
+
|
24
|
+
def params
|
25
|
+
header['params']
|
26
|
+
end
|
27
|
+
|
28
|
+
def ok?
|
29
|
+
header['status'] == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing *args, &blk
|
33
|
+
self.original_hash.send *args, &blk
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
module Response
|
39
|
+
|
40
|
+
def response
|
41
|
+
self[:response]
|
42
|
+
end
|
43
|
+
|
44
|
+
# short cut to response['numFound']
|
45
|
+
def total
|
46
|
+
response[:numFound]
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/lib/rsolr-ext.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# add this directory to the load path if it hasn't already been added
|
2
|
+
|
3
|
+
lambda { |base|
|
4
|
+
$: << base unless $:.include?(base) || $:.include?(File.expand_path(base))
|
5
|
+
}.call(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'mash' unless defined?(Mash)
|
8
|
+
|
9
|
+
unless Hash.respond_to?(:to_mash)
|
10
|
+
class Hash
|
11
|
+
def to_mash
|
12
|
+
Mash.new(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'rubygems'
|
18
|
+
require 'rsolr'
|
19
|
+
|
20
|
+
module RSolr
|
21
|
+
|
22
|
+
module Ext
|
23
|
+
|
24
|
+
VERSION = '0.9.5'
|
25
|
+
|
26
|
+
autoload :Connection, 'rsolr-ext/connection.rb'
|
27
|
+
autoload :Doc, 'rsolr-ext/doc.rb'
|
28
|
+
autoload :Request, 'rsolr-ext/request.rb'
|
29
|
+
autoload :Response, 'rsolr-ext/response.rb'
|
30
|
+
autoload :Model, 'rsolr-ext/model.rb'
|
31
|
+
|
32
|
+
# c = RSolr::Ext.connect
|
33
|
+
# c.find(:q=>'*:*').docs.size
|
34
|
+
def self.connect(*args)
|
35
|
+
connection = RSolr.connect(*args)
|
36
|
+
connection.extend RSolr::Ext::Connection
|
37
|
+
connection
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/rsolr-ext.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "rsolr-ext"
|
3
|
+
s.version = "0.9.6.3"
|
4
|
+
s.date = "2009-09-30"
|
5
|
+
|
6
|
+
s.summary = "An extension lib for RSolr"
|
7
|
+
s.email = "goodieboy@gmail.com"
|
8
|
+
s.homepage = "http://github.com/mwmitchell/rsolr-ext"
|
9
|
+
s.description = "An extension lib for RSolr"
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.authors = ["Matt Mitchell"]
|
12
|
+
s.files = [
|
13
|
+
"lib/mash.rb",
|
14
|
+
"lib/rsolr-ext/connection.rb",
|
15
|
+
"lib/rsolr-ext/doc.rb",
|
16
|
+
"lib/rsolr-ext/model.rb",
|
17
|
+
"lib/rsolr-ext/request.rb",
|
18
|
+
"lib/rsolr-ext/response/docs.rb",
|
19
|
+
"lib/rsolr-ext/response/facets.rb",
|
20
|
+
"lib/rsolr-ext/response/spelling.rb",
|
21
|
+
"lib/rsolr-ext/response.rb",
|
22
|
+
"lib/rsolr-ext.rb",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"rsolr-ext.gemspec"
|
26
|
+
]
|
27
|
+
s.test_files = [
|
28
|
+
'test/connection_test.rb',
|
29
|
+
'test/request_test.rb',
|
30
|
+
'test/response_test.rb',
|
31
|
+
'test/test_unit_test_case.rb',
|
32
|
+
'test/helper.rb'
|
33
|
+
]
|
34
|
+
|
35
|
+
s.extra_rdoc_files = %w(LICENSE README.rdoc)
|
36
|
+
|
37
|
+
s.add_dependency("mwmitchell-rsolr", [">= 0.9.6"])
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_unit_test_case'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'rsolr-ext')
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class RSolrExtConnectionTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
test 'the #connect method' do
|
8
|
+
connection = RSolr::Ext.connect
|
9
|
+
assert connection.respond_to?(:find)
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'the #find method' do
|
13
|
+
connection = RSolr::Ext.connect
|
14
|
+
response = connection.find :q=>'*:*'
|
15
|
+
assert response.kind_of?(Mash)
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'the #find method with a custom request handler' do
|
19
|
+
connection = RSolr::Ext.connect
|
20
|
+
response = connection.find '/select', :q=>'*:*'
|
21
|
+
assert response.adapter_response[:path]=~/\/select/
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'the response' do
|
25
|
+
connection = RSolr::Ext.connect
|
26
|
+
response = connection.find :q=>'*:*'
|
27
|
+
assert response.respond_to?(:ok?)
|
28
|
+
assert response.ok?
|
29
|
+
assert_equal response.docs[0][:id], response.docs[0].id
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'the #luke method' do
|
33
|
+
info = RSolr::Ext.connect.luke
|
34
|
+
assert info.kind_of?(Mash)
|
35
|
+
assert info.key?('fields')
|
36
|
+
assert info.key?('index')
|
37
|
+
assert info.key?('info')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
def mock_query_response
|
2
|
+
%({'responseHeader'=>{'status'=>0,'QTime'=>5,'params'=>{'facet.limit'=>'10','wt'=>'ruby','rows'=>'11','facet'=>'true','facet.field'=>['manu','cat'],'echoParams'=>'EXPLICIT','q'=>'*:*','facet.sort'=>'true'}},'response'=>{'numFound'=>26,'start'=>0,'docs'=>[{'id'=>'SP2514N','inStock'=>true,'manu'=>'Samsung Electronics Co. Ltd.','name'=>'Samsung SpinPoint P120 SP2514N - hard drive - 250 GB - ATA-133','popularity'=>6,'price'=>92.0,'sku'=>'SP2514N','timestamp'=>'2009-03-20T14:42:49.795Z','cat'=>['electronics','hard drive'],'spell'=>['Samsung SpinPoint P120 SP2514N - hard drive - 250 GB - ATA-133'],'features'=>['7200RPM, 8MB cache, IDE Ultra ATA-133','NoiseGuard, SilentSeek technology, Fluid Dynamic Bearing (FDB) motor']},{'id'=>'6H500F0','inStock'=>true,'manu'=>'Maxtor Corp.','name'=>'Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300','popularity'=>6,'price'=>350.0,'sku'=>'6H500F0','timestamp'=>'2009-03-20T14:42:49.877Z','cat'=>['electronics','hard drive'],'spell'=>['Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300'],'features'=>['SATA 3.0Gb/s, NCQ','8.5ms seek','16MB cache']},{'id'=>'F8V7067-APL-KIT','inStock'=>false,'manu'=>'Belkin','name'=>'Belkin Mobile Power Cord for iPod w/ Dock','popularity'=>1,'price'=>19.95,'sku'=>'F8V7067-APL-KIT','timestamp'=>'2009-03-20T14:42:49.937Z','weight'=>4.0,'cat'=>['electronics','connector'],'spell'=>['Belkin Mobile Power Cord for iPod w/ Dock'],'features'=>['car power adapter, white']},{'id'=>'IW-02','inStock'=>false,'manu'=>'Belkin','name'=>'iPod & iPod Mini USB 2.0 Cable','popularity'=>1,'price'=>11.5,'sku'=>'IW-02','timestamp'=>'2009-03-20T14:42:49.944Z','weight'=>2.0,'cat'=>['electronics','connector'],'spell'=>['iPod & iPod Mini USB 2.0 Cable'],'features'=>['car power adapter for iPod, white']},{'id'=>'MA147LL/A','inStock'=>true,'includes'=>'earbud headphones, USB cable','manu'=>'Apple Computer Inc.','name'=>'Apple 60 GB iPod with Video Playback Black','popularity'=>10,'price'=>399.0,'sku'=>'MA147LL/A','timestamp'=>'2009-03-20T14:42:49.962Z','weight'=>5.5,'cat'=>['electronics','music'],'spell'=>['Apple 60 GB iPod with Video Playback Black'],'features'=>['iTunes, Podcasts, Audiobooks','Stores up to 15,000 songs, 25,000 photos, or 150 hours of video','2.5-inch, 320x240 color TFT LCD display with LED backlight','Up to 20 hours of battery life','Plays AAC, MP3, WAV, AIFF, Audible, Apple Lossless, H.264 video','Notes, Calendar, Phone book, Hold button, Date display, Photo wallet, Built-in games, JPEG photo playback, Upgradeable firmware, USB 2.0 compatibility, Playback speed control, Rechargeable capability, Battery level indication']},{'id'=>'TWINX2048-3200PRO','inStock'=>true,'manu'=>'Corsair Microsystems Inc.','name'=>'CORSAIR XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory - Retail','popularity'=>5,'price'=>185.0,'sku'=>'TWINX2048-3200PRO','timestamp'=>'2009-03-20T14:42:49.99Z','cat'=>['electronics','memory'],'spell'=>['CORSAIR XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory - Retail'],'features'=>['CAS latency 2, 2-3-3-6 timing, 2.75v, unbuffered, heat-spreader']},{'id'=>'VS1GB400C3','inStock'=>true,'manu'=>'Corsair Microsystems Inc.','name'=>'CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - Retail','popularity'=>7,'price'=>74.99,'sku'=>'VS1GB400C3','timestamp'=>'2009-03-20T14:42:50Z','cat'=>['electronics','memory'],'spell'=>['CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - Retail']},{'id'=>'VDBDB1A16','inStock'=>true,'manu'=>'A-DATA Technology Inc.','name'=>'A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM','popularity'=>5,'sku'=>'VDBDB1A16','timestamp'=>'2009-03-20T14:42:50.004Z','cat'=>['electronics','memory'],'spell'=>['A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM'],'features'=>['CAS latency 3, 2.7v']},{'id'=>'3007WFP','inStock'=>true,'includes'=>'USB cable','manu'=>'Dell, Inc.','name'=>'Dell Widescreen UltraSharp 3007WFP','popularity'=>6,'price'=>2199.0,'sku'=>'3007WFP','timestamp'=>'2009-03-20T14:42:50.017Z','weight'=>401.6,'cat'=>['electronics','monitor'],'spell'=>['Dell Widescreen UltraSharp 3007WFP'],'features'=>['30" TFT active matrix LCD, 2560 x 1600, .25mm dot pitch, 700:1 contrast']},{'id'=>'VA902B','inStock'=>true,'manu'=>'ViewSonic Corp.','name'=>'ViewSonic VA902B - flat panel display - TFT - 19"','popularity'=>6,'price'=>279.95,'sku'=>'VA902B','timestamp'=>'2009-03-20T14:42:50.034Z','weight'=>190.4,'cat'=>['electronics','monitor'],'spell'=>['ViewSonic VA902B - flat panel display - TFT - 19"'],'features'=>['19" TFT active matrix LCD, 8ms response time, 1280 x 1024 native resolution']},{'id'=>'0579B002','inStock'=>true,'manu'=>'Canon Inc.','name'=>'Canon PIXMA MP500 All-In-One Photo Printer','popularity'=>6,'price'=>179.99,'sku'=>'0579B002','timestamp'=>'2009-03-20T14:42:50.062Z','weight'=>352.0,'cat'=>['electronics','multifunction printer','printer','scanner','copier'],'spell'=>['Canon PIXMA MP500 All-In-One Photo Printer'],'features'=>['Multifunction ink-jet color photo printer','Flatbed scanner, optical scan resolution of 1,200 x 2,400 dpi','2.5" color LCD preview screen','Duplex Copying','Printing speed up to 29ppm black, 19ppm color','Hi-Speed USB','memory card: CompactFlash, Micro Drive, SmartMedia, Memory Stick, Memory Stick Pro, SD Card, and MultiMediaCard']}]},'facet_counts'=>{'facet_queries'=>{},'facet_fields'=>{'manu'=>['inc',8,'apach',2,'belkin',2,'canon',2,'comput',2,'corp',2,'corsair',2,'foundat',2,'microsystem',2,'softwar',2],'cat'=>['electronics',14,'memory',3,'card',2,'connector',2,'drive',2,'graphics',2,'hard',2,'monitor',2,'search',2,'software',2]},'facet_dates'=>{}}})
|
3
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_unit_test_case'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'rsolr-ext')
|
3
|
+
|
4
|
+
class RSolrExtRequestTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
test 'standard request' do
|
7
|
+
solr_params = RSolr::Ext::Request.map(
|
8
|
+
:page=>2,
|
9
|
+
:per_page=>10,
|
10
|
+
:phrases=>{:name=>'This is a phrase'},
|
11
|
+
:filters=>['test', {:price=>(1..10)}],
|
12
|
+
:phrase_filters=>{:manu=>['Apple']},
|
13
|
+
:queries=>'ipod',
|
14
|
+
:facets=>{:fields=>['cat', 'blah']},
|
15
|
+
:spellcheck => true
|
16
|
+
)
|
17
|
+
assert_equal ["test", "price:[1 TO 10]", "manu:\"Apple\""], solr_params[:fq]
|
18
|
+
assert_equal 10, solr_params[:start]
|
19
|
+
assert_equal 10, solr_params[:rows]
|
20
|
+
assert_equal "ipod name:\"This is a phrase\"", solr_params[:q]
|
21
|
+
assert_equal ['cat', 'blah'], solr_params['facet.field']
|
22
|
+
assert_equal true, solr_params[:facet]
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'fq param using the phrase_filters mapping' do
|
26
|
+
solr_params = RSolr::Ext::Request.map(
|
27
|
+
:phrase_filters=>{:manu=>['Apple', 'ASG'], :color=>['red', 'blue']}
|
28
|
+
)
|
29
|
+
expected = {:fq=>["color:\"red\"", "color:\"blue\"", "manu:\"Apple\"", "manu:\"ASG\""]}
|
30
|
+
assert expected, solr_params
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'test_unit_test_case'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'rsolr-ext')
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class RSolrExtResponseTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
test 'base response class' do
|
8
|
+
raw_response = eval(mock_query_response)
|
9
|
+
r = RSolr::Ext::Response::Base.new(raw_response)
|
10
|
+
assert r.respond_to?(:header)
|
11
|
+
assert r.ok?
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'standard response class' do
|
15
|
+
raw_response = eval(mock_query_response)
|
16
|
+
r = RSolr::Ext::Response::Base.new(raw_response)
|
17
|
+
assert r.respond_to?(:response)
|
18
|
+
assert r.ok?
|
19
|
+
assert_equal 11, r.docs.size
|
20
|
+
assert_equal 'EXPLICIT', r.params[:echoParams]
|
21
|
+
assert_equal 1, r.docs.previous_page
|
22
|
+
assert_equal 2, r.docs.next_page
|
23
|
+
#
|
24
|
+
assert r.kind_of?(RSolr::Ext::Response::Docs)
|
25
|
+
assert r.kind_of?(RSolr::Ext::Response::Facets)
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'standard response doc ext methods' do
|
29
|
+
raw_response = eval(mock_query_response)
|
30
|
+
r = RSolr::Ext::Response::Base.new(raw_response)
|
31
|
+
doc = r.docs.first
|
32
|
+
assert doc.has?(:cat, /^elec/)
|
33
|
+
assert ! doc.has?(:cat, 'elec')
|
34
|
+
assert doc.has?(:cat, 'electronics')
|
35
|
+
|
36
|
+
assert 'electronics', doc.get(:cat)
|
37
|
+
assert_nil doc.get(:xyz)
|
38
|
+
assert_equal 'def', doc.get(:xyz, :default=>'def')
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'Response::Standard facets' do
|
42
|
+
raw_response = eval(mock_query_response)
|
43
|
+
r = RSolr::Ext::Response::Base.new(raw_response)
|
44
|
+
assert_equal 2, r.facets.size
|
45
|
+
|
46
|
+
field_names = r.facets.collect{|facet|facet.name}
|
47
|
+
assert field_names.include?('cat')
|
48
|
+
assert field_names.include?('manu')
|
49
|
+
|
50
|
+
first_facet = r.facets.first
|
51
|
+
assert_equal 'cat', first_facet.name
|
52
|
+
assert_equal 10, first_facet.items.size
|
53
|
+
|
54
|
+
expected = first_facet.items.collect do |item|
|
55
|
+
item.value + ' - ' + item.hits.to_s
|
56
|
+
end.join(', ')
|
57
|
+
assert_equal "electronics - 14, memory - 3, card - 2, connector - 2, drive - 2, graphics - 2, hard - 2, monitor - 2, search - 2, software - 2", expected
|
58
|
+
|
59
|
+
r.facets.each do |facet|
|
60
|
+
assert facet.respond_to?(:name)
|
61
|
+
facet.items.each do |item|
|
62
|
+
assert item.respond_to?(:value)
|
63
|
+
assert item.respond_to?(:hits)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
test 'response::standard facet_by_field_name' do
|
70
|
+
raw_response = eval(mock_query_response)
|
71
|
+
r = RSolr::Ext::Response::Base.new(raw_response)
|
72
|
+
facet = r.facet_by_field_name('cat')
|
73
|
+
assert_equal 'cat', facet.name
|
74
|
+
end
|
75
|
+
|
76
|
+
=begin
|
77
|
+
|
78
|
+
# pagination for facets has been commented out in the response/facets module.
|
79
|
+
# ...need to think more about how this can be handled
|
80
|
+
|
81
|
+
test 'response::standard facets.paginate' do
|
82
|
+
raw_response = eval(mock_query_response)
|
83
|
+
raw_response['responseHeader']['params']['facet.offset'] = 1
|
84
|
+
raw_response['responseHeader']['params']['facet.limit'] = 2
|
85
|
+
|
86
|
+
r = RSolr::Ext::Response::Standard.new(raw_response)
|
87
|
+
|
88
|
+
assert_equal 2, r.facets.current_page
|
89
|
+
|
90
|
+
# always 1 less than facet.limit
|
91
|
+
assert_equal 1, r.facets.per_page
|
92
|
+
|
93
|
+
assert_equal 3, r.facets.next_page
|
94
|
+
|
95
|
+
assert_equal 1, r.facets.previous_page
|
96
|
+
|
97
|
+
# can't know how many pages there are with facets.... so we set it to -1
|
98
|
+
assert_equal -1, r.facets.total_pages
|
99
|
+
|
100
|
+
assert r.facets.has_next?
|
101
|
+
assert r.facets.has_previous?
|
102
|
+
end
|
103
|
+
=end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
class Test::Unit::TestCase
|
4
|
+
|
5
|
+
def self.test(name, &block)
|
6
|
+
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
|
7
|
+
defined = instance_method(test_name) rescue false
|
8
|
+
raise "#{test_name} is already defined in #{self}" if defined
|
9
|
+
if block_given?
|
10
|
+
define_method(test_name, &block)
|
11
|
+
else
|
12
|
+
define_method(test_name) do
|
13
|
+
flunk "No implementation provided for #{name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rsolr-ext
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.6.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Mitchell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-30 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mwmitchell-rsolr
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.9.6
|
24
|
+
version:
|
25
|
+
description: An extension lib for RSolr
|
26
|
+
email: goodieboy@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- lib/mash.rb
|
36
|
+
- lib/rsolr-ext/connection.rb
|
37
|
+
- lib/rsolr-ext/doc.rb
|
38
|
+
- lib/rsolr-ext/model.rb
|
39
|
+
- lib/rsolr-ext/request.rb
|
40
|
+
- lib/rsolr-ext/response/docs.rb
|
41
|
+
- lib/rsolr-ext/response/facets.rb
|
42
|
+
- lib/rsolr-ext/response/spelling.rb
|
43
|
+
- lib/rsolr-ext/response.rb
|
44
|
+
- lib/rsolr-ext.rb
|
45
|
+
- LICENSE
|
46
|
+
- README.rdoc
|
47
|
+
- rsolr-ext.gemspec
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/mwmitchell/rsolr-ext
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.4
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: An extension lib for RSolr
|
76
|
+
test_files:
|
77
|
+
- test/connection_test.rb
|
78
|
+
- test/request_test.rb
|
79
|
+
- test/response_test.rb
|
80
|
+
- test/test_unit_test_case.rb
|
81
|
+
- test/helper.rb
|