thebes 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ spec/support/database.yml
5
+ spec/support/test.sphinx.conf
6
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in thebes.gemspec
4
+ gemspec
@@ -0,0 +1,84 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ thebes (0.0.3)
5
+ actionpack (>= 3.0.3)
6
+ activerecord (>= 3.0.3)
7
+ mysql2
8
+ riddle
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ abstract (1.0.0)
14
+ actionpack (3.0.3)
15
+ activemodel (= 3.0.3)
16
+ activesupport (= 3.0.3)
17
+ builder (~> 2.1.2)
18
+ erubis (~> 2.6.6)
19
+ i18n (~> 0.4)
20
+ rack (~> 1.2.1)
21
+ rack-mount (~> 0.6.13)
22
+ rack-test (~> 0.5.6)
23
+ tzinfo (~> 0.3.23)
24
+ activemodel (3.0.3)
25
+ activesupport (= 3.0.3)
26
+ builder (~> 2.1.2)
27
+ i18n (~> 0.4)
28
+ activerecord (3.0.3)
29
+ activemodel (= 3.0.3)
30
+ activesupport (= 3.0.3)
31
+ arel (~> 2.0.2)
32
+ tzinfo (~> 0.3.23)
33
+ activesupport (3.0.3)
34
+ arel (2.0.8)
35
+ builder (2.1.2)
36
+ diff-lcs (1.1.2)
37
+ erubis (2.6.6)
38
+ abstract (>= 1.0.0)
39
+ genspec (0.1.1)
40
+ rspec
41
+ sc-core-ext (>= 1.2.0)
42
+ i18n (0.5.0)
43
+ mocha (0.9.10)
44
+ rake
45
+ mysql2 (0.2.6)
46
+ rack (1.2.1)
47
+ rack-mount (0.6.13)
48
+ rack (>= 1.0.0)
49
+ rack-test (0.5.7)
50
+ rack (>= 1.0)
51
+ railties (3.0.3)
52
+ actionpack (= 3.0.3)
53
+ activesupport (= 3.0.3)
54
+ rake (>= 0.8.7)
55
+ thor (~> 0.14.4)
56
+ rake (0.8.7)
57
+ riddle (1.2.2)
58
+ rspec (2.4.0)
59
+ rspec-core (~> 2.4.0)
60
+ rspec-expectations (~> 2.4.0)
61
+ rspec-mocks (~> 2.4.0)
62
+ rspec-core (2.4.0)
63
+ rspec-expectations (2.4.0)
64
+ diff-lcs (~> 1.1.2)
65
+ rspec-mocks (2.4.0)
66
+ rspec-rails (2.4.1)
67
+ actionpack (~> 3.0)
68
+ activesupport (~> 3.0)
69
+ railties (~> 3.0)
70
+ rspec (~> 2.4.0)
71
+ sc-core-ext (1.2.1)
72
+ activesupport (>= 2.3.5)
73
+ thor (0.14.6)
74
+ tzinfo (0.3.24)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ genspec
81
+ mocha
82
+ rspec
83
+ rspec-rails
84
+ thebes!
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Iridesco LLC (support@harvestapp.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ task :default => :spec
@@ -0,0 +1,140 @@
1
+ Thebes
2
+ ======
3
+
4
+ Thebes is a thin binding layer for [Rails](http://rubyonrails.org/) and
5
+ [Sphinx](http://sphinxsearch.com/) via [Riddle](https://github.com/freelancing-god/riddle)
6
+ and [Mysql2](https://github.com/brianmario/mysql2). Thebes expects you to write
7
+ Sphinx configuration files by hand and have a rich understanding of Sphinx, but
8
+ provides configuration files and templates to ease the process.
9
+
10
+ Installation
11
+ ------------
12
+
13
+ To use Thebes, just add it to your `Gemfile`:
14
+
15
+ gem 'thebes'
16
+
17
+ # Or pin Thebes to git
18
+ # gem 'thebes',
19
+ # :git => 'git@github.com:harvesthq/thebes.git'
20
+
21
+ Then use `bundle install` to update your `Gemfile.lock`. If your project is using
22
+ Rails, you can take advantage of the generator to create your starting config files:
23
+
24
+ script/rails g sphinx_config
25
+
26
+ This will create the three config files that Thebes requires for easy setup:
27
+
28
+ * `sphinx.yml` - Configuration values for your sphinx.conf files.
29
+ * `sphinx.conf.erb` - The template for your sphinx.conf files.
30
+ * `sphinx_servers.yml` - Configuration of Rails, which Sphinx servers to connect to.
31
+
32
+ Flavor to taste, and run `rake thebes:build` to generate your sphinx configuation.
33
+
34
+ Querying via Sphinx's custom protocol
35
+ -------------------------------------
36
+
37
+ Generate a query against Sphinx via Riddle:
38
+
39
+ result = Thebes::Query.run do |a|
40
+ # a here is an instance of Riddle::Client:
41
+ # http://rdoc.info/github/freelancing-god/riddle/master/Riddle/Client
42
+ a.apply_filter_like_in_riddle
43
+ end
44
+
45
+ `result` will be an instance of [Riddle::Response](http://rdoc.info/github/freelancing-god/riddle/master/Riddle/Client/Response).
46
+ Usually you will want to call `.next` on it to get your returned values from
47
+ Sphinx. A more complex usage example:
48
+
49
+ # sphinx_res is a raw result from Riddle.
50
+ #
51
+ sphinx_res = Thebes::Query.run do |q|
52
+
53
+ # In Riddle, filters need to be added to a query before the
54
+ # actual query.
55
+ #
56
+ q.filters << Riddle::Client::Filter.new('active', [1])
57
+
58
+ # Pull search terms of a query.
59
+ #
60
+ query = params[:search_terms].split(' ').collect {|word|
61
+ "( =#{word} | #{word} | #{word}* )"
62
+ }.join(' ')
63
+
64
+ q.append_query query, 'documents' # Search index 'documents'.
65
+
66
+ # You can change the match_mode or alter the query in other
67
+ # ways.
68
+ #
69
+ # q.match_mode = :expr2
70
+ end
71
+
72
+ ids, weights = [], []
73
+ sphinx_res[0][:matches].each do |match|
74
+ ids << match[:attributes]['_id']
75
+ weights << match[:weight]
76
+ end
77
+
78
+ # Fetch documents from the database.
79
+ #
80
+ @documents = Document.find(ids)
81
+
82
+ # Sort documents by weight.
83
+ #
84
+ @documents.sort! {|a,b| weights[ids.index(b.id)] <=> weights[ids.index(a.id)] }
85
+
86
+ Additionally, you can set procs to be run before the query block or right
87
+ before the query is run:
88
+
89
+ # Usually found in config/initializers/thebes.rb
90
+ #
91
+
92
+ # Before the Thebes::Query.run block.
93
+ #
94
+ Thebes::Query.before_query = Proc.new do |q|
95
+ q.match_mode = :any
96
+ q.max_matches = 10
97
+ end
98
+
99
+ # Just before the query is run.
100
+ #
101
+ Thebes::Query.before_running = Proc.new do |q|
102
+ end
103
+
104
+ Querying via Sphinxql
105
+ ---------------------
106
+
107
+ Thebes has minimal support for querying Sphinx via the new [Sphinxql syntax](http://sphinxsearch.com/docs/manual-0.9.9.html#sphinxql).
108
+ Running a query returns a [Mysql2::Result](http://rdoc.info/github/brianmario/mysql2/master/Mysql2/Result)
109
+ object.
110
+
111
+ # Do a search with SphinxQL.
112
+ #
113
+ @results = Thebes::Sphinxql::Query.run "SELECT * FROM items WHERE MATCH('Horwitz')"
114
+
115
+ @ids = @results.collect {|r| r['_id'] }
116
+
117
+ Deployment Strategy
118
+ -------------------
119
+
120
+ Always check `config/sphinx.conf.erb` into your SCM. The `config/sphinx.yml` and
121
+ `config/sphinx_servers.yml` files probably should not be checked in, and any generated
122
+ `config/sphinx.conf` files should likely not be checked in.
123
+
124
+ Steps in a capistrano-style deployment:
125
+
126
+ * Copy server-side `config/sphinx_servers.yml` and `config/sphinx.yml` files into place.
127
+ * Run `rake thebes:build` to generate `config/sphinx.conf` or any other sphinx configuration files.
128
+ * Restart searchd, run the indexer.
129
+
130
+ If you want to use a cluster of Sphinx servers, several configuration files can
131
+ be generated by modifying `config/sphinx.conf` to generate the proper configurations.
132
+ Then update `config/sphinx_servers.yml` to tell your running Rails app where to
133
+ find any new servers.
134
+
135
+ Contributing
136
+ ------------
137
+
138
+ Fork, clone, write a test, write some code, commit, push, send a pull request. Github FTW!
139
+
140
+ This project was open-sourced by [Harvest](http://getharvest.com/). [We're hiring!](http://www.getharvest.com/careers)
@@ -0,0 +1,19 @@
1
+ require 'active_record'
2
+ require File.dirname(__FILE__)+'/thebes/config_writer'
3
+ require File.dirname(__FILE__)+'/thebes/query'
4
+ require File.dirname(__FILE__)+'/thebes/sphinx_search'
5
+ require File.dirname(__FILE__)+'/thebes/sphinxql/client'
6
+ require File.dirname(__FILE__)+'/thebes/sphinxql/query'
7
+ require File.dirname(__FILE__)+'/thebes/railtie' if defined?(Rails::Railtie)
8
+
9
+ module Thebes
10
+ end
11
+
12
+ require 'zlib'
13
+ ActiveRecord::Base.class_eval do
14
+
15
+ def self.to_crc32
16
+ Zlib.crc32(self.name)
17
+ end
18
+
19
+ end
@@ -0,0 +1,29 @@
1
+ require 'action_view'
2
+
3
+ module Thebes
4
+
5
+ class ConfigWriter
6
+
7
+ def initialize template_path=nil, template_file=nil
8
+ @template_path = template_path || File.join(File.dirname(__FILE__), '..', '..', 'templates')
9
+ @template_file = template_file || 'sphinx.conf'
10
+ end
11
+
12
+ def default_config_file
13
+ "./#{@@config_template}"
14
+ end
15
+
16
+ def build(outfile=nil, locals={})
17
+ outfile ||= default_config_file
18
+ view = ActionView::Base.new(
19
+ @template_path,
20
+ locals
21
+ )
22
+ File.open(outfile, 'w') do |file|
23
+ file.write view.render(:file => @template_file)
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,32 @@
1
+ require 'riddle'
2
+
3
+ module Thebes
4
+
5
+ class Query < Riddle::Client
6
+ cattr_accessor :before_query,
7
+ :before_running,
8
+ :servers
9
+
10
+ def initialize *args
11
+ if !args.empty? || self.class.servers.empty?
12
+ super *args
13
+ else
14
+ super *self.class.servers[rand(self.class.servers.size)]
15
+ end
16
+ end
17
+
18
+ class << self
19
+
20
+ def run &block
21
+ client = new # would take server and port
22
+ before_query.call(client) if before_query
23
+ block.call client
24
+ before_running.call(client) if before_running
25
+ client.run
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'thebes'
2
+ require 'rails'
3
+
4
+ module Thebes
5
+
6
+ class Railtie < Rails::Railtie
7
+
8
+ rake_tasks do
9
+ load "#{File.dirname(__FILE__)}/../../railties/thebes.rake"
10
+ end
11
+
12
+ generators do
13
+ load "#{File.dirname(__FILE__)}/../../railties/sphinx_config_generator.rb"
14
+ end
15
+
16
+ initializer "thebes.initialize" do |app|
17
+
18
+ config_file = File.join(Rails.root, 'config', 'sphinx_servers.yml')
19
+ if File.exists?(config_file)
20
+ config = YAML.load(ERB.new(IO.read(config_file)).result)[Rails.env.to_s]
21
+ if config['sphinx_api']
22
+ Thebes::Query.servers = config['sphinx_api']
23
+ end
24
+ if config['sphinxql']
25
+ Thebes::Sphinxql::Client.servers = config['sphinxql']
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,152 @@
1
+ #Due credit: large parts ripped out of thinking_sphinx - http://freelancing-god.github.com/ts/en/.
2
+
3
+ module Thebes
4
+
5
+ class SphinxSearch
6
+
7
+ attr_reader :results
8
+
9
+ def initialize options
10
+ @options = options
11
+ @client = Riddle::Client.new(*server)
12
+ @client.limit = options[:per_page] || 20
13
+ @client.offset = offset
14
+ @client.filters = filters
15
+ @client.field_weights = options[:field_weights] if options[:field_weights]
16
+ @client.index_weights = options[:index_weights] || {}
17
+ @client.sort_by = options[:sort_by] if options[:sort_by]
18
+ @client.match_mode = :extended
19
+ end
20
+
21
+ def search query
22
+ logger.info "Querying: '#{query}'"
23
+ @total_pages = nil
24
+ @results = { }
25
+ runtime = Benchmark.realtime {
26
+ @results = @client.query(format_query(query), indexes, '')
27
+ }
28
+ logger.error("Sphinx returned an error: #{@results[:error]}") if !@results[:error].blank?
29
+ logger.warn("Sphinx returned a warning: #{@results[:error]}") if !@results[:warning].blank?
30
+ logger.info "Found #{@results[:total_found]} results out of #{total_pages}"
31
+ logger.debug "Sphinx (#{sprintf("%f", runtime)}s)"
32
+ if @options[:ids_only]
33
+ @results[:matches].map{ |match|
34
+ match[:attributes]["sphinx_internal_id"]
35
+ }
36
+ else
37
+ instances_from_matches
38
+ end
39
+ end
40
+
41
+ def total_pages
42
+ return 0 if @results[:total].nil?
43
+ @total_pages ||= (@results[:total] / per_page.to_f).ceil
44
+ end
45
+
46
+ private
47
+
48
+ def server
49
+ Thebes::Query.servers[rand(Thebes::Query.servers.size)]
50
+ end
51
+
52
+ def format_query query
53
+ return query unless @options[:star]
54
+ @query ||= (@options[:star] ? star_query(query) : query).strip
55
+ end
56
+
57
+ def star_query(query)
58
+ token = @options[:star].is_a?(Regexp) ? @options[:star] : /\w+/u
59
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
60
+ pre, proper, post = $`, $&, $'
61
+ # E.g. "@foo", "/2", "~3", but not as part of a token
62
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z})
63
+ # E.g. "foo bar", with quotes
64
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"')
65
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
66
+ if is_operator || is_quote || has_star
67
+ proper
68
+ else
69
+ "*#{proper}*"
70
+ end
71
+ end
72
+ end
73
+
74
+ def instances_from_matches
75
+ groups = results[:matches].group_by { |match| match[:attributes]["class_crc"] }
76
+ groups.each do |crc, group|
77
+ group.replace(instances_from_class(class_from_crc(crc), group))
78
+ end
79
+ results[:matches].collect do |match|
80
+ groups.detect { |crc, group|
81
+ crc == match[:attributes]["class_crc"]
82
+ }[1].compact.detect { |obj|
83
+ obj.id == match[:attributes]["sphinx_internal_id"]
84
+ }
85
+ end
86
+ end
87
+
88
+ def instances_from_class(klass, matches)
89
+ ids = matches.map { |match| match[:attributes]["sphinx_internal_id"] }
90
+ instances = ids.length > 0 ? klass.where(:id => ids) : []
91
+ ids.map { |obj_id| instances.detect { |obj| obj.id == obj_id } }
92
+ end
93
+
94
+
95
+ def indexes
96
+ if @options[:index]
97
+ return @options[:index]
98
+ end
99
+ @options[:classes].map { |t| ["_core", "_delta"].map { |sufix| "#{t.name.underscore}#{sufix}" }}.flatten.uniq.join(',')
100
+ end
101
+
102
+ def logger
103
+ Rails.logger
104
+ end
105
+
106
+ def current_page
107
+ @options[:page].blank? ? 1 : @options[:page].to_i
108
+ end
109
+
110
+ def offset
111
+ @options[:offset] || ((current_page - 1) * per_page)
112
+ end
113
+
114
+ def per_page
115
+ @options[:per_page] || 100
116
+ end
117
+
118
+ def filters
119
+ chain = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
120
+ if @options[:with]
121
+ chain << @options[:with].map { |k, v| Riddle::Client::Filter.new(k, filter_value(v))}
122
+ end
123
+ unless @options[:classes]
124
+ raise "You'll need to explicitly pass in a list of AR classes to search as :classes"
125
+ end
126
+ chain << Riddle::Client::Filter.new('class_crc', @options[:classes].map(&:to_crc32))
127
+ chain.flatten
128
+ end
129
+
130
+ def class_from_crc(crc)
131
+ @options[:classes].detect { |klass| klass.to_crc32 == crc }
132
+ end
133
+
134
+ def filter_value(value)
135
+ case value
136
+ when Range
137
+ filter_value(value.first).first..filter_value(value.last).first
138
+ when Array
139
+ value.map { |v| filter_value(v) }.flatten
140
+ when Time
141
+ [value.to_i]
142
+ when NilClass
143
+ 0
144
+ else
145
+ Array(value)
146
+ end
147
+ end
148
+
149
+
150
+ end
151
+
152
+ end
@@ -0,0 +1,19 @@
1
+ require 'mysql2'
2
+
3
+ module Thebes::Sphinxql
4
+
5
+ class Client < ::Mysql2::Client
6
+
7
+ cattr_accessor :servers
8
+
9
+ def initialize *args
10
+ if !args.empty? || (!self.class.servers || self.class.servers.empty?)
11
+ super *args
12
+ else
13
+ super self.class.servers[rand(self.class.servers.size)]
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,35 @@
1
+ module Thebes::Sphinxql
2
+
3
+ class Query
4
+
5
+ def initialize query
6
+ @query = query
7
+ @client = Client.new
8
+ end
9
+
10
+ def run
11
+ @client.query self.to_sql
12
+ end
13
+
14
+ def to_sql
15
+ case @query
16
+ when String
17
+ @query
18
+ when Array
19
+ @query.shift % (@query.collect { |q|
20
+ @client.escape(q)
21
+ })
22
+ end
23
+ end
24
+
25
+ class << self
26
+
27
+ def run query
28
+ self.new(query).run
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,3 @@
1
+ module Thebes
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1 @@
1
+ require 'thebes/railtie'
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ class SphinxConfigGenerator < Rails::Generators::Base
4
+
5
+ source_root "#{File.dirname(__FILE__)}/../templates/"
6
+
7
+ def copy_config_defaults
8
+ copy_file 'sphinx.conf.erb', 'config/sphinx.conf.erb'
9
+ copy_file 'sphinx.yml', 'config/sphinx.yml'
10
+ copy_file 'sphinx_servers.yml', 'config/sphinx_servers.yml'
11
+ end
12
+
13
+ end
@@ -0,0 +1,18 @@
1
+ namespace :thebes do
2
+
3
+ desc "Build the sphinx config files"
4
+ task :build do
5
+ unless File.exists?(File.join(Rails.root, 'config', 'sphinx.yml'))
6
+ raise 'No config file present, please create a config/sphinx.yml'
7
+ end
8
+ config = YAML.load(ERB.new(IO.read(File.join(Rails.root, 'config', 'sphinx.yml'))).result)[Rails.env.to_s]
9
+ generator = Thebes::ConfigWriter.new(File.join(Rails.root, 'config'))
10
+ config.each do |file, conf|
11
+ if file[0].chr != '/'
12
+ file = File.join(Rails.root, 'config', file)
13
+ end
14
+ generator.build file, (conf || {})
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,83 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'thebes' # and any other gems you need
5
+
6
+ FileUtils.mkdir_p 'tmp'
7
+
8
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+ config.mock_with :mocha
12
+ config.use_transactional_fixtures = false
13
+ config.before :suite do
14
+
15
+ unless defined?(SPHINX)
16
+
17
+ # Fetch a database configuration.
18
+ #
19
+ db_config = YAML.load( ERB.new( IO.read(
20
+ File.join(File.dirname(__FILE__), 'support/database.yml')
21
+ ) ).result )['test']
22
+
23
+ # Write a sphinx configuation for test mode.
24
+ #
25
+ generator = Thebes::ConfigWriter.new \
26
+ File.join(File.dirname(__FILE__), 'support'),
27
+ 'test.sphinx.conf'
28
+ generator.build \
29
+ File.join(File.dirname(__FILE__), 'support/test.sphinx.conf'),
30
+ db_config
31
+
32
+ # Give Riddle a dummy configuration so we can use it to
33
+ # control searchd.
34
+ #
35
+ r_config = Riddle::Configuration.new
36
+ r_config.searchd.pid_file = 'tmp/searchd.pid'
37
+ r_config.searchd.log = 'tmp/searchd.log'
38
+
39
+ # Create a Riddle controller.
40
+ #
41
+ SPHINX = Riddle::Controller.new \
42
+ r_config,
43
+ File.join(File.dirname(__FILE__), 'support/test.sphinx.conf')
44
+
45
+ end
46
+
47
+ # Build an initial index.
48
+ #
49
+ SPHINX.index
50
+
51
+ # Start searchd.
52
+ #
53
+ SPHINX.start
54
+
55
+ until SPHINX.running? do
56
+ sleep 0.1
57
+ end
58
+
59
+ end
60
+ config.after :each do
61
+ ActiveRecord::Base.connection.execute('SHOW TABLES;').each do |table|
62
+ # Or whatever you think is appropriate.
63
+ next if table.index('schema_migrations') or table.index('roles')
64
+ ActiveRecord::Base.connection.execute("TRUNCATE #{table}")
65
+ end
66
+ end
67
+ config.after :suite do
68
+
69
+ # Stop searchd after specs run.
70
+ #
71
+ SPHINX.stop
72
+
73
+ while SPHINX.running? do
74
+ sleep 0.1
75
+ SPHINX.stop
76
+ end
77
+
78
+ end
79
+ end
80
+
81
+ RSpec::Matchers.define :define_constant do |expected|
82
+ match { |actual| actual.const_defined?(expected) }
83
+ end
@@ -0,0 +1,22 @@
1
+ require 'active_record'
2
+ require 'mysql2'
3
+ require 'logger'
4
+ require 'rspec/rails/adapters'
5
+ require 'rspec/rails/fixture_support'
6
+
7
+ ROOT = File.join(File.dirname(__FILE__), '..')
8
+
9
+ ActiveRecord::Base.logger = Logger.new('tmp/ar_debug.log')
10
+ ActiveRecord::Base.configurations = YAML::load(IO.read('spec/support/database.yml'))
11
+ ActiveRecord::Base.establish_connection('test')
12
+
13
+ ActiveRecord::Schema.define :version => 0 do
14
+ create_table :items, :force => true do |t|
15
+ t.string :name
16
+ t.boolean :active
17
+ t.string :body
18
+ end
19
+ end
20
+
21
+ class Item < ActiveRecord::Base
22
+ end
@@ -0,0 +1,9 @@
1
+ development:
2
+ adapter: mysql2
3
+ pool: 5
4
+ timeout: 5000
5
+ encoding: utf8
6
+ database: thebes_test
7
+ username: root
8
+ socket: /tmp/mysql.sock
9
+ password:
@@ -0,0 +1,70 @@
1
+ indexer
2
+ {
3
+ mem_limit = 32M
4
+ }
5
+
6
+ searchd
7
+ {
8
+ listen = 0.0.0.0:9333
9
+ listen = 0.0.0.0:9334:mysql41
10
+ log = tmp/searchd.log
11
+ query_log = tmp/searchd.query.log
12
+ pid_file = tmp/searchd.pid
13
+ }
14
+
15
+ source items_core_0
16
+ {
17
+ type = mysql
18
+ sql_host = <%= @host %>
19
+ sql_user = <%= @username %>
20
+ sql_pass = <%= @password %>
21
+ sql_db = <%= @database %>
22
+ sql_sock = <%= @socket %>
23
+ sql_query_pre = SET NAMES utf8
24
+ sql_query_pre = SET TIME_ZONE = '+0:00'
25
+ sql_query = SELECT SQL_NO_CACHE \
26
+ ( `items`.`id` * 66 + 0 ) AS `id`, \
27
+ `items`.`id` AS `_id`, \
28
+ `items`.`name` as `name`, \
29
+ `items`.`active` AS `active`, \
30
+ `items`.`body` AS `body` \
31
+ FROM `items` \
32
+ WHERE \
33
+ `items`.`id` >= $start AND \
34
+ `items`.`id` <= $end \
35
+ ORDER BY NULL
36
+ sql_query_range = SELECT IFNULL(MIN(`id`), 1), IFNULL(MAX(`id`), 1) FROM `items`
37
+
38
+ sql_attr_uint = _id
39
+ sql_attr_uint = active
40
+
41
+ sql_query_info = SELECT * FROM `items` WHERE `id` = (($id - 0) / 66)
42
+ }
43
+
44
+ index items_core
45
+ {
46
+ source = items_core_0
47
+ path = tmp/items_core
48
+ charset_type = utf-8
49
+
50
+ enable_star = 1
51
+
52
+ min_prefix_len = 3
53
+ index_exact_words = 1
54
+
55
+ morphology = stem_en
56
+ # , libstemmer_sv
57
+
58
+ min_stemming_len = 3
59
+ min_word_len = 2
60
+
61
+ # This expand_keywords declaration is not working.
62
+ #
63
+ # expand_keywords = 1
64
+ }
65
+
66
+ index items
67
+ {
68
+ type = distributed
69
+ local = items_core
70
+ }
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe Thebes::Query, "after configuration" do
4
+
5
+ before(:all) {
6
+ Thebes::Query.servers = [['127.0.0.2', 111]]
7
+ }
8
+
9
+ subject { Thebes::Query.new }
10
+
11
+ its(:servers) { should == [['127.0.0.2', 111]] }
12
+
13
+ context "running query" do
14
+
15
+ before(:each) {
16
+ Thebes::Query.any_instance.stubs(:run)
17
+ }
18
+
19
+ after(:each) {
20
+ Thebes::Query.run {|q| }
21
+ }
22
+
23
+ it "should run the query on an instance" do
24
+ Thebes::Query.any_instance.expects(:run)
25
+ end
26
+
27
+ it "should call the before_query callback" do
28
+ Thebes::Query.before_query = Proc.new {|q| }
29
+ Thebes::Query.before_query.expects(:call)
30
+ end
31
+
32
+ it "should call the before_running callback" do
33
+ Thebes::Query.before_running = Proc.new {|q| }
34
+ Thebes::Query.before_running.expects(:call)
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ describe Thebes::Query, "against live data" do
42
+
43
+ before {
44
+ Thebes::Query.servers = [['127.0.0.1', 9333]]
45
+ Item.create \
46
+ :name => "Larry",
47
+ :active => true,
48
+ :body => "Fine was born to a Jewish family as Louis Feinberg[1] in Philadelphia, Pennsylvania, at the corner of 3rd and South Streets. The building there is now a restaurant which is called Jon's Bar & Grill."
49
+ Item.create \
50
+ :name => "Moe",
51
+ :active => true,
52
+ :body => "Moses Horwitz was born in Brooklyn, New York, neighborhood of Brownsville, to Solomon Horwitz and Jennie Gorovitz. He was the fourth of the five Horwitz brothers and of Levite and Lithuanian Jewish ancestry."
53
+ Item.create \
54
+ :name => "Curly",
55
+ :active => false,
56
+ :body => "Curly Howard was born Jerome Lester Horwitz in Brownsville, a section of Brooklyn, New York. He was the fifth of the five Horwitz brothers and of Lithuanian Jewish ancestry."
57
+ Item.create \
58
+ :name => "Shemp",
59
+ :active => true,
60
+ :body => "Shemp, like his brothers Moe and Curly, was born in Brownsville, Brooklyn. He was the third of the five Horwitz brothers and of Levite[citation needed] and Lithuanian Jewish ancestry."
61
+ SPHINX.index # :verbose => true
62
+ }
63
+
64
+ context "searching for 'Horwitz'" do
65
+
66
+ before {
67
+ @result = Thebes::Query.run do |q|
68
+ q.append_query 'Horwitz', 'items'
69
+ end
70
+ }
71
+
72
+ subject { @result.first[:matches] }
73
+
74
+ its(:length) { should == 3 }
75
+ its(:first) { subject[:attributes]['_id'] == 2 }
76
+ its(:last) { subject[:attributes]['_id'] == 4 }
77
+
78
+ end
79
+
80
+ context "searching for 'Horwitz' with filter" do
81
+
82
+ before {
83
+ @result = Thebes::Query.run do |q|
84
+ q.filters << Riddle::Client::Filter.new('active', [1])
85
+ q.append_query 'Horwitz', 'items'
86
+ end
87
+ }
88
+
89
+ subject { @result.first[:matches] }
90
+
91
+ its(:length) { should == 2 }
92
+ its(:first) { subject[:attributes]['_id'] == 2 }
93
+ its(:last) { subject[:attributes]['_id'] == 4 }
94
+
95
+ end
96
+
97
+
98
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Thebes::Sphinxql::Client, "after configuration" do
4
+
5
+ before(:all) {
6
+ Thebes::Sphinxql::Client.servers = [
7
+ { :host => 'localhost', :port => 9009 }
8
+ ]
9
+ }
10
+
11
+ subject { Thebes::Sphinxql::Client.new }
12
+
13
+ its(:servers) { should == [{ :host => "localhost", :port => 9009 }] }
14
+
15
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Thebes::Query, "against live data" do
4
+
5
+ before {
6
+ Thebes::Sphinxql::Client.servers = [
7
+ { :host => '127.0.0.1', :port => 9334 }
8
+ ]
9
+ Item.create \
10
+ :name => "Larry",
11
+ :active => true,
12
+ :body => "Fine was born to a Jewish family as Louis Feinberg[1] in Philadelphia, Pennsylvania, at the corner of 3rd and South Streets. The building there is now a restaurant which is called Jon's Bar & Grill."
13
+ Item.create \
14
+ :name => "Moe",
15
+ :active => true,
16
+ :body => "Moses Horwitz was born in Brooklyn, New York, neighborhood of Brownsville, to Solomon Horwitz and Jennie Gorovitz. He was the fourth of the five Horwitz brothers and of Levite and Lithuanian Jewish ancestry."
17
+ Item.create \
18
+ :name => "Curly",
19
+ :active => false,
20
+ :body => "Curly Howard was born Jerome Lester Horwitz in Brownsville, a section of Brooklyn, New York. He was the fifth of the five Horwitz brothers and of Lithuanian Jewish ancestry."
21
+ Item.create \
22
+ :name => "Shemp",
23
+ :active => true,
24
+ :body => "Shemp, like his brothers Moe and Curly, was born in Brownsville, Brooklyn. He was the third of the five Horwitz brothers and of Levite[citation needed] and Lithuanian Jewish ancestry."
25
+ SPHINX.index # :verbose => true
26
+ }
27
+
28
+ context "searching for 'Horwitz'" do
29
+
30
+ before {
31
+ @result = Thebes::Sphinxql::Query.run "SELECT * FROM items WHERE MATCH('Horwitz')"
32
+ }
33
+
34
+ subject { @result.collect {|r| r } }
35
+
36
+ its(:length) { should == 3 }
37
+ its(:first) { subject['_id'] == 2 }
38
+ its(:last) { subject['_id'] == 4 }
39
+
40
+ end
41
+
42
+ context "searching for 'Horwitz' with filter" do
43
+
44
+ before {
45
+ @result = Thebes::Sphinxql::Query.run "SELECT * FROM items WHERE MATCH('Horwitz') AND active = 1"
46
+ }
47
+
48
+ subject { @result.collect {|r| r } }
49
+
50
+ its(:length) { should == 2 }
51
+ its(:first) { subject['_id'] == 2 }
52
+ its(:last) { subject['_id'] == 4 }
53
+
54
+ end
55
+
56
+
57
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Thebes do
4
+
5
+ it { should define_constant(:VERSION) }
6
+
7
+ end
@@ -0,0 +1,12 @@
1
+ indexer
2
+ {
3
+ mem_limit = <%= @mem_limit %>
4
+ }
5
+
6
+ searchd
7
+ {
8
+ listen = 127.0.0.1:9312
9
+ log = <%= Rails.root %>/log/searchd.log
10
+ query_log = <%= Rails.root %>/searchd.query.log
11
+ pid_file = <%= Rails.root %>/searchd.<%= Rails.env.to_s %>.pid
12
+ }
@@ -0,0 +1,16 @@
1
+ development: &defaults
2
+ 'sphinx.conf':
3
+ mem_limit: '96M'
4
+
5
+ production:
6
+ 'sphinx.server1.conf':
7
+ mem_limit: '96M'
8
+ 'sphinx.server2.conf':
9
+ mem_limit: '128M'
10
+
11
+ staging:
12
+ <<: *defaults
13
+
14
+ test:
15
+ <<: *defaults
16
+
@@ -0,0 +1,16 @@
1
+ development: &defaults
2
+ sphinx_api:
3
+ - [ localhost, 3312 ]
4
+ # sphinxql:
5
+ # -
6
+ # :host: localhost
7
+ # :port: 3313
8
+
9
+ production:
10
+ <<: *defaults
11
+
12
+ staging:
13
+ <<: *defaults
14
+
15
+ test:
16
+ <<: *defaults
@@ -0,0 +1,30 @@
1
+ # -*- Mode:Ruby; encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "thebes/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "thebes"
7
+ s.version = Thebes::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Matthew Beale"]
10
+ s.email = ["matt.beale@madhatted.com"]
11
+ s.homepage = "https://github.com/harvesthq/thebes"
12
+ s.summary = %q{Thebes is a thin binding layer for Rails and Sphinx via Riddle and Mysql2.}
13
+ s.description = %q{Thebes is a thin binding layer for Rails and Sphinx via Riddle and Mysql2. Thebes expects you to write Sphinx configuration files by hand and have a rich understanding of Sphinx, but provides configuration files and templates to ease the process.}
14
+
15
+ s.add_dependency "riddle"
16
+ s.add_dependency "mysql2"
17
+ s.add_dependency "actionpack", ">= 3.0.3"
18
+ s.add_dependency "activerecord", ">= 3.0.3"
19
+ s.add_development_dependency "rspec"
20
+ s.add_development_dependency "rspec-rails"
21
+ s.add_development_dependency "genspec"
22
+ s.add_development_dependency "mocha"
23
+
24
+ # s.rubyforge_project = "thebes"
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thebes
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 3
10
+ version: 0.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Matthew Beale
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-07 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: riddle
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: mysql2
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: actionpack
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 1
58
+ segments:
59
+ - 3
60
+ - 0
61
+ - 3
62
+ version: 3.0.3
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: activerecord
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 1
74
+ segments:
75
+ - 3
76
+ - 0
77
+ - 3
78
+ version: 3.0.3
79
+ type: :runtime
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ prerelease: false
84
+ requirement: &id005 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ type: :development
94
+ version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec-rails
97
+ prerelease: false
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ type: :development
108
+ version_requirements: *id006
109
+ - !ruby/object:Gem::Dependency
110
+ name: genspec
111
+ prerelease: false
112
+ requirement: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ type: :development
122
+ version_requirements: *id007
123
+ - !ruby/object:Gem::Dependency
124
+ name: mocha
125
+ prerelease: false
126
+ requirement: &id008 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ hash: 3
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ type: :development
136
+ version_requirements: *id008
137
+ description: Thebes is a thin binding layer for Rails and Sphinx via Riddle and Mysql2. Thebes expects you to write Sphinx configuration files by hand and have a rich understanding of Sphinx, but provides configuration files and templates to ease the process.
138
+ email:
139
+ - matt.beale@madhatted.com
140
+ executables: []
141
+
142
+ extensions: []
143
+
144
+ extra_rdoc_files: []
145
+
146
+ files:
147
+ - .gitignore
148
+ - .rspec
149
+ - Gemfile
150
+ - Gemfile.lock
151
+ - LICENSE
152
+ - Rakefile
153
+ - Readme.mkd
154
+ - lib/thebes.rb
155
+ - lib/thebes/config_writer.rb
156
+ - lib/thebes/query.rb
157
+ - lib/thebes/railtie.rb
158
+ - lib/thebes/sphinx_search.rb
159
+ - lib/thebes/sphinxql/client.rb
160
+ - lib/thebes/sphinxql/query.rb
161
+ - lib/thebes/version.rb
162
+ - rails/init.rb
163
+ - railties/sphinx_config_generator.rb
164
+ - railties/thebes.rake
165
+ - spec/spec_helper.rb
166
+ - spec/support/active_record.rb
167
+ - spec/support/database.yml.example
168
+ - spec/support/test.sphinx.conf.erb
169
+ - spec/thebes/query_spec.rb
170
+ - spec/thebes/sphinxql/client_spec.rb
171
+ - spec/thebes/sphinxql/query_spec.rb
172
+ - spec/thebes/version_spec.rb
173
+ - templates/sphinx.conf.erb
174
+ - templates/sphinx.yml
175
+ - templates/sphinx_servers.yml
176
+ - thebes.gemspec
177
+ has_rdoc: true
178
+ homepage: https://github.com/harvesthq/thebes
179
+ licenses: []
180
+
181
+ post_install_message:
182
+ rdoc_options: []
183
+
184
+ require_paths:
185
+ - lib
186
+ required_ruby_version: !ruby/object:Gem::Requirement
187
+ none: false
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ hash: 3
192
+ segments:
193
+ - 0
194
+ version: "0"
195
+ required_rubygems_version: !ruby/object:Gem::Requirement
196
+ none: false
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ hash: 3
201
+ segments:
202
+ - 0
203
+ version: "0"
204
+ requirements: []
205
+
206
+ rubyforge_project:
207
+ rubygems_version: 1.5.0
208
+ signing_key:
209
+ specification_version: 3
210
+ summary: Thebes is a thin binding layer for Rails and Sphinx via Riddle and Mysql2.
211
+ test_files:
212
+ - spec/spec_helper.rb
213
+ - spec/support/active_record.rb
214
+ - spec/support/database.yml.example
215
+ - spec/support/test.sphinx.conf.erb
216
+ - spec/thebes/query_spec.rb
217
+ - spec/thebes/sphinxql/client_spec.rb
218
+ - spec/thebes/sphinxql/query_spec.rb
219
+ - spec/thebes/version_spec.rb