pduey-sunspot 1.2.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 (166) hide show
  1. data/.gitignore +12 -0
  2. data/Gemfile +5 -0
  3. data/History.txt +225 -0
  4. data/LICENSE +18 -0
  5. data/Rakefile +15 -0
  6. data/TODO +13 -0
  7. data/VERSION.yml +4 -0
  8. data/bin/sunspot-installer +19 -0
  9. data/installer/config/schema.yml +95 -0
  10. data/lib/light_config.rb +40 -0
  11. data/lib/sunspot.rb +568 -0
  12. data/lib/sunspot/adapters.rb +265 -0
  13. data/lib/sunspot/composite_setup.rb +202 -0
  14. data/lib/sunspot/configuration.rb +46 -0
  15. data/lib/sunspot/data_extractor.rb +50 -0
  16. data/lib/sunspot/dsl.rb +5 -0
  17. data/lib/sunspot/dsl/adjustable.rb +47 -0
  18. data/lib/sunspot/dsl/field_query.rb +279 -0
  19. data/lib/sunspot/dsl/fields.rb +103 -0
  20. data/lib/sunspot/dsl/fulltext.rb +243 -0
  21. data/lib/sunspot/dsl/function.rb +14 -0
  22. data/lib/sunspot/dsl/functional.rb +44 -0
  23. data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
  24. data/lib/sunspot/dsl/paginatable.rb +28 -0
  25. data/lib/sunspot/dsl/query_facet.rb +36 -0
  26. data/lib/sunspot/dsl/restriction.rb +25 -0
  27. data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
  28. data/lib/sunspot/dsl/scope.rb +217 -0
  29. data/lib/sunspot/dsl/search.rb +30 -0
  30. data/lib/sunspot/dsl/standard_query.rb +121 -0
  31. data/lib/sunspot/field.rb +193 -0
  32. data/lib/sunspot/field_factory.rb +129 -0
  33. data/lib/sunspot/indexer.rb +131 -0
  34. data/lib/sunspot/installer.rb +31 -0
  35. data/lib/sunspot/installer/library_installer.rb +45 -0
  36. data/lib/sunspot/installer/schema_builder.rb +219 -0
  37. data/lib/sunspot/installer/solrconfig_updater.rb +76 -0
  38. data/lib/sunspot/installer/task_helper.rb +18 -0
  39. data/lib/sunspot/java.rb +8 -0
  40. data/lib/sunspot/query.rb +11 -0
  41. data/lib/sunspot/query/abstract_field_facet.rb +52 -0
  42. data/lib/sunspot/query/boost_query.rb +24 -0
  43. data/lib/sunspot/query/common_query.rb +85 -0
  44. data/lib/sunspot/query/composite_fulltext.rb +36 -0
  45. data/lib/sunspot/query/connective.rb +206 -0
  46. data/lib/sunspot/query/date_field_facet.rb +14 -0
  47. data/lib/sunspot/query/dismax.rb +128 -0
  48. data/lib/sunspot/query/field_facet.rb +41 -0
  49. data/lib/sunspot/query/filter.rb +38 -0
  50. data/lib/sunspot/query/function_query.rb +52 -0
  51. data/lib/sunspot/query/geo.rb +53 -0
  52. data/lib/sunspot/query/highlighting.rb +55 -0
  53. data/lib/sunspot/query/more_like_this.rb +61 -0
  54. data/lib/sunspot/query/more_like_this_query.rb +12 -0
  55. data/lib/sunspot/query/pagination.rb +38 -0
  56. data/lib/sunspot/query/query_facet.rb +16 -0
  57. data/lib/sunspot/query/restriction.rb +262 -0
  58. data/lib/sunspot/query/scope.rb +9 -0
  59. data/lib/sunspot/query/sort.rb +95 -0
  60. data/lib/sunspot/query/sort_composite.rb +33 -0
  61. data/lib/sunspot/query/standard_query.rb +16 -0
  62. data/lib/sunspot/query/text_field_boost.rb +17 -0
  63. data/lib/sunspot/schema.rb +151 -0
  64. data/lib/sunspot/search.rb +9 -0
  65. data/lib/sunspot/search/abstract_search.rb +335 -0
  66. data/lib/sunspot/search/date_facet.rb +35 -0
  67. data/lib/sunspot/search/facet_row.rb +27 -0
  68. data/lib/sunspot/search/field_facet.rb +88 -0
  69. data/lib/sunspot/search/highlight.rb +38 -0
  70. data/lib/sunspot/search/hit.rb +150 -0
  71. data/lib/sunspot/search/more_like_this_search.rb +31 -0
  72. data/lib/sunspot/search/paginated_collection.rb +55 -0
  73. data/lib/sunspot/search/query_facet.rb +67 -0
  74. data/lib/sunspot/search/standard_search.rb +21 -0
  75. data/lib/sunspot/session.rb +260 -0
  76. data/lib/sunspot/session_proxy.rb +87 -0
  77. data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
  78. data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
  79. data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
  80. data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
  81. data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
  82. data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
  83. data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
  84. data/lib/sunspot/setup.rb +350 -0
  85. data/lib/sunspot/text_field_setup.rb +29 -0
  86. data/lib/sunspot/type.rb +372 -0
  87. data/lib/sunspot/util.rb +243 -0
  88. data/lib/sunspot/version.rb +3 -0
  89. data/pduey-sunspot.gemspec +38 -0
  90. data/script/console +10 -0
  91. data/spec/api/adapters_spec.rb +33 -0
  92. data/spec/api/binding_spec.rb +50 -0
  93. data/spec/api/indexer/attributes_spec.rb +149 -0
  94. data/spec/api/indexer/batch_spec.rb +46 -0
  95. data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
  96. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  97. data/spec/api/indexer/fulltext_spec.rb +43 -0
  98. data/spec/api/indexer/removal_spec.rb +53 -0
  99. data/spec/api/indexer/spec_helper.rb +1 -0
  100. data/spec/api/indexer_spec.rb +14 -0
  101. data/spec/api/query/advanced_manipulation_examples.rb +35 -0
  102. data/spec/api/query/connectives_examples.rb +189 -0
  103. data/spec/api/query/dsl_spec.rb +18 -0
  104. data/spec/api/query/dynamic_fields_examples.rb +165 -0
  105. data/spec/api/query/faceting_examples.rb +397 -0
  106. data/spec/api/query/fulltext_examples.rb +313 -0
  107. data/spec/api/query/function_spec.rb +70 -0
  108. data/spec/api/query/geo_examples.rb +68 -0
  109. data/spec/api/query/highlighting_examples.rb +223 -0
  110. data/spec/api/query/more_like_this_spec.rb +140 -0
  111. data/spec/api/query/ordering_pagination_examples.rb +95 -0
  112. data/spec/api/query/scope_examples.rb +275 -0
  113. data/spec/api/query/spec_helper.rb +1 -0
  114. data/spec/api/query/standard_spec.rb +28 -0
  115. data/spec/api/query/text_field_scoping_examples.rb +30 -0
  116. data/spec/api/query/types_spec.rb +20 -0
  117. data/spec/api/search/dynamic_fields_spec.rb +33 -0
  118. data/spec/api/search/faceting_spec.rb +360 -0
  119. data/spec/api/search/highlighting_spec.rb +69 -0
  120. data/spec/api/search/hits_spec.rb +131 -0
  121. data/spec/api/search/paginated_collection_spec.rb +26 -0
  122. data/spec/api/search/results_spec.rb +66 -0
  123. data/spec/api/search/search_spec.rb +23 -0
  124. data/spec/api/search/spec_helper.rb +1 -0
  125. data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
  126. data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
  127. data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
  128. data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
  129. data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
  130. data/spec/api/session_proxy/spec_helper.rb +9 -0
  131. data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +39 -0
  132. data/spec/api/session_spec.rb +220 -0
  133. data/spec/api/spec_helper.rb +3 -0
  134. data/spec/api/sunspot_spec.rb +18 -0
  135. data/spec/ext.rb +11 -0
  136. data/spec/helpers/indexer_helper.rb +29 -0
  137. data/spec/helpers/query_helper.rb +38 -0
  138. data/spec/helpers/search_helper.rb +80 -0
  139. data/spec/integration/dynamic_fields_spec.rb +57 -0
  140. data/spec/integration/faceting_spec.rb +238 -0
  141. data/spec/integration/highlighting_spec.rb +24 -0
  142. data/spec/integration/indexing_spec.rb +33 -0
  143. data/spec/integration/keyword_search_spec.rb +317 -0
  144. data/spec/integration/local_search_spec.rb +64 -0
  145. data/spec/integration/more_like_this_spec.rb +43 -0
  146. data/spec/integration/scoped_search_spec.rb +354 -0
  147. data/spec/integration/spec_helper.rb +7 -0
  148. data/spec/integration/stored_fields_spec.rb +12 -0
  149. data/spec/integration/test_pagination.rb +32 -0
  150. data/spec/mocks/adapters.rb +32 -0
  151. data/spec/mocks/blog.rb +3 -0
  152. data/spec/mocks/comment.rb +21 -0
  153. data/spec/mocks/connection.rb +126 -0
  154. data/spec/mocks/mock_adapter.rb +30 -0
  155. data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
  156. data/spec/mocks/mock_record.rb +52 -0
  157. data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
  158. data/spec/mocks/photo.rb +11 -0
  159. data/spec/mocks/post.rb +85 -0
  160. data/spec/mocks/super_class.rb +2 -0
  161. data/spec/mocks/user.rb +13 -0
  162. data/spec/spec_helper.rb +28 -0
  163. data/tasks/rdoc.rake +27 -0
  164. data/tasks/schema.rake +19 -0
  165. data/tasks/todo.rake +4 -0
  166. metadata +369 -0
@@ -0,0 +1,9 @@
1
+ module Sunspot
2
+ module Query
3
+ class Scope < Connective::Conjunction
4
+ def to_params
5
+ { :fq => @components.map { |component| component.to_filter_query }}
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The classes in this module implement query components that build sort
5
+ # parameters for Solr. As well as regular sort on fields, there are several
6
+ # "special" sorts that allow ordering for metrics calculated during the
7
+ # search.
8
+ #
9
+ module Sort #:nodoc: all
10
+ DIRECTIONS = {
11
+ :asc => 'asc',
12
+ :ascending => 'asc',
13
+ :desc => 'desc',
14
+ :descending => 'desc'
15
+ }
16
+
17
+ class <<self
18
+ #
19
+ # Certain field names are "special", referring to specific non-field
20
+ # sorts, which are generally by other metrics associated with hits.
21
+ #
22
+ # XXX I'm not entirely convinced it's a good idea to prevent anyone from
23
+ # ever sorting by a field named 'score', etc.
24
+ #
25
+ def special(name)
26
+ special_class_name = "#{Util.camel_case(name.to_s)}Sort"
27
+ if const_defined?(special_class_name) && special_class_name != 'FieldSort'
28
+ const_get(special_class_name)
29
+ end
30
+ end
31
+ end
32
+
33
+ #
34
+ # Base class for sorts. All subclasses should implement the #to_param
35
+ # method, which is a string that is then concatenated with other sort
36
+ # strings by the SortComposite to form the sort parameter.
37
+ #
38
+ class Abstract
39
+ def initialize(direction)
40
+ @direction = (direction || :asc).to_sym
41
+ end
42
+
43
+ private
44
+
45
+ #
46
+ # Translate fairly forgiving direction argument into solr direction
47
+ #
48
+ def direction_for_solr
49
+ DIRECTIONS[@direction] ||
50
+ raise(
51
+ ArgumentError,
52
+ "Unknown sort direction #{@direction}. Acceptable input is: #{DIRECTIONS.keys.map { |input| input.inspect } * ', '}"
53
+ )
54
+ end
55
+ end
56
+
57
+ #
58
+ # A FieldSort is the usual kind of sort, by the value of a particular
59
+ # field, ascending or descending
60
+ #
61
+ class FieldSort < Abstract
62
+ def initialize(field, direction = nil)
63
+ if field.multiple?
64
+ raise(ArgumentError, "#{field.name} cannot be used for ordering because it is a multiple-value field")
65
+ end
66
+ @field, @direction = field, (direction || :asc).to_sym
67
+ end
68
+
69
+ def to_param
70
+ "#{@field.indexed_name.to_sym} #{direction_for_solr}"
71
+ end
72
+ end
73
+
74
+ #
75
+ # A RandomSort uses Solr's random field functionality to sort results
76
+ # (usually) randomly.
77
+ #
78
+ class RandomSort < Abstract
79
+ def to_param
80
+ "random_#{rand(1<<16)} #{direction_for_solr}"
81
+ end
82
+ end
83
+
84
+ #
85
+ # A ScoreSort sorts by keyword relevance score. This is only useful when
86
+ # performing fulltext search.
87
+ #
88
+ class ScoreSort < Abstract
89
+ def to_param
90
+ "score #{direction_for_solr}"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The SortComposite class encapsulates an ordered collection of Sort
5
+ # objects. It's necessary to keep this as a separate class as Solr takes
6
+ # the sort as a single parameter, so adding sorts as regular components
7
+ # would not merge correctly in the #to_params method.
8
+ #
9
+ class SortComposite #:nodoc:
10
+ def initialize
11
+ @sorts = []
12
+ end
13
+
14
+ #
15
+ # Add a sort to the composite
16
+ #
17
+ def <<(sort)
18
+ @sorts << sort
19
+ end
20
+
21
+ #
22
+ # Combine the sorts into a single param by joining them
23
+ #
24
+ def to_params
25
+ unless @sorts.empty?
26
+ { :sort => @sorts.map { |sort| sort.to_param } * ', ' }
27
+ else
28
+ {}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module Sunspot
2
+ module Query
3
+ class StandardQuery < CommonQuery
4
+ attr_accessor :scope, :fulltext
5
+
6
+ def initialize(types)
7
+ super
8
+ @components << @fulltext = CompositeFulltext.new
9
+ end
10
+
11
+ def add_fulltext(keywords)
12
+ @fulltext.add(keywords)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Sunspot
2
+ module Query
3
+ class TextFieldBoost #:nodoc:
4
+ attr_reader :boost
5
+
6
+ def initialize(field, boost = nil)
7
+ @field, @boost = field, boost
8
+ end
9
+
10
+ def to_boosted_field
11
+ boosted_field = @field.indexed_name
12
+ boosted_field.concat("^#{@boost}") if @boost
13
+ boosted_field
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,151 @@
1
+ require 'erb'
2
+
3
+ module Sunspot
4
+ #
5
+ # Object that encapsulates schema information for building a Solr schema.xml
6
+ # file. This class is used by the schema:compile task as well as the
7
+ # sunspot-configure-solr executable.
8
+ #
9
+ class Schema #:nodoc:all
10
+ FieldType = Struct.new(:name, :class_name, :suffix)
11
+ FieldVariant = Struct.new(:attribute, :suffix)
12
+
13
+ DEFAULT_TOKENIZER = 'solr.StandardTokenizerFactory'
14
+ DEFAULT_FILTERS = %w(solr.StandardFilterFactory solr.LowerCaseFilterFactory)
15
+
16
+ FIELD_TYPES = [
17
+ FieldType.new('boolean', 'Bool', 'b'),
18
+ FieldType.new('sfloat', 'SortableFloat', 'f'),
19
+ FieldType.new('date', 'Date', 'd'),
20
+ FieldType.new('sint', 'SortableInt', 'i'),
21
+ FieldType.new('string', 'Str', 's'),
22
+ FieldType.new('sdouble', 'SortableDouble', 'e'),
23
+ FieldType.new('slong', 'SortableLong', 'l'),
24
+ FieldType.new('tint', 'TrieInteger', 'it'),
25
+ FieldType.new('tfloat', 'TrieFloat', 'ft'),
26
+ FieldType.new('tdate', 'TrieInt', 'dt')
27
+
28
+ ]
29
+
30
+ FIELD_VARIANTS = [
31
+ FieldVariant.new('multiValued', 'm'),
32
+ FieldVariant.new('stored', 's')
33
+ ]
34
+
35
+ attr_reader :tokenizer, :filters
36
+
37
+ def initialize
38
+ @tokenizer = DEFAULT_TOKENIZER
39
+ @filters = DEFAULT_FILTERS.dup
40
+ end
41
+
42
+ #
43
+ # Attribute field types defined in the schema
44
+ #
45
+ def types
46
+ FIELD_TYPES
47
+ end
48
+
49
+ #
50
+ # DynamicField instances representing all the available types and variants
51
+ #
52
+ def dynamic_fields
53
+ fields = []
54
+ variant_combinations.each do |field_variants|
55
+ FIELD_TYPES.each do |type|
56
+ fields << DynamicField.new(type, field_variants)
57
+ end
58
+ end
59
+ fields
60
+ end
61
+
62
+ #
63
+ # Which tokenizer to use for text fields
64
+ #
65
+ def tokenizer=(tokenizer)
66
+ @tokenizer =
67
+ if tokenizer =~ /\./
68
+ tokenizer
69
+ else
70
+ "solr.#{tokenizer}TokenizerFactory"
71
+ end
72
+ end
73
+
74
+ #
75
+ # Add a filter for text field tokenization
76
+ #
77
+ def add_filter(filter)
78
+ @filters <<
79
+ if filter =~ /\./
80
+ filter
81
+ else
82
+ "solr.#{filter}FilterFactory"
83
+ end
84
+ end
85
+
86
+ #
87
+ # Return an XML representation of this schema using the ERB template
88
+ #
89
+ def to_xml
90
+ template = File.join(File.dirname(__FILE__), '..', '..', 'templates', 'schema.xml.erb')
91
+ ERB.new(File.read(template), nil, '-').result(binding)
92
+ end
93
+
94
+ private
95
+
96
+ #
97
+ # All of the possible combinations of variants
98
+ #
99
+ def variant_combinations
100
+ combinations = []
101
+ 0.upto(2 ** FIELD_VARIANTS.length - 1) do |b|
102
+ combinations << combination = []
103
+ FIELD_VARIANTS.each_with_index do |variant, i|
104
+ combination << variant if b & 1<<i > 0
105
+ end
106
+ end
107
+ combinations
108
+ end
109
+
110
+ #
111
+ # Represents a dynamic field (in the Solr schema sense, not the Sunspot
112
+ # sense).
113
+ #
114
+ class DynamicField
115
+ def initialize(type, field_variants)
116
+ @type, @field_variants = type, field_variants
117
+ end
118
+
119
+ #
120
+ # Name of the field in the schema
121
+ #
122
+ def name
123
+ variant_suffixes = @field_variants.map { |variant| variant.suffix }.join
124
+ "*_#{@type.suffix}#{variant_suffixes}"
125
+ end
126
+
127
+ #
128
+ # Name of the type as defined in the schema
129
+ #
130
+ def type
131
+ @type.name
132
+ end
133
+
134
+ #
135
+ # Implement magic methods to ask if a field is of a particular variant.
136
+ # Returns "true" if the field is of that variant and "false" otherwise.
137
+ #
138
+ def method_missing(name, *args, &block)
139
+ if name.to_s =~ /\?$/ && args.empty?
140
+ if @field_variants.any? { |variant| "#{variant.attribute}?" == name.to_s }
141
+ 'true'
142
+ else
143
+ 'false'
144
+ end
145
+ else
146
+ super(name.to_sym, *args, &block)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,9 @@
1
+ %w(abstract_search standard_search more_like_this_search query_facet field_facet
2
+ date_facet facet_row hit highlight).each do |file|
3
+ require File.join(File.dirname(__FILE__), 'search', file)
4
+ end
5
+
6
+ module Sunspot
7
+ module Search
8
+ end
9
+ end
@@ -0,0 +1,335 @@
1
+ require 'sunspot/search/paginated_collection'
2
+
3
+ module Sunspot
4
+ module Search #:nodoc:
5
+
6
+ #
7
+ # This class encapsulates the results of a Solr search. It provides access
8
+ # to search results, total result count, facets, and pagination information.
9
+ # Instances of Search are returned by the Sunspot.search and
10
+ # Sunspot.new_search methods.
11
+ #
12
+ class AbstractSearch
13
+ #
14
+ # Retrieve all facet objects defined for this search, in order they were
15
+ # defined. To retrieve an individual facet by name, use #facet()
16
+ #
17
+ attr_reader :facets
18
+ attr_reader :query #:nodoc:
19
+ attr_accessor :request_handler
20
+
21
+ def initialize(connection, setup, query, configuration) #:nodoc:
22
+ @connection, @setup, @query = connection, setup, query
23
+ @query.paginate(1, configuration.pagination.default_per_page)
24
+ @facets = []
25
+ @facets_by_name = {}
26
+ end
27
+
28
+ #
29
+ # Execute the search on the Solr instance and store the results. If you
30
+ # use Sunspot#search() to construct your searches, there is no need to call
31
+ # this method as it has already been called. If you use
32
+ # Sunspot#new_search(), you will need to call this method after building the
33
+ # query.
34
+ #
35
+ def execute
36
+ reset
37
+ params = @query.to_params
38
+ @solr_result = @connection.post "#{request_handler}", :params => params, :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' }
39
+ self
40
+ end
41
+
42
+ def execute! #:nodoc: deprecated
43
+ execute
44
+ end
45
+
46
+ #
47
+ # Get the collection of results as instantiated objects. If WillPaginate is
48
+ # available, the results will be a WillPaginate::Collection instance; if
49
+ # not, it will be a vanilla Array.
50
+ #
51
+ # If not all of the results referenced by the Solr hits actually exist in
52
+ # the data store, Sunspot will only return the results that do exist.
53
+ #
54
+ # ==== Returns
55
+ #
56
+ # WillPaginate::Collection or Array:: Instantiated result objects
57
+ #
58
+ def results
59
+ @results ||= paginate_collection(verified_hits.map { |hit| hit.instance })
60
+ end
61
+
62
+ #
63
+ # Access raw Solr result information. Returns a collection of Hit objects
64
+ # that contain the class name, primary key, keyword relevance score (if
65
+ # applicable), and any stored fields.
66
+ #
67
+ # ==== Options (options)
68
+ #
69
+ # :verify::
70
+ # Only return hits that reference objects that actually exist in the data
71
+ # store. This causes results to be eager-loaded from the data store,
72
+ # unlike the normal behavior of this method, which only loads the
73
+ # referenced results when Hit#result is first called.
74
+ #
75
+ # ==== Returns
76
+ #
77
+ # Array:: Ordered collection of Hit objects
78
+ #
79
+ def hits(options = {})
80
+ if options[:verify]
81
+ verified_hits
82
+ else
83
+ @hits ||=
84
+ begin
85
+ hits = if solr_response && solr_response['docs']
86
+ solr_response['docs'].map do |doc|
87
+ Hit.new(doc, highlights_for(doc), self)
88
+ end
89
+ end
90
+ paginate_collection(hits || [])
91
+ end
92
+ end
93
+ end
94
+ alias_method :raw_results, :hits
95
+
96
+ #
97
+ # Convenience method to iterate over hit and result objects. Block is
98
+ # yielded a Sunspot::Server::Hit instance and a Sunspot::Server::Result
99
+ # instance.
100
+ #
101
+ # Note that this method iterates over verified hits (see #hits method
102
+ # for more information).
103
+ #
104
+ def each_hit_with_result
105
+ verified_hits.each do |hit|
106
+ yield(hit, hit.result)
107
+ end
108
+ end
109
+
110
+ #
111
+ # The total number of documents matching the query parameters
112
+ #
113
+ # ==== Returns
114
+ #
115
+ # Integer:: Total matching documents
116
+ #
117
+ def total
118
+ @total ||= solr_response['numFound'] || 0
119
+ end
120
+
121
+ #
122
+ # Get the facet object for the given name. `name` can either be the name
123
+ # given to a query facet, or the field name of a field facet. Returns a
124
+ # Sunspot::Facet object.
125
+ #
126
+ # ==== Parameters
127
+ #
128
+ # name<Symbol>::
129
+ # Name of the field to return the facet for, or the name given to the
130
+ # query facet when the search was constructed.
131
+ # dynamic_name<Symbol>::
132
+ # If faceting on a dynamic field, this is the dynamic portion of the field
133
+ # name.
134
+ #
135
+ # ==== Example:
136
+ #
137
+ # search = Sunspot.search(Post) do
138
+ # facet :category_ids
139
+ # dynamic :custom do
140
+ # facet :cuisine
141
+ # end
142
+ # facet :age do
143
+ # row 'Less than a month' do
144
+ # with(:published_at).greater_than(1.month.ago)
145
+ # end
146
+ # row 'Less than a year' do
147
+ # with(:published_at, 1.year.ago..1.month.ago)
148
+ # end
149
+ # row 'More than a year' do
150
+ # with(:published_at).less_than(1.year.ago)
151
+ # end
152
+ # end
153
+ # end
154
+ # search.facet(:category_ids)
155
+ # #=> Facet for :category_ids field
156
+ # search.facet(:custom, :cuisine)
157
+ # #=> Facet for the dynamic field :cuisine in the :custom field definition
158
+ # search.facet(:age)
159
+ # #=> Facet for the query facet named :age
160
+ #
161
+ def facet(name, dynamic_name = nil)
162
+ if name
163
+ if dynamic_name
164
+ @facets_by_name[:"#{name}:#{dynamic_name}"]
165
+ else
166
+ @facets_by_name[name.to_sym]
167
+ end
168
+ end
169
+ end
170
+
171
+ #
172
+ # Deprecated in favor of optional second argument to #facet
173
+ #
174
+ def dynamic_facet(base_name, dynamic_name) #:nodoc:
175
+ facet(base_name, dynamic_name)
176
+ end
177
+
178
+ def facet_response #:nodoc:
179
+ @solr_result['facet_counts']
180
+ end
181
+
182
+ #
183
+ # Decomposes the Solr "Filter Query" (fq) parameter array further into a hash that can be easily
184
+ # digested when constructing FieldFacet.rows. Specifically, it is used to add a boolean "selected"
185
+ # attribute on each row so it's easy to pick out which of the facets returned in a result are part
186
+ # of the originating query. This is useful for doing something like generating "undo" links, or
187
+ # showing checkbox initial state as checked.
188
+ #
189
+ # Filter query values with keys ending in '_im' and '_s' are processed into their constituent parts.
190
+ #
191
+ # solr_response_header['params']['fq'] is an array of strings of the form, for example:
192
+ # fq: ["type:Package",
193
+ # "amenities_ids_im:(9 AND 12)",
194
+ # "neighborhood_s:(East\ Village OR Chelsea)",
195
+ # "capacity_max_is:[30 TO *]"
196
+ # ]
197
+ #
198
+ # which is converted to the equivalent hash:
199
+ # fq_response_header: ["type" => "Package",
200
+ # "amenties_ids_im" => ["9", "12"],
201
+ # "neighborhood_s" => ["East Village", "Chelsea"],
202
+ # "capacity_max_is" => "[30 TO *]"
203
+ # ]
204
+ #
205
+ def fq_response_header
206
+ @fq_response_header ||=
207
+ begin
208
+ h = Hash.new
209
+ solr_response_header['params']['fq'].each do |filter_query|
210
+ field, value = filter_query.split(':')
211
+ field = field.gsub(/^\{\!tag=.*?\}/, '') # strip exclude tags
212
+ value = value.gsub(/^\(|\)$|\\/, '') # strips surrounding parens and \,
213
+ value = value.split(/ AND | OR /) if facet_split[0] =~ /_im$|_s$/
214
+ h[field] = value
215
+ end
216
+ h
217
+ end
218
+ end
219
+
220
+ #
221
+ # Get the data accessor that will be used to load a particular class out of
222
+ # persistent storage. Data accessors can implement any methods that may be
223
+ # useful for refining how data is loaded out of storage. When building a
224
+ # search manually (e.g., using the Sunspot#new_search method), this should
225
+ # be used before calling #execute(). Use the
226
+ # Sunspot::DSL::Search#data_accessor_for method when building searches using
227
+ # the block DSL.
228
+ #
229
+ def data_accessor_for(clazz) #:nodoc:
230
+ (@data_accessors ||= {})[clazz.name.to_sym] ||=
231
+ Adapters::DataAccessor.create(clazz)
232
+ end
233
+
234
+ #
235
+ # Build this search using a DSL block. This method can be called more than
236
+ # once on an unexecuted search (e.g., Sunspot.new_search) in order to build
237
+ # a search incrementally.
238
+ #
239
+ # === Example
240
+ #
241
+ # search = Sunspot.new_search(Post)
242
+ # search.build do
243
+ # with(:published_at).less_than Time.now
244
+ # end
245
+ # search.execute
246
+ #
247
+ def build(&block)
248
+ Util.instance_eval_or_call(dsl, &block)
249
+ self
250
+ end
251
+
252
+ #
253
+ # Populate the Hit objects with their instances. This is invoked the first
254
+ # time any hit has its instance requested, and all hits are loaded as a
255
+ # batch.
256
+ #
257
+ def populate_hits #:nodoc:
258
+ id_hit_hash = Hash.new { |h, k| h[k] = {} }
259
+ hits.each do |hit|
260
+ id_hit_hash[hit.class_name][hit.primary_key] = hit
261
+ end
262
+ id_hit_hash.each_pair do |class_name, hits|
263
+ ids = hits.map { |id, hit| hit.primary_key }
264
+ data_accessor = data_accessor_for(Util.full_const_get(class_name))
265
+ hits_for_class = id_hit_hash[class_name]
266
+ data_accessor.load_all(ids).each do |result|
267
+ hit = hits_for_class.delete(Adapters::InstanceAdapter.adapt(result).id.to_s)
268
+ hit.result = result
269
+ end
270
+ hits_for_class.values.each { |hit| hit.result = nil }
271
+ end
272
+ end
273
+
274
+ def inspect #:nodoc:
275
+ "<Sunspot::Search:#{query.to_params.inspect}>"
276
+ end
277
+
278
+ def add_field_facet(field, options = {}) #:nodoc:
279
+ name = (options[:name] || field.name)
280
+ add_facet(name, FieldFacet.new(field, self, options))
281
+ end
282
+
283
+ def add_query_facet(name, options) #:nodoc:
284
+ add_facet(name, QueryFacet.new(name, self, options))
285
+ end
286
+
287
+ def add_date_facet(field, options) #:nodoc:
288
+ name = (options[:name] || field.name)
289
+ add_facet(name, DateFacet.new(field, self, options))
290
+ end
291
+
292
+ private
293
+
294
+ def dsl
295
+ raise NotImplementedError
296
+ end
297
+
298
+ def execute_request(params)
299
+ raise NotImplementedError
300
+ end
301
+
302
+ def solr_response
303
+ @solr_response ||= @solr_result['response'] || {}
304
+ end
305
+
306
+ def solr_response_header
307
+ @solr_response_header ||= @solr_result['responseHeader'] || {}
308
+ end
309
+
310
+ def highlights_for(doc)
311
+ if @solr_result['highlighting']
312
+ @solr_result['highlighting'][doc['id']]
313
+ end
314
+ end
315
+
316
+ def verified_hits
317
+ @verified_hits ||= paginate_collection(hits.select { |hit| hit.instance })
318
+ end
319
+
320
+ def paginate_collection(collection)
321
+ PaginatedCollection.new(collection, @query.page, @query.per_page, total)
322
+ end
323
+
324
+ def add_facet(name, facet)
325
+ @facets << facet
326
+ @facets_by_name[name.to_sym] = facet
327
+ end
328
+
329
+ # Clear out all the cached ivars so the search can be called again.
330
+ def reset
331
+ @results = @hits = @verified_hits = @total = @solr_response = @doc_ids = @solr_response_header = nil
332
+ end
333
+ end
334
+ end
335
+ end