ultrasphinx 1.5 → 1.5.1
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 +2 -0
- data/README +3 -1
- data/lib/ultrasphinx/configure.rb +12 -12
- data/lib/ultrasphinx/core_extensions.rb +7 -24
- data/lib/ultrasphinx/fields.rb +3 -3
- data/lib/ultrasphinx/is_indexed.rb +15 -10
- data/lib/ultrasphinx/search.rb +81 -79
- data/lib/ultrasphinx/search/internals.rb +31 -24
- data/lib/ultrasphinx/search/parser.rb +12 -1
- data/lib/ultrasphinx/ultrasphinx.rb +29 -8
- data/test/test_helper.rb +2 -2
- data/test/unit/parser_test.rb +7 -1
- data/ultrasphinx.gemspec +5 -5
- metadata +3 -3
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
|
2
|
+
v1.5.1. Various bugfixes.
|
3
|
+
|
2
4
|
v1.5. API change. Change layout of base files to allow overriding of more options, see examples/default.base. Allow sorting on text fields (use the 'sortable' key).
|
3
5
|
|
4
6
|
v1.4. New is_indexed 'fields' => {'function_sql'} key for custom field mangling; support setting textual keys in the 'filters'.
|
data/README
CHANGED
@@ -7,6 +7,8 @@ Ruby on Rails configurator and client to the Sphinx full text search engine.
|
|
7
7
|
|
8
8
|
Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright Dmytro Shteflyuk and Alexey Kovyrin, distributed under the Ruby 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
|
+
The public certificate for the gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
|
11
|
+
|
10
12
|
== Requirements
|
11
13
|
|
12
14
|
* MySQL (or Postgres, experimental)
|
@@ -96,7 +98,7 @@ See Ultrasphinx::Spell.
|
|
96
98
|
|
97
99
|
== Available Rake tasks
|
98
100
|
|
99
|
-
These
|
101
|
+
These Rake tasks are made available to your Rails app:
|
100
102
|
|
101
103
|
<tt>ultrasphinx:configure</tt>:: Rebuild the configuration file for this particular environment.
|
102
104
|
<tt>ultrasphinx:index</tt>:: Reindex the database and send an update signal to the search daemon.
|
@@ -8,13 +8,15 @@ module Ultrasphinx
|
|
8
8
|
|
9
9
|
Dir["#{RAILS_ROOT}/app/models/**/*.rb"].each do |filename|
|
10
10
|
next if filename =~ /\/(\.svn|CVS|\.bzr)\//
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
open(filename) do |file|
|
12
|
+
begin
|
13
|
+
load filename if file.grep(/is_indexed/).any?
|
14
|
+
rescue Object => e
|
15
|
+
say "warning; possibly critical autoload error on #{filename}"
|
16
|
+
say e.inspect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
18
20
|
|
19
21
|
# Build the field-to-type mappings.
|
20
22
|
Fields.instance.configure(MODEL_CONFIGURATION)
|
@@ -26,7 +28,7 @@ module Ultrasphinx
|
|
26
28
|
|
27
29
|
load_constants
|
28
30
|
|
29
|
-
puts "Rebuilding Ultrasphinx configurations for #{
|
31
|
+
puts "Rebuilding Ultrasphinx configurations for #{RAILS_ENV} environment"
|
30
32
|
puts "Available models are #{MODEL_CONFIGURATION.keys.to_sentence}"
|
31
33
|
File.open(CONF_PATH, "w") do |conf|
|
32
34
|
|
@@ -64,7 +66,7 @@ module Ultrasphinx
|
|
64
66
|
# Tentatively supporting Postgres now
|
65
67
|
connection_settings = klass.connection.instance_variable_get("@config")
|
66
68
|
|
67
|
-
adapter_defaults = ADAPTER_DEFAULTS[
|
69
|
+
adapter_defaults = ADAPTER_DEFAULTS[ADAPTER]
|
68
70
|
raise ConfigurationError, "Unsupported database adapter" unless adapter_defaults
|
69
71
|
|
70
72
|
conf = [adapter_defaults]
|
@@ -127,8 +129,6 @@ module Ultrasphinx
|
|
127
129
|
|
128
130
|
def build_query(klass, column_strings, join_strings, condition_strings)
|
129
131
|
|
130
|
-
connection_settings = klass.connection.instance_variable_get("@config")
|
131
|
-
|
132
132
|
["sql_query =",
|
133
133
|
"SELECT",
|
134
134
|
column_strings.sort_by do |string|
|
@@ -141,7 +141,7 @@ module Ultrasphinx
|
|
141
141
|
condition_strings.uniq.map do |condition|
|
142
142
|
"AND #{condition}"
|
143
143
|
end,
|
144
|
-
|
144
|
+
ADAPTER_SQL_FUNCTIONS[ADAPTER]['group_by']
|
145
145
|
].flatten.join(" ")
|
146
146
|
end
|
147
147
|
|
@@ -50,34 +50,17 @@ class Hash
|
|
50
50
|
end._flatten_once]
|
51
51
|
end
|
52
52
|
|
53
|
+
def _delete(*args)
|
54
|
+
args.map do |key|
|
55
|
+
self.delete key
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
53
59
|
def _to_conf_string(section = nil)
|
54
60
|
inner = self.map do |key, value|
|
55
61
|
" #{key} = #{value}"
|
56
62
|
end.join("\n")
|
57
63
|
section ? "#{section} {\n#{inner}\n}\n" : inner
|
58
64
|
end
|
59
|
-
|
60
|
-
def _deep_stringify_keys
|
61
|
-
Hash[*(self.map do |key, value|
|
62
|
-
# puts "#{key.inspect}, #{value.inspect}"
|
63
|
-
z = [key.to_s,
|
64
|
-
case value
|
65
|
-
when Hash
|
66
|
-
value._deep_stringify_keys
|
67
|
-
when Array
|
68
|
-
value.map do |subvalue|
|
69
|
-
if subvalue.is_a? Hash or subvalue.is_a? Array
|
70
|
-
subvalue._deep_stringify_keys
|
71
|
-
else
|
72
|
-
subvalue
|
73
|
-
end
|
74
|
-
end
|
75
|
-
else
|
76
|
-
value
|
77
|
-
end
|
78
|
-
]
|
79
|
-
# p z
|
80
|
-
# z
|
81
|
-
end._flatten_once)]
|
82
|
-
end
|
65
|
+
|
83
66
|
end
|
data/lib/ultrasphinx/fields.rb
CHANGED
@@ -55,7 +55,7 @@ module Ultrasphinx
|
|
55
55
|
|
56
56
|
def cast(source_string, field)
|
57
57
|
if types[field] == "date"
|
58
|
-
"
|
58
|
+
"#{ADAPTER_SQL_FUNCTIONS[ADAPTER]['timestamp']}#{source_string})"
|
59
59
|
elsif source_string =~ /GROUP_CONCAT/
|
60
60
|
"CAST(#{source_string} AS CHAR)"
|
61
61
|
else
|
@@ -93,7 +93,7 @@ module Ultrasphinx
|
|
93
93
|
entry['as'] = entry['field'] unless entry['as']
|
94
94
|
|
95
95
|
unless klass.columns_hash[entry['field']]
|
96
|
-
|
96
|
+
Ultrasphinx.say "WARNING: field #{entry['field']} is not present in #{model}"
|
97
97
|
else
|
98
98
|
save_and_verify_type(entry['as'], klass.columns_hash[entry['field']].type, entry['sortable'], klass)
|
99
99
|
end
|
@@ -116,7 +116,7 @@ module Ultrasphinx
|
|
116
116
|
save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
|
117
117
|
end
|
118
118
|
rescue ActiveRecord::StatementInvalid
|
119
|
-
|
119
|
+
Ultrasphinx.say "WARNING: model #{model} does not exist in the database yet"
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
@@ -120,23 +120,28 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
|
|
120
120
|
=end
|
121
121
|
|
122
122
|
def self.is_indexed opts = {}
|
123
|
-
opts = opts
|
123
|
+
opts = HashWithIndifferentAccess.new(opts)
|
124
124
|
|
125
125
|
opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include']
|
126
126
|
|
127
|
-
Array(opts[
|
128
|
-
|
127
|
+
Array(opts['fields']).each do |entry|
|
128
|
+
if entry.is_a? Hash
|
129
|
+
entry.stringify_keys!
|
130
|
+
entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable']
|
131
|
+
end
|
129
132
|
end
|
130
133
|
|
131
|
-
Array(opts[
|
132
|
-
|
133
|
-
|
134
|
-
raise Ultrasphinx::ConfigurationError, "
|
135
|
-
raise Ultrasphinx::ConfigurationError, "
|
134
|
+
Array(opts['concatenate']).each do |entry|
|
135
|
+
entry.stringify_keys!
|
136
|
+
entry.assert_valid_keys ['class_name', 'conditions', 'field', 'as', 'fields', 'association_name', 'association_sql', 'facet', 'function_sql', 'sortable']
|
137
|
+
raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if entry['fields'] and (entry['field'] or entry['class_name'] or entry['association_name'])
|
138
|
+
raise Ultrasphinx::ConfigurationError, "Group concats must not have multiple fields" if entry['field'].is_a? Array
|
139
|
+
raise Ultrasphinx::ConfigurationError, "Regular concats should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array)
|
136
140
|
end
|
137
141
|
|
138
|
-
Array(opts[
|
139
|
-
|
142
|
+
Array(opts['include']).each do |entry|
|
143
|
+
entry.stringify_keys!
|
144
|
+
entry.assert_valid_keys ['class_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
|
140
145
|
end
|
141
146
|
|
142
147
|
Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts
|
data/lib/ultrasphinx/search.rb
CHANGED
@@ -6,11 +6,11 @@ Command-interface Search object.
|
|
6
6
|
|
7
7
|
== Basic usage
|
8
8
|
|
9
|
-
To set up a search, instantiate an Ultrasphinx::Search object with a hash of parameters. Only the <tt
|
9
|
+
To set up a search, instantiate an Ultrasphinx::Search object with a hash of parameters. Only the <tt>:query</tt> key is mandatory.
|
10
10
|
@search = Ultrasphinx::Search.new(
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
:query => @query,
|
12
|
+
:sort_mode => 'descending',
|
13
|
+
:sort_by => 'created_at'
|
14
14
|
)
|
15
15
|
|
16
16
|
Now, to run the query, call its <tt>run</tt> method. Your results will be available as ActiveRecord instances via the <tt>results</tt> method. Example:
|
@@ -34,21 +34,21 @@ A Sphinx::SphinxInternalError will be raised on invalid queries. In general, que
|
|
34
34
|
|
35
35
|
The hash lets you customize internal aspects of the search.
|
36
36
|
|
37
|
-
<tt
|
38
|
-
<tt
|
39
|
-
<tt
|
40
|
-
<tt
|
41
|
-
<tt
|
42
|
-
<tt
|
43
|
-
<tt
|
44
|
-
<tt
|
37
|
+
<tt>:per_page</tt>:: An integer. How many results per page.
|
38
|
+
<tt>:page</tt>:: An integer. Which page of the results to return.
|
39
|
+
<tt>:class_names</tt>:: An array or string. The class name of the model you want to search, an array of model names to search, or <tt>nil</tt> for all available models.
|
40
|
+
<tt>:sort_mode</tt>:: <tt>'relevance'</tt> or <tt>'ascending'</tt> or <tt>'descending'</tt>. How to order the result set. Note that <tt>'time'</tt> and <tt>'extended'</tt> modes are available, but not tested.
|
41
|
+
<tt>:sort_by</tt>:: A field name. What field to order by for <tt>'ascending'</tt> or <tt>'descending'</tt> mode. Has no effect for <tt>'relevance'</tt>.
|
42
|
+
<tt>:weights</tt>:: A hash. Text-field names and associated query weighting. The default weight for every field is 1.0. Example: <tt>:weights => {'title' => 2.0}</tt>
|
43
|
+
<tt>:filters</tt>:: A hash. Names of numeric or date fields and associated values. You can use a single value, an array of values, or a range. (See the bottom of the ActiveRecord::Base page for an example.)
|
44
|
+
<tt>:facets</tt>:: An array of fields for grouping/faceting. You can access the returned facet values and their result counts with the <tt>facets</tt> method.
|
45
45
|
|
46
46
|
Note that you can set up your own query defaults in <tt>environment.rb</tt>:
|
47
47
|
|
48
48
|
Ultrasphinx::Search.query_defaults = {
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
:per_page => 10,
|
50
|
+
:sort_mode => 'relevance',
|
51
|
+
:weights => {'title' => 2.0}
|
52
52
|
}
|
53
53
|
|
54
54
|
= Advanced features
|
@@ -76,12 +76,12 @@ You need to set the <tt>content_methods</tt> key on Ultrasphinx::Search.excerpti
|
|
76
76
|
There are some other keys you can set, such as excerpt size, HTML tags to highlight with, and number of words on either side of each excerpt chunk. Example (in <tt>environment.rb</tt>):
|
77
77
|
|
78
78
|
Ultrasphinx::Search.excerpting_options = {
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
79
|
+
:before_match => '<strong>',
|
80
|
+
:after_match => '</strong>',
|
81
|
+
:chunk_separator => "...",
|
82
|
+
:limit => 256,
|
83
|
+
:around => 3,
|
84
|
+
:content_methods => [['title'], ['body', 'description', 'content'], ['metadata']]
|
85
85
|
}
|
86
86
|
|
87
87
|
Note that your database is never changed by anything Ultrasphinx does.
|
@@ -94,40 +94,40 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
94
94
|
include Parser
|
95
95
|
|
96
96
|
cattr_accessor :query_defaults
|
97
|
-
self.query_defaults ||= {
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
}
|
97
|
+
self.query_defaults ||= HashWithIndifferentAccess.new({
|
98
|
+
:query => nil,
|
99
|
+
:page => 1,
|
100
|
+
:class_names => nil,
|
101
|
+
:per_page => 20,
|
102
|
+
:sort_by => 'created_at',
|
103
|
+
:sort_mode => 'relevance',
|
104
|
+
:weights => nil,
|
105
|
+
:filters => nil,
|
106
|
+
:facets => nil
|
107
|
+
})
|
108
108
|
|
109
109
|
cattr_accessor :excerpting_options
|
110
|
-
self.excerpting_options ||= {
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
110
|
+
self.excerpting_options ||= HashWithIndifferentAccess.new({
|
111
|
+
:before_match => "<strong>", :after_match => "</strong>",
|
112
|
+
:chunk_separator => "...",
|
113
|
+
:limit => 256,
|
114
|
+
:around => 3,
|
115
115
|
# results should respond to one in each group of these, in precedence order, for the excerpting to fire
|
116
|
-
|
117
|
-
}
|
116
|
+
:content_methods => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
|
117
|
+
})
|
118
118
|
|
119
119
|
cattr_accessor :client_options
|
120
|
-
self.client_options ||= {
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
}
|
120
|
+
self.client_options ||= HashWithIndifferentAccess.new({
|
121
|
+
:with_subtotals => false,
|
122
|
+
:max_retries => 4,
|
123
|
+
:retry_sleep_time => 3,
|
124
|
+
:max_facets => 100,
|
125
|
+
:finder_methods => ['get_cache', 'find']
|
126
|
+
})
|
127
127
|
|
128
128
|
# mode to integer mappings
|
129
|
-
SPHINX_CLIENT_PARAMS = {
|
130
|
-
|
129
|
+
SPHINX_CLIENT_PARAMS = HashWithIndifferentAccess.new({
|
130
|
+
:sort_mode => HashWithIndifferentAccess.new({
|
131
131
|
'relevance' => Sphinx::Client::SPH_SORT_RELEVANCE,
|
132
132
|
'descending' => Sphinx::Client::SPH_SORT_ATTR_DESC,
|
133
133
|
'ascending' => Sphinx::Client::SPH_SORT_ATTR_ASC,
|
@@ -135,10 +135,10 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
135
135
|
'extended' => Sphinx::Client::SPH_SORT_EXTENDED,
|
136
136
|
'desc' => Sphinx::Client::SPH_SORT_ATTR_DESC, # legacy compatibility
|
137
137
|
'asc' => Sphinx::Client::SPH_SORT_ATTR_ASC
|
138
|
-
}
|
139
|
-
}
|
138
|
+
})
|
139
|
+
})
|
140
140
|
|
141
|
-
LEGACY_QUERY_KEYS = ['raw_filters'] #:nodoc:
|
141
|
+
LEGACY_QUERY_KEYS = ['raw_filters', 'filter', 'weight', 'class_name'] #:nodoc:
|
142
142
|
|
143
143
|
INTERNAL_KEYS = ['parsed_query'] #:nodoc:
|
144
144
|
|
@@ -146,7 +146,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
146
146
|
# reading the conf file makes sure that we are in sync with the actual sphinx index,
|
147
147
|
# not whatever you happened to change your models to most recently
|
148
148
|
unless File.exist? CONF_PATH
|
149
|
-
Ultrasphinx.say "configuration file not found for #{
|
149
|
+
Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
|
150
150
|
Ultrasphinx.say "please run 'rake ultrasphinx:configure'"
|
151
151
|
else
|
152
152
|
begin
|
@@ -178,12 +178,12 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
178
178
|
|
179
179
|
# Returns the query string used.
|
180
180
|
def query
|
181
|
-
#
|
181
|
+
# Redundant with method_missing
|
182
182
|
@options['query']
|
183
183
|
end
|
184
184
|
|
185
185
|
def parsed_query #:nodoc:
|
186
|
-
#
|
186
|
+
# Redundant with method_missing
|
187
187
|
@options['parsed_query']
|
188
188
|
end
|
189
189
|
|
@@ -193,19 +193,24 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
193
193
|
@results
|
194
194
|
end
|
195
195
|
|
196
|
+
# Returns the facet map for this query, if facets were used.
|
196
197
|
def facets
|
197
198
|
raise UsageError, "No facet field was configured" unless @options['facets']
|
198
199
|
run?(true)
|
199
200
|
@facets
|
200
201
|
end
|
201
|
-
|
202
|
-
|
202
|
+
|
203
203
|
# Returns the raw response from the Sphinx client.
|
204
204
|
def response
|
205
205
|
@response
|
206
206
|
end
|
207
207
|
|
208
|
-
|
208
|
+
def class_name #:nodoc:
|
209
|
+
# Legacy accessor
|
210
|
+
@options['class_name']
|
211
|
+
end
|
212
|
+
|
213
|
+
# 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.
|
209
214
|
def subtotals
|
210
215
|
raise UsageError, "Subtotals are not enabled" unless self.class.client_options['with_subtotals']
|
211
216
|
@subtotals
|
@@ -239,14 +244,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
239
244
|
def per_page
|
240
245
|
@options['per_page']
|
241
246
|
end
|
242
|
-
|
243
|
-
# Clear the associated facet caches. They will be rebuilt on your next <tt>run</tt> or <tt>excerpt</tt>.
|
244
|
-
def clear_facet_caches
|
245
|
-
Array(@options['facets']).each do |facet|
|
246
|
-
FACET_CACHE.delete(facet)
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
247
|
+
|
250
248
|
# Returns the last available page number in the result set.
|
251
249
|
def page_count
|
252
250
|
(total_entries / per_page) + (total_entries % per_page == 0 ? 0 : 1)
|
@@ -268,16 +266,20 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
268
266
|
end
|
269
267
|
|
270
268
|
# Builds a new command-interface Search object.
|
271
|
-
def initialize opts = {}
|
272
|
-
|
273
|
-
opts = opts._deep_stringify_keys
|
274
|
-
|
269
|
+
def initialize opts = {}
|
270
|
+
opts = HashWithIndifferentAccess.new(opts)
|
275
271
|
@options = self.class.query_defaults.merge(opts._deep_dup._coerce_basic_types)
|
276
272
|
|
277
|
-
|
278
|
-
|
273
|
+
# Legacy compatibility
|
274
|
+
@options['filters'] ||= @options['filter'] || @options['raw_filters'] || {}
|
275
|
+
@options['class_names'] ||= @options['class_name']
|
276
|
+
@options['weights'] ||= @options['weight']
|
277
|
+
|
278
|
+
@options._delete(*LEGACY_QUERY_KEYS)
|
279
|
+
|
280
|
+
# Coerce some special types
|
279
281
|
@options['query'] = @options['query'].to_s
|
280
|
-
@options['
|
282
|
+
@options['class_names'] = Array(@options['class_names'])
|
281
283
|
|
282
284
|
@options['parsed_query'] = if query.blank?
|
283
285
|
"@empty_searchable #{EMPTY_SEARCHABLE}"
|
@@ -288,7 +290,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
288
290
|
@results, @subtotals, @facets, @response = [], {}, {}, {}
|
289
291
|
|
290
292
|
extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(self.class.query_defaults).keys + LEGACY_QUERY_KEYS + INTERNAL_KEYS)
|
291
|
-
|
293
|
+
say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
|
292
294
|
end
|
293
295
|
|
294
296
|
# Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false if you only want the ids returned.
|
@@ -297,12 +299,12 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
297
299
|
@paginate = nil # clear cache
|
298
300
|
tries = 0
|
299
301
|
|
300
|
-
|
302
|
+
say "searching for #{@options.inspect}"
|
301
303
|
|
302
304
|
begin
|
303
305
|
|
304
306
|
@response = @request.Query(parsed_query)
|
305
|
-
|
307
|
+
say "search returned, error #{@request.GetLastError.inspect}, warning #{@request.GetLastWarning.inspect}, returned #{total_entries}/#{response['total_found']} in #{time} seconds."
|
306
308
|
|
307
309
|
@subtotals = get_subtotals(@request, parsed_query) if self.class.client_options['with_subtotals']
|
308
310
|
|
@@ -317,11 +319,11 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
317
319
|
|
318
320
|
rescue Sphinx::SphinxResponseError, Sphinx::SphinxTemporaryError, Errno::EPIPE => e
|
319
321
|
if (tries += 1) <= self.class.client_options['max_retries']
|
320
|
-
|
322
|
+
say "restarting query (#{tries} attempts already) (#{e})"
|
321
323
|
sleep(self.class.client_options['retry_sleep_time']) if tries == self.class.client_options['max_retries']
|
322
324
|
retry
|
323
325
|
else
|
324
|
-
|
326
|
+
say "query failed"
|
325
327
|
raise e
|
326
328
|
end
|
327
329
|
end
|
@@ -386,8 +388,8 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
386
388
|
end
|
387
389
|
end
|
388
390
|
|
389
|
-
def
|
390
|
-
|
391
|
+
def say msg #:nodoc:
|
392
|
+
Ultrasphinx.say msg
|
391
393
|
end
|
392
394
|
|
393
395
|
end
|
@@ -24,20 +24,20 @@ module Ultrasphinx
|
|
24
24
|
request.SetLimits offset, limit, [offset + limit, MAX_MATCHES].min
|
25
25
|
request.SetSortMode SPHINX_CLIENT_PARAMS['sort_mode'][opts['sort_mode']], opts['sort_by'].to_s
|
26
26
|
|
27
|
-
if weights = opts['
|
27
|
+
if weights = opts['weights']
|
28
28
|
# Order the weights hash according to the field order for Sphinx, and set the missing fields to 1.0
|
29
29
|
request.SetWeights(Fields.instance.types.select{|n,t| t == 'text'}.map(&:first).sort.inject([]) do |array, field|
|
30
30
|
array << (weights[field] || 1.0)
|
31
31
|
end)
|
32
32
|
end
|
33
33
|
|
34
|
-
unless opts['
|
35
|
-
request.SetFilter 'class_id', opts['
|
34
|
+
unless opts['class_names'].compact.empty?
|
35
|
+
request.SetFilter 'class_id', opts['class_names'].map{|m| MODELS_TO_IDS[m.to_s]}
|
36
36
|
end
|
37
37
|
|
38
38
|
# Extract ranged raw filters
|
39
39
|
# Some of this mangling might not be necessary
|
40
|
-
opts['
|
40
|
+
opts['filters'].each do |field, value|
|
41
41
|
begin
|
42
42
|
case value
|
43
43
|
when Fixnum, Float, BigDecimal, NilClass, Array
|
@@ -104,31 +104,33 @@ module Ultrasphinx
|
|
104
104
|
facets = facets.dup
|
105
105
|
|
106
106
|
if Fields.instance.types[facet] == 'text'
|
107
|
-
|
108
|
-
# Cache the reverse CRC map for the textual facet if it hasn't been done yet
|
109
|
-
# XXX not necessarily optimal since it requires a direct DB hit once per mongrel
|
110
|
-
Ultrasphinx.say "caching crc reverse map for text facet #{facet}"
|
111
|
-
|
112
|
-
Fields.instance.classes[facet].each do |klass|
|
113
|
-
# you can only use a facet from your own self right now; no includes allowed
|
114
|
-
field = (MODEL_CONFIGURATION[klass.name]['fields'].detect do |field_hash|
|
115
|
-
field_hash['as'] == facet
|
116
|
-
end)['field']
|
117
|
-
|
118
|
-
klass.connection.execute("SELECT #{field} AS value, CRC32(#{field}) AS crc FROM #{klass.table_name} GROUP BY #{field}").each_hash do |hash|
|
119
|
-
(FACET_CACHE[facet] ||= {})[hash['crc'].to_i] = hash['value']
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Apply the map
|
107
|
+
# Apply the map, rebuilding if the cache is missing or out-of-date
|
125
108
|
facets = Hash[*(facets.map do |crc, value|
|
109
|
+
rebuild_facet_cache(facet) unless FACET_CACHE[facet] and FACET_CACHE[facet].has_key?(crc)
|
126
110
|
[FACET_CACHE[facet][crc], value]
|
127
111
|
end.flatten)]
|
128
112
|
end
|
129
113
|
|
130
114
|
facets
|
131
115
|
end
|
116
|
+
|
117
|
+
def rebuild_facet_cache(facet)
|
118
|
+
# Cache the reverse CRC map for the textual facet if it hasn't been done yet
|
119
|
+
# XXX not necessarily optimal since it requires a direct DB hit once per mongrel
|
120
|
+
Ultrasphinx.say "caching CRC reverse map for text facet #{facet}"
|
121
|
+
|
122
|
+
Fields.instance.classes[facet].each do |klass|
|
123
|
+
# you can only use a facet from your own self right now; no includes allowed
|
124
|
+
field = (MODEL_CONFIGURATION[klass.name]['fields'].detect do |field_hash|
|
125
|
+
field_hash['as'] == facet
|
126
|
+
end)['field']
|
127
|
+
|
128
|
+
klass.connection.execute("SELECT #{field} AS value, CRC32(#{field}) AS crc FROM #{klass.table_name} GROUP BY #{field}").each_hash do |hash|
|
129
|
+
(FACET_CACHE[facet] ||= {})[hash['crc'].to_i] = hash['value']
|
130
|
+
end
|
131
|
+
end
|
132
|
+
FACET_CACHE[facet]
|
133
|
+
end
|
132
134
|
|
133
135
|
def reify_results(sphinx_ids)
|
134
136
|
|
@@ -153,7 +155,7 @@ module Ultrasphinx
|
|
153
155
|
klass.respond_to? method_name
|
154
156
|
end
|
155
157
|
|
156
|
-
|
158
|
+
# Ultrasphinx.say "using #{klass.name}.#{finder} as finder method"
|
157
159
|
|
158
160
|
begin
|
159
161
|
# XXX Does not use Memcached's multiget
|
@@ -173,8 +175,13 @@ module Ultrasphinx
|
|
173
175
|
# Put them back in order
|
174
176
|
results.sort_by do |r|
|
175
177
|
raise Sphinx::SphinxResponseError, "Bogus ActiveRecord id for #{r.class}:#{r.id}" unless r.id
|
176
|
-
|
178
|
+
|
179
|
+
model_index = MODELS_TO_IDS[r.class.base_class.name]
|
180
|
+
raise UsageError, "#{r.class.base_class} is not an indexed class. Maybe you indexed an STI child class instead of the base class?" unless model_index
|
181
|
+
|
182
|
+
index = (sphinx_ids.index(sphinx_id = r.id * MODELS_TO_IDS.size + model_index))
|
177
183
|
raise Sphinx::SphinxResponseError, "Bogus reverse id for #{r.class}:#{r.id} (Sphinx:#{sphinx_id})" unless index
|
184
|
+
|
178
185
|
index / sphinx_ids.size.to_f
|
179
186
|
end
|
180
187
|
|
@@ -58,6 +58,7 @@ module Ultrasphinx
|
|
58
58
|
|
59
59
|
def query_to_token_stream(query)
|
60
60
|
# First, split query on spaces that are not inside sets of quotes or parens
|
61
|
+
|
61
62
|
query = query.scan(/[^"() ]*["(][^")]*[")]|[^"() ]+/)
|
62
63
|
|
63
64
|
token_stream = []
|
@@ -68,7 +69,17 @@ module Ultrasphinx
|
|
68
69
|
# recurse for parens, if necessary
|
69
70
|
if subtoken =~ /^(.*?)\((.*)\)(.*?$)/
|
70
71
|
subtoken = query[index] = "#{$1}(#{parse $2})#{$3}"
|
71
|
-
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# reappend missing closing quotes
|
75
|
+
if subtoken =~ /(^|\:)\"/
|
76
|
+
subtoken = subtoken.chomp('"') + '"'
|
77
|
+
end
|
78
|
+
|
79
|
+
# strip parentheses within quoted strings
|
80
|
+
if subtoken =~ /\"(.*)\"/
|
81
|
+
subtoken.sub!($1, $1.gsub(/[()]/, ''))
|
82
|
+
end
|
72
83
|
|
73
84
|
# add to the stream, converting the operator
|
74
85
|
if !has_operator
|
@@ -59,10 +59,28 @@ sql_query_pre = SET NAMES utf8
|
|
59
59
|
'postgresql' => %(
|
60
60
|
type = pgsql
|
61
61
|
)}
|
62
|
+
|
63
|
+
ADAPTER_SQL_FUNCTIONS = {
|
64
|
+
'mysql' => {
|
65
|
+
'group_by' => 'GROUP BY id',
|
66
|
+
'timestamp' => 'UNIX_TIMESTAMP('
|
67
|
+
},
|
68
|
+
'postgresql' => {
|
69
|
+
'group_by' => '',
|
70
|
+
'timestamp' => 'EXTRACT(EPOCH FROM '
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
ADAPTER = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter]
|
62
75
|
|
63
76
|
# Logger.
|
64
77
|
def self.say msg
|
65
|
-
|
78
|
+
msg = "** ultrasphinx: #{msg}"
|
79
|
+
if defined? RAILS_DEFAULT_LOGGER
|
80
|
+
RAILS_DEFAULT_LOGGER.warn msg
|
81
|
+
else
|
82
|
+
STDERR.puts msg
|
83
|
+
end
|
66
84
|
end
|
67
85
|
|
68
86
|
# Configuration file parser.
|
@@ -100,13 +118,16 @@ type = pgsql
|
|
100
118
|
# Complain if the database names go out of sync.
|
101
119
|
def self.verify_database_name
|
102
120
|
if File.exist? CONF_PATH
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
121
|
+
begin
|
122
|
+
if options_for(
|
123
|
+
"source #{MODEL_CONFIGURATION.keys.first.tableize}",
|
124
|
+
CONF_PATH
|
125
|
+
)['sql_db'] != ActiveRecord::Base.connection.instance_variable_get("@config")[:database]
|
126
|
+
say "warning; configured database name is out-of-date"
|
127
|
+
say "please run 'rake ultrasphinx:configure'"
|
128
|
+
end
|
129
|
+
rescue Object
|
130
|
+
end
|
110
131
|
end
|
111
132
|
end
|
112
133
|
|
data/test/test_helper.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
|
2
|
+
RAILS_ENV = "test"
|
3
|
+
|
2
4
|
def silently
|
3
5
|
old_stdout, $stdout = $stdout, StringIO.new
|
4
6
|
yield
|
@@ -8,8 +10,6 @@ end
|
|
8
10
|
RAILS_ROOT = File.dirname(__FILE__)
|
9
11
|
$LOAD_PATH << "#{RAILS_ROOT}/../lib" << RAILS_ROOT
|
10
12
|
|
11
|
-
RAILS_ENV = "test"
|
12
|
-
|
13
13
|
require 'rubygems'
|
14
14
|
require 'initializer'
|
15
15
|
require 'active_support'
|
data/test/unit/parser_test.rb
CHANGED
@@ -81,7 +81,13 @@ describe "parser" do
|
|
81
81
|
'(800) 555-LOVE',
|
82
82
|
|
83
83
|
'Bend, OR',
|
84
|
-
'Bend, OR'
|
84
|
+
'Bend, OR',
|
85
|
+
|
86
|
+
'"(traditional)"',
|
87
|
+
'"traditional"',
|
88
|
+
|
89
|
+
'cuisine:"american (traditional"',
|
90
|
+
'@cuisine "american traditional"'
|
85
91
|
|
86
92
|
].in_groups_of(2).each do |query, result|
|
87
93
|
it "should parse" do
|
data/ultrasphinx.gemspec
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Ultrasphinx-1.5
|
2
|
+
# Gem::Specification for Ultrasphinx-1.5.1
|
3
3
|
# Originally generated by Echoe
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = %q{ultrasphinx}
|
7
|
-
s.version = "1.5"
|
8
|
-
s.date = %q{2007-09-
|
7
|
+
s.version = "1.5.1"
|
8
|
+
s.date = %q{2007-09-25}
|
9
9
|
s.summary = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
|
10
10
|
s.email = %q{}
|
11
|
-
s.homepage = %q{http://blog.evanweaver.com/
|
11
|
+
s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/}
|
12
12
|
s.rubyforge_project = %q{fauna}
|
13
13
|
s.description = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
|
14
14
|
s.has_rdoc = true
|
@@ -28,7 +28,7 @@ end
|
|
28
28
|
# Echoe.new("ultrasphinx") do |p|
|
29
29
|
# p.project = "fauna"
|
30
30
|
# p.summary = "Ruby on Rails configurator and client to the Sphinx fulltext search engine."
|
31
|
-
# p.url = "http://blog.evanweaver.com/
|
31
|
+
# p.url = "http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/"
|
32
32
|
# p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
|
33
33
|
# p.rdoc_pattern = /is_indexed.rb|search.rb|spell.rb|ultrasphinx.rb|^README|TODO|CHANGELOG|^LICENSE/
|
34
34
|
# p.dependencies = "chronic"
|
metadata
CHANGED
@@ -3,13 +3,13 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: ultrasphinx
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version:
|
7
|
-
date: 2007-09-
|
6
|
+
version: 1.5.1
|
7
|
+
date: 2007-09-25 00:00:00 -04:00
|
8
8
|
summary: Ruby on Rails configurator and client to the Sphinx fulltext search engine.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
11
11
|
email: ""
|
12
|
-
homepage: http://blog.evanweaver.com/
|
12
|
+
homepage: http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/
|
13
13
|
rubyforge_project: fauna
|
14
14
|
description: Ruby on Rails configurator and client to the Sphinx fulltext search engine.
|
15
15
|
autorequire:
|
metadata.gz.sig
CHANGED
Binary file
|