sunspot 0.9.7
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 +83 -0
- data/LICENSE +18 -0
- data/README.rdoc +154 -0
- data/Rakefile +9 -0
- data/TODO +9 -0
- data/VERSION.yml +4 -0
- data/bin/sunspot-configure-solr +46 -0
- data/bin/sunspot-solr +62 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot.rb +469 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/composite_setup.rb +186 -0
- data/lib/sunspot/configuration.rb +38 -0
- data/lib/sunspot/data_extractor.rb +47 -0
- data/lib/sunspot/dsl.rb +3 -0
- data/lib/sunspot/dsl/field_query.rb +72 -0
- data/lib/sunspot/dsl/fields.rb +86 -0
- data/lib/sunspot/dsl/query.rb +59 -0
- data/lib/sunspot/dsl/query_facet.rb +31 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/scope.rb +193 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/facet.rb +16 -0
- data/lib/sunspot/facet_data.rb +120 -0
- data/lib/sunspot/facet_row.rb +10 -0
- data/lib/sunspot/field.rb +157 -0
- data/lib/sunspot/field_factory.rb +126 -0
- data/lib/sunspot/indexer.rb +123 -0
- data/lib/sunspot/instantiated_facet.rb +42 -0
- data/lib/sunspot/instantiated_facet_row.rb +22 -0
- data/lib/sunspot/query.rb +191 -0
- data/lib/sunspot/query/base_query.rb +90 -0
- data/lib/sunspot/query/connective.rb +126 -0
- data/lib/sunspot/query/dynamic_query.rb +69 -0
- data/lib/sunspot/query/field_facet.rb +151 -0
- data/lib/sunspot/query/field_query.rb +63 -0
- data/lib/sunspot/query/pagination.rb +39 -0
- data/lib/sunspot/query/query_facet.rb +73 -0
- data/lib/sunspot/query/query_facet_row.rb +19 -0
- data/lib/sunspot/query/query_field_facet.rb +13 -0
- data/lib/sunspot/query/restriction.rb +233 -0
- data/lib/sunspot/query/scope.rb +165 -0
- data/lib/sunspot/query/sort.rb +36 -0
- data/lib/sunspot/query/sort_composite.rb +33 -0
- data/lib/sunspot/schema.rb +165 -0
- data/lib/sunspot/search.rb +219 -0
- data/lib/sunspot/search/hit.rb +66 -0
- data/lib/sunspot/session.rb +201 -0
- data/lib/sunspot/setup.rb +271 -0
- data/lib/sunspot/type.rb +200 -0
- data/lib/sunspot/util.rb +164 -0
- data/solr/etc/jetty.xml +212 -0
- data/solr/etc/webdefault.xml +379 -0
- data/solr/lib/jetty-6.1.3.jar +0 -0
- data/solr/lib/jetty-util-6.1.3.jar +0 -0
- data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr/solr/conf/elevate.xml +36 -0
- data/solr/solr/conf/protwords.txt +21 -0
- data/solr/solr/conf/schema.xml +50 -0
- data/solr/solr/conf/solrconfig.xml +696 -0
- data/solr/solr/conf/stopwords.txt +57 -0
- data/solr/solr/conf/synonyms.txt +31 -0
- data/solr/start.jar +0 -0
- data/solr/webapps/solr.war +0 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/build_search_spec.rb +1039 -0
- data/spec/api/indexer_spec.rb +311 -0
- data/spec/api/query_spec.rb +153 -0
- data/spec/api/search_retrieval_spec.rb +362 -0
- data/spec/api/session_spec.rb +157 -0
- data/spec/api/spec_helper.rb +1 -0
- data/spec/api/sunspot_spec.rb +18 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/integration/faceting_spec.rb +169 -0
- data/spec/integration/keyword_search_spec.rb +83 -0
- data/spec/integration/scoped_search_spec.rb +289 -0
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/integration/test_pagination.rb +32 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +19 -0
- data/spec/mocks/connection.rb +84 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_record.rb +48 -0
- data/spec/mocks/photo.rb +8 -0
- data/spec/mocks/post.rb +73 -0
- data/spec/mocks/user.rb +8 -0
- data/spec/spec_helper.rb +47 -0
- data/tasks/gemspec.rake +25 -0
- data/tasks/rcov.rake +28 -0
- data/tasks/rdoc.rake +22 -0
- data/tasks/schema.rake +19 -0
- data/tasks/spec.rake +24 -0
- data/tasks/todo.rake +4 -0
- data/templates/schema.xml.haml +24 -0
- metadata +246 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
module Connective #:nodoc:
|
4
|
+
#
|
5
|
+
# Base class for connectives (conjunctions and disjunctions).
|
6
|
+
#
|
7
|
+
class Abstract < Scope
|
8
|
+
def initialize(setup, negated = false) #:nodoc:
|
9
|
+
@setup, @negated = setup, negated
|
10
|
+
@components = []
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Connective as solr params.
|
15
|
+
#
|
16
|
+
def to_params #:nodoc:
|
17
|
+
{ :fq => to_boolean_phrase }
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Express the connective as a Lucene boolean phrase.
|
22
|
+
#
|
23
|
+
def to_boolean_phrase #:nodoc:
|
24
|
+
phrase = if @components.length == 1
|
25
|
+
@components.first.to_boolean_phrase
|
26
|
+
else
|
27
|
+
component_phrases = @components.map do |component|
|
28
|
+
component.to_boolean_phrase
|
29
|
+
end
|
30
|
+
"(#{component_phrases.join(" #{connector} ")})"
|
31
|
+
end
|
32
|
+
if negated?
|
33
|
+
"-#{phrase}"
|
34
|
+
else
|
35
|
+
phrase
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Add a component to the connective. All components must implement the
|
41
|
+
# #to_boolean_phrase method.
|
42
|
+
#
|
43
|
+
def add_component(component) #:nodoc:
|
44
|
+
@components << component
|
45
|
+
end
|
46
|
+
|
47
|
+
def negated?
|
48
|
+
@negated
|
49
|
+
end
|
50
|
+
|
51
|
+
def negate
|
52
|
+
negated = self.class.new(@setup, !negated?)
|
53
|
+
for component in @components
|
54
|
+
negated.add_component(component)
|
55
|
+
end
|
56
|
+
negated
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Disjunctions combine their components with an OR operator.
|
62
|
+
#
|
63
|
+
class Disjunction < Abstract
|
64
|
+
class <<self
|
65
|
+
def inverse
|
66
|
+
Conjunction
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_boolean_phrase
|
71
|
+
if @components.any? { |component| component.negated? }
|
72
|
+
denormalize.to_boolean_phrase
|
73
|
+
else
|
74
|
+
super
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Add a conjunction to the disjunction. This overrides the method in
|
80
|
+
# the Scope class since scopes are implicitly conjunctive and thus
|
81
|
+
# can return themselves as a conjunction. Inside a disjunction, however,
|
82
|
+
# a conjunction must explicitly be created.
|
83
|
+
#
|
84
|
+
def add_conjunction
|
85
|
+
@components << conjunction = Conjunction.new(setup)
|
86
|
+
conjunction
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_disjunction
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def connector
|
96
|
+
'OR'
|
97
|
+
end
|
98
|
+
|
99
|
+
def denormalize
|
100
|
+
denormalized = self.class.inverse.new(@setup, !negated?)
|
101
|
+
for component in @components
|
102
|
+
denormalized.add_component(component.negate)
|
103
|
+
end
|
104
|
+
denormalized
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Conjunctions combine their components with an AND operator.
|
110
|
+
#
|
111
|
+
class Conjunction < Abstract
|
112
|
+
class <<self
|
113
|
+
def inverse
|
114
|
+
Disjunction
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def connector
|
121
|
+
'AND'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# A dynamic query is a proxy object that implements the API of the FieldQuery
|
5
|
+
# class, but wraps a dynamic field factory and thus applies the query
|
6
|
+
# components using dynamic field instances.
|
7
|
+
#--
|
8
|
+
# Dynamic queries do not hold their own state, but rather proxy to the query
|
9
|
+
# that generated them, adding components directly to the owning query's
|
10
|
+
# internal state.
|
11
|
+
#++
|
12
|
+
# DynamicQuery instances are publicly generated by the Query#dynamic_query
|
13
|
+
# factory method.
|
14
|
+
#
|
15
|
+
class DynamicQuery < FieldQuery
|
16
|
+
def initialize(dynamic_field_factory, query) #:nodoc:
|
17
|
+
@dynamic_field_factory, @query = dynamic_field_factory, query
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# This has the same effect as calling Query#exclude_instance; it is
|
22
|
+
# included for interface completeness.
|
23
|
+
#
|
24
|
+
def exclude_instance(instance)
|
25
|
+
@query.exclude_instance(instance)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# This has the same effect as calling Query#exclude_instance; it is
|
30
|
+
# included for interface completeness.
|
31
|
+
#
|
32
|
+
def dynamic_query(field_name)
|
33
|
+
@query.dynamic_query(field_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Add a Sort to the query
|
38
|
+
#
|
39
|
+
def add_sort(sort) #:nodoc:
|
40
|
+
@query.add_sort(sort)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Add a component to the query
|
45
|
+
#
|
46
|
+
def add_component(component) #:nodoc:
|
47
|
+
@query.add_component(component)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
#
|
53
|
+
# DynamicFieldFactory implements the part of the Setup interface that we
|
54
|
+
# need, so methods in DynamicQuery's superclasses can rely on it without
|
55
|
+
# knowing what it is.
|
56
|
+
#
|
57
|
+
def setup
|
58
|
+
@dynamic_field_factory
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# So query facets can be added to the query from within dynamic queries
|
63
|
+
#
|
64
|
+
def query_facets
|
65
|
+
@query.query_facets
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Sunspot
|
4
|
+
module Query
|
5
|
+
#
|
6
|
+
# Encapsulates a query component representing a field facet. Users create
|
7
|
+
# instances using DSL::Query#facet
|
8
|
+
#
|
9
|
+
class FieldFacet #:nodoc:
|
10
|
+
class <<self
|
11
|
+
protected :new
|
12
|
+
|
13
|
+
#
|
14
|
+
# Return the appropriate FieldFacet instance for the field and options.
|
15
|
+
# If a :time_range option is specified, and the field type is TimeType,
|
16
|
+
# build a DateFieldFacet. Otherwise, build a normal FieldFacet.
|
17
|
+
#
|
18
|
+
# ==== Returns
|
19
|
+
#
|
20
|
+
# FieldFacet:: FieldFacet instance of appropriate class.
|
21
|
+
#
|
22
|
+
def build(field, options)
|
23
|
+
if options.has_key?(:time_range)
|
24
|
+
unless field.type == Type::TimeType
|
25
|
+
raise(
|
26
|
+
ArgumentError,
|
27
|
+
":time_range key can only be specified for time fields"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
DateFieldFacet.new(field, options)
|
31
|
+
elsif options.has_key?(:only)
|
32
|
+
QueryFieldFacet.new(field, options.delete(:only))
|
33
|
+
else
|
34
|
+
FieldFacet.new(field, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(field, options)
|
40
|
+
@field, @options = field, options
|
41
|
+
end
|
42
|
+
|
43
|
+
# ==== Returns
|
44
|
+
#
|
45
|
+
# Hash:: solr-ruby params for this field facet
|
46
|
+
#
|
47
|
+
def to_params
|
48
|
+
params = { :"facet.field" => [@field.indexed_name], :facet => 'true' }
|
49
|
+
params[param_key(:sort)] =
|
50
|
+
case @options[:sort]
|
51
|
+
when :count then 'true'
|
52
|
+
when :index then 'false'
|
53
|
+
when nil
|
54
|
+
else raise(ArgumentError, 'Allowed facet sort options are :count and :index')
|
55
|
+
end
|
56
|
+
params[param_key(:limit)] = @options[:limit]
|
57
|
+
params[param_key(:mincount)] =
|
58
|
+
if @options[:minimum_count] then @options[:minimum_count]
|
59
|
+
elsif @options[:zeros] then 0
|
60
|
+
else 1
|
61
|
+
end
|
62
|
+
params
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
#
|
68
|
+
# Given a facet parameter name, return the appropriate Solr parameter for
|
69
|
+
# this facet.
|
70
|
+
#
|
71
|
+
# ==== Returns
|
72
|
+
#
|
73
|
+
# Symbol:: Solr query parameter key
|
74
|
+
#
|
75
|
+
def param_key(name)
|
76
|
+
:"f.#{@field.indexed_name}.facet.#{name}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class DateFieldFacet < FieldFacet #:nodoc:
|
81
|
+
ALLOWED_OTHER = Set.new(%w(before after between none all))
|
82
|
+
|
83
|
+
#
|
84
|
+
# Convert the facet to date params.
|
85
|
+
#
|
86
|
+
def to_params
|
87
|
+
super.merge(
|
88
|
+
:"facet.date" => [@field.indexed_name],
|
89
|
+
param_key('date.start') => start_time.utc.xmlschema,
|
90
|
+
param_key('date.end') => end_time.utc.xmlschema,
|
91
|
+
param_key('date.gap') => "+#{interval}SECONDS",
|
92
|
+
param_key('date.other') => others
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
#
|
99
|
+
# Start time for facet range
|
100
|
+
#
|
101
|
+
# ==== Returns
|
102
|
+
#
|
103
|
+
# Time:: Start time
|
104
|
+
#
|
105
|
+
def start_time
|
106
|
+
@options[:time_range].first
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# End time for facet range
|
111
|
+
#
|
112
|
+
# ==== Returns
|
113
|
+
#
|
114
|
+
# Time:: End time
|
115
|
+
#
|
116
|
+
def end_time
|
117
|
+
@options[:time_range].last
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Time interval that each facet row should cover. Default is 1 day.
|
122
|
+
#
|
123
|
+
# ===== Returns
|
124
|
+
#
|
125
|
+
# Integer:: Time interval in seconds
|
126
|
+
#
|
127
|
+
def interval
|
128
|
+
@options[:time_interval] || 86400
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Other time ranges to create facet rows for. Allowed values are defined
|
133
|
+
# in ALLOWED_OTHER constant.
|
134
|
+
#
|
135
|
+
def others
|
136
|
+
if others = @options[:time_other]
|
137
|
+
Array(others).map do |other|
|
138
|
+
other = other.to_s
|
139
|
+
unless ALLOWED_OTHER.include?(other)
|
140
|
+
raise(
|
141
|
+
ArgumentError,
|
142
|
+
"#{other.inspect} is not a valid argument for :time_other"
|
143
|
+
)
|
144
|
+
end
|
145
|
+
other
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# This class acts as a base class for query components that encapsulate
|
5
|
+
# operations on fields. It is subclassed by the Query::Query class and the
|
6
|
+
# Query::DynamicQuery class.
|
7
|
+
#
|
8
|
+
class FieldQuery < Scope
|
9
|
+
#
|
10
|
+
# Add a field facet. See Sunspot::Facet for more information.
|
11
|
+
#
|
12
|
+
# ==== Parameters
|
13
|
+
#
|
14
|
+
# field_name<Symbol>:: Name of the field on which to get a facet
|
15
|
+
#
|
16
|
+
# ==== Returns
|
17
|
+
#
|
18
|
+
# FieldFacet:: The field facet object
|
19
|
+
#
|
20
|
+
def add_field_facet(field_name, options = nil)
|
21
|
+
options ||= {}
|
22
|
+
facet =
|
23
|
+
if only = options.delete(:only)
|
24
|
+
query_facets[field_name.to_sym] = QueryFieldFacet.new(@setup.field(field_name), only)
|
25
|
+
else
|
26
|
+
FieldFacet.build(build_field(field_name), options)
|
27
|
+
end
|
28
|
+
add_component(facet)
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Add a query facet.
|
33
|
+
#
|
34
|
+
# ==== Parameters
|
35
|
+
#
|
36
|
+
# name<Symbol>::
|
37
|
+
# The name associated with the query facet. This is not passed to Solr,
|
38
|
+
# but allows the user to retrieve the facet result by passing the name
|
39
|
+
# to the Search#facet method.
|
40
|
+
#
|
41
|
+
# ==== Returns
|
42
|
+
#
|
43
|
+
# QueryFacet:: The query facet object
|
44
|
+
#
|
45
|
+
def add_query_facet(name)
|
46
|
+
add_component(facet = QueryFacet.new(name, setup))
|
47
|
+
query_facets[name.to_sym] = facet
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Set result ordering.
|
52
|
+
#
|
53
|
+
# ==== Parameters
|
54
|
+
#
|
55
|
+
# field_name<Symbol>:: Name of the field on which to order
|
56
|
+
# direction<Symbol>:: :asc or :desc (default :asc)
|
57
|
+
#
|
58
|
+
def order_by(field_name, direction = nil)
|
59
|
+
add_sort(Sort.new(build_field(field_name), direction))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Query
|
3
|
+
#
|
4
|
+
# A query component that holds information about pagination. Unlike other
|
5
|
+
# query components, this one is mutable, because the query itself holds a
|
6
|
+
# reference to it and updates it if pagination is changed.
|
7
|
+
#
|
8
|
+
class Pagination #:nodoc:
|
9
|
+
attr_reader :page, :per_page
|
10
|
+
|
11
|
+
def initialize(configuration, page = nil, per_page = nil)
|
12
|
+
@configuration = configuration
|
13
|
+
self.page, self.per_page = page, per_page
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_params
|
17
|
+
{ :start => start, :rows => rows }
|
18
|
+
end
|
19
|
+
|
20
|
+
def page=(page)
|
21
|
+
@page = page || 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def per_page=(per_page)
|
25
|
+
@per_page = per_page || @configuration.pagination.default_per_page
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def start
|
31
|
+
(@page - 1) * @per_page
|
32
|
+
end
|
33
|
+
|
34
|
+
def rows
|
35
|
+
@per_page
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|