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 +3 -0
- data/CHANGELOG +13 -2
- data/Manifest +29 -21
- data/README +44 -18
- data/TODO +6 -0
- data/examples/default.base +31 -9
- data/lib/ultrasphinx.rb +6 -0
- data/lib/ultrasphinx/autoload.rb +1 -1
- data/lib/ultrasphinx/configure.rb +266 -0
- data/lib/ultrasphinx/core_extensions.rb +37 -5
- data/lib/ultrasphinx/fields.rb +74 -24
- data/lib/ultrasphinx/is_indexed.rb +90 -34
- data/lib/ultrasphinx/search.rb +199 -246
- data/lib/ultrasphinx/search/internals.rb +204 -0
- data/lib/ultrasphinx/search/parser.rb +115 -0
- data/lib/ultrasphinx/spell.rb +13 -6
- data/lib/ultrasphinx/ultrasphinx.rb +50 -213
- data/tasks/ultrasphinx.rake +18 -25
- data/test/config/ultrasphinx/test.base +56 -0
- data/test/test_helper.rb +32 -0
- data/test/unit/parser_test.rb +93 -0
- data/ultrasphinx.gemspec +35 -0
- data/vendor/sphinx/LICENSE +58 -0
- data/vendor/will_paginate/LICENSE +18 -0
- metadata +66 -27
- metadata.gz.sig +3 -0
- data/Rakefile +0 -21
data.tar.gz.sig
ADDED
data/CHANGELOG
CHANGED
@@ -1,3 +1,14 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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.
|
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
|
-
*
|
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
|
36
|
+
* comprehensive Rake tasks
|
33
37
|
|
34
38
|
And some other things.
|
35
39
|
|
36
|
-
|
40
|
+
= Usage
|
41
|
+
|
42
|
+
== Installation
|
37
43
|
|
38
|
-
|
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.
|
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>
|
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 => [
|
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
|
-
|
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:
|
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/
|
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
data/examples/default.base
CHANGED
@@ -1,34 +1,56 @@
|
|
1
1
|
|
2
|
-
#
|
3
|
-
#
|
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
|
-
|
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 =
|
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
|
-
|
29
|
+
client
|
23
30
|
{
|
24
|
-
#
|
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
|
-
|
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(
|
data/lib/ultrasphinx/autoload.rb
CHANGED
@@ -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
|