sunspot_rbg 1.3.0
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 +4 -0
- data/History.txt +222 -0
- data/LICENSE +18 -0
- data/Rakefile +17 -0
- data/TODO +13 -0
- data/VERSION.yml +4 -0
- data/bin/sunspot-installer +19 -0
- data/bin/sunspot-solr +74 -0
- data/installer/config/schema.yml +95 -0
- data/lib/light_config.rb +40 -0
- data/lib/sunspot/adapters.rb +265 -0
- data/lib/sunspot/composite_setup.rb +202 -0
- data/lib/sunspot/configuration.rb +46 -0
- data/lib/sunspot/data_extractor.rb +50 -0
- data/lib/sunspot/dsl/adjustable.rb +47 -0
- data/lib/sunspot/dsl/field_query.rb +279 -0
- data/lib/sunspot/dsl/fields.rb +103 -0
- data/lib/sunspot/dsl/fulltext.rb +243 -0
- data/lib/sunspot/dsl/function.rb +14 -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 +28 -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 +121 -0
- data/lib/sunspot/dsl/scope.rb +217 -0
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl/standard_query.rb +121 -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 +131 -0
- data/lib/sunspot/installer/library_installer.rb +45 -0
- data/lib/sunspot/installer/schema_builder.rb +219 -0
- data/lib/sunspot/installer/solrconfig_updater.rb +76 -0
- data/lib/sunspot/installer/task_helper.rb +18 -0
- data/lib/sunspot/installer.rb +31 -0
- data/lib/sunspot/query/abstract_field_facet.rb +52 -0
- data/lib/sunspot/query/boost_query.rb +24 -0
- data/lib/sunspot/query/common_query.rb +85 -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 +128 -0
- data/lib/sunspot/query/field_facet.rb +41 -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/highlighting.rb +55 -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 +38 -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 +95 -0
- data/lib/sunspot/query/sort_composite.rb +33 -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 +293 -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/highlight.rb +38 -0
- data/lib/sunspot/search/hit.rb +136 -0
- data/lib/sunspot/search/more_like_this_search.rb +31 -0
- data/lib/sunspot/search/paginated_collection.rb +55 -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/server.rb +152 -0
- data/lib/sunspot/session.rb +260 -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/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 +87 -0
- data/lib/sunspot/setup.rb +350 -0
- data/lib/sunspot/text_field_setup.rb +29 -0
- data/lib/sunspot/type.rb +372 -0
- data/lib/sunspot/util.rb +243 -0
- data/lib/sunspot/version.rb +3 -0
- data/lib/sunspot.rb +569 -0
- data/lib/sunspot_rbg.rb +7 -0
- data/log/.gitignore +1 -0
- data/pkg/.gitignore +1 -0
- data/script/console +10 -0
- data/solr/README.txt +42 -0
- data/solr/etc/jetty.xml +218 -0
- data/solr/etc/webdefault.xml +379 -0
- data/solr/lib/jetty-6.1.3.jar +0 -0
- data/solr/lib/jetty-util-6.1.3.jar +0 -0
- data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr/logs/.gitignore +1 -0
- data/solr/solr/.gitignore +1 -0
- data/solr/solr/README.txt +54 -0
- data/solr/solr/conf/admin-extra.html +31 -0
- data/solr/solr/conf/elevate.xml +36 -0
- data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
- data/solr/solr/conf/protwords.txt +21 -0
- data/solr/solr/conf/schema.xml +238 -0
- data/solr/solr/conf/scripts.conf +24 -0
- data/solr/solr/conf/solrconfig.xml +934 -0
- data/solr/solr/conf/spellings.txt +2 -0
- data/solr/solr/conf/stopwords.txt +58 -0
- data/solr/solr/conf/synonyms.txt +31 -0
- data/solr/solr/conf/xslt/example.xsl +132 -0
- data/solr/solr/conf/xslt/example_atom.xsl +67 -0
- data/solr/solr/conf/xslt/example_rss.xsl +66 -0
- data/solr/solr/conf/xslt/luke.xsl +337 -0
- data/solr/start.jar +0 -0
- data/solr/webapps/solr.war +0 -0
- data/solr-1.3/etc/jetty.xml +212 -0
- data/solr-1.3/etc/webdefault.xml +379 -0
- data/solr-1.3/lib/jetty-6.1.3.jar +0 -0
- data/solr-1.3/lib/jetty-util-6.1.3.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/ant-1.6.5.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/core-3.1.1.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/jsp-2.1.jar +0 -0
- data/solr-1.3/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
- data/solr-1.3/lib/servlet-api-2.5-6.1.3.jar +0 -0
- data/solr-1.3/solr/conf/elevate.xml +36 -0
- data/solr-1.3/solr/conf/protwords.txt +21 -0
- data/solr-1.3/solr/conf/schema.xml +64 -0
- data/solr-1.3/solr/conf/solrconfig.xml +725 -0
- data/solr-1.3/solr/conf/stopwords.txt +57 -0
- data/solr-1.3/solr/conf/synonyms.txt +31 -0
- data/solr-1.3/solr/lib/geoapi-nogenerics-2.1-M2.jar +0 -0
- data/solr-1.3/solr/lib/gt2-referencing-2.3.1.jar +0 -0
- data/solr-1.3/solr/lib/jsr108-0.01.jar +0 -0
- data/solr-1.3/solr/lib/locallucene.jar +0 -0
- data/solr-1.3/solr/lib/localsolr.jar +0 -0
- data/solr-1.3/start.jar +0 -0
- data/solr-1.3/webapps/solr.war +0 -0
- data/spec/api/adapters_spec.rb +33 -0
- data/spec/api/binding_spec.rb +50 -0
- data/spec/api/indexer/attributes_spec.rb +149 -0
- data/spec/api/indexer/batch_spec.rb +46 -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 +70 -0
- data/spec/api/query/geo_examples.rb +68 -0
- data/spec/api/query/highlighting_examples.rb +223 -0
- data/spec/api/query/more_like_this_spec.rb +140 -0
- data/spec/api/query/ordering_pagination_examples.rb +95 -0
- data/spec/api/query/scope_examples.rb +275 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/standard_spec.rb +28 -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 +120 -0
- data/spec/api/search/paginated_collection_spec.rb +26 -0
- data/spec/api/search/results_spec.rb +66 -0
- data/spec/api/search/search_spec.rb +23 -0
- data/spec/api/search/spec_helper.rb +1 -0
- data/spec/api/server_spec.rb +91 -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 +50 -0
- data/spec/api/session_spec.rb +220 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/api/sunspot_spec.rb +18 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +29 -0
- data/spec/helpers/query_helper.rb +38 -0
- data/spec/helpers/search_helper.rb +80 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/integration/faceting_spec.rb +238 -0
- data/spec/integration/highlighting_spec.rb +22 -0
- data/spec/integration/indexing_spec.rb +33 -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/spec_helper.rb +7 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/integration/test_pagination.rb +32 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +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 +85 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/mocks/user.rb +13 -0
- data/spec/spec_helper.rb +30 -0
- data/sunspot.gemspec +40 -0
- data/tasks/rdoc.rake +27 -0
- data/tasks/schema.rake +19 -0
- data/tasks/todo.rake +4 -0
- metadata +457 -0
|
@@ -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,11 @@
|
|
|
1
|
+
%w(filter abstract_field_facet connective boost_query date_field_facet dismax
|
|
2
|
+
field_facet highlighting pagination restriction common_query
|
|
3
|
+
standard_query more_like_this more_like_this_query geo query_facet scope
|
|
4
|
+
sort sort_composite text_field_boost function_query
|
|
5
|
+
composite_fulltext).each do |file|
|
|
6
|
+
require(File.join(File.dirname(__FILE__), 'query', file))
|
|
7
|
+
end
|
|
8
|
+
module Sunspot
|
|
9
|
+
module Query #:nodoc:all
|
|
10
|
+
end
|
|
11
|
+
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,293 @@
|
|
|
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.request("/#{request_handler}", params, {:header => {'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
|
+
# Get the data accessor that will be used to load a particular class out of
|
|
184
|
+
# persistent storage. Data accessors can implement any methods that may be
|
|
185
|
+
# useful for refining how data is loaded out of storage. When building a
|
|
186
|
+
# search manually (e.g., using the Sunspot#new_search method), this should
|
|
187
|
+
# be used before calling #execute(). Use the
|
|
188
|
+
# Sunspot::DSL::Search#data_accessor_for method when building searches using
|
|
189
|
+
# the block DSL.
|
|
190
|
+
#
|
|
191
|
+
def data_accessor_for(clazz) #:nodoc:
|
|
192
|
+
(@data_accessors ||= {})[clazz.name.to_sym] ||=
|
|
193
|
+
Adapters::DataAccessor.create(clazz)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# Build this search using a DSL block. This method can be called more than
|
|
198
|
+
# once on an unexecuted search (e.g., Sunspot.new_search) in order to build
|
|
199
|
+
# a search incrementally.
|
|
200
|
+
#
|
|
201
|
+
# === Example
|
|
202
|
+
#
|
|
203
|
+
# search = Sunspot.new_search(Post)
|
|
204
|
+
# search.build do
|
|
205
|
+
# with(:published_at).less_than Time.now
|
|
206
|
+
# end
|
|
207
|
+
# search.execute
|
|
208
|
+
#
|
|
209
|
+
def build(&block)
|
|
210
|
+
Util.instance_eval_or_call(dsl, &block)
|
|
211
|
+
self
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
#
|
|
215
|
+
# Populate the Hit objects with their instances. This is invoked the first
|
|
216
|
+
# time any hit has its instance requested, and all hits are loaded as a
|
|
217
|
+
# batch.
|
|
218
|
+
#
|
|
219
|
+
def populate_hits #:nodoc:
|
|
220
|
+
id_hit_hash = Hash.new { |h, k| h[k] = {} }
|
|
221
|
+
hits.each do |hit|
|
|
222
|
+
id_hit_hash[hit.class_name][hit.primary_key] = hit
|
|
223
|
+
end
|
|
224
|
+
id_hit_hash.each_pair do |class_name, hits|
|
|
225
|
+
ids = hits.map { |id, hit| hit.primary_key }
|
|
226
|
+
data_accessor = data_accessor_for(Util.full_const_get(class_name))
|
|
227
|
+
hits_for_class = id_hit_hash[class_name]
|
|
228
|
+
data_accessor.load_all(ids).each do |result|
|
|
229
|
+
hit = hits_for_class.delete(Adapters::InstanceAdapter.adapt(result).id.to_s)
|
|
230
|
+
hit.result = result
|
|
231
|
+
end
|
|
232
|
+
hits_for_class.values.each { |hit| hit.result = nil }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def inspect #:nodoc:
|
|
237
|
+
"<Sunspot::Search:#{query.to_params.inspect}>"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def add_field_facet(field, options = {}) #:nodoc:
|
|
241
|
+
name = (options[:name] || field.name)
|
|
242
|
+
add_facet(name, FieldFacet.new(field, self, options))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def add_query_facet(name, options) #:nodoc:
|
|
246
|
+
add_facet(name, QueryFacet.new(name, self, options))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def add_date_facet(field, options) #:nodoc:
|
|
250
|
+
name = (options[:name] || field.name)
|
|
251
|
+
add_facet(name, DateFacet.new(field, self, options))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
def dsl
|
|
257
|
+
raise NotImplementedError
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def execute_request(params)
|
|
261
|
+
raise NotImplementedError
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def solr_response
|
|
265
|
+
@solr_response ||= @solr_result['response'] || {}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def highlights_for(doc)
|
|
269
|
+
if @solr_result['highlighting']
|
|
270
|
+
@solr_result['highlighting'][doc['id']]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def verified_hits
|
|
275
|
+
@verified_hits ||= paginate_collection(hits.select { |hit| hit.instance })
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def paginate_collection(collection)
|
|
279
|
+
PaginatedCollection.new(collection, @query.page, @query.per_page, total)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def add_facet(name, facet)
|
|
283
|
+
@facets << facet
|
|
284
|
+
@facets_by_name[name.to_sym] = facet
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Clear out all the cached ivars so the search can be called again.
|
|
288
|
+
def reset
|
|
289
|
+
@results = @hits = @verified_hits = @total = @solr_response = @doc_ids = nil
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
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
|