benjaminkrause-sunspot 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. data/History.txt +107 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +159 -0
  4. data/Rakefile +9 -0
  5. data/TODO +11 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +86 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot/adapters.rb +265 -0
  11. data/lib/sunspot/composite_setup.rb +184 -0
  12. data/lib/sunspot/configuration.rb +49 -0
  13. data/lib/sunspot/data_extractor.rb +50 -0
  14. data/lib/sunspot/dsl/field_query.rb +77 -0
  15. data/lib/sunspot/dsl/fields.rb +95 -0
  16. data/lib/sunspot/dsl/fulltext.rb +106 -0
  17. data/lib/sunspot/dsl/query.rb +107 -0
  18. data/lib/sunspot/dsl/query_facet.rb +31 -0
  19. data/lib/sunspot/dsl/restriction.rb +25 -0
  20. data/lib/sunspot/dsl/scope.rb +193 -0
  21. data/lib/sunspot/dsl/search.rb +30 -0
  22. data/lib/sunspot/dsl.rb +4 -0
  23. data/lib/sunspot/facet.rb +24 -0
  24. data/lib/sunspot/facet_data.rb +152 -0
  25. data/lib/sunspot/facet_row.rb +12 -0
  26. data/lib/sunspot/field.rb +148 -0
  27. data/lib/sunspot/field_factory.rb +141 -0
  28. data/lib/sunspot/indexer.rb +129 -0
  29. data/lib/sunspot/instantiated_facet.rb +45 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +27 -0
  31. data/lib/sunspot/query/base_query.rb +55 -0
  32. data/lib/sunspot/query/boost_query.rb +20 -0
  33. data/lib/sunspot/query/connective.rb +148 -0
  34. data/lib/sunspot/query/dynamic_query.rb +61 -0
  35. data/lib/sunspot/query/field_facet.rb +129 -0
  36. data/lib/sunspot/query/field_query.rb +69 -0
  37. data/lib/sunspot/query/fulltext_base_query.rb +86 -0
  38. data/lib/sunspot/query/highlighting.rb +36 -0
  39. data/lib/sunspot/query/local.rb +24 -0
  40. data/lib/sunspot/query/pagination.rb +39 -0
  41. data/lib/sunspot/query/query_facet.rb +78 -0
  42. data/lib/sunspot/query/query_facet_row.rb +19 -0
  43. data/lib/sunspot/query/query_field_facet.rb +20 -0
  44. data/lib/sunspot/query/restriction.rb +272 -0
  45. data/lib/sunspot/query/scope.rb +185 -0
  46. data/lib/sunspot/query/sort.rb +105 -0
  47. data/lib/sunspot/query/sort_composite.rb +33 -0
  48. data/lib/sunspot/query/text_field_boost.rb +15 -0
  49. data/lib/sunspot/query.rb +108 -0
  50. data/lib/sunspot/schema.rb +147 -0
  51. data/lib/sunspot/search/highlight.rb +38 -0
  52. data/lib/sunspot/search/hit.rb +113 -0
  53. data/lib/sunspot/search.rb +240 -0
  54. data/lib/sunspot/session.rb +206 -0
  55. data/lib/sunspot/setup.rb +312 -0
  56. data/lib/sunspot/text_field_setup.rb +29 -0
  57. data/lib/sunspot/type.rb +200 -0
  58. data/lib/sunspot/util.rb +190 -0
  59. data/lib/sunspot.rb +459 -0
  60. data/solr/etc/jetty.xml +212 -0
  61. data/solr/etc/webdefault.xml +379 -0
  62. data/solr/lib/jetty-6.1.3.jar +0 -0
  63. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  64. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  65. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  66. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  67. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  68. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  69. data/solr/solr/conf/elevate.xml +36 -0
  70. data/solr/solr/conf/protwords.txt +21 -0
  71. data/solr/solr/conf/schema.xml +64 -0
  72. data/solr/solr/conf/solrconfig.xml +726 -0
  73. data/solr/solr/conf/stopwords.txt +57 -0
  74. data/solr/solr/conf/synonyms.txt +31 -0
  75. data/solr/start.jar +0 -0
  76. data/solr/webapps/solr.war +0 -0
  77. data/spec/api/adapters_spec.rb +33 -0
  78. data/spec/api/indexer/attributes_spec.rb +100 -0
  79. data/spec/api/indexer/batch_spec.rb +46 -0
  80. data/spec/api/indexer/dynamic_fields_spec.rb +33 -0
  81. data/spec/api/indexer/fixed_fields_spec.rb +57 -0
  82. data/spec/api/indexer/fulltext_spec.rb +43 -0
  83. data/spec/api/indexer/removal_spec.rb +46 -0
  84. data/spec/api/indexer/spec_helper.rb +1 -0
  85. data/spec/api/indexer_spec.rb +4 -0
  86. data/spec/api/query/connectives_spec.rb +161 -0
  87. data/spec/api/query/dsl_spec.rb +12 -0
  88. data/spec/api/query/dynamic_fields_spec.rb +148 -0
  89. data/spec/api/query/faceting_spec.rb +272 -0
  90. data/spec/api/query/fulltext_spec.rb +152 -0
  91. data/spec/api/query/highlighting_spec.rb +82 -0
  92. data/spec/api/query/local_spec.rb +37 -0
  93. data/spec/api/query/ordering_pagination_spec.rb +95 -0
  94. data/spec/api/query/scope_spec.rb +253 -0
  95. data/spec/api/query/spec_helper.rb +1 -0
  96. data/spec/api/query/text_field_scoping_spec.rb +30 -0
  97. data/spec/api/query/types_spec.rb +20 -0
  98. data/spec/api/search/dynamic_fields_spec.rb +27 -0
  99. data/spec/api/search/faceting_spec.rb +206 -0
  100. data/spec/api/search/highlighting_spec.rb +65 -0
  101. data/spec/api/search/hits_spec.rb +62 -0
  102. data/spec/api/search/results_spec.rb +52 -0
  103. data/spec/api/search/search_spec.rb +11 -0
  104. data/spec/api/search/spec_helper.rb +1 -0
  105. data/spec/api/session_spec.rb +157 -0
  106. data/spec/api/spec_helper.rb +1 -0
  107. data/spec/api/sunspot_spec.rb +18 -0
  108. data/spec/helpers/indexer_helper.rb +29 -0
  109. data/spec/helpers/query_helper.rb +13 -0
  110. data/spec/helpers/search_helper.rb +78 -0
  111. data/spec/integration/dynamic_fields_spec.rb +55 -0
  112. data/spec/integration/faceting_spec.rb +169 -0
  113. data/spec/integration/highlighting_spec.rb +22 -0
  114. data/spec/integration/keyword_search_spec.rb +148 -0
  115. data/spec/integration/local_search_spec.rb +47 -0
  116. data/spec/integration/scoped_search_spec.rb +303 -0
  117. data/spec/integration/spec_helper.rb +1 -0
  118. data/spec/integration/stored_fields_spec.rb +10 -0
  119. data/spec/integration/test_pagination.rb +32 -0
  120. data/spec/mocks/adapters.rb +32 -0
  121. data/spec/mocks/blog.rb +3 -0
  122. data/spec/mocks/comment.rb +19 -0
  123. data/spec/mocks/connection.rb +84 -0
  124. data/spec/mocks/mock_adapter.rb +30 -0
  125. data/spec/mocks/mock_record.rb +48 -0
  126. data/spec/mocks/photo.rb +8 -0
  127. data/spec/mocks/post.rb +75 -0
  128. data/spec/mocks/super_class.rb +2 -0
  129. data/spec/mocks/user.rb +8 -0
  130. data/spec/spec_helper.rb +60 -0
  131. data/tasks/gemspec.rake +35 -0
  132. data/tasks/rcov.rake +28 -0
  133. data/tasks/rdoc.rake +22 -0
  134. data/tasks/schema.rake +19 -0
  135. data/tasks/spec.rake +24 -0
  136. data/tasks/todo.rake +4 -0
  137. data/templates/schema.xml.erb +36 -0
  138. metadata +312 -0
@@ -0,0 +1,108 @@
1
+ %w(base_query fulltext_base_query scope field_query connective dynamic_query
2
+ field_facet query_facet query_facet_row query_field_facet boost_query local
3
+ pagination restriction sort sort_composite text_field_boost
4
+ highlighting).each do |file|
5
+ require File.join(File.dirname(__FILE__), 'query', file)
6
+ end
7
+
8
+ module Sunspot
9
+ module Query #:nodoc:
10
+ #
11
+ # This class encapsulates a query that is to be sent to Solr. The query is
12
+ # constructed in the block passed to the Sunspot.search method, using the
13
+ # Sunspot::DSL::Query interface. It can also be accessed directly by calling
14
+ # #query on a Search object (presumably a not-yet-run one created using
15
+ # Sunspot#new_search), which might be more suitable than the DSL when an
16
+ # intermediate object has responsibility for building the query dynamically.
17
+ #--
18
+ # Instances of Query, as well as all of the components it contains, respond to
19
+ # the #to_params method, which returns a hash of parameters in the format
20
+ # recognized by the solr-ruby API.
21
+ #
22
+ class Query < FieldQuery #:nodoc:
23
+ attr_reader :query_facets #:nodoc:
24
+
25
+ def initialize(types, setup, configuration) #:nodoc:
26
+ super(setup)
27
+ @query_facets = {}
28
+ @components[0] = @base_query = BaseQuery.new(types, setup)
29
+ @components << @pagination = Pagination.new(configuration)
30
+ @components << @sort = SortComposite.new
31
+ end
32
+
33
+ #
34
+ # Sets @start and @rows instance variables using pagination semantics
35
+ #
36
+ # ==== Parameters
37
+ #
38
+ # page<Integer>:: Page on which to start
39
+ # per_page<Integer>::
40
+ # How many rows to display per page. Default taken from
41
+ # Sunspot.config.pagination.default_per_page
42
+ #
43
+ def paginate(page, per_page = nil) #:nodoc:
44
+ @pagination.page, @pagination.per_page = page, per_page
45
+ end
46
+
47
+ def add_location_restriction(coordinates, miles) #:nodoc:
48
+ @components << Local.new(coordinates, miles)
49
+ end
50
+
51
+ def add_text_fields_scope
52
+ @components << scope = Scope.new(TextFieldSetup.new(@setup))
53
+ scope
54
+ end
55
+
56
+ #
57
+ # Page that this query will return (used by Sunspot::Search to expose
58
+ # pagination)
59
+ #
60
+ # ==== Returns
61
+ #
62
+ # Integer:: Page number
63
+ #
64
+ def page #:nodoc:
65
+ @pagination.page
66
+ end
67
+
68
+ #
69
+ # Number of rows per page that this query will return (used by
70
+ # Sunspot::Search to expose pagination)
71
+ #
72
+ # ==== Returns
73
+ #
74
+ # Integer:: Rows per page
75
+ #
76
+ def per_page #:nodoc:
77
+ @pagination.per_page
78
+ end
79
+
80
+ #
81
+ # Get the query facet with the given name. Used by the Search object to
82
+ # match query facet results with the requested query facets.
83
+ #
84
+ def query_facet(name) #:nodoc:
85
+ @query_facets[name.to_sym]
86
+ end
87
+
88
+ #
89
+ # Add a Sort object into this query's sort composite.
90
+ #
91
+ def add_sort(sort) #:nodoc:
92
+ @sort << sort
93
+ end
94
+
95
+ #
96
+ # Set the keywords for this query, along with keyword options. See
97
+ # Query::FulltextBaseQuery for information on what the options do. Returns
98
+ # a FulltextBaseQuery object.
99
+ #
100
+ def set_keywords(keywords, options = {}) #:nodoc:
101
+ if keywords.to_s =~ /\S/
102
+ @components[0] = @base_query =
103
+ FulltextBaseQuery.new(keywords, options, @base_query.types, @setup)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,147 @@
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
+ ]
25
+
26
+ FIELD_VARIANTS = [
27
+ FieldVariant.new('multiValued', 'm'),
28
+ FieldVariant.new('stored', 's')
29
+ ]
30
+
31
+ attr_reader :tokenizer, :filters
32
+
33
+ def initialize
34
+ @tokenizer = DEFAULT_TOKENIZER
35
+ @filters = DEFAULT_FILTERS.dup
36
+ end
37
+
38
+ #
39
+ # Attribute field types defined in the schema
40
+ #
41
+ def types
42
+ FIELD_TYPES
43
+ end
44
+
45
+ #
46
+ # DynamicField instances representing all the available types and variants
47
+ #
48
+ def dynamic_fields
49
+ fields = []
50
+ for field_variants in variant_combinations
51
+ for type in FIELD_TYPES
52
+ fields << DynamicField.new(type, field_variants)
53
+ end
54
+ end
55
+ fields
56
+ end
57
+
58
+ #
59
+ # Which tokenizer to use for text fields
60
+ #
61
+ def tokenizer=(tokenizer)
62
+ @tokenizer =
63
+ if tokenizer =~ /\./
64
+ tokenizer
65
+ else
66
+ "solr.#{tokenizer}TokenizerFactory"
67
+ end
68
+ end
69
+
70
+ #
71
+ # Add a filter for text field tokenization
72
+ #
73
+ def add_filter(filter)
74
+ @filters <<
75
+ if filter =~ /\./
76
+ filter
77
+ else
78
+ "solr.#{filter}FilterFactory"
79
+ end
80
+ end
81
+
82
+ #
83
+ # Return an XML representation of this schema using the ERB template
84
+ #
85
+ def to_xml
86
+ template = File.join(File.dirname(__FILE__), '..', '..', 'templates', 'schema.xml.erb')
87
+ ERB.new(File.read(template), nil, '-').result(binding)
88
+ end
89
+
90
+ private
91
+
92
+ #
93
+ # All of the possible combinations of variants
94
+ #
95
+ def variant_combinations
96
+ combinations = []
97
+ 0.upto(2 ** FIELD_VARIANTS.length - 1) do |b|
98
+ combinations << combination = []
99
+ FIELD_VARIANTS.each_with_index do |variant, i|
100
+ combination << variant if b & 1<<i > 0
101
+ end
102
+ end
103
+ combinations
104
+ end
105
+
106
+ #
107
+ # Represents a dynamic field (in the Solr schema sense, not the Sunspot
108
+ # sense).
109
+ #
110
+ class DynamicField
111
+ def initialize(type, field_variants)
112
+ @type, @field_variants = type, field_variants
113
+ end
114
+
115
+ #
116
+ # Name of the field in the schema
117
+ #
118
+ def name
119
+ variant_suffixes = @field_variants.map { |variant| variant.suffix }.join
120
+ "*_#{@type.suffix}#{variant_suffixes}"
121
+ end
122
+
123
+ #
124
+ # Name of the type as defined in the schema
125
+ #
126
+ def type
127
+ @type.name
128
+ end
129
+
130
+ #
131
+ # Implement magic methods to ask if a field is of a particular variant.
132
+ # Returns "true" if the field is of that variant and "false" otherwise.
133
+ #
134
+ def method_missing(name, *args, &block)
135
+ if name.to_s =~ /\?$/ && args.empty?
136
+ if @field_variants.any? { |variant| "#{variant.attribute}?" == name.to_s }
137
+ 'true'
138
+ else
139
+ 'false'
140
+ end
141
+ else
142
+ super(name.to_sym, *args, &block)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,38 @@
1
+ module Sunspot
2
+ class Search
3
+ #
4
+ # A Highlight represents a single highlighted fragment of text from a
5
+ # document. Depending on the highlighting parameters used for search, there
6
+ # may be more than one Highlight object for a given field in a given result.
7
+ #
8
+ class Highlight
9
+ HIGHLIGHT_MATCHER = /@@@hl@@@(.*?)@@@endhl@@@/ #:nodoc:
10
+
11
+ #
12
+ # The name of the field in which the highlight appeared.
13
+ #
14
+ attr_reader :field_name
15
+
16
+ def initialize(field_name, highlight) #:nodoc:
17
+ @field_name = field_name.to_sym
18
+ @highlight = highlight.to_s.strip
19
+ end
20
+
21
+ #
22
+ # Returns the highlighted text with formatting according to the template given in &block.
23
+ # When no block is given, <em> and </em> are used to surround the highlight.
24
+ #
25
+ # ==== Example
26
+ #
27
+ # search.highlights(:body).first.format { |word| "<strong>#{word}</strong>" }
28
+ #
29
+ def format(&block)
30
+ block ||= proc { |word| "<em>#{word}</em>" }
31
+ @highlight.gsub(HIGHLIGHT_MATCHER) do
32
+ block.call(Regexp.last_match[1])
33
+ end
34
+ end
35
+ alias_method :formatted, :format
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,113 @@
1
+ module Sunspot
2
+ class Search
3
+ #
4
+ # Hit objects represent the raw information returned by Solr for a single
5
+ # document. As well as the primary key and class name, hit objects give
6
+ # access to stored field values, keyword relevance score, and geographical
7
+ # distance (for geographical search).
8
+ #
9
+ class Hit
10
+ SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:
11
+
12
+ #
13
+ # Primary key of object associated with this hit, as string.
14
+ #
15
+ attr_reader :primary_key
16
+ #
17
+ # Class name of object associated with this hit, as string.
18
+ #
19
+ attr_reader :class_name
20
+ #
21
+ # Keyword relevance score associated with this result. Nil if this hit
22
+ # is not from a keyword search.
23
+ #
24
+ attr_reader :score
25
+ #
26
+ # For geographical searches, this is the distance between the search
27
+ # centerpoint and the document's location. Otherwise, it's nil.
28
+ #
29
+ attr_reader :distance
30
+
31
+ attr_writer :instance #:nodoc:
32
+
33
+ def initialize(raw_hit, highlights, search) #:nodoc:
34
+ @class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
35
+ @score = raw_hit['score']
36
+ @distance = raw_hit['geo_distance'].to_f if raw_hit['geo_distance']
37
+ @search = search
38
+ @stored_values = raw_hit
39
+ @stored_cache = {}
40
+ @highlights = highlights
41
+ end
42
+
43
+ #
44
+ # Returns all highlights for this hit when called without parameters.
45
+ # When a field_name is provided, returns only the highlight for this field.
46
+ #
47
+ def highlights(field_name = nil)
48
+ if field_name.nil?
49
+ highlights_cache.values.flatten
50
+ else
51
+ highlights_cache[field_name.to_sym]
52
+ end
53
+ end
54
+
55
+ #
56
+ # Retrieve stored field value. For any attribute field configured with
57
+ # :stored => true, the Hit object will contain the stored value for
58
+ # that field. The value of this field will be typecast according to the
59
+ # type of the field.
60
+ #
61
+ # ==== Parameters
62
+ #
63
+ # field_name<Symbol>::
64
+ # The name of the field for which to retrieve the stored value.
65
+ #
66
+ def stored(field_name)
67
+ @stored_cache[field_name.to_sym] ||=
68
+ begin
69
+ field = setup.field(field_name)
70
+ field.cast(@stored_values[field.indexed_name])
71
+ end
72
+ end
73
+
74
+ #
75
+ # Retrieve the instance associated with this hit. This is lazy-loaded, but
76
+ # the first time it is called on any hit, all the hits for the search will
77
+ # load their instances using the adapter's #load_all method.
78
+ #
79
+ def instance
80
+ if @instance.nil?
81
+ @search.populate_hits!
82
+ end
83
+ @instance
84
+ end
85
+
86
+ def inspect #:nodoc:
87
+ "#<Sunspot::Search::Hit:#{@class_name} #{@primary_key}>"
88
+ end
89
+
90
+ private
91
+
92
+ def setup
93
+ @setup ||= Sunspot::Setup.for(@class_name)
94
+ end
95
+
96
+ def highlights_cache
97
+ @highlights_cache ||=
98
+ begin
99
+ cache = {}
100
+ if @highlights
101
+ @highlights.each_pair do |indexed_field_name, highlight_strings|
102
+ field_name = indexed_field_name.sub(/_[a-z]+$/, '').to_sym
103
+ cache[field_name] = highlight_strings.map do |highlight_string|
104
+ Highlight.new(field_name, highlight_string)
105
+ end
106
+ end
107
+ end
108
+ cache
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,240 @@
1
+ %w(hit highlight).each do |file|
2
+ require File.join(File.dirname(__FILE__), 'search', file)
3
+ end
4
+
5
+ module Sunspot
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 Search
13
+ # Query information for this search. If you wish to build the query without
14
+ # using the search DSL, this method allows you to access the query API
15
+ # directly. See Sunspot#new_search for how to construct the search object
16
+ # in this case.
17
+ attr_reader :query
18
+
19
+ def initialize(connection, setup, query) #:nodoc:
20
+ @connection, @setup, @query = connection, setup, query
21
+ end
22
+
23
+ #
24
+ # Execute the search on the Solr instance and store the results. If you
25
+ # use Sunspot#search() to construct your searches, there is no need to call
26
+ # this method as it has already been called. If you use
27
+ # Sunspot#new_search(), you will need to call this method after building the
28
+ # query.
29
+ #
30
+ def execute
31
+ params = @query.to_params
32
+ @solr_result = @connection.select(params)
33
+ self
34
+ end
35
+ alias_method :execute!, :execute #:nodoc: deprecated
36
+
37
+ #
38
+ # Get the collection of results as instantiated objects. If WillPaginate is
39
+ # available, the results will be a WillPaginate::Collection instance; if
40
+ # not, it will be a vanilla Array.
41
+ #
42
+ # ==== Returns
43
+ #
44
+ # WillPaginate::Collection or Array:: Instantiated result objects
45
+ #
46
+ def results
47
+ @results ||= if @query.page && defined?(WillPaginate::Collection)
48
+ WillPaginate::Collection.create(@query.page, @query.per_page, total) do |pager|
49
+ pager.replace(hits.map { |hit| hit.instance })
50
+ end
51
+ else
52
+ hits.map { |hit| hit.instance }
53
+ end
54
+ end
55
+
56
+ #
57
+ # Access raw Solr result information. Returns a collection of Hit objects
58
+ # that contain the class name, primary key, keyword relevance score (if
59
+ # applicable), and any stored fields.
60
+ #
61
+ # ==== Returns
62
+ #
63
+ # Array:: Ordered collection of Hit objects
64
+ #
65
+ def hits
66
+ @hits ||= solr_response['docs'].map { |doc| Hit.new(doc, highlights_for(doc), self) }
67
+ end
68
+ alias_method :raw_results, :hits
69
+
70
+ #
71
+ # The total number of documents matching the query parameters
72
+ #
73
+ # ==== Returns
74
+ #
75
+ # Integer:: Total matching documents
76
+ #
77
+ def total
78
+ @total ||= solr_response['numFound']
79
+ end
80
+
81
+ #
82
+ # Get the facet object for the given name. `name` can either be the name
83
+ # given to a query facet, or the field name of a field facet. Returns a
84
+ # Sunspot::Facet object.
85
+ #
86
+ def facet(name)
87
+ (@facets_cache ||= {})[name.to_sym] ||=
88
+ begin
89
+ facet_data = query_facet_data(name) ||
90
+ begin
91
+ field = field(name)
92
+ date_facet_data(field) ||
93
+ FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field)
94
+ end
95
+ facet_class = facet_data.reference ? InstantiatedFacet : Facet
96
+ facet_class.new(facet_data)
97
+ end
98
+ end
99
+
100
+ #
101
+ # Get the facet object for a given dynamic field. This dynamic field will
102
+ # need to have been requested as a field facet inside the search block.
103
+ #
104
+ # ==== Parameters
105
+ #
106
+ # base_name<Symbol>::
107
+ # Base name of the dynamic field definiton (as specified in the setup
108
+ # block)
109
+ # dynamic_name<Symbol>::
110
+ # Dynamic field name to facet on
111
+ #
112
+ # ==== Returns
113
+ #
114
+ # Sunspot::Facet:: Facet object for given dynamic field
115
+ #
116
+ # ==== Example
117
+ #
118
+ # search = Sunspot.search(Post) do
119
+ # dynamic :custom do
120
+ # facet :cuisine
121
+ # end
122
+ # end
123
+ # search.dynamic_facet(:custom, :cuisine)
124
+ # #=> Facet for the dynamic field :cuisine in the :custom field definition
125
+ #
126
+ def dynamic_facet(base_name, dynamic_name)
127
+ (@dynamic_facets_cache ||= {})[[base_name.to_sym, dynamic_name.to_sym]] ||=
128
+ begin
129
+ field = @setup.dynamic_field_factory(base_name).build(dynamic_name)
130
+ Facet.new(FacetData::FieldFacetData.new(@solr_result['facet_counts']['facet_fields'][field.indexed_name], field))
131
+ end
132
+ end
133
+
134
+ #
135
+ # Get the data accessor that will be used to load a particular class out of
136
+ # persistent storage. Data accessors can implement any methods that may be
137
+ # useful for refining how data is loaded out of storage. When building a
138
+ # search manually (e.g., using the Sunspot#new_search method), this should
139
+ # be used before calling #execute(). Use the
140
+ # Sunspot::DSL::Search#data_accessor_for method when building searches using
141
+ # the block DSL.
142
+ #
143
+ def data_accessor_for(clazz) #:nodoc:
144
+ (@data_accessors ||= {})[clazz.name.to_sym] ||=
145
+ Adapters::DataAccessor.create(clazz)
146
+ end
147
+
148
+ #
149
+ # Build this search using a DSL block. This method can be called more than
150
+ # once on an unexecuted search (e.g., Sunspot.new_search) in order to build
151
+ # a search incrementally.
152
+ #
153
+ # === Example
154
+ #
155
+ # search = Sunspot.new_search(Post)
156
+ # search.build do
157
+ # with(:published_at).less_than Time.now
158
+ # end
159
+ # search.execute!
160
+ #
161
+ def build(&block)
162
+ Util.instance_eval_or_call(dsl, &block)
163
+ self
164
+ end
165
+
166
+ #
167
+ # Populate the Hit objects with their instances. This is invoked the first
168
+ # time any hit has its instance requested, and all hits are loaded as a
169
+ # batch.
170
+ #
171
+ def populate_hits! #:nodoc:
172
+ id_hit_hash = Hash.new { |h, k| h[k] = {} }
173
+ hits.each do |hit|
174
+ id_hit_hash[hit.class_name][hit.primary_key] = hit
175
+ end
176
+ id_hit_hash.each_pair do |class_name, hits|
177
+ ids = hits.map { |id, hit| hit.primary_key }
178
+ data_accessor_for(Util.full_const_get(class_name)).load_all(ids).each do |instance|
179
+ hit = id_hit_hash[class_name][Adapters::InstanceAdapter.adapt(instance).id.to_s]
180
+ hit.instance = instance
181
+ end
182
+ end
183
+ end
184
+
185
+ def inspect #:nodoc:
186
+ "<Sunspot::Search:#{query.to_params.inspect}>"
187
+ end
188
+
189
+ private
190
+
191
+ def solr_response
192
+ @solr_response ||= @solr_result['response']
193
+ end
194
+
195
+ def doc_ids
196
+ @doc_ids ||= solr_response['docs'].map { |doc| doc['id'] }
197
+ end
198
+
199
+ def dsl
200
+ DSL::Search.new(self)
201
+ end
202
+
203
+ def raw_facet(field)
204
+ if field.type == Type::TimeType
205
+ @solr_result['facet_counts']['facet_dates'][field.indexed_name]
206
+ end || @solr_result['facet_counts']['facet_fields'][field.indexed_name]
207
+ end
208
+
209
+ def date_facet_data(field)
210
+ if field.type == Type::TimeType
211
+ if @solr_result['facet_counts'].has_key?('facet_dates')
212
+ if facet_result = @solr_result['facet_counts']['facet_dates'][field.indexed_name]
213
+ FacetData::DateFacetData.new(facet_result, field)
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def query_facet_data(name)
220
+ if query_facet = @query.query_facet(name.to_sym)
221
+ if @solr_result['facet_counts'].has_key?('facet_queries')
222
+ FacetData::QueryFacetData.new(
223
+ query_facet,
224
+ @solr_result['facet_counts']['facet_queries']
225
+ )
226
+ end
227
+ end
228
+ end
229
+
230
+ def highlights_for(doc)
231
+ if @solr_result['highlighting']
232
+ @solr_result['highlighting'][doc['id']]
233
+ end
234
+ end
235
+
236
+ def field(name)
237
+ @setup.field(name)
238
+ end
239
+ end
240
+ end