ultrasphinx 1.6.7 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data.tar.gz.sig +0 -0
  2. data/CHANGELOG +2 -0
  3. data/DEPLOYMENT_NOTES +29 -0
  4. data/Manifest +7 -2
  5. data/RAKE_TASKS +15 -0
  6. data/README +14 -17
  7. data/TODO +7 -1
  8. data/examples/default.base +16 -2
  9. data/lib/ultrasphinx.rb +2 -2
  10. data/lib/ultrasphinx/associations.rb +25 -0
  11. data/lib/ultrasphinx/configure.rb +26 -16
  12. data/lib/ultrasphinx/fields.rb +7 -4
  13. data/lib/ultrasphinx/is_indexed.rb +37 -25
  14. data/lib/ultrasphinx/search.rb +1 -0
  15. data/lib/ultrasphinx/search/internals.rb +10 -3
  16. data/lib/ultrasphinx/search/parser.rb +14 -11
  17. data/lib/ultrasphinx/ultrasphinx.rb +6 -4
  18. data/test/integration/app/app/models/geo/address.rb +1 -1
  19. data/test/integration/app/app/models/geo/country.rb +4 -0
  20. data/test/integration/app/config/boot.rb +95 -32
  21. data/test/integration/app/config/environment.rb +4 -1
  22. data/test/integration/app/config/environments/development.rb +4 -4
  23. data/test/integration/app/config/ultrasphinx/development.conf.canonical +45 -13
  24. data/test/integration/app/db/migrate/009_create_countries.rb +11 -0
  25. data/test/integration/app/test/fixtures/countries.yml +1 -0
  26. data/test/integration/configure_test.rb +14 -11
  27. data/test/integration/search_test.rb +1 -0
  28. data/test/integration/server_test.rb +2 -2
  29. data/test/setup.rb +0 -3
  30. data/test/teardown.rb +0 -0
  31. data/test/test_helper.rb +1 -6
  32. data/ultrasphinx.gemspec +21 -7
  33. data/vendor/riddle/README +2 -2
  34. data/vendor/riddle/Rakefile +94 -0
  35. data/vendor/riddle/lib/riddle.rb +1 -1
  36. data/vendor/riddle/lib/riddle/client.rb +18 -6
  37. data/vendor/riddle/lib/riddle/client/filter.rb +3 -1
  38. data/vendor/riddle/lib/riddle/client/message.rb +1 -1
  39. data/vendor/riddle/lib/riddle/client/response.rb +1 -1
  40. data/vendor/riddle/spec/fixtures/data/anchor.bin +0 -0
  41. data/vendor/riddle/spec/fixtures/data/filter_floats.bin +0 -0
  42. data/vendor/riddle/spec/fixtures/data/filter_floats_exclude.bin +0 -0
  43. data/vendor/riddle/spec/sphinx_helper.rb +1 -0
  44. data/vendor/riddle/spec/unit/message_spec.rb +3 -3
  45. data/vendor/riddle/spec/unit/response_spec.rb +2 -2
  46. metadata +10 -5
  47. metadata.gz.sig +0 -0
  48. data/test/integration/app/config/ultrasphinx/development.conf +0 -159
  49. data/test/integration/app/db/schema.rb +0 -52
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ v1.7. Deployment docs. Postgres fixes. Support association_name instead of class_name (Daniel Higginbotham).
3
+
2
4
  v1.6.7. Fix GROUP_CONCAT aggregate problem. Discourage enable_star in default.base. Allow faceting on includes.
3
5
 
4
6
  v1.6.6. Only use DISTINCT when necessary to improve indexing speed.
@@ -0,0 +1,29 @@
1
+
2
+ == The configuration files
3
+
4
+ Please note that the generated <tt>.conf</tt> file in <tt>config/ultrasphinx</tt> should not be modified by hand. This is the configuration for Sphinx itself, and includes all the generated SQL. It is <b>never the same</b> as the <tt>.base</tt> file.
5
+
6
+ You will want to keep your generated <tt>production.conf</tt> in your repository. You can get a <tt>production.conf</tt> by running:
7
+
8
+ RAILS_ENV=production rake ultrasphinx:configure
9
+
10
+ If you don't have local read access to the production database, you'll need to write a post-deploy task to generate the configuration server-side. It is important that every checkout of the app has a copy of <tt>production.conf</tt>. It is not enough to only have it on the server where the search daemon is running--Rails uses it too.
11
+
12
+ You will <b>not</b> want to keep the generated <tt>development.conf</tt> in the repository. This helps to catch bugs by discouraging out-of-date configurations.
13
+
14
+ == Indexing and monitoring
15
+
16
+ It's easy to keep the search daemon and the indexer running in a production environment. Cronjobs are the best way:
17
+
18
+ 0,30 * * * * bash -c 'cd /path/to/production/current/; RAILS_ENV=production \
19
+ rake ultrasphinx:index >> /log/ultrasphinx-index.log 2>&1'
20
+ */3 * * * * bash -c 'cd /path/to/production/current/; RAILS_ENV=production \
21
+ rake ultrasphinx:daemon:start >> /log/ultrasphinx-daemon.log 2>&1'
22
+
23
+ The first line runs the indexer every thirty minutes. The second line will try to restart the search daemon every three minutes. If it's already running, nothing happens.
24
+
25
+ If you are under severe memory limitations you might want to manage the daemon with Monit instead, so you can keep a closer eye on it. The search daemon is extremely reliable, so don't bother with fancy monitoring infrastructure unless you're sure you need it.
26
+
27
+ == Gotchas
28
+
29
+ If you change the field configuration or model set, you will need to rerun <tt>rake ultrasphinx:configure</tt> to update the <tt>.conf</tt> file. Make sure to completely stop and restart the search daemon when you deploy a changed <tt>.conf</tt>. It will not reload it automatically.
data/Manifest CHANGED
@@ -1,7 +1,9 @@
1
1
  CHANGELOG
2
+ DEPLOYMENT_NOTES
2
3
  examples/ap.multi
3
4
  examples/default.base
4
5
  init.rb
6
+ lib/ultrasphinx/associations.rb
5
7
  lib/ultrasphinx/autoload.rb
6
8
  lib/ultrasphinx/configure.rb
7
9
  lib/ultrasphinx/core_extensions.rb
@@ -21,6 +23,7 @@ lib/ultrasphinx/ultrasphinx.rb
21
23
  lib/ultrasphinx.rb
22
24
  LICENSE
23
25
  Manifest
26
+ RAKE_TASKS
24
27
  README
25
28
  tasks/ultrasphinx.rake
26
29
  test/config/ultrasphinx/test.base
@@ -35,6 +38,7 @@ test/integration/app/app/helpers/sellers_helper.rb
35
38
  test/integration/app/app/helpers/states_helper.rb
36
39
  test/integration/app/app/helpers/users_helper.rb
37
40
  test/integration/app/app/models/geo/address.rb
41
+ test/integration/app/app/models/geo/country.rb
38
42
  test/integration/app/app/models/geo/state.rb
39
43
  test/integration/app/app/models/person/user.rb
40
44
  test/integration/app/app/models/seller.rb
@@ -67,7 +71,6 @@ test/integration/app/config/environments/test.rb
67
71
  test/integration/app/config/locomotive.yml
68
72
  test/integration/app/config/routes.rb
69
73
  test/integration/app/config/ultrasphinx/default.base
70
- test/integration/app/config/ultrasphinx/development.conf
71
74
  test/integration/app/config/ultrasphinx/development.conf.canonical
72
75
  test/integration/app/db/migrate/001_create_users.rb
73
76
  test/integration/app/db/migrate/002_create_sellers.rb
@@ -77,7 +80,7 @@ test/integration/app/db/migrate/005_add_capitalization_to_seller.rb
77
80
  test/integration/app/db/migrate/006_add_deleted_to_user.rb
78
81
  test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb
79
82
  test/integration/app/db/migrate/008_add_mission_statement_to_seller.rb
80
- test/integration/app/db/schema.rb
83
+ test/integration/app/db/migrate/009_create_countries.rb
81
84
  test/integration/app/doc/README_FOR_APP
82
85
  test/integration/app/public/404.html
83
86
  test/integration/app/public/500.html
@@ -110,6 +113,7 @@ test/integration/app/script/process/spawner
110
113
  test/integration/app/script/runner
111
114
  test/integration/app/script/server
112
115
  test/integration/app/test/fixtures/addresses.yml
116
+ test/integration/app/test/fixtures/countries.yml
113
117
  test/integration/app/test/fixtures/sellers.yml
114
118
  test/integration/app/test/fixtures/states.yml
115
119
  test/integration/app/test/fixtures/users.yml
@@ -127,6 +131,7 @@ test/integration/search_test.rb
127
131
  test/integration/server_test.rb
128
132
  test/integration/spell_test.rb
129
133
  test/setup.rb
134
+ test/teardown.rb
130
135
  test/test_all.rb
131
136
  test/test_helper.rb
132
137
  test/ts.multi
@@ -0,0 +1,15 @@
1
+
2
+ == Available Rake tasks
3
+
4
+ These Rake tasks are made available to your Rails app:
5
+
6
+ <tt>ultrasphinx:configure</tt>:: Rebuild the configuration file for this particular environment.
7
+ <tt>ultrasphinx:index</tt>:: Reindex the database and send an update signal to the search daemon.
8
+ <tt>ultrasphinx:daemon:restart</tt>:: Restart the search daemon.
9
+ <tt>ultrasphinx:daemon:start</tt>:: Start the search daemon.
10
+ <tt>ultrasphinx:daemon:stop</tt>:: Stop the search daemon.
11
+ <tt>ultrasphinx:daemon:status</tt>:: Check if the search daemon is running.
12
+ <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.
13
+ <tt>ultrasphinx:bootstrap</tt>:: Bootstrap a full Sphinx environment by running configure, index, then daemon:start.
14
+
15
+ 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>.
data/README CHANGED
@@ -11,9 +11,11 @@ The public certificate for the gem is at http://rubyforge.org/frs/download.php/2
11
11
 
12
12
  == Requirements
13
13
 
14
- * MySQL (or Postgres, experimental)
15
- * Sphinx 0.9.8-dev r871 or greater
16
- * Rails 1.2.3 or greater
14
+ * MySQL 5.0, or PostgreSQL 8.2
15
+ * Sphinx 0.9.8-dev r871
16
+ * Rails 1.2.3
17
+
18
+ More recent versions than listed are ok.
17
19
 
18
20
  == Features
19
21
 
@@ -65,15 +67,13 @@ For more index options, see ActiveRecord::Base .is_indexed.
65
67
 
66
68
  == Building the index
67
69
 
68
- Run:
70
+ Now run:
69
71
 
70
72
  rake ultrasphinx:configure
71
73
  rake ultrasphinx:index
72
74
  rake ultrasphinx:daemon:start
73
75
 
74
- To rotate the index, just rerun <tt>rake ultrasphinx:index</tt>. If the search daemon is running, it will have its index rotated. Otherwise the new index will be installed but the daemon will remain stopped.
75
-
76
- Make sure to manually stop and restart the daemon if you change the field configuration or model set. It will not reload the configuration file automatically.
76
+ To rotate the index, just rerun <tt>rake ultrasphinx:index</tt>. If the search daemon is running, it will have its index rotated live. Otherwise the new index will be installed but the daemon will remain stopped.
77
77
 
78
78
  == Running queries
79
79
 
@@ -97,18 +97,15 @@ See Ultrasphinx::Spell.
97
97
 
98
98
  == Available Rake tasks
99
99
 
100
- These Rake tasks are made available to your Rails app:
100
+ See RAKE_TASKS[link:files/RAKE_TASKS.html].
101
+
102
+ == Deployment notes
103
+
104
+ See DEPLOYMENT_NOTES[link:files/DEPLOYMENT_NOTES.html].
101
105
 
102
- <tt>ultrasphinx:configure</tt>:: Rebuild the configuration file for this particular environment.
103
- <tt>ultrasphinx:index</tt>:: Reindex the database and send an update signal to the search daemon.
104
- <tt>ultrasphinx:daemon:restart</tt>:: Restart the search daemon.
105
- <tt>ultrasphinx:daemon:start</tt>:: Start the search daemon.
106
- <tt>ultrasphinx:daemon:stop</tt>:: Stop the search daemon.
107
- <tt>ultrasphinx:daemon:status</tt>:: Check if the search daemon is running.
108
- <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.
109
- <tt>ultrasphinx:bootstrap</tt>:: Bootstrap a full Sphinx environment by running configure, index, then daemon:start.
106
+ == A note about PostgreSQL
110
107
 
111
- 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>.
108
+ PostgreSQL 8.2 and higher are well supported. However, make sure you have executed <tt>CREATE LANGUAGE plpgsql;</tt> at least once. This step does not need to be repeated, so depending on your DB permissions, you might be able to put it in a migration.
112
109
 
113
110
  == Reporting problems
114
111
 
data/TODO CHANGED
@@ -1,7 +1,13 @@
1
1
 
2
- * Use Pat Allan's association configurator, possibly with an API change to avoid the message-passing DSL
2
+ Planned:
3
+
3
4
  * Finish unifying filters and textfields
4
5
  * Support exclude filters
5
6
  * Make sure filters can be set to nil
6
7
  * Geolocation search
8
+
9
+ Not sure:
10
+
11
+ * Delta indexing
12
+ * Use Pat Allan's association configurator, possibly with an API change to avoid the message-passing DSL
7
13
  * Use Treetop for the query parser instead of regexes
@@ -2,8 +2,21 @@
2
2
  #
3
3
  # Sphinx/Ultrasphinx user-configurable options.
4
4
  #
5
- # Copy this file to RAILS_ROOT/config/ultrasphinx.
6
- # You can use individual namespaces if you want (e.g. "development.base").
5
+ # Copy this file to RAILS_ROOT/config/ultrasphinx. You can use individual
6
+ # namespaces if you want (e.g. development.base, production.base,
7
+ # test.base).
8
+ #
9
+ # This file should not be handed directly to Sphinx. Use the rake task
10
+ #
11
+ # rake ultrasphinx::configure
12
+ #
13
+ # to generate a parallel default.conf file. This is the file that Sphinx itself will
14
+ # use. The Ultrasphinx rake tasks automatically pass the correct file to
15
+ # to Sphinx.
16
+ #
17
+ # It is safe to edit .base files by hand. It is not safe to edit the generated
18
+ # .conf files. Do not symlink the .conf file to the .base file! I don't know why
19
+ # people think they need to do that. It's wrong.
7
20
  #
8
21
 
9
22
  indexer
@@ -18,6 +31,7 @@ searchd
18
31
  # What interface the search daemon should listen on and where to store its logs
19
32
  address = 0.0.0.0
20
33
  port = 3312
34
+ seamless_rotate = 1
21
35
  log = /opt/local/var/db/sphinx/log/searchd.log
22
36
  query_log = /opt/local/var/db/sphinx/log/query.log
23
37
  read_timeout = 5
@@ -12,12 +12,12 @@ end
12
12
 
13
13
  $LOAD_PATH << "#{File.dirname(__FILE__)}/../vendor/riddle/lib"
14
14
  require 'riddle'
15
-
16
15
  require 'ultrasphinx/ultrasphinx'
16
+ require 'ultrasphinx/associations'
17
17
  require 'ultrasphinx/core_extensions'
18
18
  require 'ultrasphinx/is_indexed'
19
19
 
20
- if (ActiveRecord::Base.connection rescue nil)
20
+ if (ActiveRecord::Base.connection rescue nil) # XXX Forget why this needed to be wrapped
21
21
  require 'ultrasphinx/configure'
22
22
  require 'ultrasphinx/autoload'
23
23
  require 'ultrasphinx/fields'
@@ -0,0 +1,25 @@
1
+ module Ultrasphinx
2
+ module Associations
3
+
4
+ def get_association(klass, entry)
5
+ if value = entry['class_name']
6
+ klass.reflect_on_all_associations.detect do |assoc|
7
+ assoc.class_name == value
8
+ end
9
+ elsif value = entry['association_name']
10
+ klass.reflect_on_all_associations.detect do |assoc|
11
+ assoc.name.to_s == value.to_s
12
+ end
13
+ end
14
+ end
15
+
16
+ def get_association_model(klass, entry)
17
+ get_association(klass, entry).class_name.constantize
18
+ end
19
+
20
+ def entry_identifies_association?(entry)
21
+ entry['class_name'] || entry['association_name']
22
+ end
23
+
24
+ end
25
+ end
@@ -1,7 +1,9 @@
1
1
 
2
2
  module Ultrasphinx
3
3
  class Configure
4
- class << self
4
+ class << self
5
+
6
+ include Associations
5
7
 
6
8
  # Force all the indexed models to load and register in the MODEL_CONFIGURATION hash.
7
9
  def load_constants
@@ -75,7 +77,7 @@ module Ultrasphinx
75
77
  # Supporting Postgres now
76
78
  connection_settings = klass.connection.instance_variable_get("@config")
77
79
 
78
- adapter_defaults = ADAPTER_DEFAULTS[ADAPTER]
80
+ adapter_defaults = DEFAULTS[ADAPTER]
79
81
  raise ConfigurationError, "Unsupported database adapter" unless adapter_defaults
80
82
 
81
83
  conf = [adapter_defaults]
@@ -100,7 +102,12 @@ module Ultrasphinx
100
102
 
101
103
 
102
104
  def range_select_string(klass)
103
- "sql_query_range = SELECT MIN(#{klass.primary_key}), MAX(#{klass.primary_key}) FROM #{klass.table_name}"
105
+ ["sql_query_range = SELECT",
106
+ SQL_FUNCTIONS[ADAPTER]['range_cast']._interpolate("MIN(#{klass.primary_key})"),
107
+ ", ",
108
+ SQL_FUNCTIONS[ADAPTER]['range_cast']._interpolate("MAX(#{klass.primary_key})"),
109
+ "FROM #{klass.table_name}"
110
+ ].join(" ")
104
111
  end
105
112
 
106
113
 
@@ -189,11 +196,15 @@ module Ultrasphinx
189
196
 
190
197
  def build_includes(klass, fields, entries, column_strings, join_strings, group_bys, remaining_columns)
191
198
  entries.to_a.each do |entry|
199
+ raise ConfigurationError, "You must identify your association with either class_name or association_name, but not both" if entry['class_name'] && entry ['association_name']
192
200
 
193
- join_klass = entry['class_name'].constantize
194
- association = association_by_class_name(klass, entry['class_name'])
201
+ association = get_association(klass, entry)
202
+
203
+ # You can use 'class_name' and 'association_sql' to associate to a model that doesn't actually
204
+ # have an association
205
+ join_klass = association ? association.class_name.constantize : entry['class_name'].constantize
195
206
 
196
- raise ConfigurationError, "Unknown association from #{klass} to #{entry['class_name']}" if not association and not entry['association_sql']
207
+ raise ConfigurationError, "Unknown association from #{klass} to #{entry['class_name'] || entry['association_name']}" if not association and not entry['association_sql']
197
208
 
198
209
  join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
199
210
  "LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON " +
@@ -217,21 +228,26 @@ module Ultrasphinx
217
228
 
218
229
  def build_concatenations(klass, fields, entries, column_strings, join_strings, group_bys, use_distinct, remaining_columns)
219
230
  entries.to_a.each do |entry|
220
- if entry['class_name'] and entry['field']
231
+ if entry_identifies_association?(entry) and entry['field']
221
232
  # Group concats
233
+
222
234
  # Only has_many's or explicit sql right now
223
- join_klass = entry['class_name'].constantize
235
+ association = get_association(klass, entry)
236
+
237
+ # You can use 'class_name' and 'association_sql' to associate to a model that doesn't actually
238
+ # have an association
239
+ join_klass = association ? association.class_name.constantize : entry['class_name'].constantize
224
240
 
225
241
  join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
226
242
  # XXX make sure foreign key is right for polymorphic relationships
227
- association = association_by_class_name(klass, entry['class_name'])
243
+ association = get_association(klass, entry)
228
244
  "LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON #{klass.table_name}.#{klass.primary_key} = #{entry['table']}.#{association.primary_key_name}" +
229
245
  (entry['conditions'] ? " AND (#{entry['conditions']})" : "")
230
246
  end
231
247
 
232
248
  source_string = "#{entry['table']}.#{entry['field']}"
233
249
  # We are using the field in an aggregate, so we don't want to add it to group_bys
234
- source_string = ADAPTER_SQL_FUNCTIONS[ADAPTER]['group_concat']._interpolate(source_string)
250
+ source_string = SQL_FUNCTIONS[ADAPTER]['group_concat']._interpolate(source_string)
235
251
  use_distinct = true
236
252
 
237
253
  column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
@@ -284,12 +300,6 @@ module Ultrasphinx
284
300
  def install_join_unless_association_sql(association_sql, join_string, join_strings)
285
301
  join_strings << (association_sql or join_string or yield)
286
302
  end
287
-
288
- def association_by_class_name(klass, class_name)
289
- klass.reflect_on_all_associations.detect do |assoc|
290
- assoc.class_name == class_name
291
- end
292
- end
293
303
 
294
304
  def say(s)
295
305
  Ultrasphinx.say s
@@ -7,6 +7,7 @@ This is a special singleton configuration class that stores the index field conf
7
7
 
8
8
  class Fields
9
9
  include Singleton
10
+ include Associations
10
11
 
11
12
  TYPE_MAP = {
12
13
  'string' => 'text',
@@ -114,14 +115,16 @@ This is a special singleton configuration class that stores the index field conf
114
115
  options['include'].to_a.each do |entry|
115
116
  extract_table_alias!(entry, klass)
116
117
  extract_field_alias!(entry, klass)
117
-
118
- save_and_verify_type(entry['as'] || entry['field'], entry['class_name'].constantize.columns_hash[entry['field']].type, entry['sortable'], klass)
118
+
119
+ association_model = get_association_model(klass, entry)
120
+
121
+ save_and_verify_type(entry['as'] || entry['field'], association_model.columns_hash[entry['field']].type, entry['sortable'], klass)
119
122
  install_facets!(entry, klass)
120
123
  end
121
124
 
122
125
  # Regular concats are CHAR, group_concats are BLOB and need to be cast to CHAR
123
126
  options['concatenate'].to_a.each do |entry|
124
- extract_table_alias!(entry, klass) # XXX Doesn't actually do anything useful
127
+ extract_table_alias!(entry, klass)
125
128
  save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
126
129
  install_facets!(entry, klass)
127
130
  end
@@ -155,7 +158,7 @@ This is a special singleton configuration class that stores the index field conf
155
158
  # This field is referenced by a table alias
156
159
  entry['table'], entry['field'] = entry['field'].split(".")
157
160
  else
158
- klass = entry['class_name'].constantize if entry['class_name']
161
+ klass = get_association_model(klass, entry) if entry_identifies_association?(entry)
159
162
  entry['table'] = klass.table_name
160
163
  end
161
164
  end
@@ -32,47 +32,59 @@ To apply an SQL function to a field before it is indexed, use the key <tt>:funct
32
32
 
33
33
  Note that <tt>float</tt> fields are supported, but require Sphinx 0.98.
34
34
 
35
+ == Requiring conditions
36
+
37
+ Use the <tt>:conditions</tt> key.
38
+
39
+ SQL conditions, to scope which records are selected for indexing. Accepts a string.
40
+
41
+ :conditions => "created_at < NOW() AND deleted IS NOT NULL"
42
+
43
+ The <tt>:conditions</tt> key is especially useful if you delete records by marking them deleted rather than removing them from the database.
44
+
35
45
  == Including a field from an association
36
46
 
37
47
  Use the <tt>:include</tt> key.
38
48
 
39
49
  Accepts an array of hashes.
40
50
 
41
- :include => [{:class_name => 'Category', :field => 'name', :as => 'category'}]
51
+ :include => [{:association_name => 'category', :field => 'name', :as => 'category_name'}]
42
52
 
43
- Each should contain a <tt>:class_name</tt> key (the class name of the included model), a <tt>:field</tt> key (the name of the field to include), and an optional <tt>:as</tt> key (what to name the field in the parent). You can use the optional key <tt>:association_sql</tt> if you need to pass a custom JOIN string, in which case the default JOIN for <tt>belongs_to</tt> will not be generated.
53
+ Each should contain an <tt>:association_name</tt> key (the association name for the included model), a <tt>:field</tt> key (the name of the field to include), and an optional <tt>:as</tt> key (what to name the field in the parent).
44
54
 
45
- The keys <tt>:facet</tt>, <tt>:sortable</tt>, and <tt>:function_sql</tt> are also recognized, just like for regular fields.
55
+ <tt>:include</tt> hashes also accept their own <tt>:conditions</tt> key. You can use this if you need custom WHERE conditions for this particular association (e.g, this JOIN).
46
56
 
47
- == Requiring conditions
57
+ The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:class_name</tt>, <tt>:association_sql</tt>, and <tt>:function_sql</tt> are also recognized.
48
58
 
49
- Use the <tt>:conditions</tt> key.
50
-
51
- SQL conditions, to scope which records are selected for indexing. Accepts a string.
52
- :conditions => "created_at < NOW() AND deleted IS NOT NULL"
53
- The <tt>:conditions</tt> key is especially useful if you delete records by marking them deleted rather than removing them from the database.
54
-
55
- == Concatenating several fields within a record
59
+ == Concatenating several fields within one record
56
60
 
57
61
  Use the <tt>:concatenate</tt> key (MySQL only).
58
62
 
59
63
  Accepts an array of option hashes.
60
64
 
61
- To concatenate several fields within one record as a combined field, use a regular (or horizontal) concatenation. Regular concatenations contain a <tt>:fields</tt> key (again, an array of field names), and a mandatory <tt>:as</tt> key (the name of the result of the concatenation). For example, to concatenate the <tt>title</tt> and <tt>body</tt> into one field called <tt>text</tt>:
65
+ To concatenate several fields within one record as a combined field, use a regular (or lateral) concatenation. Regular concatenations contain a <tt>:fields</tt> key (again, an array of field names), and a mandatory <tt>:as</tt> key (the name of the result of the concatenation). For example, to concatenate the <tt>title</tt> and <tt>body</tt> into one field called <tt>text</tt>:
62
66
  :concatenate => [{:fields => ['title', 'body'], :as => 'text'}]
63
67
 
64
- The keys <tt>:facet</tt>, <tt>:sortable</tt>, and <tt>:function_sql</tt> are also recognized, just like for regular fields.
68
+ The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized.
69
+
70
+ Lateral concatenations are implemented with CONCAT_WS on MySQL and with a stored procedure on PostgreSQL.
65
71
 
66
- == Concatenating one field from a set of associated records
72
+ == Concatenating the same field from a set of associated records
67
73
 
68
74
  Also use the <tt>:concatenate</tt> key.
69
75
 
70
- To concatenate one field from a set of associated records as a combined field in the parent record, use a group (or vertical) concatenation. A group concatenation should contain a <tt>:class_name</tt> key (the class name of the included model), a <tt>:field</tt> key (the field on the included model to concatenate), and an optional <tt>:as</tt> key (also the name of the result of the concatenation). For example, to concatenate all <tt>Post#body</tt> contents into the parent's <tt>responses</tt> field:
71
- :concatenate => [{:class_name => 'Post', :field => 'body', :as => 'responses'}]
76
+ To concatenate one field from a set of associated records as a combined field in the parent record, use a group (or vertical) concatenation. A group concatenation should contain an <tt>:association_name</tt> key (the association name for the included model), a <tt>:field</tt> key (the field on the included model to concatenate), and an optional <tt>:as</tt> key (also the name of the result of the concatenation). For example, to concatenate all <tt>Post#body</tt> contents into the parent's <tt>responses</tt> field:
77
+ :concatenate => [{:association_name => 'posts', :field => 'body', :as => 'responses'}]
78
+
79
+ The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized.
80
+
81
+ Vertical concatenations are implemented with GROUP_CONCAT on MySQL and with an aggregate and a stored procedure on PostgreSQL.
82
+
83
+ == Custom joins
72
84
 
73
- Optional group concatenation keys are <tt>:association_sql</tt>, if you need to pass a custom JOIN string (for example, a double JOIN for a <tt>has_many :through</tt>), and <tt>:conditions</tt> (if you need custom WHERE conditions for this particular association).
85
+ <tt>:include</tt> and <tt>:concatenate</tt> accept an <tt>:association_sql</tt> key. You can use this if you need to pass a custom JOIN string, for example, a double JOIN for a <tt>has_many :through</tt>). If <tt>:association_sql</tt> is present, the default JOIN for <tt>belongs_to</tt> will not be generated.
74
86
 
75
- The keys <tt>:facet</tt>, <tt>:sortable</tt>, and <tt>:function_sql</tt> are also recognized, just like for regular fields.
87
+ Also, If you want to include a model that you don't have an actual ActiveRecord association for, you can use <tt>:association_sql</tt> combined with <tt>:class_name</tt> instead of <tt>:association_name</tt>. <tt>:class_name</tt> should be camelcase.
76
88
 
77
89
  Ultrasphinx is not an object-relational mapper, and the association generation is intended to stay minimal--don't be afraid of <tt>:association_sql</tt>.
78
90
 
@@ -89,13 +101,13 @@ Here's an example configuration using most of the options, taken from production
89
101
  {:field => 'author', :facet => true}
90
102
  ],
91
103
  :include => [
92
- {:class_name => 'Category', :field => 'name', :as => 'category'}
104
+ {:association_name => 'category', :field => 'name', :as => 'category_name'}
93
105
  ],
94
106
  :concatenate => [
95
107
  {:fields => ['title', 'long_description', 'short_description'],
96
108
  :as => 'editorial'},
97
- {:class_name => 'Page', :field => 'body', :as => 'body'},
98
- {:class_name => 'Comment', :field => 'body', :as => 'comments',
109
+ {:association_name => 'pages', :field => 'body', :as => 'body'},
110
+ {:association_name => 'comments', :field => 'body', :as => 'comments',
99
111
  :conditions => "comments.item_type = '#{base_class}'"}
100
112
  ],
101
113
  :conditions => self.live_condition_string
@@ -110,7 +122,7 @@ A common use case is to only search records that belong to a particular parent m
110
122
  For example, say a Company <tt>has_many :users</tt> and each User <tt>has_many :articles</tt>. If you want to to filter Articles by Company, add <tt>company_id</tt> to the Article's <tt>is_indexed</tt> method. The best way is to grab it from the User association:
111
123
 
112
124
  class Article < ActiveRecord::Base
113
- is_indexed :include => [{:class_name => 'User', :field => 'company_id'}]
125
+ is_indexed :include => [{:association_name => 'users', :field => 'company_id'}]
114
126
  end
115
127
 
116
128
  Now you can run:
@@ -136,8 +148,8 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
136
148
 
137
149
  Array(opts['concatenate']).each do |entry|
138
150
  entry.stringify_keys!
139
- entry.assert_valid_keys ['class_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable']
140
- raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if entry['fields'] and (entry['field'] or entry['class_name'])
151
+ entry.assert_valid_keys ['class_name', 'association_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable']
152
+ 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'])
141
153
  raise Ultrasphinx::ConfigurationError, "Concatenations must specify an :as key" unless entry['as']
142
154
  raise Ultrasphinx::ConfigurationError, "Group concatenations must not have multiple fields" if entry['field'].is_a? Array
143
155
  raise Ultrasphinx::ConfigurationError, "Regular concatenations should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array)
@@ -145,7 +157,7 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
145
157
 
146
158
  Array(opts['include']).each do |entry|
147
159
  entry.stringify_keys!
148
- entry.assert_valid_keys ['class_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
160
+ entry.assert_valid_keys ['class_name', 'association_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
149
161
  end
150
162
 
151
163
  Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts