gsolr_ext 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/LICENSE +13 -0
- data/README.rdoc +126 -0
- data/Rakefile +46 -0
- data/TODO.txt +1 -0
- data/VERSION +1 -0
- data/gsolr_ext.gemspec +32 -0
- data/gsolr_ext.tmproj +258 -0
- data/lib/gsolr_ext.rb +42 -0
- data/lib/gsolr_ext/client.rb +65 -0
- data/lib/gsolr_ext/doc.rb +44 -0
- data/lib/gsolr_ext/model.rb +111 -0
- data/lib/gsolr_ext/request.rb +110 -0
- data/lib/gsolr_ext/response.rb +65 -0
- data/lib/gsolr_ext/response/docs.rb +56 -0
- data/lib/gsolr_ext/response/facets.rb +58 -0
- data/lib/gsolr_ext/response/spelling.rb +92 -0
- data/lib/mash.rb +143 -0
- data/spec/gsolr_ext_spec.rb +276 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +32 -0
- metadata +111 -0
data/lib/gsolr_ext.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# add this directory to the load path if it hasn't already been added
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
4
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
5
|
+
|
6
|
+
require File.join(File.dirname(__FILE__), 'mash') unless defined?(Mash)
|
7
|
+
|
8
|
+
unless Hash.respond_to?(:to_mash)
|
9
|
+
class Hash
|
10
|
+
def to_mash
|
11
|
+
Mash.new(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'gsolr'
|
17
|
+
require 'gsolr_ext'
|
18
|
+
|
19
|
+
module GSolr
|
20
|
+
module Ext
|
21
|
+
autoload :Client, 'gsolr_ext/client.rb'
|
22
|
+
autoload :Doc, 'gsolr_ext/doc.rb'
|
23
|
+
autoload :Request, 'gsolr_ext/request.rb'
|
24
|
+
autoload :Response, 'gsolr_ext/response.rb'
|
25
|
+
autoload :Model, 'gsolr_ext/model.rb'
|
26
|
+
|
27
|
+
def self.version
|
28
|
+
@version ||= File.read(File.join(File.dirname(__FILE__), '..', 'VERSION'))
|
29
|
+
end
|
30
|
+
|
31
|
+
# modify the GSolr::Client (provides #find and #luke methods)
|
32
|
+
GSolr::Client.class_eval do
|
33
|
+
include GSolr::Ext::Client
|
34
|
+
end
|
35
|
+
|
36
|
+
# this is for backward compatibility: GSolr::Ext.connect
|
37
|
+
# recommended way is to just use GSolr.connect
|
38
|
+
def self.connect(*args, &blk)
|
39
|
+
GSolr.connect(*args, &blk)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module GSolr
|
2
|
+
module Ext
|
3
|
+
module Client
|
4
|
+
# TWO modes of arguments:
|
5
|
+
#
|
6
|
+
# <request-handler-path>, <solr-params-hash>
|
7
|
+
# OR
|
8
|
+
# <solr-params-hash>
|
9
|
+
#
|
10
|
+
# The default request-handler-path is /select
|
11
|
+
#
|
12
|
+
# If a hash is used for solr params, all of the normal GSolr::Ext::Request
|
13
|
+
# mappings are available (everything else gets passed to solr).
|
14
|
+
# Returns a new GSolr::Ext::Response::Base object.
|
15
|
+
def find *args
|
16
|
+
# remove the handler arg - the first, if it is a string OR set default
|
17
|
+
path = args.first.is_a?(String) ? args.shift : '/select'
|
18
|
+
# remove the params - the first, if it is a Hash OR set default
|
19
|
+
params = args.first.kind_of?(Hash) ? args.shift : {}
|
20
|
+
# send path, map params and send the rest of the args along
|
21
|
+
response = self.request path, GSolr::Ext::Request.map(params), *args
|
22
|
+
GSolr::Ext::Response::Base.new(response, path, params)
|
23
|
+
end
|
24
|
+
|
25
|
+
# TWO modes of arguments:
|
26
|
+
#
|
27
|
+
# <request-handler-path>, <solr-params-hash>
|
28
|
+
# OR
|
29
|
+
# <solr-params-hash>
|
30
|
+
#
|
31
|
+
# The default request-handler-path is /admin/luke
|
32
|
+
# The default params are numTerms=0
|
33
|
+
#
|
34
|
+
# Returns a new Mash object.
|
35
|
+
def luke *args
|
36
|
+
path = args.first.is_a?(String) ? args.shift : '/admin/luke'
|
37
|
+
params = args.pop || {}
|
38
|
+
params['numTerms'] ||= 0
|
39
|
+
self.request(path, params).to_mash
|
40
|
+
end
|
41
|
+
|
42
|
+
# sends request to /admin/ping
|
43
|
+
def ping *args
|
44
|
+
path = args.first.is_a?(String) ? args.shift : '/admin/ping'
|
45
|
+
params = args.pop || {}
|
46
|
+
self.request(path, params).to_mash
|
47
|
+
end
|
48
|
+
|
49
|
+
# Ping the server and make sure it is alright
|
50
|
+
# solr.ping?
|
51
|
+
#
|
52
|
+
# It returns true if the server pings and the status is OK
|
53
|
+
# It returns false otherwise -- which probably cannot happen
|
54
|
+
# Or raises an exception if there is a failure to connect or
|
55
|
+
# the ping service is not activated in the solr server
|
56
|
+
#
|
57
|
+
# The default configuration point of the PingRequestHandler
|
58
|
+
# in the solr server of '/admin/ping' is assumed.
|
59
|
+
#
|
60
|
+
def ping?
|
61
|
+
ping['status'] == 'OK'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module GSolr::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,111 @@
|
|
1
|
+
# include this module into a plain ruby class:
|
2
|
+
# class Book
|
3
|
+
# include GSolr::Ext::Model
|
4
|
+
# connection = GSolr::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 GSolr::Ext::Model
|
12
|
+
|
13
|
+
# ripped from MongoMapper!
|
14
|
+
module Pluggable
|
15
|
+
|
16
|
+
def plugins
|
17
|
+
@plugins ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def plugin(mod)
|
21
|
+
extend mod::ClassMethods if mod.const_defined?(:ClassMethods)
|
22
|
+
include mod::InstanceMethods if mod.const_defined?(:InstanceMethods)
|
23
|
+
mod.configure(self) if mod.respond_to?(:configure)
|
24
|
+
plugins << mod
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Class level methods for altering object instances
|
30
|
+
module Callbacks
|
31
|
+
|
32
|
+
# method that only accepts a block
|
33
|
+
# The block is executed when an object is created via #new -> SolrDoc.new
|
34
|
+
# The blocks scope is the instance of the object.
|
35
|
+
def after_initialize(&blk)
|
36
|
+
hooks << blk
|
37
|
+
end
|
38
|
+
|
39
|
+
# Removes the current set of after_initialize blocks.
|
40
|
+
# You would use this if you wanted to open a class back up,
|
41
|
+
# but clear out the previously defined blocks.
|
42
|
+
def clear_after_initialize_blocks!
|
43
|
+
@hooks = []
|
44
|
+
end
|
45
|
+
|
46
|
+
# creates the @hooks container ("hooks" are blocks or procs).
|
47
|
+
# returns an array
|
48
|
+
def hooks
|
49
|
+
@hooks ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Findable is a module that gets mixed into the SolrDocument *class* object.
|
56
|
+
# These methods will be available through the class: SolrDocument.find
|
57
|
+
#
|
58
|
+
module Findable
|
59
|
+
|
60
|
+
attr_accessor :connection
|
61
|
+
|
62
|
+
def connection
|
63
|
+
@connection ||= GSolr::Ext.connect
|
64
|
+
end
|
65
|
+
|
66
|
+
# this method decorates the connection find method
|
67
|
+
# and then creates new instance of the class that uses this module.
|
68
|
+
def find *args, &block
|
69
|
+
response = connection.find(*args)
|
70
|
+
response.docs.map {|doc|
|
71
|
+
d = self.new doc, response
|
72
|
+
yield d if block_given?
|
73
|
+
d
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# Called by Ruby Module API
|
80
|
+
# extends this *class* object
|
81
|
+
def self.included(base)
|
82
|
+
base.extend Pluggable
|
83
|
+
base.extend Callbacks
|
84
|
+
base.extend Findable
|
85
|
+
base.send :include, GSolr::Ext::Doc
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_reader :solr_response
|
89
|
+
|
90
|
+
# The original object passed in to the #new method
|
91
|
+
attr :_source
|
92
|
+
|
93
|
+
# Constructor **for the class that is getting this module included**
|
94
|
+
# source_doc should be a hash or something similar
|
95
|
+
# calls each of after_initialize blocks
|
96
|
+
def initialize(source_doc={}, solr_response=nil)
|
97
|
+
@_source = source_doc.to_mash
|
98
|
+
@solr_response = solr_response
|
99
|
+
self.class.hooks.each do |h|
|
100
|
+
instance_eval &h
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# the wrapper method to the @_source object.
|
105
|
+
# If a method is missing, it gets sent to @_source
|
106
|
+
# with all of the original params and block
|
107
|
+
def method_missing(m, *args, &b)
|
108
|
+
@_source.send(m, *args, &b)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module GSolr::Ext::Request
|
2
|
+
|
3
|
+
module Params
|
4
|
+
|
5
|
+
def map input_params
|
6
|
+
input = input_params.dup
|
7
|
+
|
8
|
+
output = {}
|
9
|
+
|
10
|
+
if input[:per_page]
|
11
|
+
output[:rows] = input.delete(:per_page).to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
if page = input.delete(:page)
|
15
|
+
raise ':per_page must be set when using :page' unless output[:rows]
|
16
|
+
page = page.to_s.to_i-1
|
17
|
+
page = page < 1 ? 0 : page
|
18
|
+
output[:start] = page * output[:rows]
|
19
|
+
end
|
20
|
+
|
21
|
+
# remove the input :q params
|
22
|
+
output[:q] = input.delete :q
|
23
|
+
output[:fq] = input.delete(:fq) if input[:fq]
|
24
|
+
|
25
|
+
if queries = input.delete(:queries)
|
26
|
+
output[:q] = append_to_param output[:q], build_query(queries, false)
|
27
|
+
end
|
28
|
+
if phrases = input.delete(:phrases)
|
29
|
+
output[:q] = append_to_param output[:q], build_query(phrases, true)
|
30
|
+
end
|
31
|
+
if filters = input.delete(:filters)
|
32
|
+
output[:fq] = append_to_param output[:fq], build_query(filters), false
|
33
|
+
end
|
34
|
+
if phrase_filters = input.delete(:phrase_filters)
|
35
|
+
output[:fq] = append_to_param output[:fq], build_query(phrase_filters, true), false
|
36
|
+
end
|
37
|
+
if facets = input.delete(:facets)
|
38
|
+
output[:facet] = true
|
39
|
+
output['facet.field'] = append_to_param output['facet.field'], build_query(facets.values), false
|
40
|
+
end
|
41
|
+
output.merge input
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
module QueryHelpers
|
47
|
+
|
48
|
+
# Wraps a string around double quotes
|
49
|
+
def quote(value)
|
50
|
+
%("#{value}")
|
51
|
+
end
|
52
|
+
|
53
|
+
# builds a solr range query from a Range object
|
54
|
+
def build_range(r)
|
55
|
+
"[#{r.min} TO #{r.max}]"
|
56
|
+
end
|
57
|
+
|
58
|
+
# builds a solr query fragment
|
59
|
+
# if "quote_string" is true, the values will be quoted.
|
60
|
+
# if "value" is a string/symbol, the #to_s method is called
|
61
|
+
# if the "value" is an array, each item in the array is
|
62
|
+
# send to build_query (recursive)
|
63
|
+
# if the "value" is a Hash, a fielded query is built
|
64
|
+
# where the keys are used as the field names and
|
65
|
+
# the values are either processed as a Range or
|
66
|
+
# passed back into build_query (recursive)
|
67
|
+
def build_query(value, quote_string=false)
|
68
|
+
case value
|
69
|
+
when String,Symbol,Numeric
|
70
|
+
quote_string ? quote(value.to_s) : value.to_s
|
71
|
+
when Array
|
72
|
+
value.collect do |v|
|
73
|
+
build_query(v, quote_string)
|
74
|
+
end.flatten
|
75
|
+
when Range
|
76
|
+
build_range(value)
|
77
|
+
when Hash
|
78
|
+
return value.collect do |(k,v)|
|
79
|
+
if v.is_a?(Range)
|
80
|
+
"#{k}:#{build_range(v)}"
|
81
|
+
# If the value is an array, we want the same param, multiple times (not a query join)
|
82
|
+
elsif v.is_a?(Array)
|
83
|
+
v.collect do |vv|
|
84
|
+
"#{k}:#{build_query(vv, quote_string)}"
|
85
|
+
end
|
86
|
+
else
|
87
|
+
"#{k}:#{build_query(v, quote_string)}"
|
88
|
+
end
|
89
|
+
end.flatten
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# creates an array where the "existing_value" param is first
|
94
|
+
# and the "new_value" is the last.
|
95
|
+
# All empty/nil items are removed.
|
96
|
+
# the return result is either the result of the
|
97
|
+
# array being joined on a space, or the array itself.
|
98
|
+
# "auto_join" should be true or false.
|
99
|
+
def append_to_param(existing_value, new_value, auto_join=true)
|
100
|
+
values = [existing_value, new_value]
|
101
|
+
values.delete_if{|v|v.nil?}
|
102
|
+
auto_join ? values.join(' ') : values.flatten
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
extend QueryHelpers
|
108
|
+
extend Params
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module GSolr::Ext::Response
|
2
|
+
|
3
|
+
autoload :Docs, 'gsolr_ext/response/docs'
|
4
|
+
autoload :Facets, 'gsolr_ext/response/facets'
|
5
|
+
autoload :Spelling, 'gsolr_ext/response/spelling'
|
6
|
+
|
7
|
+
class Base < Mash
|
8
|
+
|
9
|
+
attr :original_hash
|
10
|
+
attr_reader :request_path, :request_params
|
11
|
+
|
12
|
+
def initialize hash, handler, request_params
|
13
|
+
super hash
|
14
|
+
@original_hash = hash
|
15
|
+
@request_path, @request_params = request_path, request_params
|
16
|
+
extend Response
|
17
|
+
extend Docs
|
18
|
+
extend Facets
|
19
|
+
extend Spelling
|
20
|
+
end
|
21
|
+
|
22
|
+
def header
|
23
|
+
self['responseHeader']
|
24
|
+
end
|
25
|
+
|
26
|
+
def rows
|
27
|
+
params[:rows].to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def params
|
31
|
+
(header and header['params']) ? header['params'] : request_params
|
32
|
+
end
|
33
|
+
|
34
|
+
def ok?
|
35
|
+
(header and header['status']) ? header['status'] == 0 : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing *args, &blk
|
39
|
+
self.original_hash.send *args, &blk
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
module Response
|
45
|
+
|
46
|
+
def response
|
47
|
+
self[:response]
|
48
|
+
end
|
49
|
+
|
50
|
+
# short cut to response['numFound']
|
51
|
+
def total
|
52
|
+
response[:numFound].to_s.to_i
|
53
|
+
end
|
54
|
+
|
55
|
+
def total
|
56
|
+
response[:numFound].to_s.to_i
|
57
|
+
end
|
58
|
+
|
59
|
+
def start
|
60
|
+
response[:start].to_s.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module GSolr::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 GSolr::Ext::Doc }
|
46
|
+
d.extend Pageable
|
47
|
+
d.per_page = base.rows
|
48
|
+
d.start = base.start
|
49
|
+
d.total = base.total
|
50
|
+
end
|
51
|
+
|
52
|
+
def docs
|
53
|
+
response['docs']
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|