delsolr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|