UnderpantsGnome-sunspot 0.9.1.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.
Files changed (103) hide show
  1. data/History.txt +39 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +4 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +62 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot.rb +470 -0
  11. data/lib/sunspot/adapters.rb +265 -0
  12. data/lib/sunspot/composite_setup.rb +186 -0
  13. data/lib/sunspot/configuration.rb +38 -0
  14. data/lib/sunspot/data_extractor.rb +47 -0
  15. data/lib/sunspot/date_facet.rb +36 -0
  16. data/lib/sunspot/date_facet_row.rb +17 -0
  17. data/lib/sunspot/dsl.rb +3 -0
  18. data/lib/sunspot/dsl/field_query.rb +72 -0
  19. data/lib/sunspot/dsl/fields.rb +86 -0
  20. data/lib/sunspot/dsl/query.rb +59 -0
  21. data/lib/sunspot/dsl/query_facet.rb +31 -0
  22. data/lib/sunspot/dsl/restriction.rb +25 -0
  23. data/lib/sunspot/dsl/scope.rb +193 -0
  24. data/lib/sunspot/dsl/search.rb +30 -0
  25. data/lib/sunspot/facet.rb +51 -0
  26. data/lib/sunspot/facet_row.rb +34 -0
  27. data/lib/sunspot/field.rb +157 -0
  28. data/lib/sunspot/field_factory.rb +126 -0
  29. data/lib/sunspot/indexer.rb +127 -0
  30. data/lib/sunspot/instantiated_facet.rb +38 -0
  31. data/lib/sunspot/instantiated_facet_row.rb +12 -0
  32. data/lib/sunspot/query.rb +190 -0
  33. data/lib/sunspot/query/base_query.rb +90 -0
  34. data/lib/sunspot/query/connective.rb +77 -0
  35. data/lib/sunspot/query/dynamic_query.rb +69 -0
  36. data/lib/sunspot/query/field_facet.rb +149 -0
  37. data/lib/sunspot/query/field_query.rb +57 -0
  38. data/lib/sunspot/query/pagination.rb +39 -0
  39. data/lib/sunspot/query/query_facet.rb +72 -0
  40. data/lib/sunspot/query/query_facet_row.rb +19 -0
  41. data/lib/sunspot/query/restriction.rb +225 -0
  42. data/lib/sunspot/query/scope.rb +165 -0
  43. data/lib/sunspot/query/sort.rb +36 -0
  44. data/lib/sunspot/query/sort_composite.rb +33 -0
  45. data/lib/sunspot/query_facet.rb +33 -0
  46. data/lib/sunspot/query_facet_row.rb +21 -0
  47. data/lib/sunspot/schema.rb +165 -0
  48. data/lib/sunspot/search.rb +222 -0
  49. data/lib/sunspot/search/hit.rb +62 -0
  50. data/lib/sunspot/session.rb +201 -0
  51. data/lib/sunspot/setup.rb +271 -0
  52. data/lib/sunspot/type.rb +200 -0
  53. data/lib/sunspot/util.rb +164 -0
  54. data/solr/etc/jetty.xml +212 -0
  55. data/solr/etc/webdefault.xml +379 -0
  56. data/solr/lib/jetty-6.1.3.jar +0 -0
  57. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  58. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  59. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  60. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  61. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  62. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  63. data/solr/solr/conf/elevate.xml +36 -0
  64. data/solr/solr/conf/protwords.txt +21 -0
  65. data/solr/solr/conf/schema.xml +50 -0
  66. data/solr/solr/conf/solrconfig.xml +696 -0
  67. data/solr/solr/conf/stopwords.txt +57 -0
  68. data/solr/solr/conf/synonyms.txt +31 -0
  69. data/solr/start.jar +0 -0
  70. data/solr/webapps/solr.war +0 -0
  71. data/spec/api/adapters_spec.rb +33 -0
  72. data/spec/api/build_search_spec.rb +918 -0
  73. data/spec/api/indexer_spec.rb +311 -0
  74. data/spec/api/query_spec.rb +153 -0
  75. data/spec/api/search_retrieval_spec.rb +325 -0
  76. data/spec/api/session_spec.rb +157 -0
  77. data/spec/api/spec_helper.rb +1 -0
  78. data/spec/api/sunspot_spec.rb +18 -0
  79. data/spec/integration/dynamic_fields_spec.rb +55 -0
  80. data/spec/integration/faceting_spec.rb +169 -0
  81. data/spec/integration/keyword_search_spec.rb +83 -0
  82. data/spec/integration/scoped_search_spec.rb +188 -0
  83. data/spec/integration/spec_helper.rb +1 -0
  84. data/spec/integration/stored_fields_spec.rb +10 -0
  85. data/spec/integration/test_pagination.rb +32 -0
  86. data/spec/mocks/adapters.rb +32 -0
  87. data/spec/mocks/blog.rb +3 -0
  88. data/spec/mocks/comment.rb +19 -0
  89. data/spec/mocks/connection.rb +84 -0
  90. data/spec/mocks/mock_adapter.rb +30 -0
  91. data/spec/mocks/mock_record.rb +41 -0
  92. data/spec/mocks/photo.rb +8 -0
  93. data/spec/mocks/post.rb +70 -0
  94. data/spec/mocks/user.rb +8 -0
  95. data/spec/spec_helper.rb +47 -0
  96. data/tasks/gemspec.rake +25 -0
  97. data/tasks/rcov.rake +28 -0
  98. data/tasks/rdoc.rake +21 -0
  99. data/tasks/schema.rake +19 -0
  100. data/tasks/spec.rake +24 -0
  101. data/tasks/todo.rake +4 -0
  102. data/templates/schema.xml.haml +24 -0
  103. metadata +245 -0
@@ -0,0 +1,38 @@
1
+ module Sunspot
2
+ #
3
+ # InstantiatedFacet instances allow access to a model instance based on a
4
+ # primary key stored in facet rows' values. The rows are hydrated lazily, but
5
+ # all rows are hydrated the first time #instance is called on any of the rows.
6
+ #
7
+ # The #rows method returns InstantiatedFacetRow objects.
8
+ #
9
+ class InstantiatedFacet < Facet
10
+ #
11
+ # Hydrate all rows for the facet. For data accessors that can efficiently
12
+ # batch load, this is more efficient than individually lazy-loading
13
+ # instances for each row, but allows us to still stay lazy and not do work
14
+ # in the persistent store if the instances are not needed.
15
+ #
16
+ def populate_instances! #:nodoc:
17
+ ids = rows.map { |row| row.value }
18
+ reference_class = Sunspot::Util.full_const_get(@field.reference.to_s)
19
+ accessor = Adapters::DataAccessor.create(reference_class)
20
+ instance_map = accessor.load_all(ids).inject({}) do |map, instance|
21
+ map[Adapters::InstanceAdapter.adapt(instance).id] = instance
22
+ map
23
+ end
24
+ for row in rows
25
+ row.instance = instance_map[row.value]
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ #
32
+ # Override the Facet#new_row method to return an InstantiateFacetRow
33
+ #
34
+ def new_row(pair)
35
+ InstantiatedFacetRow.new(pair, self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ module Sunspot
2
+ class InstantiatedFacetRow < FacetRow
3
+ attr_writer :instance
4
+
5
+ def instance
6
+ unless defined?(@instance)
7
+ @facet.populate_instances!
8
+ end
9
+ @instance
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,190 @@
1
+ %w(base_query scope field_query connective dynamic_query field_facet query_facet
2
+ query_facet_row pagination restriction sort sort_composite).each do |file|
3
+ require File.join(File.dirname(__FILE__), 'query', file)
4
+ end
5
+
6
+ module Sunspot
7
+ module Query #:nodoc:
8
+ #
9
+ # This class encapsulates a query that is to be sent to Solr. The query is
10
+ # constructed in the block passed to the Sunspot.search method, using the
11
+ # Sunspot::DSL::Query interface. It can also be accessed directly by calling
12
+ # #query on a Search object (presumably a not-yet-run one created using
13
+ # Sunspot#new_search), which might be more suitable than the DSL when an
14
+ # intermediate object has responsibility for building the query dynamically.
15
+ #--
16
+ # Instances of Query, as well as all of the components it contains, respond to
17
+ # the #to_params method, which returns a hash of parameters in the format
18
+ # recognized by the solr-ruby API.
19
+ #
20
+ class Query < FieldQuery
21
+ attr_reader :query_facets #:nodoc:
22
+
23
+ def initialize(setup, configuration) #:nodoc:
24
+ @setup, @configuration = setup, configuration
25
+ @components = []
26
+ @query_facets = {}
27
+ @components << @base_query = BaseQuery.new(setup)
28
+ @components << @pagination = Pagination.new(@configuration)
29
+ @components << @sort = SortComposite.new
30
+ end
31
+
32
+ #
33
+ # Set the keywords for this query. Keywords are parsed with Solr's dismax
34
+ # handler.
35
+ #
36
+ def keywords=(keywords)
37
+ set_keywords(keywords)
38
+ end
39
+
40
+ #
41
+ # Add a component to the query. Used by objects that proxy to the query
42
+ # object.
43
+ #
44
+ # ==== Parameters
45
+ #
46
+ # component<~to_params>:: Query component to add.
47
+ #
48
+ def add_component(component) #:nodoc:
49
+ @components << component
50
+ end
51
+
52
+ #
53
+ # Sets @start and @rows instance variables using pagination semantics
54
+ #
55
+ # ==== Parameters
56
+ #
57
+ # page<Integer>:: Page on which to start
58
+ # per_page<Integer>::
59
+ # How many rows to display per page. Default taken from
60
+ # Sunspot.config.pagination.default_per_page
61
+ #
62
+ def paginate(page, per_page = nil)
63
+ @pagination.page, @pagination.per_page = page, per_page
64
+ end
65
+
66
+ #
67
+ # Add random ordering to the search. This can be added after other
68
+ # field-based sorts if desired.
69
+ #
70
+ def order_by_random
71
+ add_sort(Sort.new(RandomField.new))
72
+ end
73
+
74
+ #
75
+ # Representation of this query as solr-ruby parameters. Constructs the hash
76
+ # by deep-merging scope and facet parameters, adding in various other
77
+ # parameters from instance data.
78
+ #
79
+ # Note that solr-ruby takes the :q parameter as a separate argument; for
80
+ # the sake of consistency, the Query object ignores this fact (the Search
81
+ # object extracts it back out).
82
+ #
83
+ # ==== Returns
84
+ #
85
+ # Hash:: Representation of query in solr-ruby form
86
+ #
87
+ def to_params #:nodoc:
88
+ params = {}
89
+ query_components = []
90
+ for component in @components
91
+ Util.deep_merge!(params, component.to_params)
92
+ end
93
+ params
94
+ end
95
+
96
+ #
97
+ # Page that this query will return (used by Sunspot::Search to expose
98
+ # pagination)
99
+ #
100
+ # ==== Returns
101
+ #
102
+ # Integer:: Page number
103
+ #
104
+ def page #:nodoc:
105
+ @pagination.page
106
+ end
107
+
108
+ #
109
+ # Number of rows per page that this query will return (used by
110
+ # Sunspot::Search to expose pagination)
111
+ #
112
+ # ==== Returns
113
+ #
114
+ # Integer:: Rows per page
115
+ #
116
+ def per_page #:nodoc:
117
+ @pagination.per_page
118
+ end
119
+
120
+ #
121
+ # Get the query facet with the given name. Used by the Search object to
122
+ # match query facet results with the requested query facets.
123
+ #
124
+ def query_facet(name) #:nodoc:
125
+ @query_facets[name.to_sym]
126
+ end
127
+
128
+ #
129
+ # Add a Sort object into this query's sort composite.
130
+ #
131
+ def add_sort(sort) #:nodoc:
132
+ @sort << sort
133
+ end
134
+
135
+ #
136
+ # Set the keywords for this query, along with keyword options. See
137
+ # Query::BaseQuery for information on what the options do.
138
+ #
139
+ def set_keywords(keywords, options = {}) #:nodoc:
140
+ @base_query.keywords = keywords
141
+ @base_query.keyword_options = options
142
+ end
143
+
144
+ #
145
+ # Pass in search options as a hash. This is not the preferred way of
146
+ # building a Sunspot search, but it is made available as experience shows
147
+ # Ruby developers like to pass in hashes. Probably nice for quick one-offs
148
+ # on the console, anyway.
149
+ #
150
+ # ==== Options (+options+)
151
+ #
152
+ # :keywords:: Keyword string for fulltext search
153
+ # :conditions::
154
+ # Hash of key-value pairs, where keys are field names, and values are one
155
+ # of scalar, Array, or Range. Scalars are evaluated as EqualTo
156
+ # restrictions; Arrays are AnyOf restrictions, and Ranges are Between
157
+ # restrictions.
158
+ # :order::
159
+ # Order the search results. Either a string or array of strings of the
160
+ # form "field_name direction"
161
+ # :page::
162
+ # Page to use for pagination
163
+ # :per_page::
164
+ # Number of results to show per page
165
+ #
166
+ def options=(options) #:nodoc:
167
+ if options.has_key?(:keywords)
168
+ self.keywords = options[:keywords]
169
+ end
170
+ if options.has_key?(:conditions)
171
+ options[:conditions].each_pair do |field_name, value|
172
+ begin
173
+ add_shorthand_restriction(field_name, value)
174
+ rescue UnrecognizedFieldError
175
+ # ignore fields we don't recognize
176
+ end
177
+ end
178
+ end
179
+ if options.has_key?(:order)
180
+ for order in Array(options[:order])
181
+ order_by(*order.split(' '))
182
+ end
183
+ end
184
+ if options.has_key?(:page)
185
+ paginate(options[:page], options[:per_page])
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,90 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # Encapsulates information common to all queries - in particular, keywords
5
+ # and types.
6
+ #
7
+ class BaseQuery #:nodoc:
8
+ include RSolr::Char
9
+
10
+ attr_writer :keywords
11
+
12
+ def initialize(setup)
13
+ @setup = setup
14
+ end
15
+
16
+ #
17
+ # Generate params for the base query. If keywords are specified, build
18
+ # params for a dismax query, request all stored fields plus the score,
19
+ # and put the types in a filter query. If keywords are not specified,
20
+ # put the types query in the q parameter.
21
+ #
22
+ def to_params
23
+ params = {}
24
+ if @keywords
25
+ params[:q] = @keywords
26
+ params[:fl] = '* score'
27
+ params[:fq] = types_phrase
28
+ params[:qf] = text_field_names.join(' ')
29
+ params[:defType] = 'dismax'
30
+ else
31
+ params[:q] = types_phrase
32
+ end
33
+ params
34
+ end
35
+
36
+ #
37
+ # Set keyword options
38
+ #
39
+ def keyword_options=(options)
40
+ if options
41
+ @text_field_names = options.delete(:fields)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ #
48
+ # Boolean phrase that restricts results to objects of the type(s) under
49
+ # query. If this is an open query (no types specified) then it sends a
50
+ # no-op phrase because Solr requires that the :q parameter not be empty.
51
+ #
52
+ # ==== Returns
53
+ #
54
+ # String:: Boolean phrase for type restriction
55
+ #
56
+ def types_phrase
57
+ if escaped_types.length == 1 then "type:#{escaped_types.first}"
58
+ else "type:(#{escaped_types * ' OR '})"
59
+ end
60
+ end
61
+
62
+ #
63
+ # Wraps each type in quotes to escape names of the form Namespace::Class
64
+ #
65
+ def escaped_types
66
+ @escaped_types ||=
67
+ @setup.type_names.map { |name| escape(name)}
68
+ end
69
+
70
+ #
71
+ # Returns the names of text fields that should be queried in a keyword
72
+ # search. If specific fields are requested, use those; otherwise use the
73
+ # union of all fields configured for the types under search.
74
+ #
75
+ def text_field_names
76
+ text_fields =
77
+ if @text_field_names
78
+ Array(@text_field_names).map do |field_name|
79
+ @setup.text_field(field_name.to_sym)
80
+ end
81
+ else
82
+ @setup.text_fields
83
+ end
84
+ text_fields.map do |text_field|
85
+ text_field.indexed_name
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,77 @@
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) #:nodoc:
9
+ @setup = setup
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
+ 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
+ end
33
+
34
+ #
35
+ # Add a component to the connective. All components must implement the
36
+ # #to_boolean_phrase method.
37
+ #
38
+ def add_component(component) #:nodoc:
39
+ @components << component
40
+ end
41
+ end
42
+
43
+ #
44
+ # Disjunctions combine their components with an OR operator.
45
+ #
46
+ class Disjunction < Abstract
47
+ #
48
+ # Add a conjunction to the disjunction. This overrides the method in
49
+ # the Scope class since scopes are implicitly conjunctive and thus
50
+ # can return themselves as a conjunction. Inside a disjunction, however,
51
+ # a conjunction must explicitly be created.
52
+ #
53
+ def add_conjunction
54
+ @components << conjunction = Conjunction.new(setup)
55
+ conjunction
56
+ end
57
+
58
+ private
59
+
60
+ def connector
61
+ 'OR'
62
+ end
63
+ end
64
+
65
+ #
66
+ # Conjunctions combine their components with an AND operator.
67
+ #
68
+ class Conjunction < Abstract
69
+ private
70
+
71
+ def connector
72
+ 'AND'
73
+ end
74
+ end
75
+ end
76
+ end
77
+ 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