outoftime-sunspot 0.8.9 → 0.9.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/README.rdoc +13 -21
- data/Rakefile +0 -2
- data/TODO +2 -15
- data/VERSION.yml +2 -2
- data/bin/sunspot-configure-solr +46 -0
- data/bin/sunspot-solr +15 -7
- data/lib/sunspot/adapters.rb +5 -1
- data/lib/sunspot/composite_setup.rb +186 -0
- data/lib/sunspot/configuration.rb +7 -1
- data/lib/sunspot/data_extractor.rb +10 -0
- data/lib/sunspot/date_facet.rb +36 -0
- data/lib/sunspot/date_facet_row.rb +17 -0
- data/lib/sunspot/dsl/field_query.rb +72 -0
- data/lib/sunspot/dsl/fields.rb +30 -3
- data/lib/sunspot/dsl/query.rb +16 -35
- data/lib/sunspot/dsl/query_facet.rb +31 -0
- data/lib/sunspot/dsl/scope.rb +76 -20
- data/lib/sunspot/dsl/search.rb +30 -0
- data/lib/sunspot/dsl.rb +1 -1
- data/lib/sunspot/facet.rb +17 -3
- data/lib/sunspot/facet_row.rb +4 -4
- data/lib/sunspot/field.rb +130 -207
- data/lib/sunspot/field_factory.rb +126 -0
- data/lib/sunspot/indexer.rb +61 -14
- data/lib/sunspot/instantiated_facet.rb +38 -0
- data/lib/sunspot/instantiated_facet_row.rb +12 -0
- data/lib/sunspot/query/base_query.rb +90 -0
- data/lib/sunspot/query/connective.rb +77 -0
- data/lib/sunspot/query/dynamic_query.rb +39 -56
- data/lib/sunspot/query/field_facet.rb +132 -4
- data/lib/sunspot/query/field_query.rb +57 -0
- data/lib/sunspot/query/pagination.rb +1 -1
- data/lib/sunspot/query/query_facet.rb +72 -0
- data/lib/sunspot/query/query_facet_row.rb +19 -0
- data/lib/sunspot/query/restriction.rb +9 -7
- data/lib/sunspot/query/scope.rb +165 -0
- data/lib/sunspot/query/sort.rb +17 -14
- data/lib/sunspot/query/sort_composite.rb +33 -0
- data/lib/sunspot/query.rb +162 -351
- data/lib/sunspot/query_facet.rb +33 -0
- data/lib/sunspot/query_facet_row.rb +21 -0
- data/lib/sunspot/schema.rb +165 -0
- data/lib/sunspot/search/hit.rb +62 -0
- data/lib/sunspot/search.rb +104 -41
- data/lib/sunspot/session.rb +64 -32
- data/lib/sunspot/setup.rb +119 -48
- data/lib/sunspot/type.rb +48 -2
- data/lib/sunspot.rb +74 -8
- data/solr/solr/conf/schema.xml +44 -225
- data/spec/api/build_search_spec.rb +557 -63
- data/spec/api/indexer_spec.rb +156 -74
- data/spec/api/query_spec.rb +55 -31
- data/spec/api/search_retrieval_spec.rb +210 -33
- data/spec/api/session_spec.rb +81 -26
- data/spec/api/sunspot_spec.rb +5 -7
- data/spec/integration/faceting_spec.rb +130 -0
- data/spec/integration/keyword_search_spec.rb +72 -31
- data/spec/integration/scoped_search_spec.rb +13 -0
- data/spec/integration/stored_fields_spec.rb +10 -0
- data/spec/mocks/blog.rb +3 -0
- data/spec/mocks/comment.rb +12 -23
- data/spec/mocks/connection.rb +84 -0
- data/spec/mocks/mock_adapter.rb +11 -3
- data/spec/mocks/mock_record.rb +41 -0
- data/spec/mocks/photo.rb +8 -0
- data/spec/mocks/post.rb +18 -23
- data/spec/spec_helper.rb +29 -14
- data/tasks/gemspec.rake +4 -3
- data/tasks/rdoc.rake +2 -2
- data/tasks/schema.rake +19 -0
- data/templates/schema.xml.haml +24 -0
- metadata +48 -7
- data/spec/mocks/base_class.rb +0 -2
data/README.rdoc
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
http://outoftime.github.com/sunspot
|
|
4
4
|
|
|
5
5
|
Sunspot is a Ruby library for expressive, powerful interaction with the Solr search engine.
|
|
6
|
-
Sunspot is built on top of the
|
|
6
|
+
Sunspot is built on top of the RSolr gem, which provides a low-level interface for Solr
|
|
7
7
|
interaction; Sunspot provides a simple, intuitive, expressive DSL backed by powerful
|
|
8
8
|
features for indexing objects and searching for them.
|
|
9
9
|
|
|
@@ -19,6 +19,7 @@ objects such as the filesystem.
|
|
|
19
19
|
* Intuitive DSL for scoping searches, with all the usual boolean operators available
|
|
20
20
|
* Intuitive interface for requesting facets on indexed fields
|
|
21
21
|
* Extensible adapter architecture for easy integration of other ORMs or non-model classes
|
|
22
|
+
* Refine search using field facets, date range facets, or ultra-powerful query facets
|
|
22
23
|
* Full compatibility with will_paginate
|
|
23
24
|
* Ordering
|
|
24
25
|
|
|
@@ -87,6 +88,10 @@ to define your own. See Sunspot::Adapters for more information.
|
|
|
87
88
|
with(:blog_id).any_of [2, 14]
|
|
88
89
|
with(:category_ids).all_of [4, 10]
|
|
89
90
|
with(:published_at).less_than Time.now
|
|
91
|
+
any_of do
|
|
92
|
+
with(:expired_at).greater_than(Time.now)
|
|
93
|
+
with(:expired_at, nil)
|
|
94
|
+
end
|
|
90
95
|
without :title, 'Bad Title'
|
|
91
96
|
without bad_instance # specifically exclude this instance from results
|
|
92
97
|
|
|
@@ -106,23 +111,6 @@ See Sunspot.search for more information.
|
|
|
106
111
|
search.per_page
|
|
107
112
|
search.facet(:blog_id)
|
|
108
113
|
|
|
109
|
-
=== Building searches manually:
|
|
110
|
-
|
|
111
|
-
The search DSL is great for building searches from fairly static parameters,
|
|
112
|
-
but a highly dynamic search might want to leverage an intermediate approach
|
|
113
|
-
(such as an application of the Builder pattern). For these cases, Sunspot
|
|
114
|
-
exposes direct access to the Query object:
|
|
115
|
-
|
|
116
|
-
search = Sunspot.new_search(Post)
|
|
117
|
-
search.query.keywords = 'great pizza'
|
|
118
|
-
search.query.add_restriction(:author_name, :equal_to, 'Mark Twain')
|
|
119
|
-
search.query.add_restriction(:title, :equal_to, 'Bad Title', true) # negate the restriction
|
|
120
|
-
search.query.exclude_instance(bad_instance)
|
|
121
|
-
search.query.paginate(3, 15)
|
|
122
|
-
search.query.order_by(:average_rating, :desc)
|
|
123
|
-
search.query.add_field_facet(:blog_id)
|
|
124
|
-
search.execute!
|
|
125
|
-
|
|
126
114
|
== About the API documentation
|
|
127
115
|
|
|
128
116
|
All of the methods documented in the RDoc are considered part of Sunspot's
|
|
@@ -133,10 +121,14 @@ me so I can rectify the situation!
|
|
|
133
121
|
|
|
134
122
|
== Dependencies
|
|
135
123
|
|
|
136
|
-
1.
|
|
137
|
-
2.
|
|
124
|
+
1. RSolr
|
|
125
|
+
2. Daemons
|
|
126
|
+
3. OptiFlag
|
|
127
|
+
4. Haml
|
|
128
|
+
5. Java
|
|
138
129
|
|
|
139
|
-
Sunspot has been tested with MRI 1.8.6, YARV 1.9.1, and
|
|
130
|
+
Sunspot has been tested with MRI 1.8.6 and 1.8.7, REE 1.8.6, YARV 1.9.1, and
|
|
131
|
+
JRuby 1.2.0
|
|
140
132
|
|
|
141
133
|
== Bugs
|
|
142
134
|
|
data/Rakefile
CHANGED
data/TODO
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
=== 0.9 ===
|
|
2
|
-
* Descriptive error messages during indexing
|
|
3
|
-
* Date type
|
|
4
|
-
* High-endian fields
|
|
5
|
-
* Add omitNorms to typed fields
|
|
6
|
-
* Instantiated facets!
|
|
7
|
-
* Direct access to adapter
|
|
8
|
-
* sunspot-configure-solr
|
|
9
|
-
* Text fields allow multiple
|
|
10
|
-
* Don't allow ordering on multiple-allowed fields
|
|
11
|
-
* Detect Ranges in short-form #with()
|
|
12
|
-
* Expose delete by ID
|
|
13
|
-
* Support composite IDs
|
|
14
|
-
* Transactions
|
|
15
|
-
* Switch to RSolr
|
|
16
|
-
* Facet by type (?)
|
|
17
2
|
* Query-based faceting (?)
|
|
3
|
+
=== 0.10 ===
|
|
4
|
+
* Intelligently decide whether to instantiate all facet rows at once
|
data/VERSION.yml
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
using_gems = false
|
|
4
|
+
begin
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'optiflag'
|
|
7
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'sunspot', 'schema')
|
|
8
|
+
rescue LoadError => e
|
|
9
|
+
if using_gems
|
|
10
|
+
raise(e)
|
|
11
|
+
else
|
|
12
|
+
using_gems = true
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
retry
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ConfigureSolrFlags extend OptiFlagSet
|
|
19
|
+
optional_flag 'tokenizer'
|
|
20
|
+
optional_flag 'extra_filters'
|
|
21
|
+
optional_flag 'dir'
|
|
22
|
+
and_process!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
solr_directory = ARGV.flags.dir || FileUtils.pwd
|
|
26
|
+
conf_directory = File.join(solr_directory, 'conf')
|
|
27
|
+
schema_file = File.join(conf_directory, 'schema.xml')
|
|
28
|
+
FileUtils.mkdir_p(conf_directory)
|
|
29
|
+
|
|
30
|
+
schema = Sunspot::Schema.new
|
|
31
|
+
schema.tokenizer = ARGV.flags.tokenizer if ARGV.flags.tokenizer
|
|
32
|
+
if ARGV.flags.extra_filters
|
|
33
|
+
for filter in ARGV.flags.extra_filters.split(',')
|
|
34
|
+
schema.add_filter(filter)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if File.exist?(schema_file)
|
|
39
|
+
backup_file = File.join(conf_directory, "schema-#{File.mtime(schema_file).strftime('%Y%m%d%H%M%S')}.xml")
|
|
40
|
+
STDERR.puts("Backing up current schema file to #{File.expand_path(backup_file)}")
|
|
41
|
+
FileUtils.mv(schema_file, backup_file)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
File.open(File.join(conf_directory, 'schema.xml'), 'w') do |file|
|
|
45
|
+
file << schema.to_xml
|
|
46
|
+
end
|
data/bin/sunspot-solr
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require '
|
|
6
|
-
require '
|
|
7
|
-
require '
|
|
8
|
-
|
|
2
|
+
using_gems = false
|
|
3
|
+
begin
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'daemons'
|
|
7
|
+
require 'optiflag'
|
|
8
|
+
rescue LoadError => e
|
|
9
|
+
if using_gems
|
|
10
|
+
raise(e)
|
|
11
|
+
else
|
|
12
|
+
using_gems = true
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
retry
|
|
15
|
+
end
|
|
16
|
+
end
|
|
9
17
|
|
|
10
18
|
working_directory = FileUtils.pwd
|
|
11
19
|
solr_home = File.join(File.dirname(__FILE__), '..', 'solr')
|
data/lib/sunspot/adapters.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Sunspot
|
|
|
61
61
|
# String:: ID for use in Solr
|
|
62
62
|
#
|
|
63
63
|
def index_id #:nodoc:
|
|
64
|
-
|
|
64
|
+
InstanceAdapter.index_id_for(@instance.class.name, id)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
class <<self
|
|
@@ -127,6 +127,10 @@ module Sunspot
|
|
|
127
127
|
"No adapter is configured for #{original_class_name} or its superclasses. See the documentation for Sunspot::Adapters")
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def index_id_for(class_name, id)
|
|
131
|
+
"#{class_name} #{id}"
|
|
132
|
+
end
|
|
133
|
+
|
|
130
134
|
protected
|
|
131
135
|
|
|
132
136
|
# Lazy-initialize the hash of registered instance adapters
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
#
|
|
3
|
+
# The CompositeSetup class encapsulates a collection of setups, and responds
|
|
4
|
+
# to a subset of the methods that Setup responds to (in particular, the
|
|
5
|
+
# methods required to build queries).
|
|
6
|
+
#
|
|
7
|
+
class CompositeSetup #:nodoc:
|
|
8
|
+
class << self
|
|
9
|
+
alias_method :for, :new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(types)
|
|
13
|
+
@types = types
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Collection of Setup objects for the enclosed types
|
|
18
|
+
#
|
|
19
|
+
# ==== Returns
|
|
20
|
+
#
|
|
21
|
+
# Array:: Collection of Setup objects
|
|
22
|
+
#
|
|
23
|
+
def setups
|
|
24
|
+
@setups ||= @types.map { |type| Setup.for(type) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Return the names of the encapsulated types
|
|
29
|
+
#
|
|
30
|
+
# ==== Returns
|
|
31
|
+
#
|
|
32
|
+
# Array:: Collection of class names
|
|
33
|
+
#
|
|
34
|
+
def type_names
|
|
35
|
+
@type_names ||= @types.map { |clazz| clazz.name }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Get a text field object by its public name. A field will be returned if
|
|
40
|
+
# it is configured for any of the enclosed types.
|
|
41
|
+
#
|
|
42
|
+
# ==== Returns
|
|
43
|
+
#
|
|
44
|
+
# Sunspot::FulltextField:: Text field with the given public name
|
|
45
|
+
#
|
|
46
|
+
# ==== Raises
|
|
47
|
+
#
|
|
48
|
+
# UnrecognizedFieldError::
|
|
49
|
+
# If no field with that name is configured for any of the enclosed types.
|
|
50
|
+
#
|
|
51
|
+
def text_field(field_name)
|
|
52
|
+
text_fields_hash[field_name.to_sym] || raise(
|
|
53
|
+
UnrecognizedFieldError,
|
|
54
|
+
"No text field configured for #{@types * ', '} with name '#{field_name}'"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#
|
|
59
|
+
# Get a Sunspot::AttributeField instance corresponding to the given field name
|
|
60
|
+
#
|
|
61
|
+
# ==== Parameters
|
|
62
|
+
#
|
|
63
|
+
# field_name<Symbol>:: The public field name for which to find a field
|
|
64
|
+
#
|
|
65
|
+
# ==== Returns
|
|
66
|
+
#
|
|
67
|
+
# Sunspot::AttributeField The field object corresponding to the given name
|
|
68
|
+
#
|
|
69
|
+
# ==== Raises
|
|
70
|
+
#
|
|
71
|
+
# ArgumentError::
|
|
72
|
+
# If the given field name is not configured for the types being queried
|
|
73
|
+
#
|
|
74
|
+
def field(field_name) #:nodoc:
|
|
75
|
+
fields_hash[field_name.to_sym] || raise(
|
|
76
|
+
UnrecognizedFieldError,
|
|
77
|
+
"No field configured for #{@types * ', '} with name '#{field_name}'"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Get a dynamic field factory for the given base name.
|
|
83
|
+
#
|
|
84
|
+
# ==== Returns
|
|
85
|
+
#
|
|
86
|
+
# DynamicFieldFactory:: Factory for dynamic fields with the given base name
|
|
87
|
+
#
|
|
88
|
+
# ==== Raises
|
|
89
|
+
#
|
|
90
|
+
# UnrecognizedFieldError::
|
|
91
|
+
# If the given base name is not configured as a dynamic field for the types being queried
|
|
92
|
+
#
|
|
93
|
+
def dynamic_field_factory(field_name)
|
|
94
|
+
dynamic_field_factories_hash[field_name.to_sym] || raise(
|
|
95
|
+
UnrecognizedFieldError,
|
|
96
|
+
"No dynamic field configured for #{@types * ', '} with name #{field_name.inspect}"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Collection of all text fields configured for any of the enclosed types.
|
|
102
|
+
#
|
|
103
|
+
# === Returns
|
|
104
|
+
#
|
|
105
|
+
# Array:: Text fields configured for the enclosed types
|
|
106
|
+
#
|
|
107
|
+
def text_fields
|
|
108
|
+
@text_fields ||= text_fields_hash.values
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
#
|
|
114
|
+
# Return a hash of field names to text field objects, containing all fields
|
|
115
|
+
# that are configured for any of the types enclosed.
|
|
116
|
+
#
|
|
117
|
+
# ==== Returns
|
|
118
|
+
#
|
|
119
|
+
# Hash:: Hash of field names to text field objects.
|
|
120
|
+
#
|
|
121
|
+
def text_fields_hash
|
|
122
|
+
@text_fields_hash ||=
|
|
123
|
+
setups.inject({}) do |hash, setup|
|
|
124
|
+
setup.text_fields.each do |text_field|
|
|
125
|
+
hash[text_field.name] ||= text_field
|
|
126
|
+
end
|
|
127
|
+
hash
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
# Return a hash of field names to field objects, containing all fields
|
|
133
|
+
# that are common to all of the classes enclosed. In order for fields
|
|
134
|
+
# to be common, they must be of the same type and have the same
|
|
135
|
+
# value for allow_multiple? and stored?. This method is memoized.
|
|
136
|
+
#
|
|
137
|
+
# ==== Returns
|
|
138
|
+
#
|
|
139
|
+
# Hash:: field names keyed to field objects
|
|
140
|
+
#
|
|
141
|
+
def fields_hash
|
|
142
|
+
@fields_hash ||=
|
|
143
|
+
begin
|
|
144
|
+
fields_hash = @types.inject({}) do |hash, type|
|
|
145
|
+
Setup.for(type).fields.each do |field|
|
|
146
|
+
(hash[field.name.to_sym] ||= {})[type.name] = field
|
|
147
|
+
end
|
|
148
|
+
hash
|
|
149
|
+
end
|
|
150
|
+
fields_hash.each_pair do |field_name, field_configurations_hash|
|
|
151
|
+
if @types.any? { |type| field_configurations_hash[type.name].nil? } # at least one type doesn't have this field configured
|
|
152
|
+
fields_hash.delete(field_name)
|
|
153
|
+
elsif field_configurations_hash.values.map { |configuration| configuration.indexed_name }.uniq.length != 1 # fields with this name have different configs
|
|
154
|
+
fields_hash.delete(field_name)
|
|
155
|
+
else
|
|
156
|
+
fields_hash[field_name] = field_configurations_hash.values.first
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
#
|
|
163
|
+
# Return a hash of dynamic field base names to dynamic field factories for
|
|
164
|
+
# those base names. Criteria for the inclusion are the same as for
|
|
165
|
+
# #fields_hash()
|
|
166
|
+
#
|
|
167
|
+
def dynamic_field_factories_hash
|
|
168
|
+
@dynamic_field_factories_hash ||=
|
|
169
|
+
begin
|
|
170
|
+
dynamic_field_factories_hash = @types.inject({}) do |hash, type|
|
|
171
|
+
Setup.for(type).dynamic_field_factories.each do |field_factory|
|
|
172
|
+
(hash[field_factory.name.to_sym] ||= {})[type.name] = field_factory
|
|
173
|
+
end
|
|
174
|
+
hash
|
|
175
|
+
end
|
|
176
|
+
dynamic_field_factories_hash.each_pair do |field_name, field_configurations_hash|
|
|
177
|
+
if @types.any? { |type| field_configurations_hash[type.name].nil? }
|
|
178
|
+
dynamic_field_factories_hash.delete(field_name)
|
|
179
|
+
else
|
|
180
|
+
dynamic_field_factories_hash[field_name] = field_configurations_hash.values.first
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -2,6 +2,11 @@ module Sunspot
|
|
|
2
2
|
# The Sunspot::Configuration module provides a factory method for Sunspot
|
|
3
3
|
# configuration objects. Available properties are:
|
|
4
4
|
#
|
|
5
|
+
# Sunspot.config.http_client::
|
|
6
|
+
# The client to use for HTTP communication with Solr. Available options are
|
|
7
|
+
# :net_http, which is the default and uses Ruby's built-in pure-Ruby HTTP
|
|
8
|
+
# library; and :curb, which uses Ruby's libcurl bindings and requires
|
|
9
|
+
# installation of the 'curb' gem.
|
|
5
10
|
# Sunspot.config.solr.url::
|
|
6
11
|
# The URL at which to connect to Solr
|
|
7
12
|
# (default: 'http://localhost:8983/solr')
|
|
@@ -19,8 +24,9 @@ module Sunspot
|
|
|
19
24
|
#
|
|
20
25
|
def build #:nodoc:
|
|
21
26
|
LightConfig.build do
|
|
27
|
+
http_client :net_http
|
|
22
28
|
solr do
|
|
23
|
-
url 'http://
|
|
29
|
+
url 'http://127.0.0.1:8983/solr'
|
|
24
30
|
end
|
|
25
31
|
pagination do
|
|
26
32
|
default_per_page 30
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
#
|
|
3
|
+
# Date facets are retrieved by passing a :time_range key into the
|
|
4
|
+
# DSL::FieldQuery#facet options. They are only available for Date and Time
|
|
5
|
+
# type fields. The #value for date facet rows is a Range object encapsulating
|
|
6
|
+
# the time range covered by the row.
|
|
7
|
+
#
|
|
8
|
+
class DateFacet < Facet
|
|
9
|
+
def initialize(facet_values, field) #:nodoc:
|
|
10
|
+
@gap = facet_values.delete('gap')[/\+(\d+)SECONDS/,1].to_i
|
|
11
|
+
%w(start end).each { |key| facet_values.delete(key) }
|
|
12
|
+
super(facet_values.to_a.flatten, field)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Get the rows of this date facet, which are instances of DateFacetRow.
|
|
17
|
+
# The rows will always be sorted in chronological order.
|
|
18
|
+
#
|
|
19
|
+
#--
|
|
20
|
+
#
|
|
21
|
+
# The date facet info comes back from Solr as a hash, so we need to sort
|
|
22
|
+
# it manually. FIXME this currently assumes we want to do a "lexical"
|
|
23
|
+
# sort, but we should support count sort as well, even if it's not a
|
|
24
|
+
# common use case.
|
|
25
|
+
#
|
|
26
|
+
def rows
|
|
27
|
+
super.sort { |a, b| a.value.first <=> b.value.first }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def new_row(pair)
|
|
33
|
+
DateFacetRow.new(pair, @gap, self)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
#TODO document
|
|
3
|
+
class DateFacetRow < FacetRow
|
|
4
|
+
def initialize(pair, gap, facet)
|
|
5
|
+
@gap = gap
|
|
6
|
+
super(pair, facet)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def value
|
|
10
|
+
@value ||=
|
|
11
|
+
begin
|
|
12
|
+
start_date = @facet.field.cast(@pair[0])
|
|
13
|
+
Range.new(start_date, start_date + @gap)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module DSL
|
|
3
|
+
#
|
|
4
|
+
# Provides an API for areas of the query DSL that operate on specific
|
|
5
|
+
# fields. This functionality is provided by the query DSL and the dynamic
|
|
6
|
+
# query DSL.
|
|
7
|
+
#
|
|
8
|
+
class FieldQuery < Scope
|
|
9
|
+
# Specify the order that results should be returned in. This method can
|
|
10
|
+
# be called multiple times; precedence will be in the order given.
|
|
11
|
+
#
|
|
12
|
+
# ==== Parameters
|
|
13
|
+
#
|
|
14
|
+
# field_name<Symbol>:: the field to use for ordering
|
|
15
|
+
# direction<Symbol>:: :asc or :desc (default :asc)
|
|
16
|
+
#
|
|
17
|
+
def order_by(field_name, direction = nil)
|
|
18
|
+
@query.order_by(field_name, direction)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Order results randomly. This will (generally) return the results in a
|
|
23
|
+
# different order each time a search is called.
|
|
24
|
+
#
|
|
25
|
+
def order_by_random
|
|
26
|
+
@query.order_by_random
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Request facets on the given field names. If the last argument is a hash,
|
|
30
|
+
# the given options will be applied to all specified fields. See
|
|
31
|
+
# Sunspot::Search#facet and Sunspot::Facet for information on what is
|
|
32
|
+
# returned.
|
|
33
|
+
#
|
|
34
|
+
# ==== Parameters
|
|
35
|
+
#
|
|
36
|
+
# field_names...<Symbol>:: fields for which to return field facets
|
|
37
|
+
#
|
|
38
|
+
# ==== Options
|
|
39
|
+
#
|
|
40
|
+
# :sort<Symbol>::
|
|
41
|
+
# Either :count (values matching the most terms first) or :index (lexical)
|
|
42
|
+
# :limit<Integer>::
|
|
43
|
+
# The maximum number of facet rows to return
|
|
44
|
+
# :minimum_count<Integer>::
|
|
45
|
+
# The minimum count a facet row must have to be returned
|
|
46
|
+
# :zeros<Boolean>::
|
|
47
|
+
# Return facet rows for which there are no matches (equivalent to
|
|
48
|
+
# :minimum_count => 0). Default is false.
|
|
49
|
+
#
|
|
50
|
+
def facet(*field_names, &block)
|
|
51
|
+
if block
|
|
52
|
+
if field_names.length != 1
|
|
53
|
+
raise(
|
|
54
|
+
ArgumentError,
|
|
55
|
+
"wrong number of arguments (#{field_names.length} for 1)"
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
name = field_names.first
|
|
59
|
+
DSL::QueryFacet.new(@query.add_query_facet(name)).instance_eval(&block)
|
|
60
|
+
else
|
|
61
|
+
options =
|
|
62
|
+
if field_names.last.is_a?(Hash)
|
|
63
|
+
field_names.pop
|
|
64
|
+
end
|
|
65
|
+
for field_name in field_names
|
|
66
|
+
@query.add_field_facet(field_name, options)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/sunspot/dsl/fields.rb
CHANGED
|
@@ -17,16 +17,43 @@ module Sunspot
|
|
|
17
17
|
# the only fields searched in fulltext searches. If a block is passed,
|
|
18
18
|
# create a virtual field; otherwise create an attribute field.
|
|
19
19
|
#
|
|
20
|
+
# If options are passed, they will be applied to all the given fields.
|
|
21
|
+
#
|
|
20
22
|
# ==== Parameters
|
|
21
23
|
#
|
|
22
24
|
# names...<Symbol>:: One or more field names
|
|
23
25
|
#
|
|
26
|
+
# ==== Options
|
|
27
|
+
#
|
|
28
|
+
# :boost<Float>::
|
|
29
|
+
# Boost that should be applied to this field for keyword search
|
|
30
|
+
#
|
|
24
31
|
def text(*names, &block)
|
|
32
|
+
options = names.pop if names.last.is_a?(Hash)
|
|
25
33
|
for name in names
|
|
26
|
-
@setup.
|
|
34
|
+
@setup.add_text_field_factory(
|
|
35
|
+
name,
|
|
36
|
+
options || {},
|
|
37
|
+
&block
|
|
38
|
+
)
|
|
27
39
|
end
|
|
28
40
|
end
|
|
29
41
|
|
|
42
|
+
#
|
|
43
|
+
# Specify a document-level boost. As with fields, you have the option of
|
|
44
|
+
# passing an attribute name which will be called on each model, or a block
|
|
45
|
+
# to be evaluated in the model's context. As well as these two options,
|
|
46
|
+
# this method can also take a constant number, meaning that all indexed
|
|
47
|
+
# documents of this class will have the specified boost.
|
|
48
|
+
#
|
|
49
|
+
# ==== Parameters
|
|
50
|
+
#
|
|
51
|
+
# attr_name<Symbol,~.to_f>:: Attribute name to call or a numeric constant
|
|
52
|
+
#
|
|
53
|
+
def boost(attr_name = nil, &block)
|
|
54
|
+
@setup.add_document_boost(attr_name, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
30
57
|
# method_missing is used to provide access to typed fields, because
|
|
31
58
|
# developers should be able to add new Sunspot::Type implementations
|
|
32
59
|
# dynamically and have them recognized inside the Fields DSL. Like #text,
|
|
@@ -49,9 +76,9 @@ module Sunspot
|
|
|
49
76
|
end
|
|
50
77
|
name = args.shift
|
|
51
78
|
if method.to_s =~ /^dynamic_/
|
|
52
|
-
@setup.
|
|
79
|
+
@setup.add_dynamic_field_factory(name, type, *args, &block)
|
|
53
80
|
else
|
|
54
|
-
@setup.
|
|
81
|
+
@setup.add_field_factory(name, type, *args, &block)
|
|
55
82
|
end
|
|
56
83
|
end
|
|
57
84
|
end
|
data/lib/sunspot/dsl/query.rb
CHANGED
|
@@ -3,28 +3,33 @@ module Sunspot
|
|
|
3
3
|
#
|
|
4
4
|
# This class presents a DSL for constructing queries using the
|
|
5
5
|
# Sunspot.search method. Methods of this class are available inside the
|
|
6
|
-
# search block.
|
|
7
|
-
#
|
|
8
|
-
# the #dynamic() block.
|
|
6
|
+
# search block. Much of the DSL's functionality is implemented by this
|
|
7
|
+
# class's superclasses, Sunspot::DSL::FieldQuery and Sunspot::DSL::Scope
|
|
9
8
|
#
|
|
10
9
|
# See Sunspot.search for usage examples
|
|
11
10
|
#
|
|
12
|
-
class Query <
|
|
11
|
+
class Query < FieldQuery
|
|
13
12
|
# Specify a phrase that should be searched as fulltext. Only +text+
|
|
14
13
|
# fields are searched - see DSL::Fields.text
|
|
15
14
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
15
|
+
# Keyword search is executed using Solr's dismax handler, which strikes
|
|
16
|
+
# a good balance between powerful and foolproof. In particular,
|
|
17
|
+
# well-matched quotation marks can be used to group phrases, and the
|
|
18
|
+
# + and - modifiers work as expected. All other special Solr boolean
|
|
19
|
+
# syntax is escaped, and mismatched quotes are ignored entirely.
|
|
21
20
|
#
|
|
22
21
|
# ==== Parameters
|
|
23
22
|
#
|
|
24
23
|
# keywords<String>:: phrase to perform fulltext search on
|
|
25
24
|
#
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
# ==== Options
|
|
26
|
+
#
|
|
27
|
+
# :fields<Array>::
|
|
28
|
+
# List of fields that should be searched for keywords. Defaults to all
|
|
29
|
+
# fields configured for the types under search.
|
|
30
|
+
#
|
|
31
|
+
def keywords(keywords, options = {})
|
|
32
|
+
@query.set_keywords(keywords, options)
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
# Paginate your search. This works the same way as WillPaginate's
|
|
@@ -49,30 +54,6 @@ module Sunspot
|
|
|
49
54
|
raise ArgumentError, "unknown argument #{options.keys.first.inspect} passed to paginate" unless options.empty?
|
|
50
55
|
@query.paginate(page, per_page)
|
|
51
56
|
end
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
# Apply restrictions, facets, and ordering to dynamic field instances.
|
|
55
|
-
# The block API is implemented by Sunspot::DSL::Scope, which is a
|
|
56
|
-
# superclass of the Query DSL (thus providing a subset of the API, in
|
|
57
|
-
# particular only methods that refer to particular fields).
|
|
58
|
-
#
|
|
59
|
-
# ==== Parameters
|
|
60
|
-
#
|
|
61
|
-
# base_name<Symbol>:: The base name for the dynamic field definition
|
|
62
|
-
#
|
|
63
|
-
# ==== Example
|
|
64
|
-
#
|
|
65
|
-
# Sunspot.search Post do
|
|
66
|
-
# dynamic :custom do
|
|
67
|
-
# with :cuisine, 'Pizza'
|
|
68
|
-
# facet :atmosphere
|
|
69
|
-
# order_by :chef_name
|
|
70
|
-
# end
|
|
71
|
-
# end
|
|
72
|
-
#
|
|
73
|
-
def dynamic(base_name, &block)
|
|
74
|
-
Scope.new(@query.dynamic_query(base_name)).instance_eval(&block)
|
|
75
|
-
end
|
|
76
57
|
end
|
|
77
58
|
end
|
|
78
59
|
end
|