delsolr 0.0.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.
- data/History.txt +5 -0
- data/License.txt +20 -0
- data/Manifest.txt +11 -0
- data/README.txt +49 -0
- data/Rakefile +4 -0
- data/lib/delsolr.rb +97 -0
- data/lib/delsolr/configuration.rb +13 -0
- data/lib/delsolr/extensions.rb +35 -0
- data/lib/delsolr/query_builder.rb +201 -0
- data/lib/delsolr/response.rb +171 -0
- data/lib/delsolr/version.rb +9 -0
- data/test/test_client.rb +15 -0
- data/test/test_helper.rb +2 -0
- data/test/test_query_builder.rb +161 -0
- data/test/test_response.rb +131 -0
- metadata +87 -0
data/History.txt
ADDED
data/License.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 FIXME full name
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
= delsolr
|
2
|
+
|
3
|
+
http://delsolr.rubyforge.org
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
DelSolr is a light weight ruby wrapper for solr. It's intention is to expose the full power of solr queries
|
8
|
+
while keeping the interface as ruby-esque as possible.
|
9
|
+
|
10
|
+
== FEATURES/PROBLEMS:
|
11
|
+
|
12
|
+
* Only supports querying (GET), no indexing (POST) support yet
|
13
|
+
|
14
|
+
== SYNOPSIS:
|
15
|
+
|
16
|
+
See http://delsolr.rubyforge.org for more info
|
17
|
+
|
18
|
+
== REQUIREMENTS:
|
19
|
+
|
20
|
+
You need Solr installed somewhere so you can query it ;)
|
21
|
+
|
22
|
+
== INSTALL:
|
23
|
+
|
24
|
+
sudo gem install delsolr
|
25
|
+
|
26
|
+
== LICENSE:
|
27
|
+
|
28
|
+
(The MIT License)
|
29
|
+
|
30
|
+
Copyright (c) 2008 FIXME full name
|
31
|
+
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
33
|
+
a copy of this software and associated documentation files (the
|
34
|
+
'Software'), to deal in the Software without restriction, including
|
35
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
36
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
37
|
+
permit persons to whom the Software is furnished to do so, subject to
|
38
|
+
the following conditions:
|
39
|
+
|
40
|
+
The above copyright notice and this permission notice shall be
|
41
|
+
included in all copies or substantial portions of the Software.
|
42
|
+
|
43
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
44
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
45
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
46
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
47
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
48
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
49
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/lib/delsolr.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
#
|
2
|
+
# DelSolr
|
3
|
+
#
|
4
|
+
# ben@avvo.com 9.1.2008
|
5
|
+
#
|
6
|
+
# see README.txt
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'net/http'
|
10
|
+
|
11
|
+
require 'digest/md5'
|
12
|
+
|
13
|
+
require File.join(File.dirname(__FILE__), 'delsolr', 'version')
|
14
|
+
require File.join(File.dirname(__FILE__), 'delsolr', 'response')
|
15
|
+
require File.join(File.dirname(__FILE__), 'delsolr', 'configuration')
|
16
|
+
require File.join(File.dirname(__FILE__), 'delsolr', 'query_builder')
|
17
|
+
require File.join(File.dirname(__FILE__), 'delsolr', 'extensions')
|
18
|
+
|
19
|
+
module DelSolr
|
20
|
+
|
21
|
+
class Client
|
22
|
+
|
23
|
+
attr_reader :configuration, :connection, :logger
|
24
|
+
|
25
|
+
# options
|
26
|
+
# :server - the server you want to connect to
|
27
|
+
# :port - the port you want to connect to
|
28
|
+
# :cache - [optional] a cache instance (any object the supports get and set)
|
29
|
+
# :shortcuts - [options] a list of values in the doc fields that generate short cuts for (defaults to [:id, :unique_id, :score]).
|
30
|
+
# With the response you will then be able to do rsp.scores and have it return an array of scores.
|
31
|
+
def initialize(opts = {})
|
32
|
+
@configuration = DelSolr::Client::Configuration.new(opts[:server], opts[:port])
|
33
|
+
@cache = opts[:cache]
|
34
|
+
@shortcuts = opts[:shortcuts]
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
#
|
39
|
+
# query_name - type of query to perform (should match up w/ queries defined in solrconfig.xml)
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# Possible options
|
43
|
+
# :query - [required] unescape user input or solr query (can also be a hash {:field_name => value}).
|
44
|
+
# :filters - [optional] array, string, or hash of additional filters to apply
|
45
|
+
# :facet - [optional] array of hashes for all the facet params (ie: {:field => 'instock_b', :limit => 15, :mincount => 5})
|
46
|
+
# You can also specify facets using a query (ie: {:query => 'city_idm:seattle', :name => 'seattle'} or even
|
47
|
+
# {:query => {:city => 'seattle'}, :name => 'seattle'}) and then get counts for that facet by calling
|
48
|
+
# rsp.facet_query_count_by_name('seattle').
|
49
|
+
#
|
50
|
+
# :sorts - [optional] array or string of sorts
|
51
|
+
# :limit - [optional] number to return (defaults to 10)
|
52
|
+
# :offset - [optional] offset (defaults to 0)
|
53
|
+
# :enable_caching - [optional] switch to control whether or not to use the cache (for fetching or setting)
|
54
|
+
#
|
55
|
+
# Returns a DelSolr::Client::Response instance
|
56
|
+
def query(query_name, opts = {})
|
57
|
+
|
58
|
+
raise "query_name must be supplied" if query_name.blank?
|
59
|
+
|
60
|
+
enable_caching = opts.delete(:enable_caching) && !@cache.nil?
|
61
|
+
ttl = opts.delete(:ttl) || 1.hours
|
62
|
+
|
63
|
+
query_builder = DelSolr::Client::QueryBuilder.new(query_name, opts)
|
64
|
+
|
65
|
+
# it's important that the QueryBuilder returns strings in a deterministic fashion
|
66
|
+
# so that the cache keys will match for the same query.
|
67
|
+
cache_key = Digest::MD5.hexdigest(query_builder.request_string)
|
68
|
+
from_cache = false
|
69
|
+
|
70
|
+
# if we're caching, first try looking in the cache
|
71
|
+
if enable_caching
|
72
|
+
body = @cache.get(cache_key) rescue body = nil
|
73
|
+
from_cache = true unless body.blank?
|
74
|
+
end
|
75
|
+
|
76
|
+
if body.blank? # cache miss (or wasn't enabled)
|
77
|
+
|
78
|
+
# only bother to create the connection if we know we failed to hit the cache
|
79
|
+
@connection ||= Net::HTTP.new(configuration.server, configuration.port)
|
80
|
+
raise "Failed to connect to #{configuration.server}:#{configuration.port}" if @connection.nil?
|
81
|
+
|
82
|
+
header, body = @connection.get(query_builder.request_string)
|
83
|
+
|
84
|
+
# add to the cache if caching
|
85
|
+
if enable_caching
|
86
|
+
begin
|
87
|
+
@cache.set(cache_key, body, ttl)
|
88
|
+
rescue
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
DelSolr::Client::Response.new(body, query_builder, :from_cache => from_cache, :shortcuts => @shortcuts)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# Common extensions that we need to define if they don't already exist...
|
3
|
+
#
|
4
|
+
|
5
|
+
String.class_eval do
|
6
|
+
if !''.respond_to?(:blank?)
|
7
|
+
def blank?
|
8
|
+
self == ''
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
NilClass.class_eval do
|
14
|
+
if !nil.respond_to?(:blank?)
|
15
|
+
def blank?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Hash.class_eval do
|
22
|
+
if !{}.respond_to?(:blank?)
|
23
|
+
def blank?
|
24
|
+
self == {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Fixnum.class_eval do
|
30
|
+
if !1.respond_to?(:hours)
|
31
|
+
def hours
|
32
|
+
self * 60 * 60
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module DelSolr
|
4
|
+
|
5
|
+
class Client
|
6
|
+
|
7
|
+
class QueryBuilder
|
8
|
+
|
9
|
+
attr_accessor :query_name, :options
|
10
|
+
|
11
|
+
# ops can basically be straight solr URL params, but it also supports some other formats
|
12
|
+
# of different params to give it more of a "ruby" feel (ie: :filters can be an array, hash, or string,
|
13
|
+
# but you can also just specify the fq params directly
|
14
|
+
def initialize(query_name, opts = {})
|
15
|
+
@query_name = query_name
|
16
|
+
@options = opts
|
17
|
+
end
|
18
|
+
|
19
|
+
def request_string
|
20
|
+
@request_string ||= build_request_string
|
21
|
+
end
|
22
|
+
|
23
|
+
# returns the query string of the facet query for the given query name (used for resolving counts for given queries)
|
24
|
+
def facet_query_by_name(query_name)
|
25
|
+
name_to_facet_query[query_name]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def build_request_string()
|
31
|
+
raise "query_name must be set" if query_name.blank?
|
32
|
+
|
33
|
+
opts = self.options.dup
|
34
|
+
|
35
|
+
# cleanup the nils
|
36
|
+
opts.delete_if {|k,v| v.nil?}
|
37
|
+
|
38
|
+
# resolve "rubyish" names to solr names
|
39
|
+
opts[:q] ||= opts[:query]
|
40
|
+
opts[:rows] ||= opts[:limit] || 10
|
41
|
+
opts[:start] ||= opts[:offset] || 0
|
42
|
+
opts[:fl] ||= opts[:fields] || 'id,unique_id,index_type,score'
|
43
|
+
opts[:bq] ||= opts[:boost]
|
44
|
+
opts[:suggestionCount] ||= opts[:suggestion_count]
|
45
|
+
opts[:onlyMorePopular] ||= opts[:only_more_popular]
|
46
|
+
|
47
|
+
raise ":query or :q must be set" if opts[:q].blank?
|
48
|
+
|
49
|
+
# clear out the "rubyish" versions, what's left will go straight to solr
|
50
|
+
opts.delete(:query)
|
51
|
+
opts.delete(:limit)
|
52
|
+
opts.delete(:offset)
|
53
|
+
opts.delete(:fields)
|
54
|
+
opts.delete(:boost)
|
55
|
+
opts.delete(:suggestion_count)
|
56
|
+
opts.delete(:only_more_popular)
|
57
|
+
|
58
|
+
# needs to be an array of hashs because it's acceptable to have the same key present > once.
|
59
|
+
params = []
|
60
|
+
|
61
|
+
# remove params as we go so we can just pass whatever is left to solr...
|
62
|
+
|
63
|
+
params << build_query(:q, opts.delete(:q))
|
64
|
+
params << {:wt => 'ruby'}
|
65
|
+
params << {:qt => query_name}
|
66
|
+
params << {:rows => opts.delete(:rows)}
|
67
|
+
params << {:start => opts.delete(:start)}
|
68
|
+
params << {:fl => opts.delete(:fl)}
|
69
|
+
|
70
|
+
filters = opts.delete(:filters)
|
71
|
+
params += build_filters(:fq, filters)
|
72
|
+
|
73
|
+
facets = opts.delete(:facets)
|
74
|
+
if facets
|
75
|
+
if facets.is_a?(Array)
|
76
|
+
params << {:facet => true}
|
77
|
+
params += build_facets(facets)
|
78
|
+
elsif facets.is_a?(Hash)
|
79
|
+
params << {:facet => true}
|
80
|
+
params += build_facet(facets)
|
81
|
+
elsif facets.is_a?(String)
|
82
|
+
params += facets
|
83
|
+
else
|
84
|
+
raise 'facets must either be a Hash or an Array'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# handle friendly highlight name
|
89
|
+
if opts.delete(:highlight)
|
90
|
+
params << {:hl => 'true'}
|
91
|
+
params << {'hl.fl' => opts['hl.fl'] || opts[:fl] }
|
92
|
+
end
|
93
|
+
|
94
|
+
# just pass everything that's left to solr
|
95
|
+
opts.each { |k,v| params << {k => v} if !v.nil? }
|
96
|
+
|
97
|
+
# convert the params (array of hashes)
|
98
|
+
param_strings = params.collect do |h|
|
99
|
+
if h.is_a?(Hash)
|
100
|
+
ha = h.to_a
|
101
|
+
"#{ha[0][0]}=#{::CGI::escape(ha[0][1].to_s)}"
|
102
|
+
elsif h.is_a?(String)
|
103
|
+
h # just return the string
|
104
|
+
else
|
105
|
+
raise "All params should be a Hash or String"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
"/solr/select?#{param_strings.join('&')}"
|
110
|
+
end
|
111
|
+
|
112
|
+
# returns the query param
|
113
|
+
def build_query(key, queries)
|
114
|
+
query_string = ''
|
115
|
+
case queries
|
116
|
+
when String
|
117
|
+
query_string = queries
|
118
|
+
when Array
|
119
|
+
query_string = queries.join(' ')
|
120
|
+
when Hash
|
121
|
+
query_string_array = []
|
122
|
+
queries.each do |k,v|
|
123
|
+
if v.is_a?(Array) # add a filter for each value
|
124
|
+
v.each do |val|
|
125
|
+
query_string_array << "#{k}:#{val}"
|
126
|
+
end
|
127
|
+
elsif v.is_a?(Range)
|
128
|
+
query_string_array << "#{k}:[#{v.min} TO #{v.max}]"
|
129
|
+
else
|
130
|
+
query_string_array << "#{k}:#{v}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
query_string = query_string_array.join(' ')
|
134
|
+
end
|
135
|
+
|
136
|
+
{key => query_string}
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_filters(key, filters)
|
140
|
+
params = []
|
141
|
+
|
142
|
+
# handle "ruby-ish" filters
|
143
|
+
case filters
|
144
|
+
when String
|
145
|
+
params << {key => filters}
|
146
|
+
when Array
|
147
|
+
filters.each { |f| params << {key => f} }
|
148
|
+
when Hash
|
149
|
+
filters.each do |k,v|
|
150
|
+
if v.is_a?(Array) # add a filter for each value
|
151
|
+
v.each do |val|
|
152
|
+
params << {key => "#{k}:#{val}"}
|
153
|
+
end
|
154
|
+
elsif v.is_a?(Range)
|
155
|
+
params << {key => "#{k}:[#{v.min} TO #{v.max}]"}
|
156
|
+
else
|
157
|
+
params << {key => "#{k}:#{v}"}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
params
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_facets(facet_array)
|
165
|
+
params = []
|
166
|
+
facet_array.each do |facet_hash|
|
167
|
+
params += build_facet(facet_hash)
|
168
|
+
end
|
169
|
+
params
|
170
|
+
end
|
171
|
+
|
172
|
+
def build_facet(facet_hash)
|
173
|
+
params = []
|
174
|
+
facet_name = facet_hash['name'] || facet_hash[:name]
|
175
|
+
facet_hash.each do |k,v|
|
176
|
+
# handle some cases specially
|
177
|
+
if 'field' == k.to_s
|
178
|
+
params << {"facet.field" => v}
|
179
|
+
elsif 'query' == k.to_s
|
180
|
+
q = build_query("facet.query", v)
|
181
|
+
params << q
|
182
|
+
if facet_name
|
183
|
+
# keep track of names => facet_queries
|
184
|
+
name_to_facet_query[facet_name] = q['facet.query']
|
185
|
+
end
|
186
|
+
elsif ['name', :name].include?(k.to_s)
|
187
|
+
# do nothing
|
188
|
+
else
|
189
|
+
params << {"f.#{facet_hash[:field]}.facet.#{k}" => v}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
params
|
193
|
+
end
|
194
|
+
|
195
|
+
def name_to_facet_query
|
196
|
+
@name_to_facet_query ||= {}
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module DelSolr
|
2
|
+
|
3
|
+
class Client
|
4
|
+
|
5
|
+
class Response
|
6
|
+
|
7
|
+
attr_reader :raw_response, :query_builder
|
8
|
+
|
9
|
+
def initialize(solr_response_buffer, query_builder, options = {})
|
10
|
+
@query_builder = query_builder
|
11
|
+
@from_cache = options[:from_cache]
|
12
|
+
begin
|
13
|
+
@raw_response = eval(solr_response_buffer)
|
14
|
+
rescue
|
15
|
+
@raw_response = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# now define the shortcuts
|
19
|
+
options[:shortcuts] ||= [:id, :unique_id, :score]
|
20
|
+
options[:shortcuts].each do |shortcut|
|
21
|
+
instance_eval %{
|
22
|
+
def #{shortcut}s
|
23
|
+
@#{shortcut}s ||= docs.collect {|d| d['#{shortcut}'] }
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# returns the total number of matches
|
30
|
+
def total
|
31
|
+
@total ||= raw_response['response']['numFound']
|
32
|
+
end
|
33
|
+
|
34
|
+
def blank?
|
35
|
+
total < 1
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :empty?, :blank?
|
39
|
+
|
40
|
+
def from_cache?
|
41
|
+
@from_cache
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns the offset
|
45
|
+
def offset
|
46
|
+
@offset ||= raw_response['response']['start']
|
47
|
+
end
|
48
|
+
|
49
|
+
# returns the max score
|
50
|
+
def max_score
|
51
|
+
@max_score ||= raw_response['response']['maxScore'].to_f
|
52
|
+
end
|
53
|
+
|
54
|
+
# returns an array of all ids for the given search
|
55
|
+
def ids
|
56
|
+
@ids ||= docs.collect {|d| d['id']}
|
57
|
+
end
|
58
|
+
|
59
|
+
def unique_ids
|
60
|
+
@unique_ids ||= docs.collect {|d| d['unique_id']}
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns an array of all the docs
|
64
|
+
def docs
|
65
|
+
@docs ||= raw_response['response']['docs']
|
66
|
+
end
|
67
|
+
|
68
|
+
# helper for displaying a given field (first tries the highlight, then the stored value)
|
69
|
+
def display_for(doc, field)
|
70
|
+
highlights_for(doc['unique_id'], field) || doc[field]
|
71
|
+
end
|
72
|
+
|
73
|
+
# returns the highlights for a given id for a given field
|
74
|
+
def highlights_for(unique_id, field)
|
75
|
+
raw_response['highlighting'] ||= {}
|
76
|
+
raw_response['highlighting'][unique_id] ||= {}
|
77
|
+
raw_response['highlighting'][unique_id][field]
|
78
|
+
end
|
79
|
+
|
80
|
+
def suggestions
|
81
|
+
@suggestions ||= raw_response['suggestions']
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns the query time in ms
|
85
|
+
def qtime
|
86
|
+
@qtime ||= raw_response['responseHeader']['QTime'].to_i
|
87
|
+
end
|
88
|
+
|
89
|
+
# returns the status code (0 for success)
|
90
|
+
def status
|
91
|
+
@status ||= raw_response['responseHeader']['status']
|
92
|
+
end
|
93
|
+
|
94
|
+
# returns the params hash
|
95
|
+
def params
|
96
|
+
@params ||= raw_response['responseHeader']['params']
|
97
|
+
end
|
98
|
+
|
99
|
+
# returns the entire facet hash
|
100
|
+
def facets
|
101
|
+
@facets ||= raw_response['facet_counts'] || {}
|
102
|
+
end
|
103
|
+
|
104
|
+
# returns the hash of all the facet_fields (ie: {'instock_b' => ['true', 123, 'false', 20]}
|
105
|
+
def facet_fields
|
106
|
+
@facet_fields ||= facets['facet_fields'] || {}
|
107
|
+
end
|
108
|
+
|
109
|
+
def facet_queries
|
110
|
+
@facet_queries ||= facets['facet_queries'] || {}
|
111
|
+
end
|
112
|
+
|
113
|
+
# returns a hash of hashs rather than a hash of arrays (ie: {'instock_b' => {'true' => 123', 'false', => 20} })
|
114
|
+
def facet_fields_by_hash
|
115
|
+
@facet_fields_by_hash ||= begin
|
116
|
+
f = {}
|
117
|
+
if facet_fields
|
118
|
+
facet_fields.each do |field,value_and_counts|
|
119
|
+
f[field] = {}
|
120
|
+
value_and_counts.each_with_index do |v, i|
|
121
|
+
if i % 2 == 0
|
122
|
+
f[field][v] = value_and_counts[i+1]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
f
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# returns an array of value/counts for a given field (ie: ['true', 123, 'false', 20]
|
132
|
+
def facet_field(field)
|
133
|
+
facet_fields[field.to_s]
|
134
|
+
end
|
135
|
+
|
136
|
+
# returns the array of field values for the given field in the order they were returned from solr
|
137
|
+
def facet_field_values(field)
|
138
|
+
facet_field_values ||= {}
|
139
|
+
facet_field_values[field.to_s] ||= begin
|
140
|
+
a = []
|
141
|
+
facet_field(field).each_with_index do |val_or_count, i|
|
142
|
+
a << val_or_count if i % 2 == 0 && facet_field(field)[i+1] > 0
|
143
|
+
end
|
144
|
+
a
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# returns a hash of value/counts for a given field (ie: {'true' => 123, 'false' => 20}
|
149
|
+
def facet_field_by_hash(field)
|
150
|
+
facet_fields_by_hash(field.to_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
# returns the count for the given field/value pair
|
154
|
+
def facet_field_count(field, value)
|
155
|
+
facet_fields_by_hash[field.to_s][value.to_s] if facet_fields_by_hash[field.to_s]
|
156
|
+
end
|
157
|
+
|
158
|
+
# returns the counts for a given facet_query_name
|
159
|
+
def facet_query_count_by_name(facet_query_name)
|
160
|
+
query_string = query_builder.facet_query_by_name(facet_query_name)
|
161
|
+
facet_queries[query_string] if query_string
|
162
|
+
end
|
163
|
+
|
164
|
+
# returns the url send to solr
|
165
|
+
def request_url
|
166
|
+
query_builder.request_string
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ClientTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
include Test::Unit::Assertions
|
6
|
+
|
7
|
+
def test_create
|
8
|
+
s = nil
|
9
|
+
assert_nothing_raised do
|
10
|
+
s = DelSolr::Client.new(:server => 'localhost', :port => 8983)
|
11
|
+
end
|
12
|
+
assert(s)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class QueryBuilderTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
include Test::Unit::Assertions
|
6
|
+
|
7
|
+
def test_001
|
8
|
+
qb = nil
|
9
|
+
|
10
|
+
opts = {}
|
11
|
+
opts[:limit] = 13
|
12
|
+
opts[:offset] = 3
|
13
|
+
opts[:fl] = 'id'
|
14
|
+
opts[:query] = 'good book'
|
15
|
+
|
16
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('query_name', opts) }
|
17
|
+
|
18
|
+
assert(qb)
|
19
|
+
|
20
|
+
p = get_params(qb.request_string)
|
21
|
+
assert_equal(p['start'], '3')
|
22
|
+
assert_equal(p['rows'], '13')
|
23
|
+
assert_equal(p['fl'], 'id')
|
24
|
+
assert_equal(p['q'], 'good book')
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_002
|
28
|
+
qb = nil
|
29
|
+
|
30
|
+
opts = {}
|
31
|
+
opts[:query] = "blahblah"
|
32
|
+
opts[:fields] = 'id,unique_id,score'
|
33
|
+
|
34
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('query_name', opts) }
|
35
|
+
|
36
|
+
assert(qb)
|
37
|
+
|
38
|
+
p = get_params(qb.request_string)
|
39
|
+
assert_equal(p['fl'], 'id,unique_id,score')
|
40
|
+
assert_equal(p['q'], 'blahblah')
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_003
|
44
|
+
qb = nil
|
45
|
+
|
46
|
+
opts = {}
|
47
|
+
opts[:query] = {:index_type => 'books'}
|
48
|
+
opts[:fields] = 'id,unique_id,score'
|
49
|
+
|
50
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('query_name', opts) }
|
51
|
+
|
52
|
+
assert(qb)
|
53
|
+
|
54
|
+
p = get_params(qb.request_string)
|
55
|
+
assert_equal(p['fl'], 'id,unique_id,score')
|
56
|
+
assert_equal(p['q'], 'index_type:books')
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_004
|
60
|
+
qb = nil
|
61
|
+
|
62
|
+
opts = {}
|
63
|
+
opts[:query] = {:index_type => 'books'}
|
64
|
+
opts[:filters] = {:location => 'seattle'}
|
65
|
+
|
66
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('query_name', opts) }
|
67
|
+
|
68
|
+
assert(qb)
|
69
|
+
|
70
|
+
p = get_params(qb.request_string)
|
71
|
+
assert_equal(p['fq'], 'location:seattle')
|
72
|
+
assert_equal(p['q'], 'index_type:books')
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_005
|
76
|
+
qb = nil
|
77
|
+
|
78
|
+
opts = {}
|
79
|
+
opts[:query] = {:index_type => 'books'}
|
80
|
+
opts[:filters] = "location:seattle"
|
81
|
+
|
82
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('query_name', opts) }
|
83
|
+
|
84
|
+
assert(qb)
|
85
|
+
|
86
|
+
p = get_params(qb.request_string)
|
87
|
+
|
88
|
+
assert_equal(p['fq'], 'location:seattle')
|
89
|
+
assert_equal(p['q'], 'index_type:books')
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_facets
|
93
|
+
qb = nil
|
94
|
+
opts = {}
|
95
|
+
opts[:query] = "games"
|
96
|
+
opts[:facets] = [{:field => 'instock_b'}, {:field => 'on_sale_b', :limit => 1}]
|
97
|
+
|
98
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('onebox-books', opts) }
|
99
|
+
|
100
|
+
assert(qb)
|
101
|
+
|
102
|
+
p = get_params(qb.request_string)
|
103
|
+
|
104
|
+
assert_equal(p['facet'], 'true')
|
105
|
+
assert_equal(p['facet.field'].sort, ['instock_b', 'on_sale_b'].sort)
|
106
|
+
assert_equal(p['f.on_sale_b.facet.limit'], '1')
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_facets
|
110
|
+
qb = nil
|
111
|
+
opts = {}
|
112
|
+
opts[:query] = "games"
|
113
|
+
opts[:facets] = [{:query => {:city_idm => 19596}, :name => 'seattle'}, {:field => 'language_idm'}]
|
114
|
+
|
115
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('onebox-books', opts) }
|
116
|
+
|
117
|
+
assert(qb)
|
118
|
+
|
119
|
+
p = get_params(qb.request_string)
|
120
|
+
|
121
|
+
assert_equal(p['facet'], 'true')
|
122
|
+
assert_equal(p['facet.field'], 'language_idm')
|
123
|
+
assert_equal(p['facet.query'], 'city_idm:19596')
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_range
|
127
|
+
qb = nil
|
128
|
+
opts = {}
|
129
|
+
opts[:query] = "games"
|
130
|
+
opts[:filters] = {:id => (1..3)}
|
131
|
+
|
132
|
+
assert_nothing_raised { qb = DelSolr::Client::QueryBuilder.new('onebox-books', opts) }
|
133
|
+
|
134
|
+
assert(qb)
|
135
|
+
|
136
|
+
p = get_params(qb.request_string)
|
137
|
+
|
138
|
+
assert_equal('id:[1 TO 3]', p['fq'])
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# given a url returns a hash of the query params (for each duplicate key, it returns an array)
|
145
|
+
def get_params(url)
|
146
|
+
query = URI.parse(url).query
|
147
|
+
query = query.split('&')
|
148
|
+
h = {}
|
149
|
+
query.each do |p|
|
150
|
+
a = p.split('=')
|
151
|
+
if h[a[0]]
|
152
|
+
h[a[0]] = (Array(h[a[0]]) << CGI::unescape(a[1])) # convert it to an array
|
153
|
+
else
|
154
|
+
h[a[0]] = CGI::unescape(a[1])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
h
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ResponseTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
include Test::Unit::Assertions
|
6
|
+
|
7
|
+
@@test_001 = %{
|
8
|
+
{
|
9
|
+
'responseHeader'=>{
|
10
|
+
'status'=>0,
|
11
|
+
'QTime'=>151,
|
12
|
+
'params'=>{
|
13
|
+
'wt'=>'ruby',
|
14
|
+
'rows'=>'10',
|
15
|
+
'explainOther'=>'',
|
16
|
+
'start'=>'0',
|
17
|
+
'hl.fl'=>'',
|
18
|
+
'indent'=>'on',
|
19
|
+
'hl'=>'on',
|
20
|
+
'q'=>'index_type:widget',
|
21
|
+
'fl'=>'*,score',
|
22
|
+
'qt'=>'standard',
|
23
|
+
'version'=>'2.2'}},
|
24
|
+
'response'=>{'numFound'=>1522698,'start'=>0,'maxScore'=>1.5583541,'docs'=>[
|
25
|
+
{
|
26
|
+
'index_type'=>'widget',
|
27
|
+
'id'=>1,
|
28
|
+
'unique_id'=>'1_widget',
|
29
|
+
'score'=>1.5583541},
|
30
|
+
{
|
31
|
+
'index_type'=>'widget',
|
32
|
+
'id'=>3,
|
33
|
+
'unique_id'=>'3_widget',
|
34
|
+
'score'=>1.5583541},
|
35
|
+
{
|
36
|
+
'index_type'=>'widget',
|
37
|
+
'id'=>4,
|
38
|
+
'unique_id'=>'4_widget',
|
39
|
+
'score'=>1.5583541},
|
40
|
+
{
|
41
|
+
'index_type'=>'widget',
|
42
|
+
'id'=>5,
|
43
|
+
'unique_id'=>'5_widget',
|
44
|
+
'score'=>1.5583541},
|
45
|
+
{
|
46
|
+
'index_type'=>'widget',
|
47
|
+
'id'=>7,
|
48
|
+
'unique_id'=>'7_widget',
|
49
|
+
'score'=>1.5583541},
|
50
|
+
{
|
51
|
+
'index_type'=>'widget',
|
52
|
+
'id'=>8,
|
53
|
+
'unique_id'=>'8_widget',
|
54
|
+
'score'=>1.5583541},
|
55
|
+
{
|
56
|
+
'index_type'=>'widget',
|
57
|
+
'id'=>9,
|
58
|
+
'unique_id'=>'9_widget',
|
59
|
+
'score'=>1.5583541},
|
60
|
+
{
|
61
|
+
'index_type'=>'widget',
|
62
|
+
'id'=>10,
|
63
|
+
'unique_id'=>'10_widget',
|
64
|
+
'score'=>1.5583541},
|
65
|
+
{
|
66
|
+
'index_type'=>'widget',
|
67
|
+
'id'=>11,
|
68
|
+
'unique_id'=>'11_widget',
|
69
|
+
'score'=>1.5583541},
|
70
|
+
{
|
71
|
+
'index_type'=>'widget',
|
72
|
+
'id'=>12,
|
73
|
+
'unique_id'=>'12_widget',
|
74
|
+
'score'=>1.5583541}]
|
75
|
+
},
|
76
|
+
'facet_counts'=>{
|
77
|
+
'facet_queries'=>{
|
78
|
+
'city_idm:19596' => 392},
|
79
|
+
'facet_fields'=>{
|
80
|
+
'available_b'=>[
|
81
|
+
'false',1328],
|
82
|
+
'onsale_b'=>[
|
83
|
+
'false',1182,
|
84
|
+
'true',174]}},
|
85
|
+
'highlighting'=>{
|
86
|
+
'1_widget'=>{},
|
87
|
+
'3_widget'=>{},
|
88
|
+
'4_widget'=>{},
|
89
|
+
'5_widget'=>{},
|
90
|
+
'7_widget'=>{},
|
91
|
+
'8_widget'=>{},
|
92
|
+
'9_widget'=>{},
|
93
|
+
'10_widget'=>{},
|
94
|
+
'11_widget'=>{},
|
95
|
+
'12_widget'=>{}}}
|
96
|
+
}
|
97
|
+
|
98
|
+
def test_001
|
99
|
+
r = nil
|
100
|
+
qb = DelSolr::Client::QueryBuilder.new('standard', :query => {:index_type => 'widget'}, :facets => {:query => 'city_idm:19596', :name => 19596} )
|
101
|
+
qb.request_string # need to generate this...
|
102
|
+
assert_nothing_raised { r = DelSolr::Client::Response.new(@@test_001, qb) }
|
103
|
+
|
104
|
+
assert_equal(151, r.qtime)
|
105
|
+
assert_equal(1.5583541, r.max_score)
|
106
|
+
assert_equal(10, r.docs.length)
|
107
|
+
assert_equal([1, 3, 4, 5, 7, 8, 9, 10, 11, 12], r.ids)
|
108
|
+
assert_equal({
|
109
|
+
'available_b'=>[
|
110
|
+
'false',1328],
|
111
|
+
'onsale_b'=>[
|
112
|
+
'false',1182,
|
113
|
+
'true',174]}, r.facet_fields)
|
114
|
+
assert_equal(1182, r.facet_field_count('onsale_b', false))
|
115
|
+
assert_equal(174, r.facet_field_count('onsale_b', true))
|
116
|
+
assert_equal(1328, r.facet_field_count('available_b', false))
|
117
|
+
assert_equal(392, r.facet_query_count_by_name(19596))
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_shortcuts
|
121
|
+
r = nil
|
122
|
+
qb = DelSolr::Client::QueryBuilder.new('standard', :query => {:index_type => 'widget'}, :facets => {:query => 'city_idm:19596', :name => 19596} )
|
123
|
+
qb.request_string # need to generate this...
|
124
|
+
assert_nothing_raised { r = DelSolr::Client::Response.new(@@test_001, qb, :shortcuts => [:index_type, :id]) }
|
125
|
+
|
126
|
+
assert(r.respond_to?(:index_types))
|
127
|
+
assert(r.respond_to?(:ids))
|
128
|
+
assert(!r.respond_to?(:scores))
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delsolr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben VandenBos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-09-08 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.7.0
|
24
|
+
version:
|
25
|
+
description: Wrapper for querying Lucene Solr
|
26
|
+
email:
|
27
|
+
- bvandenbos@gmail.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- History.txt
|
34
|
+
- License.txt
|
35
|
+
- Manifest.txt
|
36
|
+
- README.txt
|
37
|
+
files:
|
38
|
+
- History.txt
|
39
|
+
- License.txt
|
40
|
+
- Manifest.txt
|
41
|
+
- README.txt
|
42
|
+
- Rakefile
|
43
|
+
- lib/delsolr.rb
|
44
|
+
- lib/delsolr/configuration.rb
|
45
|
+
- lib/delsolr/extensions.rb
|
46
|
+
- lib/delsolr/query_builder.rb
|
47
|
+
- lib/delsolr/response.rb
|
48
|
+
- lib/delsolr/version.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://delsolr.rubyforge.org
|
51
|
+
post_install_message: |+
|
52
|
+
|
53
|
+
For more information on delsolr, see http://delsolr.rubyforge.org
|
54
|
+
|
55
|
+
NOTE: Change this information in PostInstall.txt
|
56
|
+
You can also delete it if you don't want it.
|
57
|
+
|
58
|
+
|
59
|
+
rdoc_options:
|
60
|
+
- --main
|
61
|
+
- README.txt
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project: delsolr
|
79
|
+
rubygems_version: 1.2.0
|
80
|
+
signing_key:
|
81
|
+
specification_version: 2
|
82
|
+
summary: Wrapper for querying Lucene Solr
|
83
|
+
test_files:
|
84
|
+
- test/test_response.rb
|
85
|
+
- test/test_query_builder.rb
|
86
|
+
- test/test_helper.rb
|
87
|
+
- test/test_client.rb
|