diff --git a/.gitignore b/.gitignore index 3ef667d9f..309e7372b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.gem .bundle .rbx +gemfiles Gemfile.lock *.sublime-* pkg/* diff --git a/.travis.yml b/.travis.yml index bd3d37813..f65d32d61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,39 @@ language: ruby rvm: - - 2.0.0 - - 2.1 - - 2.2.2 - - 2.3.1 - - jruby-9.0.5.0 +- 2.2.2 +- 2.3.4 +- 2.4.1 +- jruby-9.1.10.0 before_install: - - gem update --system - - gem install bundler +- pip install --upgrade --user awscli +- gem update --system +- gem install bundler before_script: - - "mysql -e 'create database thinking_sphinx;' > /dev/null" - - "psql -c 'create database thinking_sphinx;' -U postgres >/dev/null" - - bundle exec appraisal install +- mysql -e 'create database thinking_sphinx;' > /dev/null +- psql -c 'create database thinking_sphinx;' -U postgres >/dev/null +- "./bin/loadsphinx $SPHINX_VERSION" +- "./bin/literals" +- bundle exec appraisal install script: bundle exec appraisal rspec env: - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.0.10/bin/ SPHINX_VERSION=2.0.10 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.0.10/bin/ SPHINX_VERSION=2.0.10 - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.1.9/bin/ SPHINX_VERSION=2.1.9 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.1.9/bin/ SPHINX_VERSION=2.1.9 - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.2.6/bin/ SPHINX_VERSION=2.2.6 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.2.6/bin/ SPHINX_VERSION=2.2.6 + global: + - SPHINX_BIN=ext/sphinx/bin/ + - secure: cUPinkilBafqDSPsTkl/PXYc2aXNKUQKXGK8poBBMqKN9/wjfJx1DWgtowDKalekdZELxDhc85Ye3bL1xlW4nLjOu+U6Tkt8eNw2Nhs1flodHzA/RyENdBLr/tBHt43EjkrDehZx5sBHmWQY4miHs8AJz0oKO9Ae2inTOHx9Iuc= + matrix: + - DATABASE=mysql2 SPHINX_VERSION=2.0.10 + - DATABASE=postgresql SPHINX_VERSION=2.0.10 + - DATABASE=mysql2 SPHINX_VERSION=2.1.9 + - DATABASE=postgresql SPHINX_VERSION=2.1.9 + - DATABASE=mysql2 SPHINX_VERSION=2.2.6 + - DATABASE=postgresql SPHINX_VERSION=2.2.6 sudo: false addons: - postgresql: "9.4" + postgresql: '9.4' services: - - postgresql +- postgresql matrix: - allow_failures: - - rvm: jruby-9.0.5.0 + exclude: + - rvm: jruby-9.1.10.0 + env: DATABASE=mysql2 SPHINX_VERSION=2.0.10 + - rvm: jruby-9.1.10.0 + env: DATABASE=postgresql SPHINX_VERSION=2.0.10 diff --git a/Appraisals b/Appraisals index 46a2432c5..3e0c2d169 100644 --- a/Appraisals +++ b/Appraisals @@ -1,28 +1,32 @@ appraise 'rails_3_2' do gem 'rails', '~> 3.2.22.2' gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21] -end +end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_0' do gem 'rails', '~> 4.0.13' gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21] -end +end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_1' do gem 'rails', '~> 4.1.15' gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21] -end +end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_2' do gem 'rails', '~> 4.2.6' gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21] -end +end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_5_0' do - gem 'rails', '~> 5.0.0' + gem 'rails', '~> 5.0.2' # gem 'activerecord-jdbc-adapter', # :git => 'git://github.com/jruby/activerecord-jdbc-adapter.git', # :branch => 'rails-5', # :platform => :jruby, # :ref => 'c3570ce730' end if RUBY_VERSION.to_f >= 2.2 && RUBY_PLATFORM != 'java' + +appraise 'rails_5_1' do + gem 'rails', '~> 5.1.0' +end if RUBY_VERSION.to_f >= 2.2 && RUBY_PLATFORM != 'java' diff --git a/Gemfile b/Gemfile index 542b8c568..1ae4c1081 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,12 @@ gem 'mysql2', '~> 0.3.12b4', :platform => :ruby gem 'pg', '~> 0.18.4', :platform => :ruby gem 'jdbc-mysql', '5.1.35', :platform => :jruby -gem 'activerecord-jdbcmysql-adapter', '~> 1.3.4', :platform => :jruby -gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.4', :platform => :jruby +gem 'activerecord-jdbcmysql-adapter', '~> 1.3.23', :platform => :jruby +gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.23', :platform => :jruby -gem 'rack', '~> 1.0' if RUBY_VERSION.to_f <= 2.1 +if RUBY_VERSION.to_f <= 2.1 + gem 'rack', '~> 1.0' + gem 'nokogiri', '1.6.8' +end + +gem 'activerecord', '>= 3.2.22' if RUBY_PLATFORM == 'java' diff --git a/HISTORY b/HISTORY index 0def6f37d..dddfed695 100644 --- a/HISTORY +++ b/HISTORY @@ -1,3 +1,27 @@ +2017-08-28: 3.4.0 +* [CHANGE] Delta callback logic now prioritises checking for high level settings rather than model changes. +* [FIX] Index normalisation now occurs consistently, and removes unneccesary sphinx_internal_class_name fields from real-time indices. +* [FEATURE] Rake tasks are now unified, so the original tasks will operate on real-time indices as well. +* [FEATURE] Output warnings when unknown options are used in search calls. +* [CHANGE] Allow for unsaved records when calculating document ids (and return nil). +* [CHANGE] Display SphinxQL deletion statements in the log. +* [CHANGE] Add support for Ruby's frozen string literals feature. +* [FIX] Fix Sphinx connections in JRuby. +* [FIX] Fix long SphinxQL query handling in JRuby. +* [FEATURE] Allow generation of a single real-time index (Tim Brown). +* [FIX] Always close the SphinxQL connection if Innertube's asking (@cmaion). +* [CHANGE] Use saved_changes if it's available (in Rails 5.1+). +* [FEATURE] Automatically use UTF8 in Sphinx for encodings that are extensions of UTF8. +* [FIX] Get bigint primary keys working in Rails 5.1. +* [CHANGE] Set a default connection timeout of 5 seconds. +* [FIX] Fix handling of attached starts of Sphinx (via Henne Vogelsang). +* [FIX] Fix multi-field conditions. +* [FEATURE] Basic type checking for attribute filters. +* [CHANGE] Don't search multi-table inheritance ancestors. +* [FIX] Use the base class of STI models for polymorphic join generation (via Andrés Cirugeda). +* [CHANGE] Handle non-computable queries as parse errors. +* [FIX] Ensure ts:index now respects rake silent/quiet flags. + 2016-12-13: 3.3.0 * [FEATURE] Real-time callbacks can now be used with after_commit hooks if that's preferred over after_save. * [CHANGE] Only toggle the delta value if the record has changed or is new (rather than on every single save call). diff --git a/README.textile b/README.textile index 9f30fc257..3568f429b 100644 --- a/README.textile +++ b/README.textile @@ -1,11 +1,12 @@ h1. Thinking Sphinx -Thinking Sphinx is a library for connecting ActiveRecord to the Sphinx full-text search tool, and integrates closely with Rails (but also works with other Ruby web frameworks). The current release is v3.3.0. +Thinking Sphinx is a library for connecting ActiveRecord to the Sphinx full-text search tool, and integrates closely with Rails (but also works with other Ruby web frameworks). The current release is v3.4.0. h2. Upgrading Please refer to the release notes for any changes you need to make when upgrading: +* "v3.4.0":https://github.com/pat/thinking-sphinx/releases/tag/v3.4.0 * "v3.3.0":https://github.com/pat/thinking-sphinx/releases/tag/v3.3.0 * "v3.2.0":https://github.com/pat/thinking-sphinx/releases/tag/v3.2.0 * "v3.1.4":https://github.com/pat/thinking-sphinx/releases/tag/v3.1.4 @@ -21,9 +22,9 @@ h2. Installation It's a gem, so install it like you would any other gem. You will also need to specify the mysql2 gem if you're using MRI, or jdbc-mysql if you're using JRuby: -
gem 'mysql2', '~> 0.3.18', :platform => :ruby
+gem 'mysql2', '~> 0.3', :platform => :ruby
gem 'jdbc-mysql', '= 5.1.35', :platform => :jruby
-gem 'thinking-sphinx', '~> 3.3.0'
+gem 'thinking-sphinx', '~> 3.4.0'
The MySQL gems mentioned are required for connecting to Sphinx, so please include it even when you're using PostgreSQL for your database. If you're using JRuby, there is "currently an issue with Sphinx and jdbc-mysql 5.1.36 or newer":http://sphinxsearch.com/forum/view.html?id=13939, so you'll need to stick to nothing more recent than 5.1.35.
@@ -84,4 +85,4 @@ You can then run the unit tests with @rake spec:unit@, the acceptance tests with
h2. Licence
-Copyright (c) 2007-2015, Thinking Sphinx is developed and maintained by Pat Allan, and is released under the open MIT Licence. Many thanks to "all who have contributed patches":https://github.com/pat/thinking-sphinx/contributors.
+Copyright (c) 2007-2017, Thinking Sphinx is developed and maintained by Pat Allan, and is released under the open MIT Licence. Many thanks to "all who have contributed patches":https://github.com/pat/thinking-sphinx/contributors.
diff --git a/bin/console b/bin/console
new file mode 100755
index 000000000..b0e4a5e9c
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+
+require "bundler/setup"
+require "thinking_sphinx"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/bin/literals b/bin/literals
new file mode 100755
index 000000000..7c30840fc
--- /dev/null
+++ b/bin/literals
@@ -0,0 +1,9 @@
+if (ruby -e "exit RUBY_VERSION.to_f >= 2.4")
+then
+ echo "Automatic frozen string literals are supported"
+ gem install pragmater -v 4.0.0
+ pragmater --add lib --comments "# frozen_string_literal: true" --whitelist "**/*.rb"
+ pragmater --add spec --comments "# frozen_string_literal: true" --whitelist "**/*.rb"
+else
+ echo "Automatic frozen string literals are not supported."
+fi
diff --git a/bin/loadsphinx b/bin/loadsphinx
new file mode 100755
index 000000000..217160c0d
--- /dev/null
+++ b/bin/loadsphinx
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+version=$1
+name="sphinx-$version"
+url="http://sphinxsearch.com/files/$name-release.tar.gz"
+bucket="thinking-sphinx"
+directory="ext/sphinx"
+prefix="`pwd`/$directory"
+file="ext/$name.tar.gz"
+
+download_and_compile_source () {
+ curl -O $url
+ tar -zxf $name-release.tar.gz
+ cd $name-release
+ ./configure --with-mysql --with-pgsql --enable-id64 --prefix=$prefix
+ make
+ make install
+ cd ..
+ rm -rf $name-release.tar.gz $name-release
+}
+
+load_cache () {
+ aws s3 cp s3://$bucket/bincaches/$name.tar.gz $file
+ tar -zxf $file
+}
+
+push_cache () {
+ tar -czf $file $directory
+ aws s3 cp $file s3://$bucket/bincaches/$name.tar.gz --acl public-read
+}
+
+if aws s3api head-object --bucket $bucket --key bincaches/$name.tar.gz
+then
+ load_cache
+else
+ download_and_compile_source
+ push_cache
+fi
diff --git a/gemfiles/.gitignore b/gemfiles/.gitignore
deleted file mode 100644
index 33905cb38..000000000
--- a/gemfiles/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.gemfile.lock
\ No newline at end of file
diff --git a/gemfiles/rails_3_2.gemfile b/gemfiles/rails_3_2.gemfile
deleted file mode 100644
index f8679edbe..000000000
--- a/gemfiles/rails_3_2.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "mysql2", "~> 0.3.12b4", :platform => :ruby
-gem "pg", "~> 0.18.4", :platform => :ruby
-gem "jdbc-mysql", "5.1.35", :platform => :jruby
-gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform => :jruby
-gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform => :jruby
-gem "rails", "~> 3.2.22.2"
-gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
-
-gemspec :path => "../"
diff --git a/gemfiles/rails_4_0.gemfile b/gemfiles/rails_4_0.gemfile
deleted file mode 100644
index 50c75c2fa..000000000
--- a/gemfiles/rails_4_0.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "mysql2", "~> 0.3.12b4", :platform => :ruby
-gem "pg", "~> 0.18.4", :platform => :ruby
-gem "jdbc-mysql", "5.1.35", :platform => :jruby
-gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform => :jruby
-gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform => :jruby
-gem "rails", "~> 4.0.13"
-gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
-
-gemspec :path => "../"
diff --git a/gemfiles/rails_4_1.gemfile b/gemfiles/rails_4_1.gemfile
deleted file mode 100644
index 7092e4639..000000000
--- a/gemfiles/rails_4_1.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "mysql2", "~> 0.3.12b4", :platform => :ruby
-gem "pg", "~> 0.18.4", :platform => :ruby
-gem "jdbc-mysql", "5.1.35", :platform => :jruby
-gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform => :jruby
-gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform => :jruby
-gem "rails", "~> 4.1.15"
-gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
-
-gemspec :path => "../"
diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile
deleted file mode 100644
index 07bc0eb8d..000000000
--- a/gemfiles/rails_4_2.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "mysql2", "~> 0.3.12b4", :platform => :ruby
-gem "pg", "~> 0.18.4", :platform => :ruby
-gem "jdbc-mysql", "5.1.35", :platform => :jruby
-gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform => :jruby
-gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform => :jruby
-gem "rails", "~> 4.2.6"
-gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
-
-gemspec :path => "../"
diff --git a/gemfiles/rails_5_0.gemfile b/gemfiles/rails_5_0.gemfile
deleted file mode 100644
index 2a168ba9c..000000000
--- a/gemfiles/rails_5_0.gemfile
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "mysql2", "~> 0.3.12b4", :platform => :ruby
-gem "pg", "~> 0.18.4", :platform => :ruby
-gem "jdbc-mysql", "5.1.35", :platform => :jruby
-gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform => :jruby
-gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform => :jruby
-gem "rails", "~> 5.0.0"
-
-gemspec :path => "../"
diff --git a/lib/thinking_sphinx.rb b/lib/thinking_sphinx.rb
index 892534252..fc9c4934a 100644
--- a/lib/thinking_sphinx.rb
+++ b/lib/thinking_sphinx.rb
@@ -15,7 +15,7 @@
require 'active_support/core_ext/module/attribute_accessors'
module ThinkingSphinx
- MAXIMUM_STATEMENT_LENGTH = (2 ** 23) - 1
+ MAXIMUM_STATEMENT_LENGTH = (2 ** 23) - 5
def self.count(query = '', options = {})
search_for_ids(query, options).total_entries
@@ -40,14 +40,24 @@ def self.before_index_hooks
@before_index_hooks = []
- module Subscribers; end
+ module Commands; end
module IndexingStrategies; end
+ module Interfaces; end
+ module Subscribers; end
end
# Core
+require 'thinking_sphinx/attribute_types'
require 'thinking_sphinx/batched_search'
require 'thinking_sphinx/callbacks'
require 'thinking_sphinx/core'
+require 'thinking_sphinx/with_output'
+require 'thinking_sphinx/commands/base'
+require 'thinking_sphinx/commands/configure'
+require 'thinking_sphinx/commands/index'
+require 'thinking_sphinx/commands/start_attached'
+require 'thinking_sphinx/commands/start_detached'
+require 'thinking_sphinx/commands/stop'
require 'thinking_sphinx/configuration'
require 'thinking_sphinx/connection'
require 'thinking_sphinx/controller'
@@ -63,6 +73,9 @@ module IndexingStrategies; end
require 'thinking_sphinx/indexing_strategies/all_at_once'
require 'thinking_sphinx/indexing_strategies/one_at_a_time'
require 'thinking_sphinx/index_set'
+require 'thinking_sphinx/interfaces/daemon'
+require 'thinking_sphinx/interfaces/real_time'
+require 'thinking_sphinx/interfaces/sql'
require 'thinking_sphinx/masks'
require 'thinking_sphinx/middlewares'
require 'thinking_sphinx/panes'
diff --git a/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb
index 7fec6b8f9..83891bad5 100644
--- a/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb
+++ b/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb
@@ -16,9 +16,8 @@ def after_commit
end
def before_save
- return unless new_or_changed? &&
- !ThinkingSphinx::Callbacks.suspended? &&
- delta_indices?
+ return unless !ThinkingSphinx::Callbacks.suspended? && delta_indices? &&
+ new_or_changed?
processors.each { |processor| processor.toggle instance }
end
diff --git a/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb
index 61d83bd1a..b395b1935 100644
--- a/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb
+++ b/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb
@@ -1,6 +1,12 @@
class ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks <
ThinkingSphinx::Callbacks
+ if ActiveRecord::Base.instance_methods.grep(/saved_changes/).any?
+ CHANGED_ATTRIBUTES = lambda { |instance| instance.saved_changes.keys }
+ else
+ CHANGED_ATTRIBUTES = lambda { |instance| instance.changed }
+ end
+
callbacks :after_update
def after_update
@@ -15,7 +21,7 @@ def after_update
def attributes_hash_for(index)
updateable_attributes_for(index).inject({}) do |hash, attribute|
- if instance.changed.include?(attribute.columns.first.__name.to_s)
+ if changed_attributes.include?(attribute.columns.first.__name.to_s)
hash[attribute.name] = attribute.value_for(instance)
end
@@ -23,6 +29,10 @@ def attributes_hash_for(index)
end
end
+ def changed_attributes
+ @changed_attributes ||= CHANGED_ATTRIBUTES.call instance
+ end
+
def configuration
ThinkingSphinx::Configuration.instance
end
diff --git a/lib/thinking_sphinx/active_record/index.rb b/lib/thinking_sphinx/active_record/index.rb
index a6a7d6b4c..9df465d44 100644
--- a/lib/thinking_sphinx/active_record/index.rb
+++ b/lib/thinking_sphinx/active_record/index.rb
@@ -63,7 +63,7 @@ def source_options
:delta? => @options[:delta?],
:delta_processor => @options[:delta_processor],
:delta_options => @options[:delta_options],
- :primary_key => @options[:primary_key] || model.primary_key || :id
+ :primary_key => primary_key
}
end
end
diff --git a/lib/thinking_sphinx/active_record/join_association.rb b/lib/thinking_sphinx/active_record/join_association.rb
index b95127b1b..b25259615 100644
--- a/lib/thinking_sphinx/active_record/join_association.rb
+++ b/lib/thinking_sphinx/active_record/join_association.rb
@@ -5,7 +5,9 @@ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = super
constraint = constraint.and(
- foreign_table[reflection.options[:foreign_type]].eq(base_klass.name)
+ foreign_table[reflection.options[:foreign_type]].eq(
+ base_klass.base_class.name
+ )
) if reflection.options[:sphinx_internal_filtered]
constraint
diff --git a/lib/thinking_sphinx/active_record/log_subscriber.rb b/lib/thinking_sphinx/active_record/log_subscriber.rb
index c03de51c7..76af6e572 100644
--- a/lib/thinking_sphinx/active_record/log_subscriber.rb
+++ b/lib/thinking_sphinx/active_record/log_subscriber.rb
@@ -13,6 +13,11 @@ def query(event)
identifier = color('Sphinx Query (%.1fms)' % event.duration, GREEN, true)
debug " #{identifier} #{event.payload[:query]}"
end
+
+ def warn(event)
+ identifier = color 'Sphinx', GREEN, true
+ warn " #{identifier} #{event.payload[:guard]}"
+ end
end
ThinkingSphinx::ActiveRecord::LogSubscriber.attach_to :thinking_sphinx
diff --git a/lib/thinking_sphinx/active_record/sql_source.rb b/lib/thinking_sphinx/active_record/sql_source.rb
index 3ba926540..7b6f52127 100644
--- a/lib/thinking_sphinx/active_record/sql_source.rb
+++ b/lib/thinking_sphinx/active_record/sql_source.rb
@@ -14,7 +14,7 @@ def initialize(model, options = {})
@model = model
@database_settings = model.connection.instance_variable_get(:@config).clone
@options = {
- :utf8? => (@database_settings[:encoding] == 'utf8')
+ :utf8? => (@database_settings[:encoding].to_s[/^utf8/])
}.merge options
@fields = []
diff --git a/lib/thinking_sphinx/attribute_types.rb b/lib/thinking_sphinx/attribute_types.rb
new file mode 100644
index 000000000..84954642a
--- /dev/null
+++ b/lib/thinking_sphinx/attribute_types.rb
@@ -0,0 +1,70 @@
+class ThinkingSphinx::AttributeTypes
+ def self.call
+ @call ||= new.call
+ end
+
+ def self.reset
+ @call = nil
+ end
+
+ def call
+ return {} unless File.exist?(configuration_file)
+
+ realtime_indices.each { |index|
+ map_types_with_prefix index, :rt,
+ [:uint, :bigint, :float, :timestamp, :string, :bool, :json]
+
+ index.rt_attr_multi.each { |name| attributes[name] << :uint }
+ index.rt_attr_multi_64.each { |name| attributes[name] << :bigint }
+ }
+
+ plain_sources.each { |source|
+ map_types_with_prefix source, :sql,
+ [:uint, :bigint, :float, :timestamp, :string, :bool, :json]
+
+ source.sql_attr_str2ordinal { |name| attributes[name] << :uint }
+ source.sql_attr_str2wordcount { |name| attributes[name] << :uint }
+ source.sql_attr_multi.each { |setting|
+ type, name, *ignored = setting.split(/\s+/)
+ attributes[name] << type.to_sym
+ }
+ }
+
+ attributes.values.each &:uniq!
+ attributes
+ end
+
+ private
+
+ def attributes
+ @attributes ||= Hash.new { |hash, key| hash[key] = [] }
+ end
+
+ def configuration
+ @configuration ||= Riddle::Configuration.parse!(
+ File.read(configuration_file)
+ )
+ end
+
+ def configuration_file
+ ThinkingSphinx::Configuration.instance.configuration_file
+ end
+
+ def map_types_with_prefix(object, prefix, types)
+ types.each do |type|
+ object.public_send("#{prefix}_attr_#{type}").each do |name|
+ attributes[name] << type
+ end
+ end
+ end
+
+ def plain_sources
+ configuration.indices.select { |index|
+ index.type == 'plain' || index.type.nil?
+ }.collect(&:sources).flatten
+ end
+
+ def realtime_indices
+ configuration.indices.select { |index| index.type == 'rt' }
+ end
+end
diff --git a/lib/thinking_sphinx/commands/base.rb b/lib/thinking_sphinx/commands/base.rb
new file mode 100644
index 000000000..0b697fb95
--- /dev/null
+++ b/lib/thinking_sphinx/commands/base.rb
@@ -0,0 +1,41 @@
+class ThinkingSphinx::Commands::Base
+ include ThinkingSphinx::WithOutput
+
+ def self.call(configuration, options, stream = STDOUT)
+ new(configuration, options, stream).call_with_handling
+ end
+
+ def call_with_handling
+ call
+ rescue Riddle::CommandFailedError => error
+ handle_failure error.command_result
+ end
+
+ private
+
+ delegate :controller, :to => :configuration
+
+ def command_output(output)
+ return "See above\n" if output.nil?
+
+ "\n\t" + output.gsub("\n", "\n\t")
+ end
+
+ def handle_failure(result)
+ stream.puts <<-TXT
+
+The Sphinx #{type} command failed:
+ Command: #{result.command}
+ Status: #{result.status}
+ Output: #{command_output result.output}
+There may be more information about the failure in #{configuration.searchd.log}.
+ TXT
+ exit result.status
+ end
+
+ def log(message)
+ return if options[:silent]
+
+ stream.puts message
+ end
+end
diff --git a/lib/thinking_sphinx/commands/configure.rb b/lib/thinking_sphinx/commands/configure.rb
new file mode 100644
index 000000000..cec2508df
--- /dev/null
+++ b/lib/thinking_sphinx/commands/configure.rb
@@ -0,0 +1,13 @@
+class ThinkingSphinx::Commands::Configure < ThinkingSphinx::Commands::Base
+ def call
+ log "Generating configuration to #{configuration.configuration_file}"
+
+ configuration.render_to_file
+ end
+
+ private
+
+ def type
+ 'configure'
+ end
+end
diff --git a/lib/thinking_sphinx/commands/index.rb b/lib/thinking_sphinx/commands/index.rb
new file mode 100644
index 000000000..47bb079f1
--- /dev/null
+++ b/lib/thinking_sphinx/commands/index.rb
@@ -0,0 +1,11 @@
+class ThinkingSphinx::Commands::Index < ThinkingSphinx::Commands::Base
+ def call
+ controller.index :verbose => options[:verbose]
+ end
+
+ private
+
+ def type
+ 'indexing'
+ end
+end
diff --git a/lib/thinking_sphinx/commands/start_attached.rb b/lib/thinking_sphinx/commands/start_attached.rb
new file mode 100644
index 000000000..a3e503b56
--- /dev/null
+++ b/lib/thinking_sphinx/commands/start_attached.rb
@@ -0,0 +1,20 @@
+class ThinkingSphinx::Commands::StartAttached < ThinkingSphinx::Commands::Base
+ def call
+ FileUtils.mkdir_p configuration.indices_location
+
+ unless pid = fork
+ controller.start :verbose => options[:verbose], :nodetach => true
+ end
+
+ Signal.trap('TERM') { Process.kill(:TERM, pid) }
+ Signal.trap('INT') { Process.kill(:TERM, pid) }
+
+ Process.wait(pid)
+ end
+
+ private
+
+ def type
+ 'start'
+ end
+end
diff --git a/lib/thinking_sphinx/commands/start_detached.rb b/lib/thinking_sphinx/commands/start_detached.rb
new file mode 100644
index 000000000..8f3dccb52
--- /dev/null
+++ b/lib/thinking_sphinx/commands/start_detached.rb
@@ -0,0 +1,19 @@
+class ThinkingSphinx::Commands::StartDetached < ThinkingSphinx::Commands::Base
+ def call
+ FileUtils.mkdir_p configuration.indices_location
+
+ result = controller.start :verbose => options[:verbose]
+
+ if controller.running?
+ log "Started searchd successfully (pid: #{controller.pid})."
+ else
+ handle_failure result
+ end
+ end
+
+ private
+
+ def type
+ 'start'
+ end
+end
diff --git a/lib/thinking_sphinx/commands/stop.rb b/lib/thinking_sphinx/commands/stop.rb
new file mode 100644
index 000000000..ba9d102ac
--- /dev/null
+++ b/lib/thinking_sphinx/commands/stop.rb
@@ -0,0 +1,22 @@
+class ThinkingSphinx::Commands::Stop < ThinkingSphinx::Commands::Base
+ def call
+ unless controller.running?
+ log 'searchd is not currently running.'
+ return
+ end
+
+ pid = controller.pid
+ until !controller.running? do
+ controller.stop options
+ sleep(0.5)
+ end
+
+ log "Stopped searchd daemon (pid: #{pid})."
+ end
+
+ private
+
+ def type
+ 'stop'
+ end
+end
diff --git a/lib/thinking_sphinx/configuration.rb b/lib/thinking_sphinx/configuration.rb
index 61ff50f23..f8cef1164 100644
--- a/lib/thinking_sphinx/configuration.rb
+++ b/lib/thinking_sphinx/configuration.rb
@@ -84,9 +84,8 @@ def preload_indices
end
end
- if settings['distributed_indices'].nil? || settings['distributed_indices']
- ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
- end
+ normalise
+ verify
@preloaded_indices = true
end
@@ -95,10 +94,6 @@ def preload_indices
def render
preload_indices
- ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile
- ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile
- ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile
-
super
end
@@ -137,6 +132,16 @@ def setup
private
+ def apply_sphinx_settings!
+ sphinx_sections.each do |object|
+ settings.each do |key, value|
+ next unless object.class.settings.include?(key.to_sym)
+
+ object.send("#{key}=", value)
+ end
+ end
+ end
+
def configure_searchd
configure_searchd_log_files
@@ -153,12 +158,21 @@ def configure_searchd_log_files
searchd.query_log = log_root.join("#{environment}.searchd.query.log").to_s
end
+ def framework_root
+ Pathname.new(framework.root)
+ end
+
def log_root
real_path 'log'
end
- def framework_root
- Pathname.new(framework.root)
+ def normalise
+ if settings['distributed_indices'].nil? || settings['distributed_indices']
+ ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
+ end
+
+ ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile
+ ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile
end
def real_path(*arguments)
@@ -166,25 +180,21 @@ def real_path(*arguments)
path.exist? ? path.realpath : path
end
- def settings_to_hash
- input = File.read settings_file
- input = ERB.new(input).result if defined?(ERB)
-
- contents = YAML.load input
- contents && contents[environment] || {}
+ def reset
+ @settings = nil
+ setup
end
def settings_file
framework_root.join 'config', 'thinking_sphinx.yml'
end
- def reset
- @settings = nil
- setup
- end
+ def settings_to_hash
+ input = File.read settings_file
+ input = ERB.new(input).result if defined?(ERB)
- def tmp_path
- real_path 'tmp'
+ contents = YAML.load input
+ contents && contents[environment] || {}
end
def sphinx_sections
@@ -193,14 +203,12 @@ def sphinx_sections
sections
end
- def apply_sphinx_settings!
- sphinx_sections.each do |object|
- settings.each do |key, value|
- next unless object.class.settings.include?(key.to_sym)
+ def tmp_path
+ real_path 'tmp'
+ end
- object.send("#{key}=", value)
- end
- end
+ def verify
+ ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile
end
end
diff --git a/lib/thinking_sphinx/configuration/minimum_fields.rb b/lib/thinking_sphinx/configuration/minimum_fields.rb
index 3563260bc..7b7cfe3c0 100644
--- a/lib/thinking_sphinx/configuration/minimum_fields.rb
+++ b/lib/thinking_sphinx/configuration/minimum_fields.rb
@@ -6,8 +6,8 @@ def initialize(indices)
def reconcile
return unless no_inheritance_columns?
- sources.each do |source|
- source.fields.delete_if do |field|
+ field_collections.each do |collection|
+ collection.fields.delete_if do |field|
field.name == 'sphinx_internal_class_name'
end
end
@@ -17,15 +17,18 @@ def reconcile
attr_reader :indices
+ def field_collections
+ indices_of_type('plain').collect(&:sources).flatten +
+ indices_of_type('rt')
+ end
+
+ def indices_of_type(type)
+ indices.select { |index| index.type == type }
+ end
+
def no_inheritance_columns?
indices.select { |index|
index.model.column_names.include?(index.model.inheritance_column)
}.empty?
end
-
- def sources
- @sources ||= @indices.select { |index|
- index.respond_to?(:sources)
- }.collect(&:sources).flatten
- end
end
diff --git a/lib/thinking_sphinx/connection.rb b/lib/thinking_sphinx/connection.rb
index 3b2029489..08624335f 100644
--- a/lib/thinking_sphinx/connection.rb
+++ b/lib/thinking_sphinx/connection.rb
@@ -26,7 +26,7 @@ def self.connection_class
def self.pool
@pool ||= Innertube::Pool.new(
Proc.new { ThinkingSphinx::Connection.new },
- Proc.new { |connection| connection.close }
+ Proc.new { |connection| connection.close! }
)
end
@@ -63,125 +63,8 @@ def self.persistent=(persist)
end
@persistent = true
-
- class Client
- def close
- client.close unless ThinkingSphinx::Connection.persistent?
- end
-
- def execute(statement)
- check_and_perform(statement).first
- end
-
- def query_all(*statements)
- check_and_perform statements.join('; ')
- end
-
- private
-
- def check(statements)
- if statements.length > ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH
- exception = ThinkingSphinx::QueryLengthError.new
- exception.statement = statements
- raise exception
- end
- end
-
- def check_and_perform(statements)
- check statements
- perform statements
- end
-
- def close_and_clear
- client.close
- @client = nil
- end
-
- def perform(statements)
- results_for statements
- rescue => error
- message = "#{error.message} - #{statements}"
- wrapper = ThinkingSphinx::QueryExecutionError.new message
- wrapper.statement = statements
- raise wrapper
- ensure
- close_and_clear unless ThinkingSphinx::Connection.persistent?
- end
- end
-
- class MRI < Client
- def initialize(options)
- @options = options
- end
-
- def base_error
- Mysql2::Error
- end
-
- private
-
- attr_reader :options
-
- def client
- @client ||= Mysql2::Client.new({
- :flags => Mysql2::Client::MULTI_STATEMENTS
- }.merge(options))
- rescue base_error => error
- raise ThinkingSphinx::SphinxError.new_from_mysql error
- end
-
- def results_for(statements)
- results = [client.query(statements)]
- results << client.store_result while client.next_result
- results
- end
- end
-
- class JRuby < Client
- attr_reader :address, :options
-
- def initialize(options)
- @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true"
- @options = options
- end
-
- def base_error
- Java::JavaSql::SQLException
- end
-
- private
-
- def client
- @client ||= java.sql.DriverManager.getConnection address,
- options[:username], options[:password]
- rescue base_error => error
- raise ThinkingSphinx::SphinxError.new_from_mysql error
- end
-
- def results_for(statements)
- statement = client.createStatement
- statement.execute statements
-
- results = [set_to_array(statement.getResultSet)]
- results << set_to_array(statement.getResultSet) while statement.getMoreResults
- results.compact
- end
-
- def set_to_array(set)
- return nil if set.nil?
-
- meta = set.getMetaData
- rows = []
-
- while set.next
- rows << (1..meta.getColumnCount).inject({}) do |row, index|
- name = meta.getColumnName index
- row[name] = set.getObject(index)
- row
- end
- end
-
- rows
- end
- end
end
+
+require 'thinking_sphinx/connection/client'
+require 'thinking_sphinx/connection/jruby'
+require 'thinking_sphinx/connection/mri'
diff --git a/lib/thinking_sphinx/connection/client.rb b/lib/thinking_sphinx/connection/client.rb
new file mode 100644
index 000000000..d972e7f51
--- /dev/null
+++ b/lib/thinking_sphinx/connection/client.rb
@@ -0,0 +1,48 @@
+class ThinkingSphinx::Connection::Client
+ def close
+ close! unless ThinkingSphinx::Connection.persistent?
+ end
+
+ def close!
+ client.close
+ end
+
+ def execute(statement)
+ check_and_perform(statement).first
+ end
+
+ def query_all(*statements)
+ check_and_perform statements.join('; ')
+ end
+
+ private
+
+ def check(statements)
+ if statements.length > ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH
+ exception = ThinkingSphinx::QueryLengthError.new
+ exception.statement = statements
+ raise exception
+ end
+ end
+
+ def check_and_perform(statements)
+ check statements
+ perform statements
+ end
+
+ def close_and_clear
+ client.close
+ @client = nil
+ end
+
+ def perform(statements)
+ results_for statements
+ rescue => error
+ message = "#{error.message} - #{statements}"
+ wrapper = ThinkingSphinx::QueryExecutionError.new message
+ wrapper.statement = statements
+ raise wrapper
+ ensure
+ close_and_clear unless ThinkingSphinx::Connection.persistent?
+ end
+end
diff --git a/lib/thinking_sphinx/connection/jruby.rb b/lib/thinking_sphinx/connection/jruby.rb
new file mode 100644
index 000000000..b73d0e4ff
--- /dev/null
+++ b/lib/thinking_sphinx/connection/jruby.rb
@@ -0,0 +1,53 @@
+class ThinkingSphinx::Connection::JRuby < ThinkingSphinx::Connection::Client
+ attr_reader :address, :options
+
+ def initialize(options)
+ @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true"
+ @options = options
+ end
+
+ def base_error
+ Java::JavaSql::SQLException
+ end
+
+ private
+
+ def client
+ @client ||= Java::ComMysqlJdbc::Driver.new.connect address, properties
+ rescue base_error => error
+ raise ThinkingSphinx::SphinxError.new_from_mysql error
+ end
+
+ def properties
+ object = Java::JavaUtil::Properties.new
+ object.setProperty "user", options[:username] if options[:username]
+ object.setProperty "password", options[:password] if options[:password]
+ object
+ end
+
+ def results_for(statements)
+ statement = client.createStatement
+ statement.execute statements
+
+ results = [set_to_array(statement.getResultSet)]
+ results << set_to_array(statement.getResultSet) while statement.getMoreResults
+ results.compact
+ end
+
+ def set_to_array(set)
+ return nil if set.nil?
+
+ meta = set.getMetaData
+ rows = []
+
+ while set.next
+ rows << (1..meta.getColumnCount).inject({}) do |row, index|
+ name = meta.getColumnName index
+ row[name] = set.getObject(index)
+ row
+ end
+ end
+
+ rows
+ end
+end
diff --git a/lib/thinking_sphinx/connection/mri.rb b/lib/thinking_sphinx/connection/mri.rb
new file mode 100644
index 000000000..05d97df64
--- /dev/null
+++ b/lib/thinking_sphinx/connection/mri.rb
@@ -0,0 +1,28 @@
+class ThinkingSphinx::Connection::MRI < ThinkingSphinx::Connection::Client
+ def initialize(options)
+ @options = options
+ end
+
+ def base_error
+ Mysql2::Error
+ end
+
+ private
+
+ attr_reader :options
+
+ def client
+ @client ||= Mysql2::Client.new({
+ :flags => Mysql2::Client::MULTI_STATEMENTS,
+ :connect_timeout => 5
+ }.merge(options))
+ rescue base_error => error
+ raise ThinkingSphinx::SphinxError.new_from_mysql error
+ end
+
+ def results_for(statements)
+ results = [client.query(statements)]
+ results << client.store_result while client.next_result
+ results
+ end
+end
diff --git a/lib/thinking_sphinx/core/index.rb b/lib/thinking_sphinx/core/index.rb
index cee129e65..992fe2c1d 100644
--- a/lib/thinking_sphinx/core/index.rb
+++ b/lib/thinking_sphinx/core/index.rb
@@ -25,7 +25,13 @@ def distributed?
false
end
+ def document_id_for_instance(instance)
+ document_id_for_key instance.public_send(primary_key)
+ end
+
def document_id_for_key(key)
+ return nil if key.nil?
+
key * config.indices.count + offset
end
@@ -47,6 +53,11 @@ def options
@options
end
+ def primary_key
+ @primary_key ||= @options[:primary_key] ||
+ config.settings['primary_key'] || model.primary_key || :id
+ end
+
def render
pre_render
set_path
diff --git a/lib/thinking_sphinx/deletion.rb b/lib/thinking_sphinx/deletion.rb
index 15ad08fad..64876630d 100644
--- a/lib/thinking_sphinx/deletion.rb
+++ b/lib/thinking_sphinx/deletion.rb
@@ -25,8 +25,12 @@ def document_ids_for_keys
end
def execute(statement)
- ThinkingSphinx::Connection.take do |connection|
- connection.execute statement
+ statement = statement.gsub(/\s*\n\s*/, ' ').strip
+
+ ThinkingSphinx::Logger.log :query, statement do
+ ThinkingSphinx::Connection.take do |connection|
+ connection.execute statement
+ end
end
end
diff --git a/lib/thinking_sphinx/deltas/default_delta.rb b/lib/thinking_sphinx/deltas/default_delta.rb
index 08884e8bf..010b11e6e 100644
--- a/lib/thinking_sphinx/deltas/default_delta.rb
+++ b/lib/thinking_sphinx/deltas/default_delta.rb
@@ -13,7 +13,7 @@ def clause(delta_source = false)
def delete(index, instance)
ThinkingSphinx::Deltas::DeleteJob.new(
- index.name, index.document_id_for_key(instance.id)
+ index.name, index.document_id_for_instance(instance)
).perform
end
diff --git a/lib/thinking_sphinx/deltas/delete_job.rb b/lib/thinking_sphinx/deltas/delete_job.rb
index 4c8cb3a09..90a819701 100644
--- a/lib/thinking_sphinx/deltas/delete_job.rb
+++ b/lib/thinking_sphinx/deltas/delete_job.rb
@@ -4,12 +4,22 @@ def initialize(index_name, document_id)
end
def perform
- ThinkingSphinx::Connection.take do |connection|
- connection.execute Riddle::Query.update(
- @index_name, @document_id, :sphinx_deleted => true
- )
+ return if @document_id.nil?
+
+ ThinkingSphinx::Logger.log :query, statement do
+ ThinkingSphinx::Connection.take do |connection|
+ connection.execute statement
+ end
end
rescue ThinkingSphinx::ConnectionError => error
# This isn't vital, so don't raise the error.
end
+
+ private
+
+ def statement
+ @statement ||= Riddle::Query.update(
+ @index_name, @document_id, :sphinx_deleted => true
+ )
+ end
end
diff --git a/lib/thinking_sphinx/distributed/index.rb b/lib/thinking_sphinx/distributed/index.rb
index 7720923c1..cf62bc711 100644
--- a/lib/thinking_sphinx/distributed/index.rb
+++ b/lib/thinking_sphinx/distributed/index.rb
@@ -21,4 +21,14 @@ def distributed?
def model
@model ||= reference.to_s.camelize.constantize
end
+
+ def primary_key
+ @primary_key ||= configuration.settings['primary_key'] || :id
+ end
+
+ private
+
+ def configuration
+ ThinkingSphinx::Configuration.instance
+ end
end
diff --git a/lib/thinking_sphinx/errors.rb b/lib/thinking_sphinx/errors.rb
index 7d7dcf915..d82468db4 100644
--- a/lib/thinking_sphinx/errors.rb
+++ b/lib/thinking_sphinx/errors.rb
@@ -3,7 +3,7 @@ class ThinkingSphinx::SphinxError < StandardError
def self.new_from_mysql(error)
case error.message
- when /parse error/
+ when /parse error/, /query is non-computable/
replacement = ThinkingSphinx::ParseError.new(error.message)
when /syntax error/
replacement = ThinkingSphinx::SyntaxError.new(error.message)
diff --git a/lib/thinking_sphinx/index_set.rb b/lib/thinking_sphinx/index_set.rb
index 5d3ad779f..b43a71c4f 100644
--- a/lib/thinking_sphinx/index_set.rb
+++ b/lib/thinking_sphinx/index_set.rb
@@ -1,6 +1,6 @@
class ThinkingSphinx::IndexSet
include Enumerable
-
+
def self.reference_name(klass)
@cached_results ||= {}
@cached_results[klass.name] ||= klass.name.underscore.to_sym
@@ -40,7 +40,7 @@ def classes_specified?
end
def classes_and_ancestors
- @classes_and_ancestors ||= classes.collect { |model|
+ @classes_and_ancestors ||= mti_classes + sti_classes.collect { |model|
model.ancestors.take_while { |klass|
klass != ActiveRecord::Base
}.select { |klass|
@@ -66,6 +66,12 @@ def indices_for_references
all_indices.select { |index| references.include? index.reference }
end
+ def mti_classes
+ classes.reject { |klass|
+ klass.column_names.include?(klass.inheritance_column)
+ }
+ end
+
def references
options[:references] || classes_and_ancestors.collect { |klass|
ThinkingSphinx::IndexSet.reference_name(klass)
@@ -75,4 +81,10 @@ def references
def references_specified?
options[:references] && options[:references].any?
end
+
+ def sti_classes
+ classes.select { |klass|
+ klass.column_names.include?(klass.inheritance_column)
+ }
+ end
end
diff --git a/lib/thinking_sphinx/interfaces/daemon.rb b/lib/thinking_sphinx/interfaces/daemon.rb
new file mode 100644
index 000000000..e95e0bb62
--- /dev/null
+++ b/lib/thinking_sphinx/interfaces/daemon.rb
@@ -0,0 +1,32 @@
+class ThinkingSphinx::Interfaces::Daemon
+ include ThinkingSphinx::WithOutput
+
+ def start
+ if running?
+ raise ThinkingSphinx::SphinxAlreadyRunning, 'searchd is already running'
+ end
+
+ if options[:nodetach]
+ ThinkingSphinx::Commands::StartAttached.call configuration, options
+ else
+ ThinkingSphinx::Commands::StartDetached.call configuration, options
+ end
+ end
+
+ def status
+ if running?
+ stream.puts "The Sphinx daemon searchd is currently running."
+ else
+ stream.puts "The Sphinx daemon searchd is not currently running."
+ end
+ end
+
+ def stop
+ ThinkingSphinx::Commands::Stop.call configuration, options
+ end
+
+ private
+
+ delegate :controller, :to => :configuration
+ delegate :running?, :to => :controller
+end
diff --git a/lib/thinking_sphinx/interfaces/real_time.rb b/lib/thinking_sphinx/interfaces/real_time.rb
new file mode 100644
index 000000000..504b9c35a
--- /dev/null
+++ b/lib/thinking_sphinx/interfaces/real_time.rb
@@ -0,0 +1,41 @@
+class ThinkingSphinx::Interfaces::RealTime
+ include ThinkingSphinx::WithOutput
+
+ def initialize(configuration, options, stream = STDOUT)
+ super
+
+ configuration.preload_indices
+
+ FileUtils.mkdir_p configuration.indices_location
+ end
+
+ def clear
+ indices.each do |index|
+ index.render
+ Dir["#{index.path}.*"].each { |path| FileUtils.rm path }
+ end
+
+ path = configuration.searchd.binlog_path
+ FileUtils.rm_r(path) if File.exists?(path)
+ end
+
+ def index
+ return if indices.empty? || !configuration.controller.running?
+
+ indices.each { |index| ThinkingSphinx::RealTime::Populator.populate index }
+ end
+
+ private
+
+ def indices
+ @indices ||= begin
+ indices = configuration.indices.select { |index| index.type == 'rt' }
+
+ if options[:index_filter]
+ indices.select! { |index| index.name == options[:index_filter] }
+ end
+
+ indices
+ end
+ end
+end
diff --git a/lib/thinking_sphinx/interfaces/sql.rb b/lib/thinking_sphinx/interfaces/sql.rb
new file mode 100644
index 000000000..97bfea0da
--- /dev/null
+++ b/lib/thinking_sphinx/interfaces/sql.rb
@@ -0,0 +1,41 @@
+class ThinkingSphinx::Interfaces::SQL
+ include ThinkingSphinx::WithOutput
+
+ def initialize(configuration, options, stream = STDOUT)
+ super
+
+ configuration.preload_indices
+
+ FileUtils.mkdir_p configuration.indices_location
+ end
+
+ def clear
+ indices.each do |index|
+ index.render
+ Dir["#{index.path}.*"].each { |path| FileUtils.rm path }
+ end
+ end
+
+ def index(reconfigure = true, verbose = nil)
+ stream.puts <<-TXT unless verbose.nil?
+The verbose argument to the index method is now deprecated, and can instead be
+managed by the :verbose option passed in when initialising RakeInterface. That
+option is set automatically when invoked by rake, via rake's --silent and/or
+--quiet arguments.
+ TXT
+ return if indices.empty?
+
+ ThinkingSphinx::Commands::Configure.call configuration, options if reconfigure
+ ThinkingSphinx.before_index_hooks.each { |hook| hook.call }
+
+ ThinkingSphinx::Commands::Index.call configuration, options, stream
+ end
+
+ private
+
+ def indices
+ @indices ||= configuration.indices.select do |index|
+ index.type == 'plain' || index.type.blank?
+ end
+ end
+end
diff --git a/lib/thinking_sphinx/middlewares.rb b/lib/thinking_sphinx/middlewares.rb
index 5fddee3f4..1e43b2082 100644
--- a/lib/thinking_sphinx/middlewares.rb
+++ b/lib/thinking_sphinx/middlewares.rb
@@ -1,7 +1,9 @@
module ThinkingSphinx::Middlewares; end
-%w[middleware active_record_translator geographer glazier ids_only inquirer
- sphinxql stale_id_checker stale_id_filter utf8].each do |middleware|
+%w[
+ middleware active_record_translator attribute_typer geographer glazier
+ ids_only inquirer sphinxql stale_id_checker stale_id_filter utf8 valid_options
+].each do |middleware|
require "thinking_sphinx/middlewares/#{middleware}"
end
@@ -10,7 +12,7 @@ def self.use(builder, middlewares)
middlewares.each { |m| builder.use m }
end
- BASE_MIDDLEWARES = [SphinxQL, Geographer, Inquirer]
+ BASE_MIDDLEWARES = [ValidOptions, AttributeTyper, SphinxQL, Geographer, Inquirer]
DEFAULT = ::Middleware::Builder.new do
use StaleIdFilter
diff --git a/lib/thinking_sphinx/middlewares/active_record_translator.rb b/lib/thinking_sphinx/middlewares/active_record_translator.rb
index 2d4eea628..015bf74a6 100644
--- a/lib/thinking_sphinx/middlewares/active_record_translator.rb
+++ b/lib/thinking_sphinx/middlewares/active_record_translator.rb
@@ -2,6 +2,7 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
ThinkingSphinx::Middlewares::Middleware
NO_MODEL = Struct.new(:primary_key).new(:id).freeze
+ NO_INDEX = Struct.new(:primary_key).new(:id).freeze
def call(contexts)
contexts.each do |context|
@@ -38,20 +39,23 @@ def ids_for_model(model_name)
}.compact
end
+ def index_for(model)
+ return NO_INDEX unless context[:indices]
+
+ context[:indices].detect { |index| index.model == model } || NO_INDEX
+ end
+
def model_names
@model_names ||= context[:results].collect { |row|
row['sphinx_internal_class']
}.uniq
end
- def primary_key
- @primary_key ||= primary_key_for NO_MODEL
- end
-
def primary_key_for(model)
model = NO_MODEL unless model.respond_to?(:primary_key)
- context.configuration.settings['primary_key'] || model.primary_key || :id
+ @primary_keys ||= {}
+ @primary_keys[model] ||= index_for(model).primary_key
end
def reset_memos
@@ -61,13 +65,16 @@ def reset_memos
def result_for(row)
results_for_models[row['sphinx_internal_class']].detect { |record|
- record.public_send(primary_key) == row['sphinx_internal_id']
+ record.public_send(
+ primary_key_for(record.class)
+ ) == row['sphinx_internal_id']
}
end
def results_for_models
@results_for_models ||= model_names.inject({}) do |hash, name|
model = name.constantize
+
hash[name] = model_relation_with_sql_options(model.unscoped).where(
primary_key_for(model) => ids_for_model(name)
)
diff --git a/lib/thinking_sphinx/middlewares/attribute_typer.rb b/lib/thinking_sphinx/middlewares/attribute_typer.rb
new file mode 100644
index 000000000..c2124c1f1
--- /dev/null
+++ b/lib/thinking_sphinx/middlewares/attribute_typer.rb
@@ -0,0 +1,48 @@
+class ThinkingSphinx::Middlewares::AttributeTyper <
+ ThinkingSphinx::Middlewares::Middleware
+
+ def call(contexts)
+ contexts.each do |context|
+ deprecate_filters_in context.search.options[:with]
+ deprecate_filters_in context.search.options[:without]
+ deprecate_filters_in context.search.options[:with_all]
+ deprecate_filters_in context.search.options[:without_all]
+ end
+
+ app.call contexts
+ end
+
+ private
+
+ def attributes
+ @attributes ||= ThinkingSphinx::AttributeTypes.call
+ end
+
+ def casted_value_for(type, value)
+ case type
+ when :uint, :bigint, :timestamp, :bool
+ value.to_i
+ when :float
+ value.to_f
+ else
+ value
+ end
+ end
+
+ def deprecate_filters_in(filters)
+ return if filters.nil?
+
+ filters.each do |key, value|
+ known_types = attributes[key.to_s] || [:string]
+
+ next unless value.is_a?(String) && !known_types.include?(:string)
+
+ ActiveSupport::Deprecation.warn(<<-MSG.squish, caller(11))
+You are filtering on a non-string attribute #{key} with a string value (#{value.inspect}).
+ Thinking Sphinx will quote string values by default in upcoming releases (which will cause query syntax errors on non-string attributes), so please cast these values to their appropriate types.
+ MSG
+
+ filters[key] = casted_value_for known_types.first, value
+ end
+ end
+end
diff --git a/lib/thinking_sphinx/middlewares/valid_options.rb b/lib/thinking_sphinx/middlewares/valid_options.rb
new file mode 100644
index 000000000..d2d164a9c
--- /dev/null
+++ b/lib/thinking_sphinx/middlewares/valid_options.rb
@@ -0,0 +1,23 @@
+class ThinkingSphinx::Middlewares::ValidOptions <
+ ThinkingSphinx::Middlewares::Middleware
+
+ def call(contexts)
+ contexts.each { |context| check_options context.search.options }
+
+ app.call contexts
+ end
+
+ private
+
+ def check_options(options)
+ unknown = invalid_keys options.keys
+ return if unknown.empty?
+
+ ThinkingSphinx::Logger.log :warn,
+ "Unexpected search options: #{unknown.inspect}"
+ end
+
+ def invalid_keys(keys)
+ keys - ThinkingSphinx::Search.valid_options
+ end
+end
diff --git a/lib/thinking_sphinx/rake_interface.rb b/lib/thinking_sphinx/rake_interface.rb
index b7c8d3f0c..e34051fb7 100644
--- a/lib/thinking_sphinx/rake_interface.rb
+++ b/lib/thinking_sphinx/rake_interface.rb
@@ -1,146 +1,32 @@
class ThinkingSphinx::RakeInterface
+ DEFAULT_OPTIONS = {:verbose => true}
+
def initialize(options = {})
- @options = options
+ @options = DEFAULT_OPTIONS.merge options
@options[:verbose] = false if @options[:silent]
end
- def clear_all
- [
- configuration.indices_location,
- configuration.searchd.binlog_path
- ].each do |path|
- FileUtils.rm_r(path) if File.exists?(path)
- end
- end
-
- def clear_real_time
- configuration.preload_indices
- indices = configuration.indices.select { |index| index.type == 'rt' }
- indices.each do |index|
- index.render
- Dir["#{index.path}.*"].each { |path| FileUtils.rm path }
- end
-
- path = configuration.searchd.binlog_path
- FileUtils.rm_r(path) if File.exists?(path)
- end
-
def configure
- log "Generating configuration to #{configuration.configuration_file}"
- configuration.render_to_file
- end
-
- def generate
- indices = configuration.indices.select { |index| index.type == 'rt' }
- indices.each do |index|
- ThinkingSphinx::RealTime::Populator.populate index
- end
+ ThinkingSphinx::Commands::Configure.call configuration, options
end
- def index(reconfigure = true, verbose = true)
- configure if reconfigure
- FileUtils.mkdir_p configuration.indices_location
- ThinkingSphinx.before_index_hooks.each { |hook| hook.call }
- controller.index :verbose => verbose
- rescue Riddle::CommandFailedError => error
- handle_command_failure 'indexing', error.command_result
+ def daemon
+ @daemon ||= ThinkingSphinx::Interfaces::Daemon.new configuration, options
end
- def prepare
- configuration.preload_indices
- configuration.render
-
- FileUtils.mkdir_p configuration.indices_location
+ def rt
+ @rt ||= ThinkingSphinx::Interfaces::RealTime.new configuration, options
end
- def start
- if running?
- raise ThinkingSphinx::SphinxAlreadyRunning, 'searchd is already running'
- end
-
- FileUtils.mkdir_p configuration.indices_location
-
- options[:nodetach] ? start_attached : start_detached
- end
-
- def status
- if running?
- puts "The Sphinx daemon searchd is currently running."
- else
- puts "The Sphinx daemon searchd is not currently running."
- end
- end
-
- def stop
- unless running?
- log 'searchd is not currently running.' and return
- end
-
- pid = controller.pid
- until !running? do
- controller.stop options
- sleep(0.5)
- end
-
- log "Stopped searchd daemon (pid: #{pid})."
- rescue Riddle::CommandFailedError => error
- handle_command_failure 'stop', error.command_result
+ def sql
+ @sql ||= ThinkingSphinx::Interfaces::SQL.new configuration, options
end
private
attr_reader :options
- delegate :controller, :to => :configuration
- delegate :running?, :to => :controller
-
- def command_output(output)
- return "See above\n" if output.nil?
-
- "\n\t" + output.gsub("\n", "\n\t")
- end
-
def configuration
ThinkingSphinx::Configuration.instance
end
-
- def handle_command_failure(type, result)
- puts <<-TXT
-
-The Sphinx #{type} command failed:
- Command: #{result.command}
- Status: #{result.status}
- Output: #{command_output result.output}
-There may be more information about the failure in #{configuration.searchd.log}.
- TXT
- exit result.status
- end
-
- def log(message)
- return if options[:silent]
-
- puts message
- end
-
- def start_attached
- unless pid = fork
- controller.start :verbose => options[:verbose]
- end
-
- Signal.trap('TERM') { Process.kill(:TERM, pid); }
- Signal.trap('INT') { Process.kill(:TERM, pid); }
- Process.wait(pid)
- end
-
- def start_detached
- result = controller.start :verbose => options[:verbose]
-
- if running?
- log "Started searchd successfully (pid: #{controller.pid})."
- else
- handle_command_failure 'start', result
- end
- rescue Riddle::CommandFailedError => error
- handle_command_failure 'start', error.command_result
- end
end
diff --git a/lib/thinking_sphinx/search.rb b/lib/thinking_sphinx/search.rb
index 7858595be..33160613b 100644
--- a/lib/thinking_sphinx/search.rb
+++ b/lib/thinking_sphinx/search.rb
@@ -21,6 +21,17 @@ class ThinkingSphinx::Search < Array
attr_reader :options
attr_accessor :query
+ def self.valid_options
+ @valid_options
+ end
+
+ @valid_options = [
+ :classes, :conditions, :geo, :group_by, :ids_only, :ignore_scopes, :indices,
+ :limit, :masks, :max_matches, :middleware, :offset, :order, :order_group_by,
+ :page, :per_page, :populate, :retry_stale, :select, :skip_sti, :sql, :star,
+ :with, :with_all, :without, :without_ids
+ ]
+
def initialize(query = nil, options = {})
query, options = nil, query if query.is_a?(Hash)
@query, @options = query, options
diff --git a/lib/thinking_sphinx/search/query.rb b/lib/thinking_sphinx/search/query.rb
index a385c31a9..0f844a77d 100644
--- a/lib/thinking_sphinx/search/query.rb
+++ b/lib/thinking_sphinx/search/query.rb
@@ -10,12 +10,18 @@ def to_s
(star_keyword(keywords || '') + ' ' + conditions.keys.collect { |key|
next if conditions[key].blank?
- "@#{key} #{star_keyword conditions[key], key}"
+ "#{expand_key key} #{star_keyword conditions[key], key}"
}.join(' ')).strip
end
private
+ def expand_key(key)
+ return "@#{key}" unless key.is_a?(Array)
+
+ "@(#{key.join(',')})"
+ end
+
def star_keyword(keyword, key = nil)
return keyword.to_s unless star
return keyword.to_s if key.to_s == 'sphinx_internal_class_name'
diff --git a/lib/thinking_sphinx/tasks.rb b/lib/thinking_sphinx/tasks.rb
index 8bf43034d..582f7cfd7 100644
--- a/lib/thinking_sphinx/tasks.rb
+++ b/lib/thinking_sphinx/tasks.rb
@@ -5,55 +5,114 @@
end
desc 'Generate the Sphinx configuration file and process all indices'
- task :index => :environment do
- interface.index(ENV['INDEX_ONLY'] != 'true')
- end
+ task :index => ['ts:sql:index', 'ts:rt:index']
desc 'Clear out Sphinx files'
- task :clear => :environment do
- interface.clear_all
- end
+ task :clear => ['ts:sql:clear', 'ts:rt:clear']
- desc 'Clear out real-time index files'
+ desc 'DEPRECATED: Clear out real-time index files'
task :clear_rt => :environment do
- interface.clear_real_time
+ puts <<-TXT
+The ts:clear_rt task is now deprecated due to the unified task approach, and
+invokes ts:rt:clear.
+* To delete all indices (both SQL-backed and real-time), use ts:clear.
+* To delete just real-time indices, use ts:rt:clear.
+* To delete just SQL-backed indices, use ts:sql:clear.
+
+ TXT
+
+ Rake::Task['ts:rt:clear'].invoke
end
- desc 'Generate fresh index files for real-time indices'
+ desc 'DEPRECATED: Generate fresh index files for all indices'
task :generate => :environment do
- interface.prepare
- interface.generate
+ puts <<-TXT
+The ts:generate task is now deprecated due to the unified task approach, and
+invokes ts:index.
+* To process all indices (both SQL-backed and real-time), use ts:index.
+* To process just real-time indices, use ts:rt:index.
+* To process just SQL-backed indices, use ts:sql:index.
+
+ TXT
+
+ Rake::Task['ts:index'].invoke
end
- desc 'Stop Sphinx, index and then restart Sphinx'
- task :rebuild => [:stop, :clear, :index, :start]
+ desc 'Delete and regenerate Sphinx files, restart the daemon'
+ task :rebuild => [
+ :stop, :clear, :configure, 'ts:sql:index', :start, 'ts:rt:index'
+ ]
+
+ desc 'DEPRECATED: Delete and regenerate Sphinx files, restart the daemon'
+ task :regenerate do
+ puts <<-TXT
+The ts:regenerate task is now deprecated due to the unified task approach, and
+invokes ts:rebuild.
+* To rebuild all indices (both SQL-backed and real-time), use ts:rebuild.
+* To rebuild just real-time indices, use ts:rt:rebuild.
+* To rebuild just SQL-backed indices, use ts:sql:rebuild.
- desc 'Stop Sphinx, clear files, reconfigure, start Sphinx, generate files'
- task :regenerate => [:stop, :clear_rt, :configure, :start, :generate]
+ TXT
+
+ Rake::Task['ts:rebuild'].invoke
+ end
desc 'Restart the Sphinx daemon'
task :restart => [:stop, :start]
desc 'Start the Sphinx daemon'
task :start => :environment do
- interface.start
+ interface.daemon.start
end
desc 'Stop the Sphinx daemon'
task :stop => :environment do
- interface.stop
+ interface.daemon.stop
end
desc 'Determine whether Sphinx is running'
task :status => :environment do
- interface.status
+ interface.daemon.status
+ end
+
+ namespace :sql do
+ desc 'Delete SQL-backed Sphinx files'
+ task :clear => :environment do
+ interface.sql.clear
+ end
+
+ desc 'Generate fresh index files for SQL-backed indices'
+ task :index => :environment do
+ interface.sql.index(ENV['INDEX_ONLY'] != 'true')
+ end
+
+ desc 'Delete and regenerate SQL-backed Sphinx files, restart the daemon'
+ task :rebuild => ['ts:stop', 'ts:sql:clear', 'ts:sql:index', 'ts:start']
+ end
+
+ namespace :rt do
+ desc 'Delete real-time Sphinx files'
+ task :clear => :environment do
+ interface.rt.clear
+ end
+
+ desc 'Generate fresh index files for real-time indices'
+ task :index => :environment do
+ interface.rt.index
+ end
+
+ desc 'Delete and regenerate real-time Sphinx files, restart the daemon'
+ task :rebuild => [
+ 'ts:stop', 'ts:rt:clear', 'ts:configure', 'ts:start', 'ts:rt:index'
+ ]
end
def interface
@interface ||= ThinkingSphinx::RakeInterface.new(
- :verbose => Rake::FileUtilsExt.verbose_flag,
- :silent => Rake.application.options.silent,
- :nodetach => (ENV['NODETACH'] == 'true')
+ :verbose => Rake::FileUtilsExt.verbose_flag,
+ :silent => Rake.application.options.silent,
+ :nodetach => (ENV['NODETACH'] == 'true'),
+ :index_filter => ENV['INDEX_FILTER']
)
end
end
diff --git a/lib/thinking_sphinx/with_output.rb b/lib/thinking_sphinx/with_output.rb
new file mode 100644
index 000000000..908b9c586
--- /dev/null
+++ b/lib/thinking_sphinx/with_output.rb
@@ -0,0 +1,11 @@
+module ThinkingSphinx::WithOutput
+ def initialize(configuration, options = {}, stream = STDOUT)
+ @configuration = configuration
+ @options = options
+ @stream = stream
+ end
+
+ private
+
+ attr_reader :configuration, :options, :stream
+end
diff --git a/spec/acceptance/connection_spec.rb b/spec/acceptance/connection_spec.rb
index 06dbf81be..73e232488 100644
--- a/spec/acceptance/connection_spec.rb
+++ b/spec/acceptance/connection_spec.rb
@@ -1,8 +1,8 @@
require 'acceptance/spec_helper'
RSpec.describe 'Connections', :live => true do
- let(:maximum) { (2 ** 23) - 1 }
- let(:query) { "SELECT * FROM book_core WHERE MATCH('')" }
+ let(:maximum) { (2 ** 23) - 5 }
+ let(:query) { String.new "SELECT * FROM book_core WHERE MATCH('')" }
let(:difference) { maximum - query.length }
it 'allows normal length queries through' do
@@ -10,13 +10,13 @@
ThinkingSphinx::Connection.take do |connection|
connection.execute query.insert(-3, 'a' * difference)
end
- }.to_not raise_error#(ThinkingSphinx::QueryLengthError)
+ }.to_not raise_error
end
it 'does not allow overly long queries' do
expect {
ThinkingSphinx::Connection.take do |connection|
- connection.execute query.insert(-3, 'a' * (difference + 1))
+ connection.execute query.insert(-3, 'a' * (difference + 5))
end
}.to raise_error(ThinkingSphinx::QueryLengthError)
end
diff --git a/spec/acceptance/searching_within_a_model_spec.rb b/spec/acceptance/searching_within_a_model_spec.rb
index 91138bc05..9bed1982a 100644
--- a/spec/acceptance/searching_within_a_model_spec.rb
+++ b/spec/acceptance/searching_within_a_model_spec.rb
@@ -79,6 +79,13 @@
Category.search.to_a
}.to raise_error(ThinkingSphinx::NoIndicesError)
end
+
+ it "handles models with alternative id columns" do
+ album = Album.create! :name => 'The Seldom Seen Kid', :artist => 'Elbow'
+ index
+
+ expect(Album.search.first).to eq(album)
+ end
end
describe 'Searching within a model with a realtime index', :live => true do
diff --git a/spec/acceptance/specifying_sql_spec.rb b/spec/acceptance/specifying_sql_spec.rb
index 3fde30034..be56fd38d 100644
--- a/spec/acceptance/specifying_sql_spec.rb
+++ b/spec/acceptance/specifying_sql_spec.rb
@@ -155,9 +155,27 @@
expect(query).to match(/LEFT OUTER JOIN .users. ON .users.\..id. = .articles.\..user_id./)
expect(query).to match(/.users.\..name./)
end
+
+ it "allows for STI mixed with polymorphic joins" do
+ index = ThinkingSphinx::ActiveRecord::Index.new(:event)
+ index.definition_block = Proc.new {
+ indexes eventable.name, :as => :name
+ polymorphs eventable, :to => %w(Bird Car)
+ }
+ index.render
+
+ query = index.sources.first.sql_query
+ expect(query).to match(/LEFT OUTER JOIN .animals. ON .animals.\..id. = .events.\..eventable_id. .* AND .events.\..eventable_type. = 'Animal'/)
+ expect(query).to match(/LEFT OUTER JOIN .cars. ON .cars.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Car'/)
+ expect(query).to match(/.animals.\..name., .cars.\..name./)
+ end
end if ActiveRecord::VERSION::MAJOR > 3
describe 'separate queries for MVAs' do
+ def id_type
+ ActiveRecord::VERSION::STRING.to_f > 5.0 ? 'bigint' : 'uint'
+ end
+
let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) }
let(:count) { ThinkingSphinx::Configuration.instance.indices.count }
let(:source) { index.sources.first }
@@ -174,7 +192,7 @@
}
declaration, query = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from query')
+ expect(declaration).to eq("uint tag_ids from query")
expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)$/)
end
@@ -190,7 +208,7 @@
}
declaration, query = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from query')
+ expect(declaration).to eq("#{id_type} tag_ids from query")
expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/)
end
@@ -206,7 +224,7 @@
}
declaration, query = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from query')
+ expect(declaration).to eq("#{id_type} tag_ids from query")
expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/)
end
@@ -224,7 +242,7 @@
}
declaration, query = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from query')
+ expect(declaration).to eq("#{id_type} tag_ids from query")
expect(query).to match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.articles.\..user_id. IS NOT NULL\)\s?$/)
end
@@ -242,7 +260,7 @@
}
declaration, query = attribute.split(/;\s+/)
- expect(declaration).to eq('uint genre_ids from query')
+ expect(declaration).to eq("#{id_type} genre_ids from query")
expect(query).to match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres.\s?$/)
end if ActiveRecord::VERSION::MAJOR > 3
@@ -258,7 +276,7 @@
}
declaration, query, range = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from ranged-query')
+ expect(declaration).to eq("uint tag_ids from ranged-query")
expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/)
expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
end
@@ -275,7 +293,7 @@
}
declaration, query, range = attribute.split(/;\s+/)
- expect(declaration).to eq('uint tag_ids from ranged-query')
+ expect(declaration).to eq("#{id_type} tag_ids from ranged-query")
expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/)
expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/)
end
@@ -294,7 +312,7 @@
}
declaration, query, range = attribute.split(/;\s+/)
- expect(declaration).to eq('uint genre_ids from ranged-query')
+ expect(declaration).to eq("#{id_type} genre_ids from ranged-query")
expect(query).to match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres. WHERE \(.books_genres.\..book_id. BETWEEN \$start AND \$end\)$/)
expect(range).to match(/^SELECT MIN\(.books_genres.\..book_id.\), MAX\(.books_genres.\..book_id.\) FROM .books_genres.$/)
end if ActiveRecord::VERSION::MAJOR > 3
diff --git a/spec/acceptance/sql_deltas_spec.rb b/spec/acceptance/sql_deltas_spec.rb
index 79edd939f..dc06030a3 100644
--- a/spec/acceptance/sql_deltas_spec.rb
+++ b/spec/acceptance/sql_deltas_spec.rb
@@ -41,6 +41,18 @@
expect(Book.search('Harry')).to be_empty
end
+ it "does not match on old values with alternative ids" do
+ album = Album.create :name => 'Eternal Nightcap', :artist => 'The Whitloms'
+ index
+
+ expect(Album.search('Whitloms').to_a).to eq([album])
+
+ album.reload.update_attributes(:artist => 'The Whitlams')
+ sleep 0.25
+
+ expect(Book.search('Whitloms')).to be_empty
+ end
+
it "automatically indexes new records of subclasses" do
book = Hardcover.create(
:title => 'American Gods', :author => 'Neil Gaiman'
diff --git a/spec/internal/app/indices/album_index.rb b/spec/internal/app/indices/album_index.rb
new file mode 100644
index 000000000..ad0780134
--- /dev/null
+++ b/spec/internal/app/indices/album_index.rb
@@ -0,0 +1,3 @@
+ThinkingSphinx::Index.define :album, :with => :active_record, :primary_key => :integer_id, :delta => true do
+ indexes name, artist
+end
diff --git a/spec/internal/app/models/album.rb b/spec/internal/app/models/album.rb
new file mode 100644
index 000000000..ea2f474e5
--- /dev/null
+++ b/spec/internal/app/models/album.rb
@@ -0,0 +1,19 @@
+class Album < ActiveRecord::Base
+ self.primary_key = :id
+
+ before_validation :set_id, :on => :create
+ before_validation :set_integer_id, :on => :create
+
+ validates :id, :presence => true, :uniqueness => true
+ validates :integer_id, :presence => true, :uniqueness => true
+
+ private
+
+ def set_id
+ self.id = (Album.maximum(:id) || "a").next
+ end
+
+ def set_integer_id
+ self.integer_id = (Album.maximum(:integer_id) || 0) + 1
+ end
+end
diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb
index 47c32393e..dfea2f906 100644
--- a/spec/internal/db/schema.rb
+++ b/spec/internal/db/schema.rb
@@ -4,6 +4,14 @@
t.timestamps null: false
end
+ create_table(:albums, :force => true, :id => false) do |t|
+ t.string :id
+ t.integer :integer_id
+ t.string :name
+ t.string :artist
+ t.boolean :delta, :default => true, :null => false
+ end
+
create_table(:animals, :force => true) do |t|
t.string :name
t.string :type
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b245a5443..ae79797d3 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -18,4 +18,8 @@
# enable filtering for examples
config.filter_run :wip => nil
config.run_all_when_everything_filtered = true
+
+ config.around :each, :live do |example|
+ example.run_with_retry :retry => 3
+ end
end
diff --git a/spec/support/json_column.rb b/spec/support/json_column.rb
index bbe936038..8b84f69e7 100644
--- a/spec/support/json_column.rb
+++ b/spec/support/json_column.rb
@@ -6,7 +6,7 @@ def self.call
end
def call
- sphinx? && postgresql? && column?
+ ruby? && sphinx? && postgresql? && column?
end
private
@@ -27,6 +27,10 @@ def postgresql?
ENV['DATABASE'] == 'postgresql'
end
+ def ruby?
+ RUBY_PLATFORM != 'java'
+ end
+
def sphinx?
ENV['SPHINX_VERSION'].nil? || ENV['SPHINX_VERSION'].to_f > 2.0
end
diff --git a/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb b/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb
index 5f7cef1c5..a56c2dd2a 100644
--- a/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb
+++ b/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb
@@ -40,7 +40,11 @@ module Callbacks; end
double(:name => 'baz', :updateable? => false)
])
- allow(instance).to receive_messages :changed => ['bar_column', 'baz'], :bar_column => 7
+ allow(instance).to receive_messages(
+ :changed => ['bar_column', 'baz'],
+ :bar_column => 7,
+ :saved_changes => {'bar_column' => [1, 2], 'baz' => [3, 4]}
+ )
end
it "does not send any updates to Sphinx if updates are disabled" do
diff --git a/spec/thinking_sphinx/active_record/sql_source_spec.rb b/spec/thinking_sphinx/active_record/sql_source_spec.rb
index 957b921ba..bd41d447d 100644
--- a/spec/thinking_sphinx/active_record/sql_source_spec.rb
+++ b/spec/thinking_sphinx/active_record/sql_source_spec.rb
@@ -159,6 +159,12 @@
expect(source.options[:utf8?]).to be_truthy
end
+ it "sets utf8? to true if the database encoding starts with utf8" do
+ db_config[:encoding] = 'utf8mb4'
+
+ expect(source.options[:utf8?]).to be_truthy
+ end
+
describe "#primary key" do
let(:model) { double('model', :connection => connection,
:name => 'User', :column_names => [], :inheritance_column => 'type') }
diff --git a/spec/thinking_sphinx/attribute_types_spec.rb b/spec/thinking_sphinx/attribute_types_spec.rb
new file mode 100644
index 000000000..edf957d9d
--- /dev/null
+++ b/spec/thinking_sphinx/attribute_types_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::AttributeTypes do
+ let(:configuration) {
+ double('configuration', :configuration_file => 'sphinx.conf')
+ }
+
+ before :each do
+ allow(ThinkingSphinx::Configuration).to receive(:instance).
+ and_return(configuration)
+
+ allow(File).to receive(:exist?).with('sphinx.conf').and_return(true)
+ allow(File).to receive(:read).with('sphinx.conf').and_return(<<-CONF)
+index plain_index
+{
+ source = plain_source
+}
+
+source plain_source
+{
+ type = mysql
+ sql_attr_uint = customer_id
+ sql_attr_float = price
+ sql_attr_multi = uint comment_ids from field
+}
+
+index rt_index
+{
+ type = rt
+ rt_attr_uint = user_id
+ rt_attr_multi = comment_ids
+}
+ CONF
+ end
+
+ it 'returns an empty hash if no configuration file exists' do
+ allow(File).to receive(:exist?).with('sphinx.conf').and_return(false)
+
+ expect(ThinkingSphinx::AttributeTypes.new.call).to eq({})
+ end
+
+ it 'returns all known attributes' do
+ expect(ThinkingSphinx::AttributeTypes.new.call).to eq({
+ 'customer_id' => [:uint],
+ 'price' => [:float],
+ 'comment_ids' => [:uint],
+ 'user_id' => [:uint]
+ })
+ end
+end
diff --git a/spec/thinking_sphinx/commands/configure_spec.rb b/spec/thinking_sphinx/commands/configure_spec.rb
new file mode 100644
index 000000000..61f5d5918
--- /dev/null
+++ b/spec/thinking_sphinx/commands/configure_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Commands::Configure do
+ let(:command) { ThinkingSphinx::Commands::Configure.new(
+ configuration, {}, stream
+ ) }
+ let(:configuration) { double 'configuration' }
+ let(:stream) { double :puts => nil }
+
+ before :each do
+ allow(configuration).to receive_messages(
+ :configuration_file => '/path/to/foo.conf',
+ :render_to_file => true
+ )
+ end
+
+ it "renders the configuration to a file" do
+ expect(configuration).to receive(:render_to_file)
+
+ command.call
+ end
+
+ it "prints a message stating the file is being generated" do
+ expect(stream).to receive(:puts).
+ with('Generating configuration to /path/to/foo.conf')
+
+ command.call
+ end
+end
diff --git a/spec/thinking_sphinx/commands/index_spec.rb b/spec/thinking_sphinx/commands/index_spec.rb
new file mode 100644
index 000000000..011b2276d
--- /dev/null
+++ b/spec/thinking_sphinx/commands/index_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Commands::Index do
+ let(:command) { ThinkingSphinx::Commands::Index.new(
+ configuration, {:verbose => true}, stream
+ ) }
+ let(:configuration) { double 'configuration', :controller => controller }
+ let(:controller) { double 'controller', :index => true }
+ let(:stream) { double :puts => nil }
+
+ it "indexes all indices verbosely" do
+ expect(controller).to receive(:index).with(:verbose => true)
+
+ command.call
+ end
+
+ it "does not index verbosely if requested" do
+ command = ThinkingSphinx::Commands::Index.new(
+ configuration, {:verbose => false}, stream
+ )
+
+ expect(controller).to receive(:index).with(:verbose => false)
+
+ command.call
+ end
+end
diff --git a/spec/thinking_sphinx/commands/start_detached_spec.rb b/spec/thinking_sphinx/commands/start_detached_spec.rb
new file mode 100644
index 000000000..104cf3a00
--- /dev/null
+++ b/spec/thinking_sphinx/commands/start_detached_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Commands::StartDetached do
+ let(:command) {
+ ThinkingSphinx::Commands::StartDetached.new(configuration, {}, stream)
+ }
+ let(:configuration) { double 'configuration', :controller => controller }
+ let(:controller) { double 'controller', :start => result, :pid => 101 }
+ let(:result) { double 'result', :command => 'start', :status => 1,
+ :output => '' }
+ let(:stream) { double :puts => nil }
+
+ before :each do
+ allow(controller).to receive(:running?).and_return(true)
+ allow(configuration).to receive_messages(
+ :indices_location => 'my/index/files',
+ :searchd => double(:log => '/path/to/log')
+ )
+ allow(command).to receive(:exit).and_return(true)
+
+ allow(FileUtils).to receive_messages :mkdir_p => true
+ end
+
+ it "creates the index files directory" do
+ expect(FileUtils).to receive(:mkdir_p).with('my/index/files')
+
+ command.call
+ end
+
+ it "starts the daemon" do
+ expect(controller).to receive(:start)
+
+ command.call
+ end
+
+ it "prints a success message if the daemon has started" do
+ allow(controller).to receive(:running?).and_return(true)
+
+ expect(stream).to receive(:puts).
+ with('Started searchd successfully (pid: 101).')
+
+ command.call
+ end
+
+ it "prints a failure message if the daemon does not start" do
+ allow(controller).to receive(:running?).and_return(false)
+ allow(command).to receive(:exit)
+
+ expect(stream).to receive(:puts) do |string|
+ expect(string).to match('The Sphinx start command failed')
+ end
+
+ command.call
+ end
+end
diff --git a/spec/thinking_sphinx/commands/stop_spec.rb b/spec/thinking_sphinx/commands/stop_spec.rb
new file mode 100644
index 000000000..345dd216e
--- /dev/null
+++ b/spec/thinking_sphinx/commands/stop_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Commands::Stop do
+ let(:command) {
+ ThinkingSphinx::Commands::Stop.new(configuration, {}, stream)
+ }
+ let(:configuration) { double 'configuration', :controller => controller }
+ let(:controller) { double 'controller', :stop => true, :pid => 101 }
+ let(:stream) { double :puts => nil }
+
+ before :each do
+ allow(controller).to receive(:running?).and_return(true, true, false)
+ end
+
+ it "prints a message if the daemon is not already running" do
+ allow(controller).to receive_messages :running? => false
+
+ expect(stream).to receive(:puts).with('searchd is not currently running.').
+ and_return(nil)
+ expect(stream).to_not receive(:puts).
+ with('"Stopped searchd daemon (pid: ).')
+
+ command.call
+ end
+
+ it "does not try to stop the daemon if it's not running" do
+ allow(controller).to receive_messages :running? => false
+
+ expect(controller).to_not receive(:stop)
+
+ command.call
+ end
+
+ it "stops the daemon" do
+ expect(controller).to receive(:stop)
+
+ command.call
+ end
+
+ it "prints a message informing the daemon has stopped" do
+ expect(stream).to receive(:puts).with('Stopped searchd daemon (pid: 101).')
+
+ command.call
+ end
+
+ it "should retry stopping the daemon until it stops" do
+ allow(controller).to receive(:running?).
+ and_return(true, true, true, false)
+
+ expect(controller).to receive(:stop).twice
+
+ command.call
+ end
+end
diff --git a/spec/thinking_sphinx/configuration/minimum_fields_spec.rb b/spec/thinking_sphinx/configuration/minimum_fields_spec.rb
new file mode 100644
index 000000000..27b61a84c
--- /dev/null
+++ b/spec/thinking_sphinx/configuration/minimum_fields_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Configuration::MinimumFields do
+ let(:indices) { [index_a, index_b] }
+ let(:index_a) { double 'Index A', :model => model_a, :type => 'plain',
+ :sources => [double(:fields => [field_a1, field_a2])] }
+ let(:index_b) { double 'Index B', :model => model_a, :type => 'rt',
+ :fields => [field_b1, field_b2] }
+ let(:field_a1) { double :name => 'sphinx_internal_class_name' }
+ let(:field_a2) { double :name => 'name' }
+ let(:field_b1) { double :name => 'sphinx_internal_class_name' }
+ let(:field_b2) { double :name => 'name' }
+ let(:model_a) { double :inheritance_column => 'type' }
+ let(:model_b) { double :inheritance_column => 'type' }
+ let(:subject) { ThinkingSphinx::Configuration::MinimumFields.new indices }
+
+ it 'removes the class name fields when no index models have type columns' do
+ allow(model_a).to receive(:column_names).and_return(['id', 'name'])
+ allow(model_b).to receive(:column_names).and_return(['id', 'name'])
+
+ subject.reconcile
+
+ expect(index_a.sources.first.fields).to eq([field_a2])
+ expect(index_b.fields).to eq([field_b2])
+ end
+
+ it 'keeps the class name fields when one index model has a type column' do
+ allow(model_a).to receive(:column_names).and_return(['id', 'name', 'type'])
+ allow(model_b).to receive(:column_names).and_return(['id', 'name'])
+
+ subject.reconcile
+
+ expect(index_a.sources.first.fields).to eq([field_a1, field_a2])
+ expect(index_b.fields).to eq([field_b1, field_b2])
+ end
+end
diff --git a/spec/thinking_sphinx/deletion_spec.rb b/spec/thinking_sphinx/deletion_spec.rb
index b089ebc0e..52a762a66 100644
--- a/spec/thinking_sphinx/deletion_spec.rb
+++ b/spec/thinking_sphinx/deletion_spec.rb
@@ -13,11 +13,8 @@
context 'index is SQL-backed' do
it "updates the deleted flag to false" do
- expect(connection).to receive(:execute).with <<-SQL
-UPDATE foo_core
-SET sphinx_deleted = 1
-WHERE id IN (14)
- SQL
+ expect(connection).to receive(:execute).
+ with('UPDATE foo_core SET sphinx_deleted = 1 WHERE id IN (14)')
ThinkingSphinx::Deletion.perform index, 7
end
diff --git a/spec/thinking_sphinx/deltas/default_delta_spec.rb b/spec/thinking_sphinx/deltas/default_delta_spec.rb
index f97ec225f..edfcb809b 100644
--- a/spec/thinking_sphinx/deltas/default_delta_spec.rb
+++ b/spec/thinking_sphinx/deltas/default_delta_spec.rb
@@ -21,7 +21,7 @@
describe '#delete' do
let(:connection) { double('connection', :execute => nil) }
let(:index) { double('index', :name => 'foo_core',
- :document_id_for_key => 14) }
+ :document_id_for_instance => 14) }
let(:instance) { double('instance', :id => 7) }
before :each do
diff --git a/spec/thinking_sphinx/errors_spec.rb b/spec/thinking_sphinx/errors_spec.rb
index a17d8b363..1183e814e 100644
--- a/spec/thinking_sphinx/errors_spec.rb
+++ b/spec/thinking_sphinx/errors_spec.rb
@@ -19,6 +19,13 @@
to be_a(ThinkingSphinx::ParseError)
end
+ it "translates 'query is non-computable' errors" do
+ allow(error).to receive_messages :message => 'index model_core: query is non-computable (single NOT operator)'
+
+ expect(ThinkingSphinx::SphinxError.new_from_mysql(error)).
+ to be_a(ThinkingSphinx::ParseError)
+ end
+
it "translates query errors" do
allow(error).to receive_messages :message => 'index foo: query error: something is wrong'
diff --git a/spec/thinking_sphinx/index_set_spec.rb b/spec/thinking_sphinx/index_set_spec.rb
index cc9b8ba08..291c2bc54 100644
--- a/spec/thinking_sphinx/index_set_spec.rb
+++ b/spec/thinking_sphinx/index_set_spec.rb
@@ -15,9 +15,15 @@ module ThinkingSphinx; end
stub_const 'ActiveRecord::Base', ar_base
end
- def class_double(name, *superclasses)
+ def class_double(name, methods = {}, *superclasses)
klass = double 'class', :name => name, :class => Class
- allow(klass).to receive_messages :ancestors => ([klass] + superclasses + [ar_base])
+
+ allow(klass).to receive_messages(
+ :ancestors => ([klass] + superclasses + [ar_base]),
+ :inheritance_column => :type
+ )
+ allow(klass).to receive_messages(methods)
+
klass
end
@@ -47,25 +53,42 @@ def class_double(name, *superclasses)
double(:reference => :page, :distributed? => false)
]
- options[:classes] = [class_double('Article')]
+ options[:classes] = [class_double('Article', :column_names => [])]
expect(set.to_a.length).to eq(1)
end
- it "requests indices for any superclasses" do
+ it "requests indices for any STI superclasses" do
configuration.indices.replace [
double(:reference => :article, :distributed? => false),
double(:reference => :opinion_article, :distributed? => false),
double(:reference => :page, :distributed? => false)
]
- options[:classes] = [
- class_double('OpinionArticle', class_double('Article'))
- ]
+ article = class_double('Article', :column_names => [:type])
+ opinion = class_double('OpinionArticle', {:column_names => [:type]},
+ article)
+
+ options[:classes] = [opinion]
expect(set.to_a.length).to eq(2)
end
+ it "does not use MTI superclasses" do
+ configuration.indices.replace [
+ double(:reference => :article, :distributed? => false),
+ double(:reference => :opinion_article, :distributed? => false),
+ double(:reference => :page, :distributed? => false)
+ ]
+
+ article = class_double('Article', :column_names => [])
+ opinion = class_double('OpinionArticle', {:column_names => []}, article)
+
+ options[:classes] = [opinion]
+
+ expect(set.to_a.length).to eq(1)
+ end
+
it "uses named indices if names are provided" do
article_core = double('index', :name => 'article_core')
user_core = double('index', :name => 'user_core')
diff --git a/spec/thinking_sphinx/interfaces/daemon_spec.rb b/spec/thinking_sphinx/interfaces/daemon_spec.rb
new file mode 100644
index 000000000..23bb0c727
--- /dev/null
+++ b/spec/thinking_sphinx/interfaces/daemon_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Interfaces::Daemon do
+ let(:configuration) { double 'configuration', :controller => controller }
+ let(:controller) { double 'controller', :running? => false }
+ let(:stream) { double 'stream', :puts => true }
+ let(:interface) {
+ ThinkingSphinx::Interfaces::Daemon.new(configuration, {}, stream)
+ }
+
+ describe '#start' do
+ let(:command) { double 'command', :call => true }
+
+ before :each do
+ stub_const 'ThinkingSphinx::Commands::StartDetached', command
+ end
+
+ it "starts the daemon" do
+ expect(command).to receive(:call)
+
+ interface.start
+ end
+
+ it "raises an error if the daemon is already running" do
+ allow(controller).to receive_messages :running? => true
+
+ expect {
+ interface.start
+ }.to raise_error(ThinkingSphinx::SphinxAlreadyRunning)
+ end
+ end
+
+ describe '#status' do
+ it "reports when the daemon is running" do
+ allow(controller).to receive_messages :running? => true
+
+ expect(stream).to receive(:puts).
+ with('The Sphinx daemon searchd is currently running.')
+
+ interface.status
+ end
+
+ it "reports when the daemon is not running" do
+ allow(controller).to receive_messages :running? => false
+
+ expect(stream).to receive(:puts).
+ with('The Sphinx daemon searchd is not currently running.')
+
+ interface.status
+ end
+ end
+end
diff --git a/spec/thinking_sphinx/interfaces/real_time_spec.rb b/spec/thinking_sphinx/interfaces/real_time_spec.rb
new file mode 100644
index 000000000..d643f741f
--- /dev/null
+++ b/spec/thinking_sphinx/interfaces/real_time_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Interfaces::SQL do
+ let(:interface) { ThinkingSphinx::Interfaces::RealTime.new(
+ configuration, {}, stream
+ ) }
+ let(:configuration) { double 'configuration', :controller => controller,
+ :render => true, :indices_location => '/path/to/indices',
+ :preload_indices => true }
+ let(:controller) { double 'controller', :running? => true }
+ let(:stream) { double :puts => nil }
+
+ describe '#clear' do
+ let(:plain_index) { double(:type => 'plain') }
+ let(:users_index) { double(:name => 'users', :type => 'rt', :render => true,
+ :path => '/path/to/my/index/users') }
+ let(:parts_index) { double(:name => 'parts', :type => 'rt', :render => true,
+ :path => '/path/to/my/index/parts') }
+
+ before :each do
+ allow(configuration).to receive_messages(
+ :indices => [plain_index, users_index, parts_index],
+ :searchd => double(:binlog_path => '/path/to/binlog')
+ )
+
+ allow(Dir).to receive(:[]).with('/path/to/my/index/users.*').
+ and_return(['users.a', 'users.b'])
+ allow(Dir).to receive(:[]).with('/path/to/my/index/parts.*').
+ and_return(['parts.a', 'parts.b'])
+
+ allow(FileUtils).to receive_messages :mkdir_p => true, :rm_r => true,
+ :rm => true
+ allow(File).to receive_messages :exists? => true
+ end
+
+ it 'finds each file for real-time indices' do
+ expect(Dir).to receive(:[]).with('/path/to/my/index/users.*').
+ and_return([])
+
+ interface.clear
+ end
+
+ it "removes the directory for the binlog files" do
+ expect(FileUtils).to receive(:rm_r).with('/path/to/binlog')
+
+ interface.clear
+ end
+
+ it "removes each file for real-time indices" do
+ expect(FileUtils).to receive(:rm).with('users.a')
+ expect(FileUtils).to receive(:rm).with('users.b')
+ expect(FileUtils).to receive(:rm).with('parts.a')
+ expect(FileUtils).to receive(:rm).with('parts.b')
+
+ interface.clear
+ end
+
+ context "with options[:index_filter]" do
+ let(:interface) { ThinkingSphinx::Interfaces::RealTime.new(
+ configuration, {:index_filter => 'users'}, stream
+ ) }
+
+ it "removes each file for real-time indices that match :index_filter" do
+ expect(FileUtils).to receive(:rm).with('users.a')
+ expect(FileUtils).to receive(:rm).with('users.b')
+ expect(FileUtils).not_to receive(:rm).with('parts.a')
+ expect(FileUtils).not_to receive(:rm).with('parts.b')
+
+ interface.clear
+ end
+ end
+ end
+
+ describe '#index' do
+ let(:plain_index) { double(:type => 'plain') }
+ let(:users_index) { double(name: 'users', :type => 'rt') }
+ let(:parts_index) { double(name: 'parts', :type => 'rt') }
+
+ before :each do
+ allow(configuration).to receive_messages(
+ :indices => [plain_index, users_index, parts_index]
+ )
+
+ allow(FileUtils).to receive_messages :mkdir_p => true
+ end
+
+ it 'populates each real-index' do
+ expect(ThinkingSphinx::RealTime::Populator).to receive(:populate).with(users_index)
+ expect(ThinkingSphinx::RealTime::Populator).to receive(:populate).with(parts_index)
+ expect(ThinkingSphinx::RealTime::Populator).not_to receive(:populate).with(plain_index)
+
+ interface.index
+ end
+
+ context "with options[:index_filter]" do
+ let(:interface) { ThinkingSphinx::Interfaces::RealTime.new(
+ configuration, {:index_filter => 'users'}, stream
+ ) }
+
+ it 'populates each real-index that matches :index_filter' do
+ expect(ThinkingSphinx::RealTime::Populator).to receive(:populate).with(users_index)
+ expect(ThinkingSphinx::RealTime::Populator).not_to receive(:populate).with(parts_index)
+ expect(ThinkingSphinx::RealTime::Populator).not_to receive(:populate).with(plain_index)
+
+ interface.index
+ end
+ end
+ end
+end
diff --git a/spec/thinking_sphinx/interfaces/sql_spec.rb b/spec/thinking_sphinx/interfaces/sql_spec.rb
new file mode 100644
index 000000000..1c1057772
--- /dev/null
+++ b/spec/thinking_sphinx/interfaces/sql_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Interfaces::SQL do
+ let(:interface) { ThinkingSphinx::Interfaces::SQL.new(
+ configuration, {:verbose => true}, stream
+ ) }
+ let(:configuration) { double 'configuration', :preload_indices => true,
+ :render => true, :indices => [double(:index, :type => 'plain')],
+ :indices_location => '/path/to/indices' }
+ let(:stream) { double :puts => nil }
+
+ describe '#clear' do
+ let(:users_index) { double(:name => 'users', :type => 'plain',
+ :render => true, :path => '/path/to/my/index/users') }
+ let(:parts_index) { double(:name => 'users', :type => 'plain',
+ :render => true, :path => '/path/to/my/index/parts') }
+ let(:rt_index) { double(:type => 'rt') }
+
+ before :each do
+ allow(configuration).to receive_messages(
+ :indices => [users_index, parts_index, rt_index]
+ )
+
+ allow(Dir).to receive(:[]).with('/path/to/my/index/users.*').
+ and_return(['users.a', 'users.b'])
+ allow(Dir).to receive(:[]).with('/path/to/my/index/parts.*').
+ and_return(['parts.a', 'parts.b'])
+
+ allow(FileUtils).to receive_messages :mkdir_p => true, :rm_r => true,
+ :rm => true
+ allow(File).to receive_messages :exists? => true
+ end
+
+ it 'finds each file for sql-backed indices' do
+ expect(Dir).to receive(:[]).with('/path/to/my/index/users.*').
+ and_return([])
+
+ interface.clear
+ end
+
+ it "removes each file for real-time indices" do
+ expect(FileUtils).to receive(:rm).with('users.a')
+ expect(FileUtils).to receive(:rm).with('users.b')
+ expect(FileUtils).to receive(:rm).with('parts.a')
+ expect(FileUtils).to receive(:rm).with('parts.b')
+
+ interface.clear
+ end
+ end
+
+ describe '#index' do
+ let(:index_command) { double :call => true }
+ let(:configure_command) { double :call => true }
+
+ before :each do
+ stub_const 'ThinkingSphinx::Commands::Index', index_command
+ stub_const 'ThinkingSphinx::Commands::Configure', configure_command
+
+ allow(ThinkingSphinx).to receive_messages :before_index_hooks => []
+ allow(FileUtils).to receive_messages :mkdir_p => true
+ end
+
+ it "renders the configuration to a file by default" do
+ expect(configure_command).to receive(:call)
+
+ interface.index
+ end
+
+ it "does not render the configuration if requested" do
+ expect(configure_command).not_to receive(:call)
+
+ interface.index false
+ end
+
+ it "creates the directory for the index files" do
+ expect(FileUtils).to receive(:mkdir_p).with('/path/to/indices')
+
+ interface.index
+ end
+
+ it "calls all registered hooks" do
+ called = false
+ ThinkingSphinx.before_index_hooks << Proc.new { called = true }
+
+ interface.index
+
+ expect(called).to be_truthy
+ end
+
+ it "executes the index command" do
+ expect(index_command).to receive(:call).with(
+ configuration, {:verbose => true}, stream
+ )
+
+ interface.index
+ end
+ end
+end
diff --git a/spec/thinking_sphinx/middlewares/attribute_typer_spec.rb b/spec/thinking_sphinx/middlewares/attribute_typer_spec.rb
new file mode 100644
index 000000000..f50dd83a6
--- /dev/null
+++ b/spec/thinking_sphinx/middlewares/attribute_typer_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Middlewares::AttributeTyper do
+ let(:app) { double('app', :call => true) }
+ let(:middleware) { ThinkingSphinx::Middlewares::AttributeTyper.new app }
+ let(:attributes) { {} }
+ let(:context) { double('context', :search => search) }
+ let(:search) { double('search', :options => {}) }
+
+ before :each do
+ allow(ThinkingSphinx::AttributeTypes).to receive(:call).
+ and_return(attributes)
+ allow(ActiveSupport::Deprecation).to receive(:warn)
+ end
+
+ it 'warns when providing a string value for an integer attribute' do
+ attributes['user_id'] = [:uint]
+ search.options[:with] = {:user_id => '1'}
+
+ expect(ActiveSupport::Deprecation).to receive(:warn)
+
+ middleware.call [context]
+ end
+
+ it 'warns when providing a string value for a float attribute' do
+ attributes['price'] = [:float]
+ search.options[:without] = {:price => '1.0'}
+
+ expect(ActiveSupport::Deprecation).to receive(:warn)
+
+ middleware.call [context]
+ end
+
+ it 'proceeds when providing a string value for a string attribute' do
+ attributes['status'] = [:string]
+ search.options[:with] = {:status => 'completed'}
+
+ expect(ActiveSupport::Deprecation).not_to receive(:warn)
+
+ middleware.call [context]
+ end
+end
diff --git a/spec/thinking_sphinx/middlewares/valid_options_spec.rb b/spec/thinking_sphinx/middlewares/valid_options_spec.rb
new file mode 100644
index 000000000..840f0ee35
--- /dev/null
+++ b/spec/thinking_sphinx/middlewares/valid_options_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+RSpec.describe ThinkingSphinx::Middlewares::ValidOptions do
+ let(:app) { double 'app', :call => true }
+ let(:middleware) { ThinkingSphinx::Middlewares::ValidOptions.new app }
+ let(:context) { double 'context', :search => search }
+ let(:search) { double 'search', :options => {} }
+
+ before :each do
+ allow(ThinkingSphinx::Logger).to receive(:log)
+ end
+
+ context 'with unknown options' do
+ before :each do
+ search.options[:foo] = :bar
+ end
+
+ it "adds a warning" do
+ expect(ThinkingSphinx::Logger).to receive(:log).
+ with(:warn, "Unexpected search options: [:foo]")
+
+ middleware.call [context]
+ end
+
+ it 'continues on' do
+ expect(app).to receive(:call).with([context])
+
+ middleware.call [context]
+ end
+ end
+
+ context "with known options" do
+ before :each do
+ search.options[:ids_only] = true
+ end
+
+ it "is silent" do
+ expect(ThinkingSphinx::Logger).to_not receive(:log)
+
+ middleware.call [context]
+ end
+
+ it 'continues on' do
+ expect(app).to receive(:call).with([context])
+
+ middleware.call [context]
+ end
+ end
+end
diff --git a/spec/thinking_sphinx/rake_interface_spec.rb b/spec/thinking_sphinx/rake_interface_spec.rb
index ba3bed529..7a78a3e26 100644
--- a/spec/thinking_sphinx/rake_interface_spec.rb
+++ b/spec/thinking_sphinx/rake_interface_spec.rb
@@ -1,270 +1,37 @@
require 'spec_helper'
describe ThinkingSphinx::RakeInterface do
- let(:configuration) { double('configuration', :controller => controller) }
let(:interface) { ThinkingSphinx::RakeInterface.new }
- before :each do
- allow(ThinkingSphinx::Configuration).to receive_messages :instance => configuration
- allow(interface).to receive_messages(:puts => nil)
- end
-
- describe '#clear_all' do
- let(:controller) { double 'controller' }
-
- before :each do
- allow(configuration).to receive_messages(
- :indices_location => '/path/to/indices',
- :searchd => double(:binlog_path => '/path/to/binlog')
- )
-
- allow(FileUtils).to receive_messages :rm_r => true
- allow(File).to receive_messages :exists? => true
- end
-
- it "removes the directory for the index files" do
- expect(FileUtils).to receive(:rm_r).with('/path/to/indices')
-
- interface.clear_all
- end
-
- it "removes the directory for the binlog files" do
- expect(FileUtils).to receive(:rm_r).with('/path/to/binlog')
-
- interface.clear_all
- end
- end
-
- describe '#clear_real_time' do
- let(:controller) { double 'controller' }
- let(:index) {
- double(:type => 'rt', :render => true, :path => '/path/to/my/index')
- }
-
- before :each do
- allow(configuration).to receive_messages(
- :indices => [double(:type => 'plain'), index],
- :searchd => double(:binlog_path => '/path/to/binlog'),
- :preload_indices => true
- )
-
- allow(Dir).to receive_messages :[] => ['foo.a', 'foo.b']
- allow(FileUtils).to receive_messages :rm_r => true, :rm => true
- allow(File).to receive_messages :exists? => true
- end
-
- it 'finds each file for real-time indices' do
- expect(Dir).to receive(:[]).with('/path/to/my/index.*').and_return([])
-
- interface.clear_real_time
- end
-
- it "removes each file for real-time indices" do
- expect(FileUtils).to receive(:rm).with('foo.a')
- expect(FileUtils).to receive(:rm).with('foo.b')
-
- interface.clear_real_time
- end
-
- it "removes the directory for the binlog files" do
- expect(FileUtils).to receive(:rm_r).with('/path/to/binlog')
-
- interface.clear_real_time
- end
- end
-
describe '#configure' do
- let(:controller) { double('controller') }
+ let(:command) { double 'command', :call => true }
before :each do
- allow(configuration).to receive_messages(
- :configuration_file => '/path/to/foo.conf',
- :render_to_file => true
- )
- end
-
- it "renders the configuration to a file" do
- expect(configuration).to receive(:render_to_file)
-
- interface.configure
+ stub_const 'ThinkingSphinx::Commands::Configure', command
end
- it "prints a message stating the file is being generated" do
- expect(interface).to receive(:puts).
- with('Generating configuration to /path/to/foo.conf')
+ it 'sends the configure command' do
+ expect(command).to receive(:call)
interface.configure
end
end
- describe '#index' do
- let(:controller) { double('controller', :index => true) }
-
- before :each do
- allow(ThinkingSphinx).to receive_messages :before_index_hooks => []
- allow(configuration).to receive_messages(
- :configuration_file => '/path/to/foo.conf',
- :render_to_file => true,
- :indices_location => '/path/to/indices'
- )
-
- allow(FileUtils).to receive_messages :mkdir_p => true
- end
-
- it "renders the configuration to a file by default" do
- expect(configuration).to receive(:render_to_file)
-
- interface.index
- end
-
- it "does not render the configuration if requested" do
- expect(configuration).not_to receive(:render_to_file)
-
- interface.index false
- end
-
- it "creates the directory for the index files" do
- expect(FileUtils).to receive(:mkdir_p).with('/path/to/indices')
-
- interface.index
- end
-
- it "calls all registered hooks" do
- called = false
- ThinkingSphinx.before_index_hooks << Proc.new { called = true }
-
- interface.index
-
- expect(called).to be_truthy
- end
-
- it "indexes all indices verbosely" do
- expect(controller).to receive(:index).with(:verbose => true)
-
- interface.index
- end
-
- it "does not index verbosely if requested" do
- expect(controller).to receive(:index).with(:verbose => false)
-
- interface.index true, false
- end
- end
-
- describe '#start' do
- let(:controller) { double('controller', :start => result, :pid => 101) }
- let(:result) { double 'result', :command => 'start', :status => 1,
- :output => '' }
-
- before :each do
- allow(controller).to receive(:running?).and_return(false, true)
- allow(configuration).to receive_messages(
- :indices_location => 'my/index/files',
- :searchd => double(:log => '/path/to/log')
- )
-
- allow(FileUtils).to receive_messages :mkdir_p => true
- end
-
- it "creates the index files directory" do
- expect(FileUtils).to receive(:mkdir_p).with('my/index/files')
-
- interface.start
- end
-
- it "starts the daemon" do
- expect(controller).to receive(:start)
-
- interface.start
- end
-
- it "raises an error if the daemon is already running" do
- allow(controller).to receive_messages :running? => true
-
- expect {
- interface.start
- }.to raise_error(ThinkingSphinx::SphinxAlreadyRunning)
- end
-
- it "prints a success message if the daemon has started" do
- allow(controller).to receive(:running?).and_return(false, true)
-
- expect(interface).to receive(:puts).
- with('Started searchd successfully (pid: 101).')
-
- interface.start
- end
-
- it "prints a failure message if the daemon does not start" do
- allow(controller).to receive(:running?).and_return(false, false)
- allow(interface).to receive(:exit)
-
- expect(interface).to receive(:puts) do |string|
- expect(string).to match('The Sphinx start command failed')
- end
-
- interface.start
+ describe '#daemon' do
+ it 'returns a daemon interface' do
+ expect(interface.daemon.class).to eq(ThinkingSphinx::Interfaces::Daemon)
end
end
- describe '#stop' do
- let(:controller) { double('controller', :stop => true, :pid => 101) }
- let(:result) { double 'result', :command => 'start', :status => 1,
- :output => '' }
-
- before :each do
- allow(controller).to receive(:running?).and_return(true, true, false)
- end
-
- it "prints a message if the daemon is not already running" do
- allow(controller).to receive_messages :running? => false
-
- expect(interface).to receive(:puts).with('searchd is not currently running.')
-
- interface.stop
- end
-
- it "stops the daemon" do
- expect(controller).to receive(:stop)
-
- interface.stop
- end
-
- it "prints a message informing the daemon has stopped" do
- expect(interface).to receive(:puts).with('Stopped searchd daemon (pid: 101).')
-
- interface.stop
- end
-
- it "should retry stopping the daemon until it stops" do
- allow(controller).to receive(:running?).
- and_return(true, true, true, false)
-
- expect(controller).to receive(:stop).twice
-
- interface.stop
+ describe '#rt' do
+ it 'returns a real-time interface' do
+ expect(interface.rt.class).to eq(ThinkingSphinx::Interfaces::RealTime)
end
end
- describe '#status' do
- let(:controller) { double('controller') }
-
- it "reports when the daemon is running" do
- allow(controller).to receive_messages :running? => true
-
- expect(interface).to receive(:puts).
- with('The Sphinx daemon searchd is currently running.')
-
- interface.status
- end
-
- it "reports when the daemon is not running" do
- allow(controller).to receive_messages :running? => false
-
- expect(interface).to receive(:puts).
- with('The Sphinx daemon searchd is not currently running.')
-
- interface.status
+ describe '#sql' do
+ it 'returns an SQL interface' do
+ expect(interface.sql.class).to eq(ThinkingSphinx::Interfaces::SQL)
end
end
end
diff --git a/spec/thinking_sphinx/search/query_spec.rb b/spec/thinking_sphinx/search/query_spec.rb
index f77ed00b3..372e7dcd3 100644
--- a/spec/thinking_sphinx/search/query_spec.rb
+++ b/spec/thinking_sphinx/search/query_spec.rb
@@ -74,5 +74,12 @@ class Search; end
expect(query.to_s).to eq('tasty @title pancakes')
end
+
+ it "handles multiple fields for a single condition" do
+ query = ThinkingSphinx::Search::Query.new '',
+ [:title, :content] => 'pancakes'
+
+ expect(query.to_s).to eq('@(title,content) pancakes')
+ end
end
end
diff --git a/thinking-sphinx.gemspec b/thinking-sphinx.gemspec
index 8b3a6ea97..3508ab8ed 100644
--- a/thinking-sphinx.gemspec
+++ b/thinking-sphinx.gemspec
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
Gem::Specification.new do |s|
s.name = 'thinking-sphinx'
- s.version = '3.3.0'
+ s.version = '3.4.0'
s.platform = Gem::Platform::RUBY
s.authors = ["Pat Allan"]
s.email = ["pat@freelancing-gods.com"]
@@ -29,7 +29,8 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'riddle', '>= 2.0.0'
s.add_development_dependency 'appraisal', '~> 1.0.2'
- s.add_development_dependency 'combustion', '~> 0.5.4'
- s.add_development_dependency 'database_cleaner', '~> 1.2.0'
- s.add_development_dependency 'rspec', '~> 3.5.0'
+ s.add_development_dependency 'combustion', '~> 0.7.0'
+ s.add_development_dependency 'database_cleaner', '~> 1.6.0'
+ s.add_development_dependency 'rspec', '~> 3.6.0'
+ s.add_development_dependency 'rspec-retry', '~> 0.5.4'
end