ultrasphinx 1.5.3 → 1.6
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +7 -1
- data/Manifest +6 -6
- data/README +3 -3
- data/TODO +2 -1
- data/examples/default.base +1 -0
- data/lib/ultrasphinx.rb +2 -1
- data/lib/ultrasphinx/configure.rb +10 -11
- data/lib/ultrasphinx/core_extensions.rb +2 -1
- data/lib/ultrasphinx/fields.rb +29 -18
- data/lib/ultrasphinx/search.rb +77 -92
- data/lib/ultrasphinx/search/internals.rb +133 -102
- data/lib/ultrasphinx/ultrasphinx.rb +0 -8
- data/test/integration/app/app/models/geo/state.rb +2 -1
- data/test/integration/app/app/models/person/user.rb +2 -1
- data/test/integration/app/config/environment.rb +0 -2
- data/test/integration/app/config/ultrasphinx/development.conf +6 -6
- data/test/integration/app/config/ultrasphinx/development.conf.canonical +6 -6
- data/test/integration/app/test/fixtures/sellers.yml +2 -2
- data/test/integration/app/test/fixtures/users.yml +2 -2
- data/test/integration/search_test.rb +67 -40
- data/test/setup.rb +3 -0
- data/ultrasphinx.gemspec +18 -17
- data/vendor/riddle/MIT-LICENSE +20 -0
- data/vendor/riddle/riddle.rb +15 -0
- data/vendor/riddle/riddle/client.rb +409 -0
- data/vendor/riddle/riddle/client/filter.rb +42 -0
- data/vendor/riddle/riddle/client/message.rb +54 -0
- data/vendor/riddle/riddle/client/response.rb +67 -0
- metadata +22 -16
- metadata.gz.sig +0 -0
- data/test/ts.multi +0 -2
- data/vendor/sphinx/LICENSE +0 -58
- data/vendor/sphinx/README +0 -40
- data/vendor/sphinx/Rakefile +0 -21
- data/vendor/sphinx/init.rb +0 -1
- data/vendor/sphinx/lib/client.rb +0 -647
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
|
2
|
-
|
2
|
+
vTODO. Use Pat Allan's association configurator.
|
3
|
+
|
4
|
+
v1.6. API changes! Drop Sphinx 0.9.7 compatibility; switch to Pat Allan's 0.9.8 client plugin; remove legacy keynames; fix string sorting bug; improve error handling.
|
5
|
+
|
6
|
+
v1.5.4. Various things.
|
7
|
+
|
8
|
+
v1.5.3. 90% test coverage; support multiple spelling dictionaries per machine (configurable in the .base file); association_name key is right out.
|
3
9
|
|
4
10
|
v1.5.2. Fix association reloading issue; support float attributes on Sphinx 0.9.8; fix date range filters; import and update sample app (Mark Lane); start a comprehensive integration suite.
|
5
11
|
|
data/Manifest
CHANGED
@@ -124,12 +124,12 @@ test/integration/spell_test.rb
|
|
124
124
|
test/setup.rb
|
125
125
|
test/test_all.rb
|
126
126
|
test/test_helper.rb
|
127
|
-
test/ts.multi
|
128
127
|
test/unit/parser_test.rb
|
129
128
|
TODO
|
130
|
-
vendor/
|
131
|
-
vendor/
|
132
|
-
vendor/
|
133
|
-
vendor/
|
134
|
-
vendor/
|
129
|
+
vendor/riddle/MIT-LICENSE
|
130
|
+
vendor/riddle/riddle/client/filter.rb
|
131
|
+
vendor/riddle/riddle/client/message.rb
|
132
|
+
vendor/riddle/riddle/client/response.rb
|
133
|
+
vendor/riddle/riddle/client.rb
|
134
|
+
vendor/riddle/riddle.rb
|
135
135
|
vendor/will_paginate/LICENSE
|
data/README
CHANGED
@@ -5,14 +5,14 @@ Ruby on Rails configurator and client to the Sphinx full text search engine.
|
|
5
5
|
|
6
6
|
== License
|
7
7
|
|
8
|
-
Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright
|
8
|
+
Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright Pat Allan, distributed under the MIT license, and used with permission. Some portions copyright PJ Hyett and Mislav Marohnić, distributed under the MIT license, and used with permission.
|
9
9
|
|
10
10
|
The public certificate for the gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
|
11
11
|
|
12
12
|
== Requirements
|
13
13
|
|
14
14
|
* MySQL (or Postgres, experimental)
|
15
|
-
* Sphinx 0.
|
15
|
+
* Sphinx 0.9.8-dev r877 or greater
|
16
16
|
* Rails 1.2.3 or greater
|
17
17
|
|
18
18
|
== Features
|
@@ -43,7 +43,7 @@ And some other things.
|
|
43
43
|
|
44
44
|
== Installation
|
45
45
|
|
46
|
-
First, compile and install Sphinx itself (http://www.sphinxsearch.com).
|
46
|
+
First, compile and install Sphinx itself using the 0.9.8 development snapshot (http://www.sphinxsearch.com).
|
47
47
|
|
48
48
|
You also need the <tt>chronic</tt> gem:
|
49
49
|
sudo gem install chronic
|
data/TODO
CHANGED
data/examples/default.base
CHANGED
@@ -53,6 +53,7 @@ index
|
|
53
53
|
morphology = stem_en
|
54
54
|
stopwords = # /path/to/stopwords.txt
|
55
55
|
min_word_len = 1
|
56
|
+
enable_star = 1
|
56
57
|
charset_type = utf-8 # or sbcs (Single Byte Character Set)
|
57
58
|
charset_table = 0..9, A..Z->a..z, -, _, ., &, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F,U+C5->U+E5, U+E5, U+C4->U+E4, U+E4, U+D6->U+F6, U+F6, U+16B, U+0c1->a, U+0c4->a, U+0c9->e, U+0cd->i, U+0d3->o, U+0d4->o, U+0da->u, U+0dd->y, U+0e1->a, U+0e4->a, U+0e9->e, U+0ed->i, U+0f3->o, U+0f4->o, U+0fa->u, U+0fd->y, U+104->U+105, U+105, U+106->U+107, U+10c->c, U+10d->c, U+10e->d, U+10f->d, U+116->U+117, U+117, U+118->U+119, U+11a->e, U+11b->e, U+12E->U+12F, U+12F, U+139->l, U+13a->l, U+13d->l, U+13e->l, U+141->U+142, U+142, U+143->U+144, U+144,U+147->n, U+148->n, U+154->r, U+155->r, U+158->r, U+159->r, U+15A->U+15B, U+15B, U+160->s, U+160->U+161, U+161->s, U+164->t, U+165->t, U+16A->U+16B, U+16B, U+16e->u, U+16f->u, U+172->U+173, U+173, U+179->U+17A, U+17A, U+17B->U+17C, U+17C, U+17d->z, U+17e->z,
|
58
59
|
}
|
data/lib/ultrasphinx.rb
CHANGED
@@ -10,7 +10,8 @@ if defined? RAILS_ENV and RAILS_ENV == "development"
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
$LOAD_PATH << "#{File.dirname(__FILE__)}/../vendor/riddle/"
|
14
|
+
require 'riddle'
|
14
15
|
|
15
16
|
require 'ultrasphinx/ultrasphinx'
|
16
17
|
require 'ultrasphinx/core_extensions'
|
@@ -93,9 +93,8 @@ module Ultrasphinx
|
|
93
93
|
|
94
94
|
column_strings = [
|
95
95
|
"(#{klass.table_name}.#{klass.primary_key} * #{MODEL_CONFIGURATION.size} + #{class_id}) AS id",
|
96
|
-
"#{class_id} AS class_id", "'#{klass.name}' AS class"
|
97
|
-
|
98
|
-
remaining_columns = fields.types.keys - ["class", "class_id", "empty_searchable"]
|
96
|
+
"#{class_id} AS class_id", "'#{klass.name}' AS class"]
|
97
|
+
remaining_columns = fields.types.keys - ["class", "class_id"]
|
99
98
|
[column_strings, [], condition_strings, remaining_columns]
|
100
99
|
end
|
101
100
|
|
@@ -162,7 +161,7 @@ module Ultrasphinx
|
|
162
161
|
|
163
162
|
def build_regular_fields(klass, fields, entries, column_strings, join_strings, remaining_columns)
|
164
163
|
entries.to_a.each do |entry|
|
165
|
-
source_string = "#{
|
164
|
+
source_string = "#{entry['table']}.#{entry['field']}"
|
166
165
|
column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
|
167
166
|
end
|
168
167
|
|
@@ -179,17 +178,17 @@ module Ultrasphinx
|
|
179
178
|
raise ConfigurationError, "Unknown association from #{klass} to #{entry['class_name']}" if not association and not entry['association_sql']
|
180
179
|
|
181
180
|
join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
|
182
|
-
"LEFT OUTER JOIN #{join_klass.table_name} ON " +
|
181
|
+
"LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON " +
|
183
182
|
if (macro = association.macro) == :belongs_to
|
184
|
-
"#{
|
183
|
+
"#{entry['table']}.#{join_klass.primary_key} = #{klass.table_name}.#{association.primary_key_name}"
|
185
184
|
elsif macro == :has_one
|
186
|
-
"#{klass.table_name}.#{klass.primary_key} = #{
|
185
|
+
"#{klass.table_name}.#{klass.primary_key} = #{entry['table']}.#{association.primary_key_name}"
|
187
186
|
else
|
188
187
|
raise ConfigurationError, "Unidentified association macro #{macro.inspect}. Please use the :association_sql key to manually specify the JOIN syntax."
|
189
188
|
end
|
190
189
|
end
|
191
190
|
|
192
|
-
source_string = "#{
|
191
|
+
source_string = "#{entry['table']}.#{entry['field']}"
|
193
192
|
column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
|
194
193
|
end
|
195
194
|
|
@@ -207,17 +206,17 @@ module Ultrasphinx
|
|
207
206
|
join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
|
208
207
|
# XXX make sure foreign key is right for polymorphic relationships
|
209
208
|
association = association_by_class_name(klass, entry['class_name'])
|
210
|
-
"LEFT OUTER JOIN #{join_klass.table_name} ON #{klass.table_name}.#{klass.primary_key} = #{
|
209
|
+
"LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON #{klass.table_name}.#{klass.primary_key} = #{entry['table']}.#{association.primary_key_name}" +
|
211
210
|
(entry['conditions'] ? " AND (#{entry['conditions']})" : "")
|
212
211
|
end
|
213
212
|
|
214
|
-
source_string = "GROUP_CONCAT(#{
|
213
|
+
source_string = "GROUP_CONCAT(DISTINCT #{entry['table']}.#{entry['field']} SEPARATOR ' ')"
|
215
214
|
column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
|
216
215
|
|
217
216
|
elsif entry['fields']
|
218
217
|
# regular concats
|
219
218
|
source_string = "CONCAT_WS(' ', " + entry['fields'].map do |subfield|
|
220
|
-
"#{
|
219
|
+
"#{entry['table']}.#{subfield}"
|
221
220
|
end.join(', ') + ")"
|
222
221
|
|
223
222
|
column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
|
@@ -62,6 +62,7 @@ end
|
|
62
62
|
### Filter type coercion methods
|
63
63
|
|
64
64
|
class String
|
65
|
+
# XXX Not used enough to justify such a strange abstraction
|
65
66
|
def _to_numeric
|
66
67
|
zeroless = self.squeeze(" ").strip.sub(/^0+(\d)/, '\1')
|
67
68
|
zeroless.sub!(/(\...*?)0+$/, '\1')
|
@@ -69,7 +70,7 @@ class String
|
|
69
70
|
zeroless.to_i
|
70
71
|
elsif zeroless.to_f.to_s == zeroless
|
71
72
|
zeroless.to_f
|
72
|
-
elsif date = Chronic.parse(self)
|
73
|
+
elsif date = Chronic.parse(self.gsub(/(\d)([^\d\:\s])/, '\1 \2')) # Improve Chronic's flexibility a little
|
73
74
|
date.to_i
|
74
75
|
else
|
75
76
|
raise Ultrasphinx::UsageError, "#{self.inspect} could not be coerced into a numeric value"
|
data/lib/ultrasphinx/fields.rb
CHANGED
@@ -18,9 +18,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
18
18
|
'float' => 'float',
|
19
19
|
'boolean' => 'integer'
|
20
20
|
}
|
21
|
-
|
22
|
-
VERSIONS_REQUIRED = {'float' => '0.9.8'}
|
23
|
-
|
21
|
+
|
24
22
|
attr_accessor :classes, :types
|
25
23
|
|
26
24
|
def initialize
|
@@ -37,7 +35,6 @@ This is a special singleton configuration class that stores the index field conf
|
|
37
35
|
|
38
36
|
def save_and_verify_type(field, new_type, string_sortable, klass)
|
39
37
|
# Smoosh fields together based on their name in the Sphinx query schema
|
40
|
-
check_version(new_type.to_s)
|
41
38
|
field, new_type = field.to_s, TYPE_MAP[new_type.to_s]
|
42
39
|
|
43
40
|
if types[field]
|
@@ -88,16 +85,6 @@ This is a special singleton configuration class that stores the index field conf
|
|
88
85
|
end + " AS #{field}"
|
89
86
|
end
|
90
87
|
|
91
|
-
def check_version(field)
|
92
|
-
# XXX Awkward location for the compatibility check
|
93
|
-
if req = VERSIONS_REQUIRED[field]
|
94
|
-
unless SPHINX_VERSION.include? req
|
95
|
-
# Will we eventually need to check version ranges?
|
96
|
-
Ultrasphinx.say "warning: '#{field}' type requires Sphinx #{req}, but you have #{SPHINX_VERSION}"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
88
|
def configure(configuration)
|
102
89
|
|
103
90
|
configuration.each do |model, options|
|
@@ -112,7 +99,9 @@ This is a special singleton configuration class that stores the index field conf
|
|
112
99
|
# We destructively canonicize them back onto the configuration hash
|
113
100
|
options['fields'] = options['fields'].to_a.map do |entry|
|
114
101
|
entry = {'field' => entry} unless entry.is_a? Hash
|
115
|
-
|
102
|
+
|
103
|
+
extract_table_alias!(entry, klass)
|
104
|
+
extract_field_alias!(entry, klass)
|
116
105
|
|
117
106
|
unless klass.columns_hash[entry['field']]
|
118
107
|
Ultrasphinx.say "warning: field #{entry['field']} is not present in #{model}"
|
@@ -130,15 +119,18 @@ This is a special singleton configuration class that stores the index field conf
|
|
130
119
|
|
131
120
|
# Joins are whatever they are in the target
|
132
121
|
options['include'].to_a.each do |entry|
|
133
|
-
entry
|
134
|
-
|
122
|
+
extract_table_alias!(entry, klass)
|
123
|
+
extract_field_alias!(entry, klass)
|
124
|
+
|
135
125
|
save_and_verify_type(entry['as'] || entry['field'], entry['class_name'].constantize.columns_hash[entry['field']].type, entry['sortable'], klass)
|
136
126
|
end
|
137
127
|
|
138
128
|
# Regular concats are CHAR, group_concats are BLOB and need to be cast to CHAR
|
139
129
|
options['concatenate'].to_a.each do |entry|
|
140
|
-
|
130
|
+
extract_table_alias!(entry, klass) # XXX Doesn't actually do anything useful
|
131
|
+
save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
|
141
132
|
end
|
133
|
+
|
142
134
|
rescue ActiveRecord::StatementInvalid
|
143
135
|
Ultrasphinx.say "warning: model #{model} does not exist in the database yet"
|
144
136
|
end
|
@@ -147,6 +139,25 @@ This is a special singleton configuration class that stores the index field conf
|
|
147
139
|
self
|
148
140
|
end
|
149
141
|
|
142
|
+
def extract_field_alias!(entry, klass)
|
143
|
+
unless entry['as']
|
144
|
+
entry['as'] = entry['field']
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def extract_table_alias!(entry, klass)
|
149
|
+
unless entry['table']
|
150
|
+
# Getting run twice; don't know why
|
151
|
+
if entry['field'] and entry['field'].include? "."
|
152
|
+
# This field is referenced by a table alias
|
153
|
+
entry['table'], entry['field'] = entry['field'].split(".")
|
154
|
+
else
|
155
|
+
klass = entry['class_name'].constantize if entry['class_name']
|
156
|
+
entry['table'] = klass.table_name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
150
161
|
end
|
151
162
|
end
|
152
163
|
|
data/lib/ultrasphinx/search.rb
CHANGED
@@ -97,13 +97,13 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
97
97
|
self.query_defaults ||= HashWithIndifferentAccess.new({
|
98
98
|
:query => nil,
|
99
99
|
:page => 1,
|
100
|
-
:class_names => nil,
|
101
100
|
:per_page => 20,
|
102
|
-
:sort_by =>
|
101
|
+
:sort_by => nil,
|
103
102
|
:sort_mode => 'relevance',
|
104
|
-
:weights =>
|
105
|
-
:
|
106
|
-
:
|
103
|
+
:weights => {},
|
104
|
+
:class_names => [],
|
105
|
+
:filters => {},
|
106
|
+
:facets => []
|
107
107
|
})
|
108
108
|
|
109
109
|
cattr_accessor :excerpting_options
|
@@ -112,38 +112,35 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
112
112
|
:chunk_separator => "...",
|
113
113
|
:limit => 256,
|
114
114
|
:around => 3,
|
115
|
-
#
|
115
|
+
# Results should respond to one in each group of these, in precedence order, for the excerpting to fire
|
116
116
|
:content_methods => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
|
117
117
|
})
|
118
118
|
|
119
119
|
cattr_accessor :client_options
|
120
120
|
self.client_options ||= HashWithIndifferentAccess.new({
|
121
121
|
:with_subtotals => false,
|
122
|
+
:ignore_missing_records => false,
|
122
123
|
:max_retries => 4,
|
123
|
-
:retry_sleep_time =>
|
124
|
+
:retry_sleep_time => 0.5,
|
124
125
|
:max_facets => 100,
|
125
126
|
:finder_methods => ['get_cache', 'find']
|
126
127
|
})
|
127
128
|
|
128
|
-
#
|
129
|
+
# Friendly sort mode mappings
|
129
130
|
SPHINX_CLIENT_PARAMS = HashWithIndifferentAccess.new({
|
130
131
|
:sort_mode => HashWithIndifferentAccess.new({
|
131
|
-
'relevance' =>
|
132
|
-
'descending' =>
|
133
|
-
'ascending' =>
|
134
|
-
'time' =>
|
135
|
-
'extended' =>
|
136
|
-
'desc' => Sphinx::Client::SPH_SORT_ATTR_DESC, # legacy compatibility
|
137
|
-
'asc' => Sphinx::Client::SPH_SORT_ATTR_ASC
|
132
|
+
'relevance' => :relevance,
|
133
|
+
'descending' => :attr_desc,
|
134
|
+
'ascending' => :attr_asc,
|
135
|
+
'time' => :time_segments,
|
136
|
+
'extended' => :extended,
|
138
137
|
})
|
139
138
|
})
|
140
139
|
|
141
|
-
LEGACY_QUERY_KEYS = ['raw_filters', 'filter', 'weight', 'class_name'] #:nodoc:
|
142
|
-
|
143
140
|
INTERNAL_KEYS = ['parsed_query'] #:nodoc:
|
144
141
|
|
145
142
|
def self.get_models_to_class_ids #:nodoc:
|
146
|
-
#
|
143
|
+
# Reading the conf file makes sure that we are in sync with the actual Sphinx index,
|
147
144
|
# not whatever you happened to change your models to most recently
|
148
145
|
unless File.exist? CONF_PATH
|
149
146
|
Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
|
@@ -199,54 +196,45 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
199
196
|
|
200
197
|
# Returns an array of result objects.
|
201
198
|
def results
|
202
|
-
|
199
|
+
require_run
|
203
200
|
@results
|
204
201
|
end
|
205
202
|
|
206
203
|
# Returns the facet map for this query, if facets were used.
|
207
204
|
def facets
|
208
|
-
run?(true)
|
209
205
|
raise UsageError, "No facet field was configured" unless @options['facets']
|
206
|
+
require_run
|
210
207
|
@facets
|
211
208
|
end
|
212
209
|
|
213
210
|
# Returns the raw response from the Sphinx client.
|
214
211
|
def response
|
215
|
-
|
212
|
+
require_run
|
216
213
|
@response
|
217
214
|
end
|
218
215
|
|
219
|
-
def class_name #:nodoc:
|
220
|
-
# Legacy accessor
|
221
|
-
@options['class_names']
|
222
|
-
end
|
223
|
-
|
224
216
|
# Returns a hash of total result counts, scoped to each available model. This requires extra queries against the search daemon right now. Set <tt>Ultrasphinx::Search.client_options[:with_subtotals] = true</tt> to enable the extra queries. Most of the overhead is in instantiating the AR result sets, so the performance hit is not usually significant.
|
225
217
|
def subtotals
|
226
|
-
|
227
|
-
|
218
|
+
raise UsageError, "Subtotals are not enabled" unless Ultrasphinx::Search.client_options['with_subtotals']
|
219
|
+
require_run
|
228
220
|
@subtotals
|
229
221
|
end
|
230
222
|
|
231
223
|
# Returns the total result count.
|
232
224
|
def total_entries
|
233
|
-
|
234
|
-
[response[
|
225
|
+
require_run
|
226
|
+
[response[:total_found] || 0, MAX_MATCHES].min
|
235
227
|
end
|
236
228
|
|
237
229
|
# Returns the response time of the query, in milliseconds.
|
238
230
|
def time
|
239
|
-
|
240
|
-
response[
|
231
|
+
require_run
|
232
|
+
response[:time]
|
241
233
|
end
|
242
234
|
|
243
235
|
# Returns whether the query has been run.
|
244
|
-
def run?
|
245
|
-
|
246
|
-
raise UsageError, "Search has not yet been run" unless run?
|
247
|
-
else
|
248
|
-
!@response.blank?
|
249
|
-
end
|
236
|
+
def run?
|
237
|
+
!@response.blank?
|
250
238
|
end
|
251
239
|
|
252
240
|
# Returns the current page number of the result set. (Page indexes begin at 1.)
|
@@ -261,8 +249,8 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
261
249
|
|
262
250
|
# Returns the last available page number in the result set.
|
263
251
|
def page_count
|
264
|
-
|
265
|
-
(total_entries / per_page)
|
252
|
+
require_run
|
253
|
+
(total_entries / per_page.to_f).ceil
|
266
254
|
end
|
267
255
|
|
268
256
|
# Returns the previous page number.
|
@@ -283,65 +271,48 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
283
271
|
# Builds a new command-interface Search object.
|
284
272
|
def initialize opts = {}
|
285
273
|
opts = HashWithIndifferentAccess.new(opts)
|
286
|
-
@options =
|
274
|
+
@options = Ultrasphinx::Search.query_defaults.merge(opts._deep_dup._coerce_basic_types)
|
287
275
|
|
288
|
-
# Legacy compatibility
|
289
|
-
@options['filters'] ||= @options['filter'] || @options['raw_filters'] || {}
|
290
|
-
@options['class_names'] ||= @options['class_name']
|
291
|
-
@options['weights'] ||= @options['weight']
|
292
|
-
|
293
|
-
@options._delete(*LEGACY_QUERY_KEYS)
|
294
|
-
|
295
|
-
# Coerce some special types
|
296
276
|
@options['query'] = @options['query'].to_s
|
297
277
|
@options['class_names'] = Array(@options['class_names'])
|
278
|
+
@options['facets'] = Array(@options['facets'])
|
279
|
+
|
280
|
+
raise UsageError, "Weights must be a Hash" unless @options['weights'].is_a? Hash
|
281
|
+
raise UsageError, "Filters must be a Hash" unless @options['filters'].is_a? Hash
|
298
282
|
|
299
|
-
@options['parsed_query'] =
|
300
|
-
"@empty_searchable #{EMPTY_SEARCHABLE}"
|
301
|
-
else
|
302
|
-
parse(query)
|
303
|
-
end
|
283
|
+
@options['parsed_query'] = parse(query)
|
304
284
|
|
305
285
|
@results, @subtotals, @facets, @response = [], {}, {}, {}
|
306
286
|
|
307
|
-
extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(
|
287
|
+
extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(Ultrasphinx::Search.query_defaults).keys + INTERNAL_KEYS)
|
308
288
|
say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
|
309
289
|
end
|
310
290
|
|
311
291
|
# Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false if you only want the ids returned.
|
312
292
|
def run(reify = true)
|
313
293
|
@request = build_request_with_options(@options)
|
314
|
-
@paginate = nil # clear cache
|
315
|
-
tries = 0
|
316
294
|
|
317
295
|
say "searching for #{@options.inspect}"
|
318
296
|
|
319
|
-
|
320
|
-
@response = @request.
|
321
|
-
say "search returned
|
322
|
-
|
323
|
-
|
297
|
+
perform_action_with_retries do
|
298
|
+
@response = @request.query(parsed_query, UNIFIED_INDEX_NAME)
|
299
|
+
say "search returned #{total_entries}/#{response[:total_found].to_i} in #{time.to_f} seconds."
|
300
|
+
|
301
|
+
if Ultrasphinx::Search.client_options['with_subtotals']
|
302
|
+
@subtotals = get_subtotals(@request, parsed_query)
|
303
|
+
end
|
324
304
|
|
325
305
|
Array(@options['facets']).each do |facet|
|
326
306
|
@facets[facet] = get_facets(@request, parsed_query, facet)
|
327
|
-
end
|
328
|
-
|
329
|
-
@results = response['matches']
|
307
|
+
end
|
330
308
|
|
331
|
-
|
309
|
+
@results = convert_sphinx_ids(response[:matches])
|
332
310
|
@results = reify_results(@results) if reify
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
retry
|
339
|
-
else
|
340
|
-
say "query failed"
|
341
|
-
raise Sphinx::SphinxConnectError, e.to_s
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
311
|
+
|
312
|
+
say "warning; #{response[:warning]}" if response[:warning]
|
313
|
+
raise UsageError, response[:error] if response[:error]
|
314
|
+
|
315
|
+
end
|
345
316
|
self
|
346
317
|
end
|
347
318
|
|
@@ -350,30 +321,38 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
350
321
|
# Runs run if it hasn't already been done.
|
351
322
|
def excerpt
|
352
323
|
|
353
|
-
|
324
|
+
require_run
|
354
325
|
return if results.empty?
|
355
326
|
|
356
|
-
#
|
327
|
+
# See what fields each result might respond to for our excerpting
|
357
328
|
results_with_content_methods = results.map do |result|
|
358
|
-
[result] <<
|
329
|
+
[result] << Ultrasphinx::Search.excerpting_options['content_methods'].map do |methods|
|
359
330
|
methods.detect { |x| result.respond_to? x }
|
360
331
|
end
|
361
332
|
end
|
362
333
|
|
363
|
-
#
|
364
|
-
|
334
|
+
# Fetch the actual field contents
|
335
|
+
docs = results_with_content_methods.map do |result, methods|
|
365
336
|
methods.map do |method|
|
366
337
|
method and strip_bogus_characters(result.send(method)) or ""
|
367
338
|
end
|
368
339
|
end.flatten
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
340
|
+
|
341
|
+
excerpting_options = {
|
342
|
+
:docs => docs,
|
343
|
+
:index => UNIFIED_INDEX_NAME,
|
344
|
+
:words => strip_query_commands(parsed_query)}
|
345
|
+
Ultrasphinx::Search.excerpting_options.except('content_methods').each do |key, value|
|
346
|
+
# Riddle only wants symbols
|
347
|
+
excerpting_options[key.to_sym] ||= value
|
348
|
+
end
|
349
|
+
|
350
|
+
responses = perform_action_with_retries do
|
351
|
+
# Ship to Sphinx to highlight and excerpt
|
352
|
+
@request.excerpts(excerpting_options)
|
353
|
+
end
|
354
|
+
|
355
|
+
responses = responses.in_groups_of(Ultrasphinx::Search.excerpting_options['content_methods'].size)
|
377
356
|
|
378
357
|
results_with_content_methods.each_with_index do |result_and_methods, i|
|
379
358
|
# override the individual model accessors with the excerpted data
|
@@ -391,7 +370,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
391
370
|
end
|
392
371
|
|
393
372
|
|
394
|
-
# Delegates enumerable methods to @results, if possible. This allows us to behave directly like a WillPaginate::Collection. Failing that, we delegate to the options hash if a key is set. This lets us use
|
373
|
+
# Delegates enumerable methods to @results, if possible. This allows us to behave directly like a WillPaginate::Collection. Failing that, we delegate to the options hash if a key is set. This lets us use <tt>self</tt> directly in view helpers.
|
395
374
|
def method_missing(*args, &block)
|
396
375
|
if @results.respond_to? args.first
|
397
376
|
@results.send(*args, &block)
|
@@ -406,5 +385,11 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
406
385
|
Ultrasphinx.say msg
|
407
386
|
end
|
408
387
|
|
388
|
+
private
|
389
|
+
|
390
|
+
def require_run
|
391
|
+
run unless run?
|
392
|
+
end
|
393
|
+
|
409
394
|
end
|
410
395
|
end
|