gojee-sunspot 2.0.3 → 2.0.4
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 +12 -0
- data/Gemfile +5 -0
- data/History.txt +252 -0
- data/LICENSE +18 -0
- data/Rakefile +13 -0
- data/TODO +13 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/batcher.rb +62 -0
- data/lib/sunspot/class_set.rb +23 -0
- data/lib/sunspot/composite_setup.rb +202 -0
- data/lib/sunspot/configuration.rb +53 -0
- data/lib/sunspot/data_extractor.rb +50 -0
- data/lib/sunspot/dsl/adjustable.rb +47 -0
- data/lib/sunspot/dsl/field_group.rb +57 -0
- data/lib/sunspot/dsl/field_query.rb +327 -0
- data/lib/sunspot/dsl/fields.rb +103 -0
- data/lib/sunspot/dsl/fulltext.rb +243 -0
- data/lib/sunspot/dsl/function.rb +27 -0
- data/lib/sunspot/dsl/functional.rb +44 -0
- data/lib/sunspot/dsl/more_like_this_query.rb +56 -0
- data/lib/sunspot/dsl/paginatable.rb +32 -0
- data/lib/sunspot/dsl/query_facet.rb +36 -0
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/restriction_with_near.rb +160 -0
- data/lib/sunspot/dsl/scope.rb +217 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl/standard_query.rb +123 -0
- data/lib/sunspot/dsl.rb +5 -0
- data/lib/sunspot/field.rb +193 -0
- data/lib/sunspot/field_factory.rb +129 -0
- data/lib/sunspot/indexer.rb +136 -0
- data/lib/sunspot/query/abstract_field_facet.rb +52 -0
- data/lib/sunspot/query/bbox.rb +15 -0
- data/lib/sunspot/query/boost_query.rb +24 -0
- data/lib/sunspot/query/common_query.rb +96 -0
- data/lib/sunspot/query/composite_fulltext.rb +36 -0
- data/lib/sunspot/query/connective.rb +206 -0
- data/lib/sunspot/query/date_field_facet.rb +14 -0
- data/lib/sunspot/query/dismax.rb +132 -0
- data/lib/sunspot/query/field_facet.rb +41 -0
- data/lib/sunspot/query/field_group.rb +36 -0
- data/lib/sunspot/query/filter.rb +38 -0
- data/lib/sunspot/query/function_query.rb +52 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/geofilt.rb +16 -0
- data/lib/sunspot/query/highlighting.rb +62 -0
- data/lib/sunspot/query/more_like_this.rb +61 -0
- data/lib/sunspot/query/more_like_this_query.rb +12 -0
- data/lib/sunspot/query/pagination.rb +42 -0
- data/lib/sunspot/query/query_facet.rb +16 -0
- data/lib/sunspot/query/restriction.rb +262 -0
- data/lib/sunspot/query/scope.rb +9 -0
- data/lib/sunspot/query/sort.rb +109 -0
- data/lib/sunspot/query/sort_composite.rb +34 -0
- data/lib/sunspot/query/standard_query.rb +16 -0
- data/lib/sunspot/query/text_field_boost.rb +17 -0
- data/lib/sunspot/query.rb +11 -0
- data/lib/sunspot/schema.rb +151 -0
- data/lib/sunspot/search/abstract_search.rb +281 -0
- data/lib/sunspot/search/date_facet.rb +35 -0
- data/lib/sunspot/search/facet_row.rb +27 -0
- data/lib/sunspot/search/field_facet.rb +88 -0
- data/lib/sunspot/search/field_group.rb +32 -0
- data/lib/sunspot/search/group.rb +50 -0
- data/lib/sunspot/search/highlight.rb +38 -0
- data/lib/sunspot/search/hit.rb +150 -0
- data/lib/sunspot/search/hit_enumerable.rb +72 -0
- data/lib/sunspot/search/more_like_this_search.rb +31 -0
- data/lib/sunspot/search/paginated_collection.rb +57 -0
- data/lib/sunspot/search/query_facet.rb +67 -0
- data/lib/sunspot/search/standard_search.rb +21 -0
- data/lib/sunspot/search.rb +9 -0
- data/lib/sunspot/session.rb +262 -0
- data/lib/sunspot/session_proxy/abstract_session_proxy.rb +29 -0
- data/lib/sunspot/session_proxy/class_sharding_session_proxy.rb +66 -0
- data/lib/sunspot/session_proxy/id_sharding_session_proxy.rb +89 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +43 -0
- data/lib/sunspot/session_proxy/multicore_session_proxy.rb +67 -0
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +222 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +37 -0
- data/lib/sunspot/session_proxy.rb +95 -0
- data/lib/sunspot/setup.rb +350 -0
- data/lib/sunspot/text_field_setup.rb +29 -0
- data/lib/sunspot/type.rb +393 -0
- data/lib/sunspot/util.rb +252 -0
- data/lib/sunspot/version.rb +3 -0
- data/lib/sunspot.rb +579 -0
- data/log/.gitignore +1 -0
- data/pkg/.gitignore +1 -0
- data/script/console +10 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/batcher_spec.rb +112 -0
- data/spec/api/binding_spec.rb +50 -0
- data/spec/api/class_set_spec.rb +24 -0
- data/spec/api/hit_enumerable_spec.rb +47 -0
- data/spec/api/indexer/attributes_spec.rb +149 -0
- data/spec/api/indexer/batch_spec.rb +72 -0
- data/spec/api/indexer/dynamic_fields_spec.rb +42 -0
- data/spec/api/indexer/fixed_fields_spec.rb +57 -0
- data/spec/api/indexer/fulltext_spec.rb +43 -0
- data/spec/api/indexer/removal_spec.rb +53 -0
- data/spec/api/indexer/spec_helper.rb +1 -0
- data/spec/api/indexer_spec.rb +14 -0
- data/spec/api/query/advanced_manipulation_examples.rb +35 -0
- data/spec/api/query/connectives_examples.rb +189 -0
- data/spec/api/query/dsl_spec.rb +18 -0
- data/spec/api/query/dynamic_fields_examples.rb +165 -0
- data/spec/api/query/faceting_examples.rb +397 -0
- data/spec/api/query/fulltext_examples.rb +313 -0
- data/spec/api/query/function_spec.rb +79 -0
- data/spec/api/query/geo_examples.rb +68 -0
- data/spec/api/query/group_spec.rb +32 -0
- data/spec/api/query/highlighting_examples.rb +245 -0
- data/spec/api/query/more_like_this_spec.rb +140 -0
- data/spec/api/query/ordering_pagination_examples.rb +116 -0
- data/spec/api/query/scope_examples.rb +275 -0
- data/spec/api/query/spatial_examples.rb +27 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/standard_spec.rb +29 -0
- data/spec/api/query/text_field_scoping_examples.rb +30 -0
- data/spec/api/query/types_spec.rb +20 -0
- data/spec/api/search/dynamic_fields_spec.rb +33 -0
- data/spec/api/search/faceting_spec.rb +360 -0
- data/spec/api/search/highlighting_spec.rb +69 -0
- data/spec/api/search/hits_spec.rb +131 -0
- data/spec/api/search/paginated_collection_spec.rb +36 -0
- data/spec/api/search/results_spec.rb +72 -0
- data/spec/api/search/search_spec.rb +23 -0
- data/spec/api/search/spec_helper.rb +1 -0
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +85 -0
- data/spec/api/session_proxy/id_sharding_session_proxy_spec.rb +30 -0
- data/spec/api/session_proxy/master_slave_session_proxy_spec.rb +41 -0
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +77 -0
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_proxy/spec_helper.rb +9 -0
- data/spec/api/session_proxy/thread_local_session_proxy_spec.rb +39 -0
- data/spec/api/session_spec.rb +232 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/api/sunspot_spec.rb +29 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +17 -0
- data/spec/helpers/integration_helper.rb +8 -0
- data/spec/helpers/mock_session_helper.rb +13 -0
- data/spec/helpers/query_helper.rb +26 -0
- data/spec/helpers/search_helper.rb +68 -0
- data/spec/integration/dynamic_fields_spec.rb +57 -0
- data/spec/integration/faceting_spec.rb +251 -0
- data/spec/integration/field_grouping_spec.rb +66 -0
- data/spec/integration/geospatial_spec.rb +85 -0
- data/spec/integration/highlighting_spec.rb +44 -0
- data/spec/integration/indexing_spec.rb +55 -0
- data/spec/integration/keyword_search_spec.rb +317 -0
- data/spec/integration/local_search_spec.rb +64 -0
- data/spec/integration/more_like_this_spec.rb +43 -0
- data/spec/integration/scoped_search_spec.rb +354 -0
- data/spec/integration/stored_fields_spec.rb +12 -0
- data/spec/integration/test_pagination.rb +43 -0
- data/spec/integration/unicode_spec.rb +15 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +21 -0
- data/spec/mocks/connection.rb +126 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_class_sharding_session_proxy.rb +24 -0
- data/spec/mocks/mock_record.rb +52 -0
- data/spec/mocks/mock_sharding_session_proxy.rb +15 -0
- data/spec/mocks/photo.rb +11 -0
- data/spec/mocks/post.rb +86 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/mocks/user.rb +13 -0
- data/spec/spec_helper.rb +40 -0
- data/sunspot.gemspec +42 -0
- data/tasks/rdoc.rake +27 -0
- data/tasks/schema.rake +19 -0
- data/tasks/todo.rake +4 -0
- metadata +261 -3
@@ -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,281 @@
|
|
1
|
+
require 'sunspot/search/paginated_collection'
|
2
|
+
require 'sunspot/search/hit_enumerable'
|
3
|
+
|
4
|
+
module Sunspot
|
5
|
+
module Search #:nodoc:
|
6
|
+
|
7
|
+
#
|
8
|
+
# This class encapsulates the results of a Solr search. It provides access
|
9
|
+
# to search results, total result count, facets, and pagination information.
|
10
|
+
# Instances of Search are returned by the Sunspot.search and
|
11
|
+
# Sunspot.new_search methods.
|
12
|
+
#
|
13
|
+
class AbstractSearch
|
14
|
+
#
|
15
|
+
# Retrieve all facet objects defined for this search, in order they were
|
16
|
+
# defined. To retrieve an individual facet by name, use #facet()
|
17
|
+
#
|
18
|
+
attr_reader :facets, :groups
|
19
|
+
attr_reader :query #:nodoc:
|
20
|
+
attr_accessor :request_handler
|
21
|
+
|
22
|
+
include HitEnumerable
|
23
|
+
|
24
|
+
def initialize(connection, setup, query, configuration) #:nodoc:
|
25
|
+
@connection, @setup, @query = connection, setup, query
|
26
|
+
@query.paginate(1, configuration.pagination.default_per_page)
|
27
|
+
|
28
|
+
@facets = []
|
29
|
+
@facets_by_name = {}
|
30
|
+
|
31
|
+
@groups_by_name = {}
|
32
|
+
@groups = []
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Execute the search on the Solr instance and store the results. If you
|
37
|
+
# use Sunspot#search() to construct your searches, there is no need to call
|
38
|
+
# this method as it has already been called. If you use
|
39
|
+
# Sunspot#new_search(), you will need to call this method after building the
|
40
|
+
# query.
|
41
|
+
#
|
42
|
+
def execute
|
43
|
+
reset
|
44
|
+
params = @query.to_params
|
45
|
+
@solr_result = @connection.post "#{request_handler}", :data => params
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute! #:nodoc: deprecated
|
50
|
+
execute
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Get the collection of results as instantiated objects. If WillPaginate is
|
55
|
+
# available, the results will be a WillPaginate::Collection instance; if
|
56
|
+
# not, it will be a vanilla Array.
|
57
|
+
#
|
58
|
+
# If not all of the results referenced by the Solr hits actually exist in
|
59
|
+
# the data store, Sunspot will only return the results that do exist.
|
60
|
+
#
|
61
|
+
# ==== Returns
|
62
|
+
#
|
63
|
+
# WillPaginate::Collection or Array:: Instantiated result objects
|
64
|
+
#
|
65
|
+
def results
|
66
|
+
@results ||= paginate_collection(verified_hits.map { |hit| hit.instance })
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Access raw Solr result information. Returns a collection of Hit objects
|
71
|
+
# that contain the class name, primary key, keyword relevance score (if
|
72
|
+
# applicable), and any stored fields.
|
73
|
+
#
|
74
|
+
# ==== Options (options)
|
75
|
+
#
|
76
|
+
# :verify::
|
77
|
+
# Only return hits that reference objects that actually exist in the data
|
78
|
+
# store. This causes results to be eager-loaded from the data store,
|
79
|
+
# unlike the normal behavior of this method, which only loads the
|
80
|
+
# referenced results when Hit#result is first called.
|
81
|
+
#
|
82
|
+
# ==== Returns
|
83
|
+
#
|
84
|
+
# Array:: Ordered collection of Hit objects
|
85
|
+
#
|
86
|
+
def hits(options = {})
|
87
|
+
if options[:verify]
|
88
|
+
super
|
89
|
+
else
|
90
|
+
@hits ||= paginate_collection(super)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method :raw_results, :hits
|
94
|
+
|
95
|
+
#
|
96
|
+
# The total number of documents matching the query parameters
|
97
|
+
#
|
98
|
+
# ==== Returns
|
99
|
+
#
|
100
|
+
# Integer:: Total matching documents
|
101
|
+
#
|
102
|
+
def total
|
103
|
+
@total ||= solr_response['numFound'] || 0
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# The time elapsed to generate the Solr response
|
108
|
+
#
|
109
|
+
# ==== Returns
|
110
|
+
#
|
111
|
+
# Integer:: Query runtime in milliseconds
|
112
|
+
#
|
113
|
+
def query_time
|
114
|
+
@query_time ||= solr_response_header['QTime']
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Get the facet object for the given name. `name` can either be the name
|
119
|
+
# given to a query facet, or the field name of a field facet. Returns a
|
120
|
+
# Sunspot::Facet object.
|
121
|
+
#
|
122
|
+
# ==== Parameters
|
123
|
+
#
|
124
|
+
# name<Symbol>::
|
125
|
+
# Name of the field to return the facet for, or the name given to the
|
126
|
+
# query facet when the search was constructed.
|
127
|
+
# dynamic_name<Symbol>::
|
128
|
+
# If faceting on a dynamic field, this is the dynamic portion of the field
|
129
|
+
# name.
|
130
|
+
#
|
131
|
+
# ==== Example:
|
132
|
+
#
|
133
|
+
# search = Sunspot.search(Post) do
|
134
|
+
# facet :category_ids
|
135
|
+
# dynamic :custom do
|
136
|
+
# facet :cuisine
|
137
|
+
# end
|
138
|
+
# facet :age do
|
139
|
+
# row 'Less than a month' do
|
140
|
+
# with(:published_at).greater_than(1.month.ago)
|
141
|
+
# end
|
142
|
+
# row 'Less than a year' do
|
143
|
+
# with(:published_at, 1.year.ago..1.month.ago)
|
144
|
+
# end
|
145
|
+
# row 'More than a year' do
|
146
|
+
# with(:published_at).less_than(1.year.ago)
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
# search.facet(:category_ids)
|
151
|
+
# #=> Facet for :category_ids field
|
152
|
+
# search.facet(:custom, :cuisine)
|
153
|
+
# #=> Facet for the dynamic field :cuisine in the :custom field definition
|
154
|
+
# search.facet(:age)
|
155
|
+
# #=> Facet for the query facet named :age
|
156
|
+
#
|
157
|
+
def facet(name, dynamic_name = nil)
|
158
|
+
if name
|
159
|
+
if dynamic_name
|
160
|
+
@facets_by_name[:"#{name}:#{dynamic_name}"]
|
161
|
+
else
|
162
|
+
@facets_by_name[name.to_sym]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def group(name)
|
168
|
+
if name
|
169
|
+
@groups_by_name[name.to_sym]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Deprecated in favor of optional second argument to #facet
|
175
|
+
#
|
176
|
+
def dynamic_facet(base_name, dynamic_name) #:nodoc:
|
177
|
+
facet(base_name, dynamic_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
def facet_response #:nodoc:
|
181
|
+
@solr_result['facet_counts']
|
182
|
+
end
|
183
|
+
|
184
|
+
def group_response #:nodoc:
|
185
|
+
@solr_result['grouped']
|
186
|
+
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# Build this search using a DSL block. This method can be called more than
|
190
|
+
# once on an unexecuted search (e.g., Sunspot.new_search) in order to build
|
191
|
+
# a search incrementally.
|
192
|
+
#
|
193
|
+
# === Example
|
194
|
+
#
|
195
|
+
# search = Sunspot.new_search(Post)
|
196
|
+
# search.build do
|
197
|
+
# with(:published_at).less_than Time.now
|
198
|
+
# end
|
199
|
+
# search.execute
|
200
|
+
#
|
201
|
+
def build(&block)
|
202
|
+
Util.instance_eval_or_call(dsl, &block)
|
203
|
+
self
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
def inspect #:nodoc:
|
208
|
+
"<Sunspot::Search:#{query.to_params.inspect}>"
|
209
|
+
end
|
210
|
+
|
211
|
+
def add_field_group(field, options = {}) #:nodoc:
|
212
|
+
add_group(field.name, FieldGroup.new(field, self, options))
|
213
|
+
end
|
214
|
+
|
215
|
+
def add_field_facet(field, options = {}) #:nodoc:
|
216
|
+
name = (options[:name] || field.name)
|
217
|
+
add_facet(name, FieldFacet.new(field, self, options))
|
218
|
+
end
|
219
|
+
|
220
|
+
def add_query_facet(name, options) #:nodoc:
|
221
|
+
add_facet(name, QueryFacet.new(name, self, options))
|
222
|
+
end
|
223
|
+
|
224
|
+
def add_date_facet(field, options) #:nodoc:
|
225
|
+
name = (options[:name] || field.name)
|
226
|
+
add_facet(name, DateFacet.new(field, self, options))
|
227
|
+
end
|
228
|
+
|
229
|
+
def highlights_for(doc) #:nodoc:
|
230
|
+
if @solr_result['highlighting']
|
231
|
+
@solr_result['highlighting'][doc['id']]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def dsl
|
238
|
+
raise NotImplementedError
|
239
|
+
end
|
240
|
+
|
241
|
+
def execute_request(params)
|
242
|
+
raise NotImplementedError
|
243
|
+
end
|
244
|
+
|
245
|
+
def solr_response
|
246
|
+
@solr_response ||= @solr_result['response'] || {}
|
247
|
+
end
|
248
|
+
|
249
|
+
def solr_response_header
|
250
|
+
@solr_response_header ||= @solr_result['responseHeader'] || {}
|
251
|
+
end
|
252
|
+
|
253
|
+
def solr_docs
|
254
|
+
solr_response['docs']
|
255
|
+
end
|
256
|
+
|
257
|
+
def verified_hits
|
258
|
+
@verified_hits ||= paginate_collection(super)
|
259
|
+
end
|
260
|
+
|
261
|
+
def paginate_collection(collection)
|
262
|
+
PaginatedCollection.new(collection, @query.page, @query.per_page, total)
|
263
|
+
end
|
264
|
+
|
265
|
+
def add_facet(name, facet)
|
266
|
+
@facets << facet
|
267
|
+
@facets_by_name[name.to_sym] = facet
|
268
|
+
end
|
269
|
+
|
270
|
+
def add_group(name, group)
|
271
|
+
@groups << group
|
272
|
+
@groups_by_name[name.to_sym] = group
|
273
|
+
end
|
274
|
+
|
275
|
+
# Clear out all the cached ivars so the search can be called again.
|
276
|
+
def reset
|
277
|
+
@results = @hits = @verified_hits = @total = @solr_response = @doc_ids = nil
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Search
|
3
|
+
class DateFacet
|
4
|
+
def initialize(field, search, options)
|
5
|
+
@field, @search, @options = field, search, options
|
6
|
+
end
|
7
|
+
|
8
|
+
def field_name
|
9
|
+
@field.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def rows
|
13
|
+
@rows ||=
|
14
|
+
begin
|
15
|
+
data = @search.facet_response['facet_dates'][@field.indexed_name]
|
16
|
+
gap = (@options[:time_interval] || 86400).to_i
|
17
|
+
rows = []
|
18
|
+
data.each_pair do |value, count|
|
19
|
+
if value =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
|
20
|
+
start_time = @field.cast(value)
|
21
|
+
end_time = start_time + gap
|
22
|
+
rows << FacetRow.new(start_time..end_time, count, self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
if @options[:sort] == :count
|
26
|
+
rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
|
27
|
+
else
|
28
|
+
rows.sort! { |lrow, rrow| lrow.value.first <=> rrow.value.first }
|
29
|
+
end
|
30
|
+
rows
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Search
|
3
|
+
class FacetRow
|
4
|
+
attr_reader :value, :count
|
5
|
+
attr_writer :instance #:nodoc:
|
6
|
+
|
7
|
+
def initialize(value, count, facet) #:nodoc:
|
8
|
+
@value, @count, @facet = value, count, facet
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Return the instance referenced by this facet row. Only valid for field
|
13
|
+
# facets whose fields are defined with the :references key.
|
14
|
+
#
|
15
|
+
def instance
|
16
|
+
if !defined?(@instance)
|
17
|
+
@facet.populate_instances
|
18
|
+
end
|
19
|
+
@instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"<Sunspot::Search::FacetRow:#{value.inspect} (#{count})>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Search
|
3
|
+
#
|
4
|
+
# A FieldFacet is a facet whose rows are all values for a certain field, in
|
5
|
+
# contrast to a QueryFacet, whose rows represent arbitrary queries.
|
6
|
+
#
|
7
|
+
class FieldFacet < QueryFacet
|
8
|
+
def initialize(field, search, options) #:nodoc:
|
9
|
+
super((options[:name] || field.name).to_sym, search, options)
|
10
|
+
@field = field
|
11
|
+
end
|
12
|
+
|
13
|
+
def field_name
|
14
|
+
@field.name
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Get the rows returned for this facet.
|
19
|
+
#
|
20
|
+
# ==== Options (options)
|
21
|
+
#
|
22
|
+
# :verify::
|
23
|
+
# Only return rows for which the referenced object exists in the data
|
24
|
+
# store. This option is ignored unless the field associated with this
|
25
|
+
# facet is configured with a :references argument.
|
26
|
+
#
|
27
|
+
# ==== Returns
|
28
|
+
#
|
29
|
+
# Array:: Array of FacetRow objects
|
30
|
+
#
|
31
|
+
def rows(options = {})
|
32
|
+
if options[:verify]
|
33
|
+
verified_rows
|
34
|
+
else
|
35
|
+
@rows ||=
|
36
|
+
begin
|
37
|
+
rows = super
|
38
|
+
has_query_facets = !rows.empty?
|
39
|
+
if @search.facet_response['facet_fields']
|
40
|
+
if data = @search.facet_response['facet_fields'][key]
|
41
|
+
data.each_slice(2) do |value, count|
|
42
|
+
row = FacetRow.new(@field.cast(value), count, self)
|
43
|
+
rows << row
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
sort_rows!(rows) if has_query_facets
|
48
|
+
rows
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# If this facet references a model class, populate the rows with instances
|
55
|
+
# of the model class by loading them out of the appropriate adapter.
|
56
|
+
#
|
57
|
+
def populate_instances #:nodoc:
|
58
|
+
if reference = @field.reference
|
59
|
+
values_hash = rows.inject({}) do |hash, row|
|
60
|
+
hash[row.value] = row
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
instances = Adapters::DataAccessor.create(Sunspot::Util.full_const_get(reference)).load_all(
|
64
|
+
values_hash.keys
|
65
|
+
)
|
66
|
+
instances.each do |instance|
|
67
|
+
values_hash[Adapters::InstanceAdapter.adapt(instance).id].instance = instance
|
68
|
+
end
|
69
|
+
true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def verified_rows
|
76
|
+
if @field.reference
|
77
|
+
@verified_rows ||= rows.select { |row| row.instance }
|
78
|
+
else
|
79
|
+
rows
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def key
|
84
|
+
@key ||= (@options[:name] || @field.indexed_name).to_s
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module Search
|
3
|
+
class FieldGroup
|
4
|
+
def initialize(field, search, options) #:nodoc:
|
5
|
+
@field, @search, @options = field, search, options
|
6
|
+
end
|
7
|
+
|
8
|
+
def groups
|
9
|
+
@groups ||=
|
10
|
+
begin
|
11
|
+
if solr_response
|
12
|
+
solr_response['groups'].map do |group|
|
13
|
+
Group.new(group['groupValue'], group['doclist'], @search)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def matches
|
20
|
+
if solr_response
|
21
|
+
solr_response['matches'].to_i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def solr_response
|
28
|
+
@search.group_response[@field.indexed_name.to_s]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'sunspot/search/hit_enumerable'
|
2
|
+
|
3
|
+
module Sunspot
|
4
|
+
module Search
|
5
|
+
class Group
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
include HitEnumerable
|
9
|
+
|
10
|
+
def initialize(value, doclist, search)
|
11
|
+
@value, @doclist, @search = value, doclist, search
|
12
|
+
end
|
13
|
+
|
14
|
+
def hits(options = {})
|
15
|
+
if options[:verify]
|
16
|
+
super
|
17
|
+
else
|
18
|
+
@hits ||= super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def verified_hits
|
23
|
+
@verified_hits ||= super
|
24
|
+
end
|
25
|
+
|
26
|
+
def results
|
27
|
+
@results ||= verified_hits.map { |hit| hit.instance }
|
28
|
+
end
|
29
|
+
|
30
|
+
def highlights_for(doc)
|
31
|
+
@search.highlights_for(doc)
|
32
|
+
end
|
33
|
+
|
34
|
+
def solr_docs
|
35
|
+
@doclist['docs']
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# The total number of documents matching the query for this group
|
40
|
+
#
|
41
|
+
# ==== Returns
|
42
|
+
#
|
43
|
+
# Integer:: Total matching documents
|
44
|
+
#
|
45
|
+
def total
|
46
|
+
@doclist['numFound']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Sunspot
|
2
|
+
module 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
|