ultrasphinx 1 → 1.5

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig ADDED
@@ -0,0 +1,3 @@
1
+ &��yKl��� =
2
+ ��$RR�”vuւ�>Ҟ��dӜp�Fz��[I��%]װ����snu?߫Z���;-�M��#�s:
3
+ ��| �6��������OW����-�8`u"y���Ԁ]!��j�̯�myng�Ǟ�������q�!�N�$@����lO���Ƭ�j���NRA��s�1G,JA��X9�#9I���UIX(�np�ٰ�y
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
1
 
2
- 1. documentation, refactoring
3
- 0. spike
2
+ 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
+
4
+ v1.4. New is_indexed 'fields' => {'function_sql'} key for custom field mangling; support setting textual keys in the 'filters'.
5
+
6
+ v1.3. Facets; configurable finder method array; support symbolic keys very reluctantly for legacy purposes.
7
+
8
+ v1.2. API change. Support searching with empty query. Most keys are now singular strings (plural only if they *require* an Array value). Verbs have been un-nouned ('concatenate', not 'concats'; 'include', not 'includes'). 'filter', not 'raw_filters'; 'class_name', not 'model'. Finally, use a 'query' keypair instead of the first parameter to Ultrasphinx::Search.new for the query string (this simplifies form helpers).
9
+
10
+ v1.1. Parser rewrite and tests.
11
+
12
+ v1. Documentation; refactoring.
13
+
14
+ v0. Spike.
data/Manifest CHANGED
@@ -1,21 +1,29 @@
1
- ./CHANGELOG
2
- ./LICENSE
3
- ./Manifest
4
- ./README
5
- ./Rakefile
6
- ./examples/app.multi
7
- ./examples/default.base
8
- ./init.rb
9
- ./lib/ultrasphinx/autoload.rb
10
- ./lib/ultrasphinx/core_extensions.rb
11
- ./lib/ultrasphinx/fields.rb
12
- ./lib/ultrasphinx/is_indexed.rb
13
- ./lib/ultrasphinx/search.rb
14
- ./lib/ultrasphinx/spell.rb
15
- ./lib/ultrasphinx/ultrasphinx.rb
16
- ./lib/ultrasphinx.rb
17
- ./tasks/ultrasphinx.rake
18
- ./vendor/sphinx/README
19
- ./vendor/sphinx/Rakefile
20
- ./vendor/sphinx/init.rb
21
- ./vendor/sphinx/lib/client.rb
1
+ vendor/will_paginate/LICENSE
2
+ vendor/sphinx/README
3
+ vendor/sphinx/Rakefile
4
+ vendor/sphinx/LICENSE
5
+ vendor/sphinx/lib/client.rb
6
+ vendor/sphinx/init.rb
7
+ TODO
8
+ test/unit/parser_test.rb
9
+ test/test_helper.rb
10
+ test/config/ultrasphinx/test.base
11
+ tasks/ultrasphinx.rake
12
+ README
13
+ Manifest
14
+ LICENSE
15
+ lib/ultrasphinx.rb
16
+ lib/ultrasphinx/ultrasphinx.rb
17
+ lib/ultrasphinx/spell.rb
18
+ lib/ultrasphinx/search.rb
19
+ lib/ultrasphinx/search/parser.rb
20
+ lib/ultrasphinx/search/internals.rb
21
+ lib/ultrasphinx/is_indexed.rb
22
+ lib/ultrasphinx/fields.rb
23
+ lib/ultrasphinx/core_extensions.rb
24
+ lib/ultrasphinx/configure.rb
25
+ lib/ultrasphinx/autoload.rb
26
+ init.rb
27
+ examples/default.base
28
+ examples/app.multi
29
+ CHANGELOG
data/README CHANGED
@@ -5,46 +5,59 @@ 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 included LICENSE file. Portions copyright Dmytro Shteflyuk and Alexey Kovyrin, distributed under the Ruby License, and used with permission.
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
10
  == Requirements
11
11
 
12
12
  * MySQL (or Postgres, experimental)
13
13
  * Sphinx 0.97
14
+ * Rails 1.2.3 or greater
14
15
 
15
16
  == Features
16
17
 
17
18
  Advanced Sphinx usage:
18
- * multi-source merging
19
+ * searching and ranking across orthogonal models
19
20
  * excerpt highlighting
20
21
  * field weighting
22
+ * faceting on text and numeric fields
21
23
 
22
24
  ActiveRecord-style SQL generation:
23
- * belongs_to and has_many includes
25
+ * <tt>belongs_to</tt> and <tt>has_many</tt> includes
24
26
  * field merging
25
27
  * field aliasing
26
28
 
27
29
  Good Rails integration:
28
- * automatic memcached loads via cache_fu
30
+ * automatic memcached loads via <tt>cache_fu</tt>
31
+ * <tt>will_paginate</tt> compatibility
29
32
  * query spellcheck
30
33
  * Google-style query parser
34
+ * temporary error recovery
31
35
  * multiple deployment environments
32
- * comprehensive rake tasks
36
+ * comprehensive Rake tasks
33
37
 
34
38
  And some other things.
35
39
 
36
- == Basic setup
40
+ = Usage
41
+
42
+ == Installation
37
43
 
38
- Install the plugin, and also Sphinx itself (http://www.sphinxsearch.com).
44
+ First, compile and install Sphinx itself (http://www.sphinxsearch.com).
45
+
46
+ You also need the <tt>chronic</tt> gem:
47
+ sudo gem install chronic
48
+
49
+ Then, install the plugin:
50
+ script/plugin install --svn svn://rubyforge.org/var/svn/fauna/ultrasphinx/trunk
51
+
39
52
 
40
- Next, copy the <tt>example/default.base</tt> file to <tt>RAILS_ROOT/config/ultrasphinx/default.base</tt>.
53
+ Next, copy the <tt>example/default.base</tt> file to <tt>RAILS_ROOT/config/ultrasphinx/default.base</tt>. This file sets up the Sphinx daemon options such as port, host, and index location.
41
54
 
42
- If you need per-environment configuration, you can use <tt>RAILS_ROOT/config/ultrasphinx/development.base</tt>, etc. This file sets up the basic Sphinx and Ultrasphinx options such as ports, hosts, and index locations.
55
+ If you need per-environment configuration, you can use <tt>RAILS_ROOT/config/ultrasphinx/development.base</tt>, etc.
43
56
 
44
- Now, in your models, use the <tt>is_indexed</tt> macro to configure a model as searchable. For example:
57
+ Now, in your models, use the <tt>is_indexed</tt> method to configure a model as searchable. For example:
45
58
 
46
59
  class Post
47
- is_indexed :fields => ["created_at", "title", "body"]
60
+ is_indexed :fields => ['created_at', 'title', 'body']
48
61
  end
49
62
 
50
63
  For more index options, see ActiveRecord::Base .is_indexed.
@@ -65,13 +78,19 @@ Make sure to manually stop and restart the daemon if you change the field config
65
78
 
66
79
  Query the daemon as so:
67
80
 
68
- @search = Ultrasphinx::Search.new(@query)
81
+ @search = Ultrasphinx::Search.new(:query => @query)
69
82
  @search.run
70
- @search.results
83
+ @search.results
71
84
 
72
85
  For more query options, including excerpt mode, see Ultrasphinx::Search.
73
86
 
74
- == Spellcheck
87
+ = Extras
88
+
89
+ == Pagination
90
+
91
+ Once the <tt>@search</tt> object has been <tt>run</tt>, it is directly compatible with the <tt>will_paginate</tt> view helper.
92
+
93
+ == Spell checking
75
94
 
76
95
  See Ultrasphinx::Spell.
77
96
 
@@ -83,12 +102,19 @@ These rake tasks are made available to your Rails app:
83
102
  <tt>ultrasphinx:index</tt>:: Reindex the database and send an update signal to the search daemon.
84
103
  <tt>ultrasphinx:daemon:restart</tt>:: Restart the search daemon.
85
104
  <tt>ultrasphinx:daemon:start</tt>:: Start the search daemon.
86
- <tt>ultrasphinx:daemon:status</tt>:: Check if the search daemon is running.
87
105
  <tt>ultrasphinx:daemon:stop</tt>:: Stop the search daemon.
88
- <tt>ultrasphinx:spelling:build</tt>:: Rebuild the custom spelling dictionary.
106
+ <tt>ultrasphinx:daemon:status</tt>:: Check if the search daemon is running.
107
+ <tt>ultrasphinx:spelling:build</tt>:: Rebuild the custom spelling dictionary. You may need to use <tt>sudo</tt> if your Aspell folder is not writable by the app user.
89
108
  <tt>ultrasphinx:bootstrap</tt>:: Bootstrap a full Sphinx environment by running configure, index, then daemon:start.
90
109
 
110
+ All tasks have shortcuts. Use <tt>us:conf</tt>, <tt>us:in</tt>, <tt>us:restart</tt>, <tt>us:start</tt>, <tt>us:stop</tt>, <tt>us:stat</tt>, <tt>us:spell</tt>, and <tt>us:boot</tt>.
111
+
112
+ == Reporting problems
113
+
114
+ * http://rubyforge.org/forum/forum.php?forum_id=14244
115
+
116
+ Patches and contributions are very welcome. Please note that contributors are required to assign copyright for their additions to Cloudburst, LLC.
117
+
91
118
  == Further resources
92
119
 
93
- http://blog.evanweaver.com/pages/code#ultrasphinx
94
- http://rubyforge.org/forum/forum.php?forum_id=14244
120
+ * http://blog.evanweaver.com/articles/2007/07/09/ultrasphinx-searching-the-world-in-231-seconds
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+
2
+ * Support multiple spelling dictionaries per machine
3
+ * Finish unifying filters and textfields
4
+ * Support exclude filters
5
+ * Make sure filters can be set to nil
6
+
@@ -1,34 +1,56 @@
1
1
 
2
- # to put in RAILS_ROOT/config/ultrasphinx
3
- # can be namespaced individually (development.base)
2
+ #
3
+ # Sphinx/Ultrasphinx user-configurable options.
4
+ #
5
+ # Copy this file to RAILS_ROOT/config/ultrasphinx.
6
+ # You can use individual namespaces if you want (e.g. "development.base").
7
+ #
4
8
 
5
9
  indexer
6
10
  {
7
- mem_limit = 256M
11
+ # Indexer running options
12
+ mem_limit = 256M
8
13
  }
9
14
 
10
15
  searchd
11
16
  {
17
+ # Daemon options
18
+ # What interface the search daemon should listen on and where to store its logs
12
19
  address = 0.0.0.0
13
20
  port = 3312
14
21
  log = /opt/local/var/db/sphinx/log/searchd.log
15
22
  query_log = /opt/local/var/db/sphinx/log/query.log
16
23
  read_timeout = 5
17
- max_children = 30
24
+ max_children = 300
18
25
  pid_file = /opt/local/var/db/sphinx/log/searchd.pid
19
26
  max_matches = 100000
20
27
  }
21
28
 
22
- ultrasphinx
29
+ client
23
30
  {
24
- # indexer options
31
+ # Client options
32
+ # How your application connects to the search daemon (not necessarily the same as above)
33
+ server_host = localhost
34
+ server_port = 3312
35
+ }
36
+
37
+ source
38
+ {
39
+ # Individual SQL source options
40
+ sql_range_step = 20000
41
+ strip_html = 0
42
+ index_html_attrs =
43
+ sql_query_post =
44
+ }
45
+
46
+ index
47
+ {
48
+ # Index building options
25
49
  path = /opt/local/var/db/sphinx/
26
50
  docinfo = extern # just leave this alone
27
51
  morphology = stem_en
28
52
  stopwords = # /path/to/stopwords.txt
29
53
  min_word_len = 1
30
54
  charset_type = utf-8 # or sbcs (Single Byte Character Set)
31
- # client options
32
- server_host = localhost
33
- server_port = 3312
55
+ 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,
34
56
  }
data/lib/ultrasphinx.rb CHANGED
@@ -1,11 +1,17 @@
1
1
 
2
+ require 'fileutils'
3
+ require 'chronic'
4
+
2
5
  require "#{File.dirname(__FILE__)}/../vendor/sphinx/lib/client"
3
6
 
4
7
  require 'ultrasphinx/core_extensions'
5
8
  require 'ultrasphinx/ultrasphinx'
9
+ require 'ultrasphinx/configure'
6
10
  require 'ultrasphinx/autoload'
7
11
  require 'ultrasphinx/fields'
8
12
  require 'ultrasphinx/is_indexed'
13
+ require 'ultrasphinx/search/internals'
14
+ require 'ultrasphinx/search/parser'
9
15
  require 'ultrasphinx/search'
10
16
 
11
17
  Ultrasphinx.say(
@@ -5,7 +5,7 @@ class Rails::Initializer
5
5
 
6
6
  def after_initialize_with_ultrasphinx_configuration
7
7
  after_initialize_without_ultrasphinx_configuration
8
- Ultrasphinx.load_constants
8
+ Ultrasphinx::Configure.load_constants
9
9
  Ultrasphinx.verify_database_name
10
10
  end
11
11
 
@@ -0,0 +1,266 @@
1
+
2
+ module Ultrasphinx
3
+ class Configure
4
+ class << self
5
+
6
+ # Force all the indexed models to load and fill the MODEL_CONFIGURATION hash.
7
+ def load_constants
8
+
9
+ Dir["#{RAILS_ROOT}/app/models/**/*.rb"].each do |filename|
10
+ next if filename =~ /\/(\.svn|CVS|\.bzr)\//
11
+ begin
12
+ open(filename) {|file| load filename if file.grep(/is_indexed/).any?}
13
+ rescue Object => e
14
+ say "warning; possibly critical autoload error on #{filename}"
15
+ say e.inspect
16
+ end
17
+ end
18
+
19
+ # Build the field-to-type mappings.
20
+ Fields.instance.configure(MODEL_CONFIGURATION)
21
+ end
22
+
23
+
24
+ # Main SQL builder.
25
+ def run
26
+
27
+ load_constants
28
+
29
+ puts "Rebuilding Ultrasphinx configurations for #{ENV['RAILS_ENV']} environment"
30
+ puts "Available models are #{MODEL_CONFIGURATION.keys.to_sentence}"
31
+ File.open(CONF_PATH, "w") do |conf|
32
+
33
+ conf.puts global_header
34
+ sources = []
35
+
36
+ puts "Generating SQL"
37
+ cached_groups = Fields.instance.groups.join("\n")
38
+ MODEL_CONFIGURATION.each_with_index do |model_options, class_id|
39
+ model, options = model_options
40
+ klass, source = model.constantize, model.tableize
41
+ sources << source
42
+ conf.puts build_source(Fields.instance, model, options, class_id, klass, source, cached_groups)
43
+ end
44
+
45
+ conf.puts build_index(sources)
46
+ end
47
+ end
48
+
49
+
50
+ ######
51
+
52
+ private
53
+
54
+ def global_header
55
+ ["\n# Auto-generated at #{Time.now}.",
56
+ "# Hand modifications will be overwritten.",
57
+ "# #{BASE_PATH}",
58
+ INDEXER_SETTINGS._to_conf_string('indexer'),
59
+ DAEMON_SETTINGS._to_conf_string("searchd")]
60
+ end
61
+
62
+
63
+ def setup_source_database(klass)
64
+ # Tentatively supporting Postgres now
65
+ connection_settings = klass.connection.instance_variable_get("@config")
66
+
67
+ adapter_defaults = ADAPTER_DEFAULTS[connection_settings[:adapter]]
68
+ raise ConfigurationError, "Unsupported database adapter" unless adapter_defaults
69
+
70
+ conf = [adapter_defaults]
71
+ connection_settings.reverse_merge(CONNECTION_DEFAULTS).each do |key, value|
72
+ conf << "#{CONFIG_MAP[key]} = #{value}" if CONFIG_MAP[key]
73
+ end
74
+ conf.sort.join("\n")
75
+ end
76
+
77
+
78
+ def setup_source_arrays(klass, fields, class_id, conditions)
79
+ condition_strings = Array(conditions).map do |condition|
80
+ "(#{condition})"
81
+ end
82
+
83
+ column_strings = [
84
+ "(#{klass.table_name}.#{klass.primary_key} * #{MODEL_CONFIGURATION.size} + #{class_id}) AS id",
85
+ "#{class_id} AS class_id", "'#{klass.name}' AS class",
86
+ "'#{EMPTY_SEARCHABLE}' AS empty_searchable"]
87
+ remaining_columns = fields.types.keys - ["class", "class_id", "empty_searchable"]
88
+ [column_strings, [], condition_strings, remaining_columns]
89
+ end
90
+
91
+
92
+ def range_select_string(klass)
93
+ "sql_query_range = SELECT MIN(#{klass.primary_key}), MAX(#{klass.primary_key}) FROM #{klass.table_name}"
94
+ end
95
+
96
+
97
+ def query_info_string(klass, class_id)
98
+ "sql_query_info = SELECT * FROM #{klass.table_name} WHERE #{klass.table_name}.#{klass.primary_key} = (($id - #{class_id}) / #{MODEL_CONFIGURATION.size})"
99
+ end
100
+
101
+
102
+ def build_source(fields, model, options, class_id, klass, source, groups)
103
+
104
+ column_strings, join_strings, condition_strings, remaining_columns =
105
+ setup_source_arrays(klass, fields, class_id, options['conditions'])
106
+
107
+ column_strings, join_strings, remaining_columns =
108
+ build_regular_fields(klass, fields, options['fields'], column_strings, join_strings, remaining_columns)
109
+ column_strings, join_strings, remaining_columns =
110
+ build_includes(klass, fields, options['include'], column_strings, join_strings, remaining_columns)
111
+ column_strings, join_strings, remaining_columns =
112
+ build_concatenations(klass, fields, options['concatenate'], column_strings, join_strings, remaining_columns)
113
+
114
+ column_strings = add_missing_columns(fields, remaining_columns, column_strings)
115
+
116
+ ["\n# Source configuration\n\n",
117
+ "source #{source}\n{",
118
+ SOURCE_SETTINGS._to_conf_string,
119
+ setup_source_database(klass),
120
+ range_select_string(klass),
121
+ build_query(klass, column_strings, join_strings, condition_strings),
122
+ "\n" + groups,
123
+ query_info_string(klass, class_id),
124
+ "}\n\n"]
125
+ end
126
+
127
+
128
+ def build_query(klass, column_strings, join_strings, condition_strings)
129
+
130
+ connection_settings = klass.connection.instance_variable_get("@config")
131
+
132
+ ["sql_query =",
133
+ "SELECT",
134
+ column_strings.sort_by do |string|
135
+ # sphinx wants them always in the same order, but "id" must be first
136
+ (field = string[/.*AS (.*)/, 1]) == "id" ? "*" : field
137
+ end.join(", "),
138
+ "FROM #{klass.table_name}",
139
+ join_strings.uniq,
140
+ "WHERE #{klass.table_name}.#{klass.primary_key} >= $start AND #{klass.table_name}.#{klass.primary_key} <= $end",
141
+ condition_strings.uniq.map do |condition|
142
+ "AND #{condition}"
143
+ end,
144
+ ("GROUP BY id" if connection_settings[:adapter] == 'mysql') # XXX should be somewhere more obvious
145
+ ].flatten.join(" ")
146
+ end
147
+
148
+
149
+ def add_missing_columns(fields, remaining_columns, column_strings)
150
+ remaining_columns.each do |field|
151
+ column_strings << fields.null(field)
152
+ end
153
+ column_strings
154
+ end
155
+
156
+
157
+ def build_regular_fields(klass, fields, entries, column_strings, join_strings, remaining_columns)
158
+ entries.to_a.each do |entry|
159
+ source_string = "#{klass.table_name}.#{entry['field']}"
160
+ column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
161
+ end
162
+
163
+ [column_strings, join_strings, remaining_columns]
164
+ end
165
+
166
+
167
+ def build_includes(klass, fields, entries, column_strings, join_strings, remaining_columns)
168
+ entries.to_a.each do |entry|
169
+
170
+ join_klass = entry['class_name'].constantize
171
+ association = klass.reflect_on_association(entry['class_name'].underscore.to_sym)
172
+
173
+ raise ConfigurationError, "Unknown association from #{klass} to #{entry['class_name']}" if not association and not entry['association_sql']
174
+
175
+ join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
176
+ "LEFT OUTER JOIN #{join_klass.table_name} ON " +
177
+ if (macro = association.macro) == :belongs_to
178
+ "#{join_klass.table_name}.#{join_klass.primary_key} = #{klass.table_name}.#{association.primary_key_name}"
179
+ elsif macro == :has_one
180
+ "#{klass.table_name}.#{klass.primary_key} = #{join_klass.table_name}.#{association.instance_variable_get('@foreign_key_name')}"
181
+ else
182
+ raise ConfigurationError, "Unidentified association macro #{macro.inspect}"
183
+ end
184
+ end
185
+
186
+ source_string = "#{join_klass.table_name}.#{entry['field']}"
187
+ column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
188
+ end
189
+
190
+ [column_strings, join_strings, remaining_columns]
191
+ end
192
+
193
+
194
+ def build_concatenations(klass, fields, entries, column_strings, join_strings, remaining_columns)
195
+ entries.to_a.each do |entry|
196
+ if entry['class_name'] and entry['field']
197
+ # group concats
198
+ # only has_many's or explicit sql right now
199
+ join_klass = entry['class_name'].constantize
200
+
201
+ join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
202
+ # XXX make sure foreign key is right for polymorphic relationships
203
+ association = klass.reflect_on_association(entry['association_name'] ? entry['association_name'].to_sym : entry['class_name'].underscore.pluralize.to_sym)
204
+ "LEFT OUTER JOIN #{join_klass.table_name} ON #{klass.table_name}.#{klass.primary_key} = #{join_klass.table_name}.#{association.primary_key_name}" +
205
+ (entry['conditions'] ? " AND (#{entry['conditions']})" : "")
206
+ end
207
+
208
+ source_string = "GROUP_CONCAT(#{join_klass.table_name}.#{entry['field']} SEPARATOR ' ')"
209
+ column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
210
+
211
+ elsif entry['fields']
212
+ # regular concats
213
+ source_string = "CONCAT_WS(' ', " + entry['fields'].map do |subfield|
214
+ "#{klass.table_name}.#{subfield}"
215
+ end.join(', ') + ")"
216
+
217
+ column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
218
+
219
+ else
220
+ raise ConfigurationError, "Invalid concatenate parameters for #{model}: #{entry.inspect}."
221
+ end
222
+ end
223
+
224
+ [column_strings, join_strings, remaining_columns]
225
+ end
226
+
227
+
228
+ def build_index(sources)
229
+ ["\n# Index configuration\n\n",
230
+ "index #{UNIFIED_INDEX_NAME}\n{",
231
+ sources.sort.map do |source|
232
+ " source = #{source}"
233
+ end.join("\n"),
234
+ INDEX_SETTINGS.merge('path' => INDEX_SETTINGS['path'] + "/sphinx_index_#{UNIFIED_INDEX_NAME}")._to_conf_string,
235
+ "}\n\n"]
236
+ end
237
+
238
+
239
+ def install_field(fields, source_string, as, function_sql, with_facet, column_strings, remaining_columns)
240
+ source_string = function_sql.sub('?', source_string) if function_sql
241
+
242
+ column_strings << fields.cast(source_string, as)
243
+ remaining_columns.delete(as)
244
+
245
+ # Generate CRC integer fields for text grouping
246
+ if with_facet
247
+ # Postgres probably doesn't handle this
248
+ column_strings << "CRC32(#{source_string}) AS #{as}_facet"
249
+ remaining_columns.delete("#{as}_facet")
250
+ end
251
+ [column_strings, remaining_columns]
252
+ end
253
+
254
+
255
+ def install_join_unless_association_sql(association_sql, join_string, join_strings)
256
+ join_strings << (association_sql or join_string or yield)
257
+ end
258
+
259
+
260
+ def say(s)
261
+ Ultrasphinx.say s
262
+ end
263
+
264
+ end
265
+ end
266
+ end