pax_global_header00006660000000000000000000000064134113213010014477gustar00rootroot0000000000000052 comment=edc0050394deebb1dc55c617c0b62e70f9618394 thinking-sphinx-4.1.0/000077500000000000000000000000001341132130100146235ustar00rootroot00000000000000thinking-sphinx-4.1.0/.gitignore000066400000000000000000000003331341132130100166120ustar00rootroot00000000000000*.gem .bundle .rbx .rspec .tool-versions gemfiles Gemfile.lock *.sublime-* pkg/* spec/internal/config/test.sphinx.conf spec/internal/db/sphinx spec/internal/log !spec/internal/tmp/.gitkeep spec/internal/tmp/* tmp _site thinking-sphinx-4.1.0/.travis.yml000066400000000000000000000030331341132130100167330ustar00rootroot00000000000000language: ruby rvm: - 2.3.8 - 2.4.5 - 2.5.3 - 2.6.0 addons: apt: packages: - cmake - bison - flex before_install: - 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 - "./bin/loadsphinx $SPHINX_VERSION $SPHINX_ENGINE" - bundle exec appraisal install script: bundle exec appraisal rspec env: global: - SPHINX_BIN=ext/sphinx/bin/ - secure: cUPinkilBafqDSPsTkl/PXYc2aXNKUQKXGK8poBBMqKN9/wjfJx1DWgtowDKalekdZELxDhc85Ye3bL1xlW4nLjOu+U6Tkt8eNw2Nhs1flodHzA/RyENdBLr/tBHt43EjkrDehZx5sBHmWQY4miHs8AJz0oKO9Ae2inTOHx9Iuc= matrix: - DATABASE=mysql2 SPHINX_VERSION=2.1.9 SPHINX_ENGINE=sphinx - DATABASE=postgresql SPHINX_VERSION=2.1.9 SPHINX_ENGINE=sphinx - DATABASE=mysql2 SPHINX_VERSION=2.2.11 SPHINX_ENGINE=sphinx - DATABASE=postgresql SPHINX_VERSION=2.2.11 SPHINX_ENGINE=sphinx - DATABASE=mysql2 SPHINX_VERSION=3.0.3 SPHINX_ENGINE=sphinx - DATABASE=postgresql SPHINX_VERSION=3.0.3 SPHINX_ENGINE=sphinx - DATABASE=mysql2 SPHINX_VERSION=3.1.1 SPHINX_ENGINE=sphinx - DATABASE=mysql2 SPHINX_VERSION=2.6.3 SPHINX_ENGINE=manticore - DATABASE=postgresql SPHINX_VERSION=2.6.3 SPHINX_ENGINE=manticore - DATABASE=mysql2 SPHINX_VERSION=2.7.4 SPHINX_ENGINE=manticore - DATABASE=postgresql SPHINX_VERSION=2.7.4 SPHINX_ENGINE=manticore # - DATABASE=postgresql SPHINX_VERSION=3.1.1 SPHINX_ENGINE=sphinx sudo: false addons: postgresql: '9.4' services: - postgresql thinking-sphinx-4.1.0/Appraisals000066400000000000000000000024701341132130100166500ustar00rootroot00000000000000appraise 'rails_3_2' do gem 'rails', '~> 3.2.22.2' gem 'mysql2', '~> 0.3.10', :platform => :ruby end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_0' do gem 'rails', '~> 4.0.13' gem 'mysql2', '~> 0.3.10', :platform => :ruby end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_1' do gem 'rails', '~> 4.1.15' gem 'mysql2', '~> 0.3.13', :platform => :ruby end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_4_2' do gem 'rails', '~> 4.2.6' gem 'mysql2', '~> 0.4.0', :platform => :ruby end if RUBY_VERSION.to_f <= 2.3 appraise 'rails_5_0' do if RUBY_PLATFORM == "java" gem 'rails', '5.0.6' else gem 'rails', '~> 5.0.7' end gem 'mysql2', '~> 0.4.0', :platform => :ruby gem 'jdbc-mysql', '~> 5.1.36', :platform => :jruby gem 'activerecord-jdbcmysql-adapter', '~> 50.0', :platform => :jruby gem 'activerecord-jdbcpostgresql-adapter', '~> 50.0', :platform => :jruby end if RUBY_PLATFORM != "java" || ENV["SPHINX_VERSION"].to_f > 2.1 appraise 'rails_5_1' do gem 'rails', '~> 5.1.0' gem 'mysql2', '~> 0.4.0', :platform => :ruby end if RUBY_PLATFORM != 'java' appraise 'rails_5_2' do gem 'rails', '~> 5.2.0' gem 'mysql2', '~> 0.5.0', :platform => :ruby gem 'pg', '~> 1.0', :platform => :ruby end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f >= 2.3 thinking-sphinx-4.1.0/CHANGELOG.markdown000066400000000000000000000656601341132130100176730ustar00rootroot00000000000000# Changelog All notable changes to this project (at least, from v3.0.0 onwards) are documented in this file. ## 4.1.0 - 2018-12-28 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.1.0) ### Added * The `:sql` search option can now accept per-model settings with model names as keys. e.g. `ThinkingSphinx.search "foo", :sql => {'Article' => {:include => :user}}` (Sergey Malykh in [#1120](https://github.com/pat/thinking-sphinx/pull/1120)). ### Changed * Drop MRI 2.2 from the test matrix, and thus no longer officially supported (though the code will likely continue to work with 2.2 for a while). * Added MRI 2.6, Sphinx 3.1 and Manticore 2.7 to the test matrix. ### Fixed * Real-time indices now work with non-default integer primary keys (alongside UUIDs or other non-integer primary keys). ## 4.0.0 - 2018-04-10 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.0.0) ### Added * Support Sphinx 3.0. * Allow disabling of docinfo setting via `skip_docinfo: true` in `config/thinking_sphinx.yml`. * Support merging of delta indices into their core counterparts using ts:merge. * Support UNIX sockets as an alternative for TCP connections to the daemon (MRI-only). * Translate relative paths to absolute when generating configuration when `absolute_paths: true` is set per environment in `config/thinking_sphinx.yml`. ### Changed * Drop Sphinx 2.0 support. * Drop auto-typing of filter values. * INDEX_FILTER environment variable is applied when running ts:index on SQL-backed indices. * Drop MRI 2.0/2.1 support. * Display a useful error message if processing real-time indices but the daemon isn't running. * Refactor interface code into separate command classes, and allow for a custom rake interface. * Add frozen_string_literal pragma comments. * Log exceptions when processing real-time indices, but don't stop. * Update polymorphic properties to support Rails 5.2. * Allow configuration of the index guard approach. * Output a warning if guard files exist when calling ts:index. * Delete index guard files as part of ts:rebuild and ts:clear. ### Fixed * Handle situations where no exit code is provided for Sphinx binary calls. * Don't attempt to interpret indices for models that don't have a database table. ## 3.4.2 - 2017-09-29 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.2) ### Changed * Allow use of deletion callbacks for rollback events. * Remove extra deletion code in the Populator - it's also being done by the real-time rake interface. ### Fixed * Real-time callback syntax for namespaced models accepts a string (as documented). * Fix up logged warnings. * Add missing search options to known values to avoid incorrect warnings. ## 3.4.1 - 2017-08-29 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.1) ### Changed * Treat "Lost connection to MySQL server" as a connection error (Manuel Schnitzer). ### Fixed * Index normalisation will now work even when index model tables don't exist. ## 3.4.0 - 2017-08-28 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.0) ### Added * Rake tasks are now unified, so the original tasks will operate on real-time indices as well. * Output warnings when unknown options are used in search calls. * Allow generation of a single real-time index (Tim Brown). * Automatically use UTF8 in Sphinx for encodings that are extensions of UTF8. * Basic type checking for attribute filters. ### Changed * Delta callback logic now prioritises checking for high level settings rather than model changes. * Allow for unsaved records when calculating document ids (and return nil). * Display SphinxQL deletion statements in the log. * Add support for Ruby's frozen string literals feature. * Use saved_changes if it's available (in Rails 5.1+). * Set a default connection timeout of 5 seconds. * Don't search multi-table inheritance ancestors. * Handle non-computable queries as parse errors. ### Fixed * Index normalisation now occurs consistently, and removes unneccesary sphinx_internal_class_name fields from real-time indices. * Fix Sphinx connections in JRuby. * Fix long SphinxQL query handling in JRuby. * Always close the SphinxQL connection if Innertube's asking (@cmaion). * Get bigint primary keys working in Rails 5.1. * Fix handling of attached starts of Sphinx (via Henne Vogelsang). * Fix multi-field conditions. * Use the base class of STI models for polymorphic join generation (via Andrés Cirugeda). * Ensure ts:index now respects rake silent/quiet flags. ## 3.3.0 - 2016-12-13 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.3.0) ### Added * Real-time callbacks can now be used with after_commit hooks if that's preferred over after_save. * Allow for custom batch sizes when populating real-time indices. ### Changed * Only toggle the delta value if the record has changed or is new (rather than on every single save call). * Delta indexing is now quiet by default (rather than verbose). * Use Riddle's reworked command interface for interacting with Sphinx's command-line tools. * Respect Rake's quiet and silent flags for the Thinking Sphinx rake tasks. * ts:start and ts:stop tasks default to verbose. * Sort engine paths for loading indices to ensure they're consistent. * Custom exception class for invalid database adapters. * Memoize the default primary keys per context. ### Fixed * Explicit source method in the SQLQuery Builder instead of relying on method missing, thus avoiding any global methods named 'source' (Asaf Bartov). * Load indices before deleting index files, to ensure the files are actually found and deleted. * Avoid loading ActiveRecord earlier than necessary. This avoids loading Rails out of order, which caused problems with Rails 5. * Handle queries that are too long for Sphinx. * Improve Rails 5 / JRuby support. * Fixed handling of multiple field tokens in wildcarding logic. * Ensure custom primary key columns are handled consistently (Julio Monteiro). ## 3.2.0 - 2016-05-13 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.2.0) ### Added * Add JSON attribute support for real-time indices. * Add ability to disable *all* Sphinx-related callbacks via ThinkingSphinx::Callbacks.suspend! and ThinkingSphinx::Callbacks.resume!. Particularly useful for unit tests. * Add native OutOfBoundsError for search queries outside the pagination bounds. * Support MySQL SSL options on a per-index level (@arrtchiu). * Allow for different indexing strategies (e.g. all at once, or one by one). * Allow rand_seed as a select option (Mattia Gheda). * Add primary_key option for index definitions (Nathaneal Gray). * Add ability to start searchd in the foreground (Andrey Novikov). ### Changed * Improved error messages for duplicate property names and missing columns. * Don't populate search results when requesting just the count values (Andrew Roth). * Reset delta column before core indexing begins (reverting behaviour introduced in 3.1.0). See issue #958 for further discussion. * Use Sphinx's bulk insert ability (Chance Downs). * Reduce memory/object usage for model references (Jonathan del Strother). * Disable deletion callbacks when real-time indices are in place and all other real-time callbacks are disabled. * Only use ERB to parse the YAML file if ERB is loaded. ### Fixed * Ensure SQL table aliases are reliable for SQL-backed index queries. * Fixed mysql2 compatibility for memory references (Roman Usherenko). * Fixed JRuby compatibility with camelCase method names (Brandon Dewitt). * Fix stale id handling for multiple search contexts (Jonathan del Strother). * Handle quoting of namespaced tables (Roman Usherenko). * Make preload_indices thread-safe. * Improved handling of marshalled/demarshalled search results. ## 3.1.4 - 2015-06-01 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.4) ### Added * Add JSON as a Sphinx type for attributes (Daniel Vandersluis). * minimal_group_by? can now be set in config/thinking_sphinx.yml to automatically apply to all index definitions. ### Changed * Add a contributor code of conduct. * Remove polymorphic association and HABTM query support (when related to Thinking Sphinx) when ActiveRecord 3.2 is involved. * Remove default charset_type - no longer required for Sphinx 2.2. * Removing sql_query_info setting, as it's no longer used by Sphinx (nor is it actually used by Thinking Sphinx). ### Fixed * Kaminari expects prev_page to be available. * Don't try to delete guard files if they don't exist (@exAspArk). * Handle database settings reliably, now that ActiveRecord 4.2 uses strings all the time. * More consistent with escaping table names. * Bug fix for association creation (with polymophic fields/attributes). ## 3.1.3 - 2015-01-21 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.3) ### Added * Allow for custom offset references with the :offset_as option - thus one model across many schemas with Apartment can be treated differently. * Allow for custom IndexSet classes. ### Changed * Log excerpt SphinxQL queries just like the search queries. * Load Railtie if Rails::Railtie is defined, instead of just Rails (Andrew Cone). * Convert raw Sphinx results to an array when querying (Bryan Ricker). * Add bigint support for real-time indices, and use bigints for the sphinx_internal_id attribute (mapped to model primary keys) (Chance Downs). ### Fixed * Generate de-polymorphised associations properly for Rails 4.2 * Use reflect_on_association instead of reflections, to stick to the public ActiveRecord::Base API. * Don't load ActiveRecord early - fixes a warning in Rails 4.2. * Don't double-up on STI filtering, already handled by Rails. ## 3.1.2 - 2014-11-04 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.2) ### Added * Allow for custom paths for index files using :path option in the ThinkingSphinx::Index.define call. * Allow the binlog path to be an empty string (Bobby Uhlenbrock). * Add status task to report on whether Sphinx is running. * Real-time index callbacks can take a block for dynamic scoping. * Allow casting of document ids pre-offset as bigints (via big_documents_id option). ### Changed * regenerate task now only deletes index files for real-time indices. * Raise an exception when a populated search query is modified (as it can't be requeried). * Log indices that aren't processed due to guard files existing. * Paginate records by 1000 results at a time when flagging as deleted. * Default the Capistrano TS Rails environment to use rails_env, and then fall back to stage. * rebuild task uses clear between stopping the daemon and indexing. ### Fixed * Ensure indexing guard files are removed when an exception is raised (Bobby Uhlenbrock). * Don't update real-time indices for objects that are not persisted (Chance Downs). * Use STI base class for polymorphic association replacements. * Convert database setting keys to symbols for consistency with Rails (@dimko). * Field weights and other search options are now respected from set_property. * Models with more than one index have correct facet counts (using Sphinx 2.1.x or newer). * Some association fixes for Rails 4.1. * Clear connections when raising connection errors. ## 3.1.1 - 2014-04-22 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.1) ### Added * Allow for common section in generated Sphinx configuration files for Sphinx 2.2.x (disabled by default, though) (Trevor Smith). * Basic support for HABTM associations and MVAs with query/ranged-query sources. * Real-time indices callbacks can be disabled (useful for unit tests). * ThinkingSphinx::Test has a clear method and no-index option for starting for real-time setups. * Allow disabling of distributed indices. ### Changed * Include full statements when query execution errors are raised (uglier, but more useful when debugging). * Connection error messages now mention Sphinx, instead of just MySQL. * Raise an exception when a referenced column does not exist. * Capistrano tasks use thinking_sphinx_rails_env (defaults to standard environment) (Robert Coleman). * Alias group and count columns for easier referencing in other clauses. * Log real-time index updates (Demian Ferreiro). * All indices now respond to a public attributes method. ### Fixed * Don't apply attribute-only updates to real-time indices. * Don't instantiate blank strings (via inheritance type columns) as constants. * Don't presume all indices for a model have delta pairs, even if one does. * Always use connection options for connection information. * respond_to? works reliably with masks (Konstantin Burnaev). * Avoid null values in MVA query/ranged-query sources. * Don't send unicode null characters to real-time Sphinx indices. * :populate option is now respected for single-model searches. * :thinking_sphinx_roles is now used consistently in Capistrano v3 tasks. * Only expand log directory if it exists. * Handle JDBC connection errors appropriately (Adam Hutchison). * Fixing wildcarding of Unicode strings. * Improved handling of association searches with real-time indices, including via has_many :though associations (Rob Anderton). ## 3.1.0 - 2014-01-11 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.0) ### Added * Support for Capistrano v3 (Alexander Tipugin). * JRuby support (with Sphinx 2.1 or newer). * Support for Sphinx 2.2.x's HAVING and GROUP N BY SphinxQL options. * Adding max_predicted_time search option (Sphinx 2.2.x). * Wildcard/starring can be applied directly to strings using ThinkingSphinx::Query.wildcard('pancakes'), and escaping via ThinkingSphinx::Query.escape('pancakes'). * Capistrano recipe now includes tasks for realtime indices. * :group option within :sql options in a search call is passed through to the underlying ActiveRecord relation (Siarhei Hanchuk). * Persistent connections can be disabled if you wish. * Track what's being indexed, and don't double-up while indexing is running. Single indices (e.g. deltas) can be processed while a full index is happening, though. * Pass through :delta_options to delta processors (Timo Virkalla). * All delta records can have their core pairs marked as deleted after a suspended delta (use ThinkingSphinx::Deltas.suspend_and_update instead of ThinkingSphinx::Deltas.suspend). * Set custom database settings within the index definition, using the set_database method. A more sane approach with multiple databases. ### Changed * Updating Riddle requirement to >= 1.5.10. * Extracting join generation into its own gem: Joiner. * Geodist calculation is now prepended to the SELECT statement, so it can be referred to by other dynamic attributes. * Auto-wildcard/starring (via :star => true) now treats escaped characters as word separators. * Capistrano recipe no longer automatically adds thinking_sphinx:index and thinking_sphinx:start to be run after deploy:cold. * UTF-8 forced encoding is now disabled by default (in line with Sphinx 2.1.x). * Sphinx functions are now the default, instead of the legacy special variables (in line with Sphinx 2.1.x). * Rails 3.1 is no longer supported. * MRI 1.9.2 is no longer supported. * Insist on at least * for SphinxQL SELECT statements. * Reset the delta column to true after core indexing is completed, instead of before, and don't filter out delta records from the core source. * Provide a distributed index per model that covers both core and delta indices. ### Fixed * Indices will be detected in Rails engines upon configuration. * Destroy callbacks are ignored for non-persisted objects. * Blank STI values are converted to the parent class in Sphinx index data (Jonathan Greenberg). * Track indices on parent STI models when marking documents as deleted. * Separate per_page/max_matches values are respected in facet searches (Timo Virkkala). * Don't split function calls when casting timestamps (Timo Virkalla). ## 3.0.6 - 2013-10-20 [Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.0.6) ### Added * Raise an error if no indices match the search criteria (Bryan Ricker). * skip_time_zone setting is now available per environment via config/thinking_sphinx.yml to avoid the sql_query_pre time zone command. * Added new search options in Sphinx 2.1.x. * Added ability to disable UTF-8 forced encoding, now that Sphinx 2.1.2 returns UTF-8 strings by default. This will be disabled by default in Thinking Sphinx 3.1.0. * Added ability to switch between Sphinx special variables and the equivalent functions. Sphinx 2.1.x requires the latter, and that behaviour will become the default in Sphinx 3.1.0. * Adding search_for_ids on scoped search calls. * MySQL users can enable a minimal GROUP BY statement, to speed up queries: set_property :minimal_group_by? => true. ### Changed * Updating Riddle dependency to be >= 1.5.9. * Separated directory preparation from data generation for real-time index (re)generation tasks. * Have tests index UTF-8 characters where appropriate (Pedro Cunha). * Always use DISTINCT in group concatenation. * Sphinx connection failures now have their own class, ThinkingSphinx::ConnectionError, instead of the standard Mysql2::Error. * Don't clobber custom :select options for facet searches (Timo Virkkala). * Automatically load Riddle's Sphinx 2.0.5 compatability changes. * Realtime fields and attributes now accept symbols as well as column objects, and fields can be sortable (with a _sort prefix for the matching attribute). * Insist on the log directory existing, to ensure correct behaviour for symlinked paths. (Michael Pearson). * Rake's silent mode is respected for indexing (@endoscient). ### Fixed * Cast every column to a timestamp for timestamp attributes with multiple columns. * Don't use Sphinx ordering if SQL order option is supplied to a search. * Custom middleware and mask options now function correctly with model-scoped searches. * Suspended deltas now no longer update core indices as well. * Use alphabetical ordering for index paths consistently (@grin). * Convert very small floats to fixed format for geo-searches. ## 3.0.5 - 2013-08-26 ### Added * Allow scoping of real-time index models. ### Changed * Updating Riddle dependency to be >= 1.5.8. * Real-time index population presentation and logic are now separated. * Using the connection pool for update callbacks, excerpts, deletions. * Don't add the sphinx_internal_class_name unless STI models are indexed. * Use Mysql2's reconnect option and have it turned on by default. * Improved auto-starring with escaped characters. ### Fixed * Respect existing sql_query_range/sql_query_info settings. * Don't add select clauses or joins to sql_query if they're for query/ranged-query properties. * Set database timezones as part of the indexing process. * Chaining scopes with just options works again. ## 3.0.4 - 2013-07-09 ### Added * ts:regenerate rake task for rebuilding Sphinx when realtime indices are involved. * ts:clear task removes all Sphinx index and binlog files. * Facet search calls now respect the limit option (which otherwise defaults to max_matches) (Demian Ferreiro). * Excerpts words can be overwritten with the words option (@groe). * The :facets option can be used in facet searches to limit which facets are queried. * A separate role can be set for Sphinx actions with Capistrano (Andrey Chernih). * Facet searches can now be called from Sphinx scopes. ### Changed * Updating Riddle dependency to be >= 1.5.7. * Glaze now responds to respond_to? (@groe). * Deleted ActiveRecord objects are deleted in realtime indices as well. * Realtime callbacks are no longer automatically added, but they're now more flexible (for association situations). * Cleaning and refactoring so Code Climate ranks this as A-level code (Philip Arndt, Shevaun Coker, Garrett Heinlen). * Exceptions raised when communicating with Sphinx are now mentioned in the logs when queries are retried (instead of STDOUT). * Excerpts now use just the query and standard conditions, instead of parsing Sphinx's keyword metadata (which had model names in it). * Get database connection details from ActiveRecord::Base, not each model, as this is where changes are reflected. * Default Sphinx scopes are applied to new facet searches. ### Fixed * Empty queries with the star option set to true are handled gracefully. * Excerpts are now wildcard-friendly. * Facet searches now use max_matches value (with a default of 1000) to ensure as many results as possible are returned. * The settings cache is now cleared when the configuration singleton is reset (Pedro Cunha). * Escaped @'s in queries are considered part of each word, instead of word separators. * Internal class name conditions are ignored with auto-starred queries. * RDoc doesn't like constant hierarchies split over multiple lines. ## 3.0.3 - 2013-05-07 ### Added * INDEX_ONLY environment flag is passed through when invoked through Capistrano (Demian Ferreiro). * use_64_bit option returns as cast_to_timestamp instead (Denis Abushaev). * Collection of hooks (lambdas) that get called before indexing. Useful for delta libraries. ### Changed * Updating Riddle dependency to be >= 1.5.6 * Delta jobs get common classes to allow third-party delta behaviours to leverage Thinking Sphinx. * Raise ThinkingSphinx::MixedScopesError if a search is called through an ActiveRecord scope. * GroupEnumeratorsMask is now a default mask, as masks need to be in place before search results are populated/the middleware is called (and previously it was being added within a middleware call). * The current_page method is now a part of ThinkingSphinx::Search, as it is used when populating results. ### Fixed * Update to association handling for Rails/ActiveRecord 4.0.0.rc1. * Cast and concatenate multi-column attributes correctly. * Don't load fields or attributes when building a real-time index - otherwise the index is translated before it has a chance to be built. * Default search panes are cloned for each search. * Index-level settings (via set_property) are now applied consistently after global settings (in thinking_sphinx.yml). * All string values returned from Sphinx are now properly converted to UTF8. * The default search masks are now cloned for each search, instead of referring to the constant (and potentially modifying it often). ## 3.0.2 - 2013-03-23 ### Added * Ruby 2.0 support. * Rails 4.0.0 beta1 support. * Indexes defined in app/indices in engines are now loaded (Antonio Tapiador del Dujo). * Query errors are classified as such, instead of getting the base SphinxError. ### Changed * per_page now accepts an optional paging limit, to match WillPaginate's behaviour. If none is supplied, it just returns the page size. * Strings and regular expressions in ThinkingSphinx::Search::Query are now treated as UTF-8. * Setting a custom framework will rebuild the core configuration around its provided settings (path and environment). * Search masks don't rely on respond_to?, and so Object/Kernel methods are passed through to the underlying array instead. * Empty search conditions are now ignored, instead of being appended with no value (Nicholas Klick). * Custom conditions are no longer added to the sql_query_range value, as they may involve associations. ### Fixed * :utf8? option within index definitions is now supported, and defaults to true if the database configuration's encoding is set to 'utf8'. * indices_location and configuration_file values in thinking_sphinx.yml will be applied to the configuration. * Primary keys that are not 'id' now work correctly. * Search options specified in index definitions and thinking_sphinx.yml are now used in search requests (eg: max_matches, field_weights). * Custom association conditions are no longer presumed to be an array. * Capistrano tasks use the correct ts rake task prefix (David Celis). ## 3.0.1 - 2013-02-04 ### Added * Provide Capistrano deployment tasks (David Celis). * Allow specifying of Sphinx version. Is only useful for Flying Sphinx purposes at this point - has no impact on Riddle or Sphinx. * Support new JDBC configuration style (when JDBC can be used) (Kyle Stevens). * Mysql2::Errors are wrapped as ThinkingSphinx::SphinxErrors, with subclasses of SyntaxError and ParseError used appropriately. Syntax and parse errors do not prompt a retry on a new connection. * Polymorphic associations can be used within index definitions when the appropriate classes are set out. * Allow custom strings for SQL joins in index definitions. * indexer and searchd settings are added to the appropriate objects from config/thinking_sphinx.yml (@ygelfand). ### Changed * Use connection pool for search queries. If a query fails, it will be retried on a new connection before raising if necessary. * Glaze always passes methods through to the underlying ActiveRecord::Base object if they don't exist on any of the panes. ### Fixed * Referring to associations via polymorphic associations in an index definition now works. * Don't override foreign keys for polymorphic association replacements. * Quote namespaced model names in class field condition. * New lines are maintained and escaped in custom source queries. * Subclasses of indexed models fire delta callbacks properly. * Thinking Sphinx can be loaded via thinking/sphinx, to satisfy Bundler. * New lines are maintained and escaped in sql_query values. ## 3.0.0 - 2013-01-02 ### Added * Initial realtime index support, including the ts:generate task for building index datasets. Sphinx 2.0.6 is required. * SphinxQL connection pooling via the Innertube gem. ### Changed * Updating Riddle dependency to 1.5.4. * UTF-8 is now the default charset again (as it was in earlier Thinking Sphinx versions). * Removing ts:version rake task. ### Fixed * Respect source options as well as underlying settings via the set_property method in index definitions. * Load real-time index definitions when listing fields, attributes, and/or conditions. ## 3.0.0.rc - 2012-12-22 ### Added * Source type support (query and ranged query) for both attributes and fields. Custom SQL strings can be supplied as well. * Wordcount attributes and fields now supported. * Support for Sinatra and other non-Rails frameworks. * A sphinx scope can be defined as the default. * An index can have multiple sources, by using define_source within the index definition. * sanitize_sql is available within an index definition. * Providing :prefixes => true or :infixes => true as an option when declaring a field means just the noted fields have infixes/prefixes applied. * ThinkingSphinx::Search#query_time returns the time Sphinx took to make the query. * Namespaced model support. * Default settings for index definition arguments can be set in config/thinking_sphinx.yml. * A custom Riddle/Sphinx controller can be supplied. Useful for Flying Sphinx to have an API layer over Sphinx commands, without needing custom gems for different Thinking Sphinx/Flying Sphinx combinations. ### Fixed * Correctly escape nulls in inheritance column (Darcy Laycock). * Use ThinkingSphinx::Configuration#render_to_file instead of ThinkingSphinx::Configuration#build in test helpers (Darcy Laycock). * Suppressing delta output in test helpers now works (Darcy Laycock). ## 3.0.0.pre - 2012-10-06 First pre-release of v3. Not quite feature complete, but the important stuff is certainly covered. See the README for more the finer details. thinking-sphinx-4.1.0/Gemfile000066400000000000000000000007171341132130100161230ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'mysql2', '~> 0.5.0', :platform => :ruby gem 'pg', '~> 0.18.4', :platform => :ruby if RUBY_PLATFORM == 'java' gem 'jdbc-mysql', '5.1.35', :platform => :jruby gem 'activerecord-jdbcmysql-adapter', '>= 1.3.23', :platform => :jruby gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.23', :platform => :jruby gem 'activerecord', '>= 3.2.22' end thinking-sphinx-4.1.0/LICENCE000066400000000000000000000020351341132130100156100ustar00rootroot00000000000000Copyright (c) 2011 Pat Allan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. thinking-sphinx-4.1.0/README.textile000066400000000000000000000122411341132130100171600ustar00rootroot00000000000000h1. 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 v4.1.0. h2. Upgrading Please refer to "the changelog":https://github.com/pat/thinking-sphinx/blob/develop/CHANGELOG.markdown and "release notes":https://github.com/pat/thinking-sphinx/releases for any changes you need to make when upgrading. The release notes in particular are quite good at covering breaking changes and more details for new features. The documentation also has more details on what's involved for upgrading from "v3 to v4":https://freelancing-gods.com/thinking-sphinx/v4/upgrading.html, and "v1/v2 to v3":https://freelancing-gods.com/thinking-sphinx/v3/upgrading.html. 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',    :platform => :ruby
gem 'jdbc-mysql',      '~> 5.1.35', :platform => :jruby
gem 'thinking-sphinx', '~> 4.1'
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 with a version of Sphinx prior to 2.2.11, 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, or upgrade Sphinx. You'll also need to install Sphinx - this is covered in "the extended documentation":https://freelancing-gods.com/thinking-sphinx/installing_sphinx.html. h2. Usage Begin by reading the "quick-start guide":https://freelancing-gods.com/thinking-sphinx/quickstart.html, and beyond that, "the documentation":https://freelancing-gods.com/thinking-sphinx/ should serve you pretty well. h2. Requirements The current release of Thinking Sphinx works with the following versions of its dependencies: |_. Library |_. Minimum |_. Tested Against | | Ruby | v2.3 | v2.3.8, v2.4.5, v2.5.3, v2.6.0 | | Sphinx | v2.1.2 | v2.1.9, v2.2.11, v3.0.3, v3.1.1 | | Manticore | v2.6.3 | v2.6.3, v2.7.4 | | ActiveRecord | v3.2 | v3.2, v4.0, v4.1, v4.2, v5.0, v5.1, v5.2 | It _might_ work with older versions of Ruby, but it's highly recommended to update to a supported release. It should also work with JRuby, but the test environment on Travis CI has been timing out, hence that's not actively tested against at the moment. h3. Sphinx or Manticore Thinking Sphinx v3 is currently built for Sphinx 2.1.2 or newer, or Manticore v2.6+. h3. Rails and ActiveRecord Currently Thinking Sphinx 3 is built to support Rails/ActiveRecord 3.2 or newer. If you're using Sinatra and ActiveRecord instead of Rails, that's fine - just make sure you add the @:require => 'thinking_sphinx/sinatra'@ option when listing @thinking-sphinx@ in your Gemfile. Please note that if you're referring to polymorphic associations in your index definitions, you'll want to be using Rails/ActiveRecord 4.0 or newer. Supporting polymorphic associations and Rails/ActiveRecord 3.2 is problematic, and likely will not be addressed in the future. If you want ActiveRecord 3.1 support, then refer to the 3.0.x releases of Thinking Sphinx. Anything older than that, then you're stuck with Thinking Sphinx v2.x (for Rails/ActiveRecord 3.0) or v1.x (Rails 2.3). Please note that these older versions are no longer actively supported. h3. Ruby You'll need either the standard Ruby (v2.2 or newer) or JRuby (9.1 or newer). h3. Database Versions MySQL 5.x and Postgres 8.4 or better are supported. h2. Contributing Please note that this project has a "Contributor Code of Conduct":http://contributor-covenant.org/version/1/0/0/. By participating in this project you agree to abide by its terms. To contribute, clone this repository and have a good look through the specs - you'll notice the distinction between acceptance tests that actually use Sphinx and go through the full stack, and unit tests (everything else) which use liberal test doubles to ensure they're only testing the behaviour of the class in question. I've found this leads to far better code design. All development is done on the @develop@ branch; please base any pull requests off of that branch. Please write the tests and then the code to get them passing, and send through a pull request. In order to run the tests, you'll need to create a database named @thinking_sphinx@:
# Either fire up a MySQL console:
mysql -u root
# OR a PostgreSQL console:
psql
# In that console, create the database:
CREATE DATABASE thinking_sphinx;
You can then run the unit tests with @rake spec:unit@, the acceptance tests with @rake spec:acceptance@, or all of the tests with just @rake@. To run these with PostgreSQL, you'll need to set the @DATABASE@ environment variable accordingly:
DATABASE=postgresql rake
h2. Licence Copyright (c) 2007-2018, 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. thinking-sphinx-4.1.0/Rakefile000066400000000000000000000010041341132130100162630ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler' require 'appraisal' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new namespace :spec do desc 'Run unit specs only' RSpec::Core::RakeTask.new(:unit) do |task| task.pattern = 'spec' task.rspec_opts = '--tag "~live"' end desc 'Run acceptance specs only' RSpec::Core::RakeTask.new(:acceptance) do |task| task.pattern = 'spec' task.rspec_opts = '--tag "live"' end end task :default => :spec thinking-sphinx-4.1.0/bin/000077500000000000000000000000001341132130100153735ustar00rootroot00000000000000thinking-sphinx-4.1.0/bin/console000077500000000000000000000005751341132130100167720ustar00rootroot00000000000000#! /usr/bin/env ruby # frozen_string_literal: true 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__) thinking-sphinx-4.1.0/bin/loadsphinx000077500000000000000000000025661341132130100175030ustar00rootroot00000000000000#!/usr/bin/env bash version=$1 engine=$2 name="$engine-$version" bucket="thinking-sphinx" directory="ext/sphinx" prefix="`pwd`/$directory" file="ext/$name.tar.gz" if [ "$engine" == "sphinx" ]; then url="http://sphinxsearch.com/files/$name-release.tar.gz" else url="https://github.com/manticoresoftware/manticore.git" fi download_and_compile_source () { if [ "$engine" == "sphinx" ]; then download_and_compile_sphinx else download_and_compile_manticore fi } download_and_compile_sphinx () { 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 } download_and_compile_manticore () { git clone $url $engine cd $engine git checkout $version mkdir build cd build cmake -D WITH_MYSQL=TRUE -D WITH_PGSQL=TRUE -D DISABLE_TESTING=TRUE -D CMAKE_INSTALL_PREFIX=$prefix .. make -j4 make install cd ../.. rm -rf $engine } load_cache () { mkdir ext curl -o $file http://$bucket.s3.amazonaws.com/bincaches/$name.tar.gz tar -zxf $file } push_cache () { tar -czf $file $directory aws s3 cp $file s3://$bucket/bincaches/$name.tar.gz --acl public-read } if curl -i --head --fail http://$bucket.s3.amazonaws.com/bincaches/$name.tar.gz then load_cache else download_and_compile_source push_cache fi thinking-sphinx-4.1.0/lib/000077500000000000000000000000001341132130100153715ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking-sphinx.rb000066400000000000000000000000711341132130100210360ustar00rootroot00000000000000# frozen_string_literal: true require 'thinking_sphinx' thinking-sphinx-4.1.0/lib/thinking/000077500000000000000000000000001341132130100172045ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking/sphinx.rb000066400000000000000000000000711341132130100210400ustar00rootroot00000000000000# frozen_string_literal: true require 'thinking_sphinx' thinking-sphinx-4.1.0/lib/thinking_sphinx.rb000066400000000000000000000056451341132130100211340ustar00rootroot00000000000000# frozen_string_literal: true if RUBY_PLATFORM == 'java' require 'java' require 'jdbc/mysql' Jdbc::MySQL.load_driver else require 'mysql2' end require 'riddle' require 'riddle/2.1.0' require 'middleware' require 'active_record' require 'innertube' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/attribute_accessors' module ThinkingSphinx MAXIMUM_STATEMENT_LENGTH = (2 ** 23) - 5 def self.count(query = '', options = {}) search_for_ids(query, options).total_entries end def self.facets(query = '', options = {}) ThinkingSphinx::FacetSearch.new query, options end def self.search(query = '', options = {}) ThinkingSphinx::Search.new query, options end def self.search_for_ids(query = '', options = {}) search = ThinkingSphinx::Search.new query, options ThinkingSphinx::Search::Merger.new(search).merge! nil, :ids_only => true end def self.before_index_hooks @before_index_hooks end @before_index_hooks = [] def self.output @output end @output = STDOUT def self.rake_interface @rake_interface ||= ThinkingSphinx::RakeInterface end def self.rake_interface=(interface) @rake_interface = interface end module Hooks; end module IndexingStrategies; 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/commander' require 'thinking_sphinx/commands' require 'thinking_sphinx/configuration' require 'thinking_sphinx/connection' require 'thinking_sphinx/deletion' require 'thinking_sphinx/errors' require 'thinking_sphinx/excerpter' require 'thinking_sphinx/facet' require 'thinking_sphinx/facet_search' require 'thinking_sphinx/float_formatter' require 'thinking_sphinx/frameworks' require 'thinking_sphinx/guard' require 'thinking_sphinx/hooks/guard_presence' require 'thinking_sphinx/index' 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' require 'thinking_sphinx/masks' require 'thinking_sphinx/middlewares' require 'thinking_sphinx/panes' require 'thinking_sphinx/query' require 'thinking_sphinx/rake_interface' require 'thinking_sphinx/scopes' require 'thinking_sphinx/search' require 'thinking_sphinx/settings' require 'thinking_sphinx/subscribers/populator_subscriber' require 'thinking_sphinx/test' require 'thinking_sphinx/utf8' require 'thinking_sphinx/wildcard' # Extended require 'thinking_sphinx/active_record' require 'thinking_sphinx/deltas' require 'thinking_sphinx/distributed' require 'thinking_sphinx/logger' require 'thinking_sphinx/real_time' require 'thinking_sphinx/railtie' if defined?(Rails::Railtie) ThinkingSphinx.before_index_hooks << ThinkingSphinx::Hooks::GuardPresence thinking-sphinx-4.1.0/lib/thinking_sphinx/000077500000000000000000000000001341132130100205755ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record.rb000066400000000000000000000034651341132130100237430ustar00rootroot00000000000000# frozen_string_literal: true require 'active_record' require 'joiner' module ThinkingSphinx::ActiveRecord module Callbacks; end module Depolymorph; end end require 'thinking_sphinx/active_record/property' require 'thinking_sphinx/active_record/association' require 'thinking_sphinx/active_record/association_proxy' require 'thinking_sphinx/active_record/attribute' require 'thinking_sphinx/active_record/base' require 'thinking_sphinx/active_record/column' require 'thinking_sphinx/active_record/column_sql_presenter' require 'thinking_sphinx/active_record/database_adapters' require 'thinking_sphinx/active_record/field' require 'thinking_sphinx/active_record/index' require 'thinking_sphinx/active_record/interpreter' require 'thinking_sphinx/active_record/join_association' require 'thinking_sphinx/active_record/log_subscriber' require 'thinking_sphinx/active_record/polymorpher' require 'thinking_sphinx/active_record/property_query' require 'thinking_sphinx/active_record/property_sql_presenter' require 'thinking_sphinx/active_record/simple_many_query' require 'thinking_sphinx/active_record/source_joins' require 'thinking_sphinx/active_record/sql_builder' require 'thinking_sphinx/active_record/sql_source' require 'thinking_sphinx/active_record/callbacks/delete_callbacks' require 'thinking_sphinx/active_record/callbacks/delta_callbacks' require 'thinking_sphinx/active_record/callbacks/update_callbacks' require 'thinking_sphinx/active_record/depolymorph/base_reflection' require 'thinking_sphinx/active_record/depolymorph/association_reflection' require 'thinking_sphinx/active_record/depolymorph/conditions_reflection' require 'thinking_sphinx/active_record/depolymorph/overridden_reflection' require 'thinking_sphinx/active_record/depolymorph/scoped_reflection' require 'thinking_sphinx/active_record/filter_reflection' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/000077500000000000000000000000001341132130100234065ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/association.rb000066400000000000000000000004231341132130100262460ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Association def initialize(column) @column = column end def stack @column.__stack + [@column.__name] end def string? @column.is_a?(String) end def to_s @column.to_s end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/association_proxy.rb000066400000000000000000000017241341132130100275140ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::ActiveRecord::AssociationProxy extend ActiveSupport::Concern def search(query = nil, options = {}) perform_search super(*normalise_search_arguments(query, options)) end def search_for_ids(query = nil, options = {}) perform_search super(*normalise_search_arguments(query, options)) end private def normalise_search_arguments(query, options) query, options = nil, query if query.is_a?(Hash) options[:ignore_scopes] = true [query, options] end def perform_search(searcher) ThinkingSphinx::Search::Merger.new(searcher).merge! nil, :with => association_filter end def association_filter attribute = AttributeFinder.new(proxy_association).attribute {attribute.name.to_sym => proxy_association.owner.id} end end require 'thinking_sphinx/active_record/association_proxy/attribute_finder' require 'thinking_sphinx/active_record/association_proxy/attribute_matcher' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/association_proxy/000077500000000000000000000000001341132130100271635ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb000066400000000000000000000017761341132130100330550ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeFinder def initialize(association) @association = association end def attribute attributes.detect { |attribute| ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeMatcher.new( attribute, foreign_key ).matches? } or raise "Missing Attribute for Foreign Key #{foreign_key}" end private def attributes indices.collect(&:attributes).flatten end def configuration ThinkingSphinx::Configuration.instance end def foreign_key @foreign_key ||= reflection_target.foreign_key end def indices @indices ||= begin configuration.preload_indices configuration.indices_for_references( *ThinkingSphinx::IndexSet.reference_name(@association.klass) ).reject &:distributed? end end def reflection_target target = @association.reflection target = target.through_reflection if target.through_reflection target end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb000066400000000000000000000015741341132130100332250ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeMatcher def initialize(attribute, foreign_key) @attribute, @foreign_key = attribute, foreign_key.to_s end def matches? return false if many? column_name_matches? || attribute_name_matches? || multi_singular_match? end private attr_reader :attribute, :foreign_key delegate :name, :multi?, :to => :attribute def attribute_name_matches? name == foreign_key end def column_name_matches? column.__name.to_s == foreign_key end def column attribute.respond_to?(:columns) ? attribute.columns.first : attribute.column end def many? attribute.respond_to?(:columns) && attribute.columns.many? end def multi_singular_match? multi? && name.singularize == foreign_key end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/attribute.rb000066400000000000000000000011741341132130100257410ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Attribute < ThinkingSphinx::ActiveRecord::Property delegate :type, :type=, :multi?, :updateable?, :to => :typist delegate :value_for, :to => :values private def typist @typist ||= ThinkingSphinx::ActiveRecord::Attribute::Type.new self, @model end def values @values ||= ThinkingSphinx::ActiveRecord::Attribute::Values.new self end end require 'thinking_sphinx/active_record/attribute/sphinx_presenter' require 'thinking_sphinx/active_record/attribute/type' require 'thinking_sphinx/active_record/attribute/values' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/attribute/000077500000000000000000000000001341132130100254115ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb000066400000000000000000000017651341132130100313470ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Attribute::SphinxPresenter SPHINX_TYPES = { :integer => :uint, :boolean => :bool, :timestamp => :timestamp, :float => :float, :string => :string, :bigint => :bigint, :ordinal => :str2ordinal, :wordcount => :str2wordcount, :json => :json } def initialize(attribute, source) @attribute, @source = attribute, source end def collection_type @attribute.multi? ? :multi : sphinx_type end def declaration if @attribute.multi? multi_declaration else @attribute.name end end def sphinx_type SPHINX_TYPES[@attribute.type] end private def multi_declaration case @attribute.source_type when :query, :ranged_query query else "#{sphinx_type} #{@attribute.name} from field" end end def query ThinkingSphinx::ActiveRecord::PropertyQuery.new( @attribute, @source, sphinx_type ).to_s end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/attribute/type.rb000066400000000000000000000047411341132130100267250ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Attribute::Type UPDATEABLE_TYPES = [:integer, :timestamp, :boolean, :float] def initialize(attribute, model) @attribute, @model = attribute, model end def multi? @multi ||= attribute.options[:multi] || multi_from_associations end def timestamp? type == :timestamp end def type @type ||= attribute.options[:type] || type_from_database end def type=(value) @type = attribute.options[:type] = value end def updateable? UPDATEABLE_TYPES.include?(type) && single_column_reference? end private attr_reader :attribute, :model def associations @associations ||= begin klass = model attribute.columns.first.__stack.collect { |name| association = klass.reflect_on_association(name) klass = association.klass association } end end def big_integer? type_symbol == :integer && database_column.sql_type[/bigint/i] end def column_name attribute.columns.first.__name.to_s end def database_column @database_column ||= klass.columns.detect { |db_column| db_column.name == column_name } end def klass @klass ||= associations.any? ? associations.last.klass : model end def multi_from_associations associations.any? { |association| [:has_many, :has_and_belongs_to_many].include?(association.macro) } end def single_column_reference? attribute.columns.length == 1 && attribute.columns.first.__stack.length == 0 && !attribute.columns.first.string? end def type_from_database raise ThinkingSphinx::MissingColumnError, "Cannot determine the database type of column #{column_name}, as it does not exist" if database_column.nil? return :bigint if big_integer? case type_symbol when :datetime, :date :timestamp when :text :string when :decimal :float when :integer, :boolean, :timestamp, :float, :string, :bigint, :json type_symbol else raise ThinkingSphinx::UnknownAttributeType, <<-ERROR Unable to determine an equivalent Sphinx attribute type from #{database_column.type.class.name} for attribute #{attribute.name}. You may want to manually set the type. e.g. has my_column, :type => :integer ERROR end end def type_symbol return database_column.type if database_column.type.is_a?(Symbol) database_column.type.class.name.demodulize.downcase.to_sym end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/attribute/values.rb000066400000000000000000000006261341132130100272410ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Attribute::Values def initialize(attribute) @attribute = attribute end def value_for(instance) object = column.__stack.inject(instance) { |object, name| object.nil? ? nil : object.send(name) } object.nil? ? nil : object.send(column.__name) end private def column @attribute.columns.first end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/base.rb000066400000000000000000000034411341132130100246470ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::ActiveRecord::Base extend ActiveSupport::Concern included do after_destroy ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks before_save ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks after_update ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks after_commit ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks ::ActiveRecord::Associations::CollectionProxy.send :include, ThinkingSphinx::ActiveRecord::AssociationProxy end module ClassMethods def facets(query = nil, options = {}) merge_search ThinkingSphinx.facets, query, options end def search(query = nil, options = {}) merge_search ThinkingSphinx.search, query, options end def search_count(query = nil, options = {}) search_for_ids(query, options).total_entries end def search_for_ids(query = nil, options = {}) ThinkingSphinx::Search::Merger.new( search(query, options) ).merge! nil, :ids_only => true end private def default_sphinx_scope? respond_to?(:default_sphinx_scope) && default_sphinx_scope end def default_sphinx_scope_response [sphinx_scopes[default_sphinx_scope].call].flatten end def merge_search(search, query, options) merger = ThinkingSphinx::Search::Merger.new search merger.merge! *default_sphinx_scope_response if default_sphinx_scope? merger.merge! query, options if current_scope && !merger.search.options[:ignore_scopes] raise ThinkingSphinx::MixedScopesError, 'You cannot search with Sphinx through ActiveRecord scopes' end result = merger.merge! nil, :classes => [self] result.populate if result.options[:populate] result end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/callbacks/000077500000000000000000000000001341132130100253255ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb000066400000000000000000000011471341132130100311160ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks < ThinkingSphinx::Callbacks callbacks :after_destroy, :after_rollback def after_destroy delete_from_sphinx end def after_rollback delete_from_sphinx end private def delete_from_sphinx return if ThinkingSphinx::Callbacks.suspended? || instance.new_record? indices.each { |index| ThinkingSphinx::Deletion.perform index, instance.id } end def indices ThinkingSphinx::Configuration.instance.index_set_class.new( :classes => [instance.class] ).to_a end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb000066400000000000000000000025561341132130100307520ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks < ThinkingSphinx::Callbacks callbacks :after_commit, :before_save def after_commit return unless !suspended? && delta_indices? && toggled? delta_indices.each do |index| index.delta_processor.index index end core_indices.each do |index| index.delta_processor.delete index, instance end end def before_save return unless !ThinkingSphinx::Callbacks.suspended? && delta_indices? && new_or_changed? processors.each { |processor| processor.toggle instance } end private def config ThinkingSphinx::Configuration.instance end def core_indices @core_indices ||= indices.select(&:delta_processor).reject(&:delta?) end def delta_indices @delta_indices ||= indices.select &:delta? end def delta_indices? delta_indices.any? end def indices @indices ||= config.index_set_class.new(:classes => [instance.class]). select { |index| index.type == "plain" } end def new_or_changed? instance.new_record? || instance.changed? end def processors delta_indices.collect &:delta_processor end def suspended? ThinkingSphinx::Callbacks.suspended? || ThinkingSphinx::Deltas.suspended? end def toggled? processors.any? { |processor| processor.toggled?(instance) } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb000066400000000000000000000035411341132130100311360ustar00rootroot00000000000000# frozen_string_literal: true 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 return unless !ThinkingSphinx::Callbacks.suspended? && updates_enabled? indices.each do |index| update index unless index.distributed? end end private def attributes_hash_for(index) updateable_attributes_for(index).inject({}) do |hash, attribute| if changed_attributes.include?(attribute.columns.first.__name.to_s) hash[attribute.name] = attribute.value_for(instance) end hash end end def changed_attributes @changed_attributes ||= CHANGED_ATTRIBUTES.call instance end def configuration ThinkingSphinx::Configuration.instance end def indices @indices ||= begin all = configuration.indices_for_references(reference) all.reject { |index| index.type == 'rt' } end end def reference ThinkingSphinx::IndexSet.reference_name(instance.class) end def update(index) attributes = attributes_hash_for(index) return if attributes.empty? sphinxql = Riddle::Query.update( index.name, index.document_id_for_key(instance.id), attributes ) ThinkingSphinx::Connection.take do |connection| connection.execute(sphinxql) end rescue ThinkingSphinx::ConnectionError => error # This isn't vital, so don't raise the error. end def updateable_attributes_for(index) index.sources.collect(&:attributes).flatten.select { |attribute| attribute.updateable? } end def updates_enabled? configuration.settings['attribute_updates'] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/column.rb000066400000000000000000000012411341132130100252260ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Column def initialize(*stack) @stack = stack @name = stack.pop end def __name @name end def __path @stack + [@name] end def __replace(stack, replacements) return [self] if string? || __stack[0..stack.length-1] != stack replacements.collect { |replacement| self.class.new *(replacement + __stack[stack.length..-1]), __name } end def __stack @stack end def string? __name.is_a?(String) end def to_ary [self] end private def method_missing(method, *args, &block) @stack << @name @name = method self end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/column_sql_presenter.rb000066400000000000000000000020731341132130100302000ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::ColumnSQLPresenter def initialize(model, column, adapter, associations) @model, @column, @adapter, @associations = model, column, adapter, associations end def aggregate? path.aggregate? rescue Joiner::AssociationNotFound false end def with_table return __name if string? return nil unless exists? quoted_table = escape_table? ? escape_table(table) : table "#{quoted_table}.#{adapter.quote __name}" end private attr_reader :model, :column, :adapter, :associations delegate :__stack, :__name, :string?, :to => :column def escape_table(table_name) table_name.split('.').map { |t| adapter.quote(t) }.join('.') end def escape_table? table[/[`"]/].nil? end def exists? path.model.column_names.include?(column.__name.to_s) rescue Joiner::AssociationNotFound false end def path Joiner::Path.new model, column.__stack end def table associations.alias_for __stack end def version ActiveRecord::VERSION end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/database_adapters.rb000066400000000000000000000031101341132130100273550ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::ActiveRecord::DatabaseAdapters class << self attr_accessor :default def adapter_for(model) return default.new(model) if default adapter = adapter_type_for(model) klass = case adapter when :mysql MySQLAdapter when :postgresql PostgreSQLAdapter else raise ThinkingSphinx::InvalidDatabaseAdapter, "Invalid adapter '#{adapter}': Thinking Sphinx only supports MySQL and PostgreSQL." end klass.new model end def adapter_type_for(model) class_name = model.connection.class.name case class_name.split('::').last when 'MysqlAdapter', 'Mysql2Adapter' :mysql when 'PostgreSQLAdapter' :postgresql when 'JdbcAdapter' adapter_type_for_jdbc(model) else class_name end end def adapter_type_for_jdbc(model) case adapter = model.connection.config[:adapter] when 'jdbcmysql' :mysql when 'jdbcpostgresql' :postgresql when 'jdbc' adapter_type_for_jdbc_plain(adapter, model.connection.config[:url]) else adapter end end def adapter_type_for_jdbc_plain(adapter, url) return adapter unless match = /^jdbc:(?mysql|postgresql):\/\//.match(url) match[:adapter].to_sym end end end require 'thinking_sphinx/active_record/database_adapters/abstract_adapter' require 'thinking_sphinx/active_record/database_adapters/mysql_adapter' require 'thinking_sphinx/active_record/database_adapters/postgresql_adapter' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/database_adapters/000077500000000000000000000000001341132130100270355ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb000066400000000000000000000005041341132130100326640ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter def initialize(model) @model = model end def quote(column) @model.connection.quote_column_name(column) end def quoted_table_name @model.quoted_table_name end def utf8_query_pre [] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb000066400000000000000000000016661341132130100322400ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter < ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter def boolean_value(value) value ? 1 : 0 end def cast_to_bigint(clause) "CAST(#{clause} AS UNSIGNED INTEGER)" end def cast_to_string(clause) "CAST(#{clause} AS char)" end def cast_to_timestamp(clause) "UNIX_TIMESTAMP(#{clause})" end def concatenate(clause, separator = ' ') "CONCAT_WS('#{separator}', #{clause})" end def convert_nulls(clause, default = '') "IFNULL(#{clause}, #{default})" end def convert_blank(clause, default = '') "COALESCE(NULLIF(#{clause}, ''), #{default})" end def group_concatenate(clause, separator = ' ') "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')" end def time_zone_query_pre ["SET TIME_ZONE = '+0:00'"] end def utf8_query_pre ['SET NAMES utf8'] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb000066400000000000000000000021241341132130100332640ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter < ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter def boolean_value(value) value ? 'TRUE' : 'FALSE' end def cast_to_bigint(clause) "#{clause}::bigint" end def cast_to_string(clause) "#{clause}::varchar" end def cast_to_timestamp(clause) if ThinkingSphinx::Configuration.instance.settings['64bit_timestamps'] "extract(epoch from #{clause})::bigint" else "extract(epoch from #{clause})::int" end end def concatenate(clause, separator = ' ') clause.split(', ').collect { |part| convert_nulls(part, "''") }.join(" || '#{separator}' || ") end def convert_nulls(clause, default = '') "COALESCE(#{clause}, #{default})" end def convert_blank(clause, default = '') "COALESCE(NULLIF(#{clause}, ''), #{default})" end def group_concatenate(clause, separator = ' ') "array_to_string(array_agg(DISTINCT #{clause}), '#{separator}')" end def time_zone_query_pre ['SET TIME ZONE UTC'] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/000077500000000000000000000000001341132130100257505ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/association_reflection.rb000066400000000000000000000016031341132130100330230ustar00rootroot00000000000000# frozen_string_literal: true # This custom association approach is only available in Rails 4.1-5.1. This # behaviour is superseded by OverriddenReflection for Rails 5.2, and was # preceded by ScopedReflection for Rails 4.0. class ThinkingSphinx::ActiveRecord::Depolymorph::AssociationReflection < ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection # Since Rails 4.2, the macro argument has been removed. The underlying # behaviour remains the same, though. def call if explicit_macro? klass.new name, nil, options, reflection.active_record else klass.new reflection.macro, name, nil, options, reflection.active_record end end private def explicit_macro? ActiveRecord::Reflection::MacroReflection.instance_method(:initialize). arity == 4 end def options super @options[:sphinx_internal_filtered] = true @options end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/base_reflection.rb000066400000000000000000000012151341132130100314200ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection def initialize(reflection, name, class_name) @reflection = reflection @name = name @class_name = class_name @options = reflection.options.clone end def call # Should be implemented by subclasses. end private attr_reader :reflection, :name, :class_name def klass reflection.class end def options @options.delete :polymorphic @options[:class_name] = class_name @options[:foreign_key] ||= "#{reflection.name}_id" @options[:foreign_type] = reflection.foreign_type @options end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/conditions_reflection.rb000066400000000000000000000017321341132130100326630ustar00rootroot00000000000000# frozen_string_literal: true # The conditions approach is only available in Rails 3. This behaviour is # superseded by ScopedReflection for Rails 4.0. class ThinkingSphinx::ActiveRecord::Depolymorph::ConditionsReflection < ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection def call klass.new reflection.macro, name, options, active_record end private delegate :foreign_type, :active_record, :to => :reflection def condition "::ts_join_alias::.#{quoted_foreign_type} = '#{class_name}'" end def options super case @options[:conditions] when nil @options[:conditions] = condition when Array @options[:conditions] << condition when Hash @options[:conditions].merge! foreign_type => @options[:class_name] else @options[:conditions] = "#{@options[:conditions]} AND #{condition}" end @options end def quoted_foreign_type active_record.connection.quote_column_name foreign_type end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/overridden_reflection.rb000066400000000000000000000015561341132130100326570ustar00rootroot00000000000000# frozen_string_literal: true # This overriding approach is only available in Rails 5.2+. This behaviour # was preceded by AssociationReflection for Rails 4.1-5.1. class ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection < ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection module JoinConstraint def build_join_constraint(table, foreign_table) super.and( foreign_table[options[:foreign_type]].eq( options[:class_name].constantize.base_class.name ) ) end end def self.overridden_classes @overridden_classes ||= {} end def call klass.new name, nil, options, reflection.active_record end private def klass self.class.overridden_classes[reflection.class] ||= begin subclass = Class.new reflection.class subclass.include JoinConstraint subclass end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/depolymorph/scoped_reflection.rb000066400000000000000000000013501341132130100317630ustar00rootroot00000000000000# frozen_string_literal: true # This scoped approach is only available in Rails 4.0. This behaviour is # superseded by AssociationReflection for Rails 4.1, and was preceded by # ConditionsReflection for Rails 3.2. class ThinkingSphinx::ActiveRecord::Depolymorph::ScopedReflection < ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection def call klass.new reflection.macro, name, scope, options, reflection.active_record end private def scope lambda { |association| reflection = association.reflection klass = reflection.class_name.constantize where( association.parent.aliased_table_name.to_sym => {reflection.foreign_type => klass.base_class.name} ) } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/field.rb000066400000000000000000000004731341132130100250220ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Field < ThinkingSphinx::ActiveRecord::Property include ThinkingSphinx::Core::Field def file? options[:file] end def with_attribute? options[:sortable] || options[:facet] end def wordcount? options[:wordcount] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/filter_reflection.rb000066400000000000000000000011251341132130100274310ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::FilterReflection ReflectionGenerator = case ActiveRecord::VERSION::STRING.to_f when 5.2..7.0 ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection when 4.1..5.1 ThinkingSphinx::ActiveRecord::Depolymorph::AssociationReflection when 4.0 ThinkingSphinx::ActiveRecord::Depolymorph::ScopedReflection when 3.2 ThinkingSphinx::ActiveRecord::Depolymorph::ConditionsReflection end def self.call(reflection, name, class_name) ReflectionGenerator.new(reflection, name, class_name).call end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/index.rb000066400000000000000000000026161341132130100250470ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Index < Riddle::Configuration::Index include ThinkingSphinx::Core::Index attr_reader :reference attr_writer :definition_block def append_source ThinkingSphinx::ActiveRecord::SQLSource.new( model, source_options.merge(:position => sources.length) ).tap do |source| sources << source end end def attributes sources.collect(&:attributes).flatten end def delta? @options[:delta?] end def delta_processor @options[:delta_processor].try(:new, adapter, @options[:delta_options] || {}) end def facets @facets ||= sources.collect(&:facets).flatten end def fields sources.collect(&:fields).flatten end def sources interpret_definition! super end def unique_attribute_names attributes.collect(&:name) end private def adapter @adapter ||= ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_for(model) end def interpreter ThinkingSphinx::ActiveRecord::Interpreter end def name_suffix @options[:delta?] ? 'delta' : 'core' end def source_options { :name => name, :offset => offset, :delta? => @options[:delta?], :delta_processor => @options[:delta_processor], :delta_options => @options[:delta_options], :primary_key => primary_key } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/interpreter.rb000066400000000000000000000036221341132130100263010ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Interpreter < ::ThinkingSphinx::Core::Interpreter def define_source(&block) @source = @index.append_source instance_eval &block end def group_by(*columns) __source.groupings += columns end def has(*columns) __source.attributes += build_properties( ::ThinkingSphinx::ActiveRecord::Attribute, columns ) end def indexes(*columns) __source.fields += build_properties( ::ThinkingSphinx::ActiveRecord::Field, columns ) end def join(*columns) __source.associations += columns.collect { |column| ::ThinkingSphinx::ActiveRecord::Association.new column } end def polymorphs(column, options) __source.polymorphs << ::ThinkingSphinx::ActiveRecord::Polymorpher.new( __source, column, options[:to] ) end def sanitize_sql(*arguments) __source.model.send :sanitize_sql, *arguments end def set_database(hash_or_key) configuration = hash_or_key.is_a?(::Hash) ? hash_or_key : ::ActiveRecord::Base.configurations[hash_or_key.to_s] __source.set_database_settings configuration.symbolize_keys end def set_property(properties) properties.each do |key, value| @index.send("#{key}=", value) if @index.class.settings.include?(key) __source.send("#{key}=", value) if __source.class.settings.include?(key) __source.options[key] = value if source_option?(key) @index.options[key] = value if search_option?(key) end end def where(*conditions) __source.conditions += conditions end private def __source @source ||= @index.append_source end def build_properties(klass, columns) options = columns.extract_options! columns.collect { |column| klass.new(__source.model, column, options) } end def source_option?(key) ::ThinkingSphinx::ActiveRecord::SQLSource::OPTIONS.include?(key) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/join_association.rb000066400000000000000000000007141341132130100272700ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = super constraint = constraint.and( foreign_table[reflection.options[:foreign_type]].eq( base_klass.base_class.name ) ) if reflection.options[:sphinx_internal_filtered] constraint end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/log_subscriber.rb000066400000000000000000000013261341132130100267410ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::LogSubscriber < ActiveSupport::LogSubscriber def guard(event) identifier = color 'Sphinx', GREEN, true warn " #{identifier} #{event.payload[:guard]}" end def message(event) identifier = color 'Sphinx', GREEN, true debug " #{identifier} #{event.payload[:message]}" end def query(event) identifier = color('Sphinx Query (%.1fms)' % event.duration, GREEN, true) debug " #{identifier} #{event.payload[:query]}" end def caution(event) identifier = color 'Sphinx', GREEN, true warn " #{identifier} #{event.payload[:caution]}" end end ThinkingSphinx::ActiveRecord::LogSubscriber.attach_to :thinking_sphinx thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/polymorpher.rb000066400000000000000000000027561341132130100263250ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Polymorpher def initialize(source, column, class_names) @source, @column, @class_names = source, column, class_names end def morph! append_reflections morph_properties end private attr_reader :source, :column, :class_names def append_reflections mappings.each do |class_name, name| next if klass.reflect_on_association(name) reflection = clone_with name, class_name if ActiveRecord::Reflection.respond_to?(:add_reflection) ActiveRecord::Reflection.add_reflection klass, name, reflection else klass.reflections[name] = reflection end end end def clone_with(name, class_name) ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, name, class_name ) end def mappings @mappings ||= class_names.inject({}) do |hash, class_name| hash[class_name] = "#{column.__name}_#{class_name.downcase}".to_sym hash end end def morphed_stacks @morphed_stacks ||= mappings.values.collect { |key| column.__stack + [key] } end def morph_properties (source.fields + source.attributes).each do |property| property.rebase column.__path, :to => morphed_stacks end end def reflection @reflection ||= klass.reflect_on_association column.__name end def klass @klass ||= column.__stack.inject(source.model) { |parent, key| parent.reflect_on_association(key).klass } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/property.rb000066400000000000000000000012601341132130100256160ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::Property include ThinkingSphinx::Core::Property attr_reader :model, :columns, :options def initialize(model, columns, options = {}) @model, @options = model, options @columns = Array(columns).collect { |column| column.respond_to?(:__name) ? column : ThinkingSphinx::ActiveRecord::Column.new(column) } end def rebase(associations, options) @columns = columns.inject([]) do |array, column| array + column.__replace(associations, options[:to]) end end def name (options[:as] || columns.first.__name).to_s end def source_type options[:source] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/property_query.rb000066400000000000000000000067221341132130100270530ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::PropertyQuery def initialize(property, source, type = nil) @property, @source, @type = property, source, type end def to_s if unsafe_habtm_column? raise <<-MESSAGE Source queries cannot be used with HABTM joins if they use anything beyond the primary key. MESSAGE end if safe_habtm_column? ThinkingSphinx::ActiveRecord::SimpleManyQuery.new( property, source, type ).to_s else "#{identifier} from #{source_type}; #{queries.join('; ')}" end end private attr_reader :property, :source, :type delegate :unscoped, :to => :base_association_class, :prefix => true def base_association reflections.first end def base_association_class base_association.klass end def column @column ||= property.columns.first end def extend_reflection(reflection) return [reflection] unless reflection.through_reflection [reflection.through_reflection, reflection.source_reflection] end def identifier [type, property.name].compact.join(' ') end def joins @joins ||= begin remainder = reflections.collect(&:name)[1..-1] return nil if remainder.empty? return remainder.first if remainder.length == 1 remainder[0..-2].reverse.inject(remainder.last) { |value, key| {key => value} } end end def macros reflections.collect &:macro end def offset "* #{ThinkingSphinx::Configuration.instance.indices.count} + #{source.offset}" end def queries queries = [] if column.string? queries << column.__name.strip.gsub(/\n/, "\\\n") else queries << to_sql queries << range_sql if ranged? end queries end def quoted_foreign_key quote_with_table(base_association_class.table_name, base_association.foreign_key) end def quoted_primary_key quote_with_table(reflections.last.klass.table_name, column.__name) end def quote_with_table(table, column) "#{quote_column(table)}.#{quote_column(column)}" end def quote_column(column) ActiveRecord::Base.connection.quote_column_name(column) end def ranged? property.source_type == :ranged_query end def range_sql base_association_class_unscoped.select( "MIN(#{quoted_foreign_key}), MAX(#{quoted_foreign_key})" ).to_sql end def reflections @reflections ||= begin base = source.model column.__stack.collect { |key| reflection = base.reflect_on_association key base = reflection.klass extend_reflection reflection }.flatten end end def safe_habtm_column? macros == [:has_and_belongs_to_many] && column.__name == :id end def source_type property.source_type.to_s.dasherize end def to_sql raise "Could not determine SQL for MVA" if reflections.empty? relation = base_association_class_unscoped.select("#{quoted_foreign_key} #{offset} AS #{quote_column('id')}, #{quoted_primary_key} AS #{quote_column(property.name)}") relation = relation.joins(joins) if joins.present? relation = relation.where("#{quoted_foreign_key} BETWEEN $start AND $end") if ranged? relation = relation.where("#{quoted_foreign_key} IS NOT NULL") relation = relation.order("#{quoted_foreign_key} ASC") if type.nil? relation.to_sql end def unsafe_habtm_column? macros.include?(:has_and_belongs_to_many) && ( macros.length > 1 || column.__name != :id ) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/property_sql_presenter.rb000066400000000000000000000036661341132130100306000ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::PropertySQLPresenter attr_reader :property, :adapter, :associations def initialize(property, adapter, associations) @property, @adapter, @associations = property, adapter, associations end def to_group return nil if sourced_by_query? || !group? columns_with_table end def to_select return nil if sourced_by_query? "#{casted_column_with_table} AS #{adapter.quote property.name}" end private delegate :multi?, :to => :property def aggregate? column_presenters.any? &:aggregate? end def aggregate_separator multi? ? ',' : ' ' end def cast_to_timestamp(clause) return adapter.cast_to_timestamp clause if property.columns.any?(&:string?) clause.split(', ').collect { |part| adapter.cast_to_timestamp part }.join(', ') end def casted_column_with_table clause = columns_with_table clause = cast_to_timestamp clause if property.type == :timestamp clause = concatenate clause if aggregate? clause = adapter.group_concatenate(clause, aggregate_separator) end clause end def column_presenters @column_presenters ||= property.columns.collect { |column| ThinkingSphinx::ActiveRecord::ColumnSQLPresenter.new( property.model, column, adapter, associations ) } end def columns_with_table column_presenters.collect(&:with_table).compact.join(', ') end def concatenating? property.columns.length > 1 end def concatenate(clause) return clause unless concatenating? if property.type.nil? adapter.concatenate clause, ' ' else clause = clause.split(', ').collect { |part| adapter.cast_to_string part }.join(', ') adapter.concatenate clause, ',' end end def group? !(aggregate? || property.columns.any?(&:string?)) end def sourced_by_query? property.source_type.to_s[/query/] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/simple_many_query.rb000066400000000000000000000017701341132130100275020ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::SimpleManyQuery < ThinkingSphinx::ActiveRecord::PropertyQuery def to_s "#{identifier} from #{source_type}; #{queries.join('; ')}" end private def reflection @reflection ||= source.model.reflect_on_association column.__stack.first end def quoted_foreign_key quote_with_table reflection.join_table, reflection.foreign_key end def quoted_primary_key quote_with_table reflection.join_table, reflection.association_foreign_key end def range_sql "SELECT MIN(#{quoted_foreign_key}), MAX(#{quoted_foreign_key}) FROM #{quote_column reflection.join_table}" end def to_sql selects = [ "#{quoted_foreign_key} #{offset} AS #{quote_column('id')}", "#{quoted_primary_key} AS #{quote_column(property.name)}" ] sql = "SELECT #{selects.join(', ')} FROM #{quote_column reflection.join_table}" sql += " WHERE (#{quoted_foreign_key} BETWEEN $start AND $end)" if ranged? sql end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/source_joins.rb000066400000000000000000000023141341132130100264350ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::SourceJoins def self.call(model, source) new(model, source).call end def initialize(model, source) @model, @source = model, source end def call append_specified_associations append_property_associations joins end private attr_reader :model, :source def append_property_associations source.properties.collect(&:columns).each do |columns| columns.each { |column| append_column_associations column } end end def append_column_associations(column) return if column.__stack.empty? joins.add_join_to column.__stack if column_exists?(column) end def append_specified_associations source.associations.reject(&:string?).each do |association| joins.add_join_to association.stack end end def column_exists?(column) Joiner::Path.new(model, column.__stack).model true rescue Joiner::AssociationNotFound false end def joins @joins ||= begin joins = Joiner::Joins.new model if joins.respond_to?(:join_association_class) joins.join_association_class = ThinkingSphinx::ActiveRecord::JoinAssociation end joins end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_builder.rb000066400000000000000000000050241341132130100262410ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord class SQLBuilder attr_reader :source def initialize(source) @source = source end def sql_query statement.to_relation.to_sql.gsub(/\n/, "\\\n") end def sql_query_range return nil if source.disable_range? statement.to_query_range_relation.to_sql end def sql_query_pre query.to_query end private delegate :adapter, :model, :delta_processor, :to => :source delegate :convert_nulls, :time_zone_query_pre, :utf8_query_pre, :cast_to_bigint, :to => :adapter def query Query.new(self) end def statement Statement.new(self) end def config ThinkingSphinx::Configuration.instance end def relation model.unscoped end def associations @associations ||= ThinkingSphinx::ActiveRecord::SourceJoins.call( model, source ) end def quote_column(column) model.connection.quote_column_name(column) end def quoted_primary_key "#{model.quoted_table_name}.#{quote_column(source.primary_key)}" end def quoted_inheritance_column "#{model.quoted_table_name}.#{quote_column(model.inheritance_column)}" end def pre_select ('SQL_NO_CACHE ' if source.type == 'mysql').to_s end def big_document_ids? source.options[:big_document_ids] || config.settings['big_document_ids'] end def document_id quoted_alias = quote_column source.primary_key column = quoted_primary_key column = cast_to_bigint column if big_document_ids? column = "#{column} * #{config.indices.count} + #{source.offset}" "#{column} AS #{quoted_alias}" end def range_condition condition = [] condition << "#{quoted_primary_key} BETWEEN $start AND $end" unless source.disable_range? condition += source.conditions condition end def groupings groupings = source.groupings if model.column_names.include?(model.inheritance_column) groupings << quoted_inheritance_column end groupings end def model_name klass = model.name klass = klass.demodulize unless model.store_full_sti_class klass end end end end require 'thinking_sphinx/active_record/sql_builder/statement' require 'thinking_sphinx/active_record/sql_builder/query' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_builder/000077500000000000000000000000001341132130100257135ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb000066400000000000000000000007751341132130100312330ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord class SQLBuilder::ClauseBuilder def initialize(first_element) @clauses = [first_element] end def compose(*additions) additions.each &method(:add_clause) self end def add_clause(clause) @clauses += Array(clause) end def separated(by = ', ') clauses.flatten.compact.join(by) end private attr_reader :clauses end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_builder/query.rb000066400000000000000000000022621341132130100274070ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord class SQLBuilder::Query def initialize(report) self.report = report self.scope = [] end def to_query filter_by_query_pre scope.compact end protected attr_accessor :report, :scope def filter_by_query_pre scope_by_time_zone scope_by_delta_processor scope_by_session scope_by_utf8 end def scope_by_delta_processor return unless delta_processor && !source.delta? self.scope << delta_processor.reset_query end def scope_by_session return unless max_len = source.options[:group_concat_max_len] self.scope << "SET SESSION group_concat_max_len = #{max_len}" end def scope_by_time_zone return if config.settings['skip_time_zone'] self.scope += time_zone_query_pre end def scope_by_utf8 self.scope += utf8_query_pre if source.options[:utf8?] end def source report.source end def method_missing(*args, &block) report.send *args, &block end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_builder/statement.rb000066400000000000000000000070031341132130100302440ustar00rootroot00000000000000# frozen_string_literal: true require 'thinking_sphinx/active_record/sql_builder/clause_builder' module ThinkingSphinx module ActiveRecord class SQLBuilder::Statement def initialize(report) @report = report @scope = relation end def to_relation filter_by_scopes scope end def to_query_range_relation filter_by_query_range scope end def to_query_pre filter_by_query_pre scope end private attr_reader :report, :scope def custom_joins @custom_joins ||= source.associations.select(&:string?).collect(&:to_s) end def filter_by_query_range minimum = convert_nulls "MIN(#{quoted_primary_key})", 1 maximum = convert_nulls "MAX(#{quoted_primary_key})", 1 @scope = scope.select("#{minimum}, #{maximum}").where( where_clause(true) ) end def filter_by_scopes scope_by_select scope_by_where_clause scope_by_group_clause scope_by_joins scope_by_custom_joins scope_by_order end def attribute_presenters @attribute_presenters ||= property_sql_presenters_for source.attributes end def field_presenters @field_presenters ||= property_sql_presenters_for source.fields end def presenters_to_group(presenters) presenters.collect(&:to_group) end def presenters_to_select(presenters) presenters.collect(&:to_select) end def property_sql_presenters_for(properties) properties.collect { |property| property_sql_presenter_for(property) } end def property_sql_presenter_for(property) ThinkingSphinx::ActiveRecord::PropertySQLPresenter.new( property, source.adapter, associations ) end def scope_by_select @scope = scope.select(pre_select + select_clause) end def scope_by_where_clause @scope = scope.where where_clause end def scope_by_group_clause @scope = scope.group(group_clause) end def scope_by_joins @scope = scope.joins(associations.join_values) end def scope_by_custom_joins @scope = scope.joins(custom_joins) if custom_joins.any? end def scope_by_order @scope = scope.order('NULL') if source.type == 'mysql' end def source report.source end def method_missing(*args, &block) report.send *args, &block end def select_clause SQLBuilder::ClauseBuilder.new(document_id).compose( presenters_to_select(field_presenters), presenters_to_select(attribute_presenters) ).separated end def where_clause(for_range = false) builder = SQLBuilder::ClauseBuilder.new(nil) builder.add_clause delta_processor.clause(source.delta?) if delta_processor builder.add_clause range_condition unless for_range builder.separated(' AND ') end def group_clause builder = SQLBuilder::ClauseBuilder.new(quoted_primary_key) builder.compose( presenters_to_group(field_presenters), presenters_to_group(attribute_presenters) ) unless minimal_group_by? builder.compose(groupings).separated end def minimal_group_by? source.options[:minimal_group_by?] || config.settings['minimal_group_by?'] || config.settings['minimal_group_by'] end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_source.rb000066400000000000000000000102011341132130100261040ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord class SQLSource < Riddle::Configuration::SQLSource include ThinkingSphinx::Core::Settings attr_reader :model, :options attr_accessor :fields, :attributes, :associations, :conditions, :groupings, :polymorphs OPTIONS = [:name, :offset, :delta_processor, :delta?, :delta_options, :disable_range?, :group_concat_max_len, :utf8?, :position, :minimal_group_by?, :big_document_ids] def initialize(model, options = {}) @model = model @options = { :utf8? => (database_settings[:encoding].to_s[/^utf8/]) }.merge options @fields = [] @attributes = [] @associations = [] @conditions = [] @groupings = [] @polymorphs = [] Template.new(self).apply name = "#{options[:name] || model.name.downcase}_#{options[:position]}" super name, type apply_defaults! end def adapter @adapter ||= DatabaseAdapters.adapter_for(@model) end def delta_processor options[:delta_processor].try(:new, adapter, @options[:delta_options] || {}) end def delta? options[:delta?] end def disable_range? options[:disable_range?] end def facets properties.select(&:facet?) end def offset options[:offset] end def primary_key options[:primary_key] end def properties fields + attributes end def render prepare_for_render unless @prepared super end def set_database_settings(settings) @sql_host ||= settings[:host] || 'localhost' @sql_user ||= settings[:username] || settings[:user] || ENV['USER'] @sql_pass ||= settings[:password].to_s.gsub('#', '\#') @sql_db ||= settings[:database] @sql_port ||= settings[:port] @sql_sock ||= settings[:socket] @mysql_ssl_cert ||= settings[:sslcert] @mysql_ssl_key ||= settings[:sslkey] @mysql_ssl_ca ||= settings[:sslca] end def type @type ||= case adapter when DatabaseAdapters::MySQLAdapter 'mysql' when DatabaseAdapters::PostgreSQLAdapter 'pgsql' else raise UnknownDatabaseAdapter, "Provided type: #{adapter.class.name}" end end private def append_presenter_to_attribute_array attributes.each do |attribute| presenter = Attribute::SphinxPresenter.new(attribute, self) attribute_array_for(presenter.collection_type) << presenter.declaration end end def attribute_array_for(type) instance_variable_get "@sql_attr_#{type}".to_sym end def builder @builder ||= SQLBuilder.new self end def build_sql_fields fields.each do |field| @sql_field_string << field.name if field.with_attribute? @sql_field_str2wordcount << field.name if field.wordcount? @sql_file_field << field.name if field.file? @sql_joined_field << PropertyQuery.new(field, self).to_s if field.source_type end end def build_sql_query @sql_query = builder.sql_query @sql_query_range ||= builder.sql_query_range @sql_query_pre += builder.sql_query_pre end def config ThinkingSphinx::Configuration.instance end def database_settings @database_settings ||= begin if model.connection.respond_to?(:config) model.connection.config.clone else model.connection.instance_variable_get(:@config).clone end end end def prepare_for_render polymorphs.each &:morph! append_presenter_to_attribute_array set_database_settings database_settings build_sql_fields build_sql_query @prepared = true end end end end require 'thinking_sphinx/active_record/sql_source/template' thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_source/000077500000000000000000000000001341132130100255655ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/active_record/sql_source/template.rb000066400000000000000000000025741341132130100277350ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::ActiveRecord::SQLSource::Template attr_reader :source def initialize(source) @source = source end def apply add_field class_column, :sphinx_internal_class_name add_attribute primary_key, :sphinx_internal_id, nil add_attribute class_column, :sphinx_internal_class, :string, :facet => true add_attribute '0', :sphinx_deleted, :integer end private def add_attribute(column, name, type, options = {}) source.attributes << ThinkingSphinx::ActiveRecord::Attribute.new( source.model, ThinkingSphinx::ActiveRecord::Column.new(column), options.merge(:as => name, :type => type) ) end def add_field(column, name, options = {}) source.fields << ThinkingSphinx::ActiveRecord::Field.new( source.model, ThinkingSphinx::ActiveRecord::Column.new(column), options.merge(:as => name) ) end def class_column if inheriting? adapter = source.adapter quoted_column = "#{adapter.quoted_table_name}.#{adapter.quote(model.inheritance_column)}" source.adapter.convert_blank quoted_column, "'#{model.sti_name}'" else "'#{model.name}'" end end def inheriting? model.column_names.include?(model.inheritance_column) end def model source.model end def primary_key source.options[:primary_key].to_sym end end thinking-sphinx-4.1.0/lib/thinking_sphinx/attribute_types.rb000066400000000000000000000033611341132130100243540ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/batched_search.rb000066400000000000000000000006741341132130100240500ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::BatchedSearch attr_accessor :searches def initialize @searches = [] end def populate(middleware = ThinkingSphinx::Middlewares::DEFAULT) return if populated? || searches.empty? middleware.call contexts searches.each &:populated! @populated = true end private def contexts searches.collect &:context end def populated? @populated end end thinking-sphinx-4.1.0/lib/thinking_sphinx/callbacks.rb000066400000000000000000000010511341132130100230360ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Callbacks attr_reader :instance def self.callbacks(*methods) mod = Module.new methods.each do |method| mod.send(:define_method, method) { |instance| new(instance).send(method) } end extend mod end def self.resume! @suspended = false end def self.suspend(&block) suspend! yield resume! end def self.suspend! @suspended = true end def self.suspended? @suspended end def initialize(instance) @instance = instance end end thinking-sphinx-4.1.0/lib/thinking_sphinx/capistrano.rb000066400000000000000000000003701341132130100232650ustar00rootroot00000000000000# frozen_string_literal: true if defined?(Capistrano::VERSION) if Gem::Version.new(Capistrano::VERSION).release >= Gem::Version.new('3.0.0') recipe_version = 3 end end recipe_version ||= 2 require_relative "capistrano/v#{recipe_version}" thinking-sphinx-4.1.0/lib/thinking_sphinx/capistrano/000077500000000000000000000000001341132130100227405ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/capistrano/v2.rb000066400000000000000000000036131341132130100236170ustar00rootroot00000000000000# frozen_string_literal: true Capistrano::Configuration.instance(:must_exist).load do _cset(:thinking_sphinx_roles) { :db } _cset(:thinking_sphinx_options) { {:roles => fetch(:thinking_sphinx_roles)} } namespace :thinking_sphinx do desc 'Generate the Sphinx configuration file.' task :configure, fetch(:thinking_sphinx_options) do rake 'ts:configure' end desc 'Build Sphinx indexes into the shared path.' task :index, fetch(:thinking_sphinx_options) do rake 'ts:index' end desc 'Generate Sphinx indexes into the shared path.' task :generate, fetch(:thinking_sphinx_options) do rake 'ts:generate' end desc 'Start the Sphinx search daemon.' task :start, fetch(:thinking_sphinx_options) do rake 'ts:start' end before 'thinking_sphinx:start', 'thinking_sphinx:configure' desc 'Stop the Sphinx search daemon.' task :stop, fetch(:thinking_sphinx_options) do rake 'ts:stop' end desc 'Restart the Sphinx search daemon.' task :restart, fetch(:thinking_sphinx_options) do rake 'ts:stop ts:configure ts:start' end desc <<-DESC Stop, reindex, and then start the Sphinx search daemon. This task must be executed \ if you alter the structure of your indexes. DESC task :rebuild, fetch(:thinking_sphinx_options) do rake 'ts:rebuild' end desc 'Stop Sphinx, clear Sphinx index files, generate configuration file, start Sphinx, repopulate all data.' task :regenerate, fetch(:thinking_sphinx_options) do rake 'ts:regenerate' end def rake(tasks) rails_env = fetch(:rails_env, 'production') rake = fetch(:rake, 'rake') tasks += ' INDEX_ONLY=true' if ENV['INDEX_ONLY'] == 'true' run "if [ -d #{release_path} ]; then cd #{release_path}; else cd #{current_path}; fi; if [ -f Rakefile ]; then #{rake} RAILS_ENV=#{rails_env} #{tasks}; fi;" end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/capistrano/v3.rb000066400000000000000000000052401341132130100236160ustar00rootroot00000000000000# frozen_string_literal: true namespace :load do task :defaults do set :thinking_sphinx_roles, :db set :thinking_sphinx_rails_env, -> { fetch(:rails_env) || fetch(:stage) } end end namespace :thinking_sphinx do desc <<-DESC Stop, reindex, and then start the Sphinx search daemon. This task must be executed \ if you alter the structure of your indexes. DESC task :rebuild do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, "ts:rebuild" end end end end desc 'Stop Sphinx, clear Sphinx index files, generate configuration file, start Sphinx, repopulate all data.' task :regenerate do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:regenerate' end end end end desc 'Build Sphinx indexes into the shared path.' task :index do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:index' end end end end desc 'Generate Sphinx indexes into the shared path.' task :generate do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:generate' end end end end desc 'Restart the Sphinx search daemon.' task :restart do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do %w(stop configure start).each do |task| execute :rake, "ts:#{task}" end end end end end desc 'Start the Sphinx search daemon.' task :start do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:start' end end end end before :start, 'thinking_sphinx:configure' desc 'Generate the Sphinx configuration file.' task :configure do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:configure' end end end end desc 'Stop the Sphinx search daemon.' task :stop do on roles fetch(:thinking_sphinx_roles) do within current_path do with rails_env: fetch(:thinking_sphinx_rails_env) do execute :rake, 'ts:stop' end end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commander.rb000066400000000000000000000022151341132130100230670ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commander def self.call(command, configuration, options, stream = STDOUT) raise ThinkingSphinx::UnknownCommand unless registry.keys.include?(command) registry[command].call configuration, options, stream end def self.registry @registry ||= { :clear_real_time => ThinkingSphinx::Commands::ClearRealTime, :clear_sql => ThinkingSphinx::Commands::ClearSQL, :configure => ThinkingSphinx::Commands::Configure, :index_sql => ThinkingSphinx::Commands::IndexSQL, :index_real_time => ThinkingSphinx::Commands::IndexRealTime, :merge => ThinkingSphinx::Commands::Merge, :merge_and_update => ThinkingSphinx::Commands::MergeAndUpdate, :prepare => ThinkingSphinx::Commands::Prepare, :rotate => ThinkingSphinx::Commands::Rotate, :running => ThinkingSphinx::Commands::Running, :start_attached => ThinkingSphinx::Commands::StartAttached, :start_detached => ThinkingSphinx::Commands::StartDetached, :stop => ThinkingSphinx::Commands::Stop } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands.rb000066400000000000000000000013061341132130100227230ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Commands # end require 'thinking_sphinx/commands/base' require 'thinking_sphinx/commands/clear_real_time' require 'thinking_sphinx/commands/clear_sql' require 'thinking_sphinx/commands/configure' require 'thinking_sphinx/commands/index_sql' require 'thinking_sphinx/commands/index_real_time' require 'thinking_sphinx/commands/merge' require 'thinking_sphinx/commands/merge_and_update' require 'thinking_sphinx/commands/prepare' require 'thinking_sphinx/commands/rotate' require 'thinking_sphinx/commands/running' require 'thinking_sphinx/commands/start_attached' require 'thinking_sphinx/commands/start_detached' require 'thinking_sphinx/commands/stop' thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/000077500000000000000000000000001341132130100223765ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/base.rb000066400000000000000000000020761341132130100236420ustar00rootroot00000000000000# frozen_string_literal: true 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(command, extra_options = {}) ThinkingSphinx::Commander.call( command, configuration, options.merge(extra_options), stream ) end 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 || 1) end def log(message) return if options[:silent] stream.puts message end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/clear_real_time.rb000066400000000000000000000006621341132130100260360ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::ClearRealTime < ThinkingSphinx::Commands::Base def call options[:indices].each do |index| index.render Dir["#{index.path}.*"].each { |path| FileUtils.rm path } end FileUtils.rm_r(binlog_path) if File.exists?(binlog_path) end private def binlog_path configuration.searchd.binlog_path end def type 'clear_realtime' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/clear_sql.rb000066400000000000000000000005611341132130100246720ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::ClearSQL < ThinkingSphinx::Commands::Base def call options[:indices].each do |index| index.render Dir["#{index.path}.*"].each { |path| FileUtils.rm path } end FileUtils.rm_r Dir["#{configuration.indices_location}/ts-*.tmp"] end private def type 'clear_sql' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/configure.rb000066400000000000000000000004301341132130100247010ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/index_real_time.rb000066400000000000000000000004551341132130100260570ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::IndexRealTime < ThinkingSphinx::Commands::Base def call options[:indices].each do |index| ThinkingSphinx::RealTime::Populator.populate index command :rotate end end private def type 'indexing' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/index_sql.rb000066400000000000000000000010231341132130100247050ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::IndexSQL < ThinkingSphinx::Commands::Base def call if indices.empty? ThinkingSphinx.before_index_hooks.each { |hook| hook.call } end configuration.indexing_strategy.call(indices) do |index_names| configuration.guarding_strategy.call(index_names) do |names| controller.index *names, :verbose => options[:verbose] end end end private def indices options[:indices] || [] end def type 'indexing' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/merge.rb000066400000000000000000000010471341132130100240240ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::Merge < ThinkingSphinx::Commands::Base def call return unless indices_exist? controller.merge( options[:core_index].name, options[:delta_index].name, :filters => options[:filters], :verbose => options[:verbose] ) end private delegate :controller, :to => :configuration def indices_exist? File.exist?("#{options[:core_index].path}.spi") && File.exist?("#{options[:delta_index].path}.spi") end def type 'merging' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/merge_and_update.rb000066400000000000000000000024601341132130100262100ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::MergeAndUpdate < ThinkingSphinx::Commands::Base def call configuration.preload_indices configuration.render index_pairs.each do |(core_index, delta_index)| command :merge, :core_index => core_index, :delta_index => delta_index, :filters => {:sphinx_deleted => 0} core_index.model.where(:delta => true).update_all(:delta => false) end end private delegate :controller, :to => :configuration def core_indices indices.select { |index| !index.delta? }.select do |index| name_filters.empty? || name_filters.include?(index.name.gsub(/_core$/, '')) end end def delta_for(core_index) name = core_index.name.gsub(/_core$/, "_delta") indices.detect { |index| index.name == name } end def index_pairs core_indices.collect { |core_index| [core_index, delta_for(core_index)] } end def indices @indices ||= configuration.indices.select { |index| index.type == "plain" && index.options[:delta_processor] } end def indices_exist?(*indices) indices.all? { |index| File.exist?("#{index.path}.spi") } end def name_filters @name_filters ||= options[:index_names] || [] end def type 'merging_and_updating' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/prepare.rb000066400000000000000000000003351341132130100243620ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::Prepare < ThinkingSphinx::Commands::Base def call FileUtils.mkdir_p configuration.indices_location end private def type 'prepare' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/rotate.rb000066400000000000000000000002741341132130100242240ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::Rotate < ThinkingSphinx::Commands::Base def call controller.rotate end private def type 'rotate' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/running.rb000066400000000000000000000003001341132130100243740ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::Running < ThinkingSphinx::Commands::Base def call controller.running? end private def type 'running' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/start_attached.rb000066400000000000000000000007121341132130100257150ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/start_detached.rb000066400000000000000000000006471341132130100257100ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::StartDetached < ThinkingSphinx::Commands::Base def call FileUtils.mkdir_p configuration.indices_location result = controller.start :verbose => options[:verbose] if command :running log "Started searchd successfully (pid: #{controller.pid})." else handle_failure result end end private def type 'start' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/commands/stop.rb000066400000000000000000000006421341132130100237120ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Commands::Stop < ThinkingSphinx::Commands::Base def call unless command :running log 'searchd is not currently running.' return end pid = controller.pid until !command :running do controller.stop options sleep(0.5) end log "Stopped searchd daemon (pid: #{pid})." end private def type 'stop' end end thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration.rb000066400000000000000000000107561341132130100240020ustar00rootroot00000000000000# frozen_string_literal: true require 'pathname' class ThinkingSphinx::Configuration < Riddle::Configuration attr_accessor :configuration_file, :indices_location, :version, :batch_size attr_reader :index_paths attr_writer :controller, :index_set_class, :indexing_strategy, :guarding_strategy delegate :environment, :to => :framework @@mutex = Mutex.new def initialize super reset end def self.instance @instance ||= new end def self.reset @instance = nil end def bin_path settings['bin_path'] end def controller @controller ||= begin rc = Riddle::Controller.new self, configuration_file rc.bin_path = bin_path.gsub(/([^\/])$/, '\1/') if bin_path.present? rc end end def framework @framework ||= ThinkingSphinx::Frameworks.current end def framework=(framework) @framework = framework reset framework end def engine_index_paths return [] unless defined?(Rails) engine_indice_paths.flatten.compact.sort end def engine_indice_paths Rails::Engine.subclasses.collect(&:instance).collect do |engine| engine.paths.add 'app/indices' unless engine.paths['app/indices'] engine.paths['app/indices'].existent end end def guarding_strategy @guarding_strategy ||= ThinkingSphinx::Guard::Files end def index_set_class @index_set_class ||= ThinkingSphinx::IndexSet end def indexing_strategy @indexing_strategy ||= ThinkingSphinx::IndexingStrategies::AllAtOnce end def indices_for_references(*references) index_set_class.new(:references => references).to_a end def next_offset(reference) @offsets[reference] ||= @offsets.keys.count end def preload_indices @@mutex.synchronize do return if @preloaded_indices index_paths.each do |path| Dir["#{path}/**/*.rb"].sort.each do |file| ActiveSupport::Dependencies.require_or_load file end end normalise verify @preloaded_indices = true end end def render preload_indices super end def render_to_file FileUtils.mkdir_p searchd.binlog_path unless searchd.binlog_path.blank? open(configuration_file, 'w') { |file| file.write render } end def settings @settings ||= ThinkingSphinx::Settings.call self end def setup @configuration_file = settings['configuration_file'] @index_paths = engine_index_paths + [Pathname.new(framework.root).join('app', 'indices').to_s] @indices_location = settings['indices_location'] @version = settings['version'] || '2.2.11' @batch_size = settings['batch_size'] || 1000 if settings['common_sphinx_configuration'] common.common_sphinx_configuration = true indexer.common_sphinx_configuration = true end configure_searchd apply_sphinx_settings! @offsets = {} end 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 searchd.socket = "#{settings["socket"]}:mysql41" if socket? if tcp? searchd.address = settings['address'].presence || Defaults::ADDRESS searchd.mysql41 = settings['mysql41'] || settings['port'] || Defaults::PORT end searchd.mysql_version_string = '5.5.21' if RUBY_PLATFORM == 'java' end 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 reset @settings = nil setup end def socket? settings["socket"].present? end def sphinx_sections sections = [indexer, searchd] sections.unshift common if settings['common_sphinx_configuration'] sections end def tcp? settings["socket"].nil? || settings["address"].present? || settings["mysql41"].present? || settings["port"].present? end def verify ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile end end require 'thinking_sphinx/configuration/consistent_ids' require 'thinking_sphinx/configuration/defaults' require 'thinking_sphinx/configuration/distributed_indices' require 'thinking_sphinx/configuration/duplicate_names' require 'thinking_sphinx/configuration/minimum_fields' thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/000077500000000000000000000000001341132130100234445ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/consistent_ids.rb000066400000000000000000000013061341132130100270210ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Configuration::ConsistentIds def initialize(indices) @indices = indices end def reconcile return unless sphinx_internal_ids.any? { |attribute| attribute.type == :bigint } sphinx_internal_ids.each do |attribute| attribute.type = :bigint end end private def attributes @attributes = sources.collect(&:attributes).flatten end def sphinx_internal_ids @sphinx_internal_ids ||= attributes.select { |attribute| attribute.name == 'sphinx_internal_id' } end def sources @sources ||= @indices.select { |index| index.respond_to?(:sources) }.collect(&:sources).flatten end end thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/defaults.rb000066400000000000000000000002121341132130100255730ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Configuration::Defaults ADDRESS = '127.0.0.1' PORT = 9306 PANES = [] end thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/distributed_indices.rb000066400000000000000000000011711341132130100300110ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Configuration::DistributedIndices def initialize(indices) @indices = indices end def reconcile grouped_indices.each do |reference, indices| append distributed_index(reference, indices) end end private attr_reader :indices def append(index) ThinkingSphinx::Configuration.instance.indices << index end def distributed_index(reference, indices) index = ThinkingSphinx::Distributed::Index.new reference index.local_indices += indices.collect &:name index end def grouped_indices indices.group_by &:reference end end thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/duplicate_names.rb000066400000000000000000000013251341132130100271270ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Configuration::DuplicateNames def initialize(indices) @indices = indices end def reconcile indices.each do |index| return if index.distributed? counts_for(index).each do |name, count| next if count <= 1 raise ThinkingSphinx::DuplicateNameError, "Duplicate field/attribute name '#{name}' in index '#{index.name}'" end end end private attr_reader :indices def counts_for(index) names_for(index).inject({}) do |hash, name| hash[name] ||= 0 hash[name] += 1 hash end end def names_for(index) index.fields.collect(&:name) + index.attributes.collect(&:name) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/configuration/minimum_fields.rb000066400000000000000000000014211341132130100267700ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Configuration::MinimumFields def initialize(indices) @indices = indices end def reconcile return unless no_inheritance_columns? field_collections.each do |collection| collection.fields.delete_if do |field| field.name == 'sphinx_internal_class_name' end end end private 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.table_exists? && index.model.column_names.include?(index.model.inheritance_column) }.empty? end end thinking-sphinx-4.1.0/lib/thinking_sphinx/connection.rb000066400000000000000000000034001341132130100232560ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Connection MAXIMUM_RETRIES = 3 def self.new configuration = ThinkingSphinx::Configuration.instance options = { :host => configuration.searchd.address, :port => configuration.searchd.mysql41, :socket => configuration.searchd.socket, :reconnect => true }.merge(configuration.settings['connection_options'] || {}) connection_class.new options end def self.connection_class return ThinkingSphinx::Connection::JRuby if RUBY_PLATFORM == 'java' ThinkingSphinx::Connection::MRI end def self.pool @pool ||= Innertube::Pool.new( Proc.new { ThinkingSphinx::Connection.new }, Proc.new { |connection| connection.close! } ) end def self.take retries = 0 original = nil begin pool.take do |connection| begin yield connection rescue ThinkingSphinx::QueryExecutionError, connection.base_error => error original = ThinkingSphinx::SphinxError.new_from_mysql error retries += MAXIMUM_RETRIES if original.is_a?(ThinkingSphinx::QueryError) raise Innertube::Pool::BadResource end end rescue Innertube::Pool::BadResource retries += 1 raise original unless retries < MAXIMUM_RETRIES ActiveSupport::Notifications.instrument( "message.thinking_sphinx", :message => "Retrying query \"#{original.statement}\" after error: #{original.message}" ) retry end end def self.persistent? @persistent end def self.persistent=(persist) @persistent = persist end @persistent = true end require 'thinking_sphinx/connection/client' require 'thinking_sphinx/connection/jruby' require 'thinking_sphinx/connection/mri' thinking-sphinx-4.1.0/lib/thinking_sphinx/connection/000077500000000000000000000000001341132130100227345ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/connection/client.rb000066400000000000000000000030611341132130100245370ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Connection::Client def initialize(options) if options[:socket].present? options[:socket] = options[:socket].remove /:mysql41$/ options.delete :host options.delete :port else options.delete :socket # If you use localhost, MySQL insists on a socket connection, but in this # situation we want a TCP connection. Using 127.0.0.1 fixes that. if options[:host].nil? || options[:host] == "localhost" options[:host] = "127.0.0.1" end end @options = options end 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/connection/jruby.rb000066400000000000000000000025331341132130100244170ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Connection::JRuby < ThinkingSphinx::Connection::Client attr_reader :address, :options def initialize(options) options.delete :socket super @address = "jdbc:mysql://#{@options[:host]}:#{@options[:port]}/?allowMultiQueries=true" 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/connection/mri.rb000066400000000000000000000011201341132130100240420ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Connection::MRI < ThinkingSphinx::Connection::Client 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/core.rb000066400000000000000000000004111341132130100220460ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Core # end require 'thinking_sphinx/core/settings' require 'thinking_sphinx/core/field' require 'thinking_sphinx/core/index' require 'thinking_sphinx/core/interpreter' require 'thinking_sphinx/core/property' thinking-sphinx-4.1.0/lib/thinking_sphinx/core/000077500000000000000000000000001341132130100215255ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/core/field.rb000066400000000000000000000002411341132130100231320ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Core::Field def infixing? options[:infixes] end def prefixing? options[:prefixes] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/core/index.rb000066400000000000000000000037441341132130100231710ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Core::Index extend ActiveSupport::Concern include ThinkingSphinx::Core::Settings included do attr_reader :reference, :offset attr_writer :definition_block end def initialize(reference, options = {}) @reference = reference.to_sym @docinfo = :extern unless config.settings["skip_docinfo"] @options = options @offset = config.next_offset(options[:offset_as] || reference) @type = 'plain' super "#{options[:name] || reference.to_s.gsub('/', '_')}_#{name_suffix}" end def delta? false end 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 def interpret_definition! return unless model.table_exists? return if @interpreted_definition apply_defaults! @interpreted_definition = true interpreter.translate! self, @definition_block if @definition_block end def model @model ||= reference.to_s.camelize.constantize end def options interpret_definition! @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 assign_infix_fields assign_prefix_fields super end private def assign_infix_fields self.infix_fields = fields.select(&:infixing?).collect(&:name) end def assign_prefix_fields self.prefix_fields = fields.select(&:prefixing?).collect(&:name) end def config ThinkingSphinx::Configuration.instance end def name_suffix 'core' end def path_prefix options[:path] || config.indices_location end def pre_render interpret_definition! end def set_path FileUtils.mkdir_p path_prefix @path = File.join path_prefix, name end end thinking-sphinx-4.1.0/lib/thinking_sphinx/core/interpreter.rb000066400000000000000000000010441341132130100244140ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Core::Interpreter < BasicObject def self.translate!(index, block) new(index, block).translate! end def initialize(index, block) @index = index mod = ::Module.new mod.send :define_method, :translate!, block mod.send :extend_object, self end private def search_option?(key) ::ThinkingSphinx::Middlewares::SphinxQL::SELECT_OPTIONS.include? key end def method_missing(method, *args) ::ThinkingSphinx::ActiveRecord::Column.new method, *args end end thinking-sphinx-4.1.0/lib/thinking_sphinx/core/property.rb000066400000000000000000000002501341132130100237330ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Core::Property def facet? options[:facet] end def multi? false end def type nil end end thinking-sphinx-4.1.0/lib/thinking_sphinx/core/settings.rb000066400000000000000000000004251341132130100237130ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Core::Settings private def apply_defaults!(defaults = self.class.settings) defaults.each do |setting| value = config.settings[setting.to_s] send("#{setting}=", value) unless value.nil? end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/deletion.rb000066400000000000000000000027541341132130100227350ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Deletion delegate :name, :to => :index def self.perform(index, ids) return if index.distributed? { 'plain' => PlainDeletion, 'rt' => RealtimeDeletion }[index.type].new(index, ids).perform rescue ThinkingSphinx::ConnectionError => error # This isn't vital, so don't raise the error. end def initialize(index, ids) @index, @ids = index, Array(ids) end private attr_reader :index, :ids def document_ids_for_keys ids.collect { |id| index.document_id_for_key id } end def 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 class RealtimeDeletion < ThinkingSphinx::Deletion def perform return unless callbacks_enabled? execute Riddle::Query::Delete.new(name, document_ids_for_keys).to_sql end private def callbacks_enabled? setting = configuration.settings['real_time_callbacks'] setting.nil? || setting end def configuration ThinkingSphinx::Configuration.instance end end class PlainDeletion < ThinkingSphinx::Deletion def perform document_ids_for_keys.each_slice(1000) do |document_ids| execute <<-SQL UPDATE #{name} SET sphinx_deleted = 1 WHERE id IN (#{document_ids.join(', ')}) SQL end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/deltas.rb000066400000000000000000000021471341132130100224020ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Deltas def self.config ThinkingSphinx::Configuration.instance end def self.processor_for(delta) case delta when TrueClass ThinkingSphinx::Deltas::DefaultDelta when Class delta when String delta.constantize else nil end end def self.resume! @suspended = false end def self.suspend(reference, &block) suspend! yield resume! config.indices_for_references(reference).each do |index| index.delta_processor.index index if index.delta? end end def self.suspend_and_update(reference, &block) suspend reference, &block ids = reference.to_s.camelize.constantize.where(delta: true).pluck(:id) config.indices_for_references(reference).each do |index| ThinkingSphinx::Deletion.perform index, ids unless index.delta? end end def self.suspend! @suspended = true end def self.suspended? @suspended end end require 'thinking_sphinx/deltas/default_delta' require 'thinking_sphinx/deltas/delete_job' require 'thinking_sphinx/deltas/index_job' thinking-sphinx-4.1.0/lib/thinking_sphinx/deltas/000077500000000000000000000000001341132130100220515ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/deltas/default_delta.rb000066400000000000000000000022331341132130100251730ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Deltas::DefaultDelta attr_reader :adapter, :options def initialize(adapter, options = {}) @adapter, @options = adapter, options end def clause(delta_source = false) return nil unless delta_source "#{adapter.quoted_table_name}.#{quoted_column} = #{adapter.boolean_value delta_source}" end def delete(index, instance) ThinkingSphinx::Deltas::DeleteJob.new( index.name, index.document_id_for_instance(instance) ).perform end def index(index) ThinkingSphinx::Deltas::IndexJob.new(index.name).perform end def reset_query (<<-SQL).strip.gsub(/\n\s*/, ' ') UPDATE #{adapter.quoted_table_name} SET #{quoted_column} = #{adapter.boolean_value false} WHERE #{quoted_column} = #{adapter.boolean_value true} SQL end def toggle(instance) instance.send "#{column}=", true end def toggled?(instance) instance.send "#{column}?" end private def column options[:column] || :delta end def config ThinkingSphinx::Configuration.instance end def controller config.controller end def quoted_column adapter.quote column end end thinking-sphinx-4.1.0/lib/thinking_sphinx/deltas/delete_job.rb000066400000000000000000000011701341132130100244710ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Deltas::DeleteJob def initialize(index_name, document_id) @index_name, @document_id = index_name, document_id end def perform 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/deltas/index_job.rb000066400000000000000000000010431341132130100243350ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Deltas::IndexJob def initialize(index_name) @index_name = index_name end def perform ThinkingSphinx::Commander.call( :index_sql, configuration, :indices => [index_name], :verbose => !quiet_deltas? ) end private attr_reader :index_name def configuration @configuration ||= ThinkingSphinx::Configuration.instance end def quiet_deltas? configuration.settings['quiet_deltas'].nil? || configuration.settings['quiet_deltas'] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/distributed.rb000066400000000000000000000001671341132130100234500ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Distributed # end require 'thinking_sphinx/distributed/index' thinking-sphinx-4.1.0/lib/thinking_sphinx/distributed/000077500000000000000000000000001341132130100231175ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/distributed/index.rb000066400000000000000000000011121341132130100245460ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Distributed::Index < Riddle::Configuration::DistributedIndex attr_reader :reference, :options def initialize(reference) @reference = reference @options = {} super reference.to_s.gsub('/', '_') end def delta? false end def distributed? true end 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 thinking-sphinx-4.1.0/lib/thinking_sphinx/errors.rb000066400000000000000000000053121341132130100224370ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::SphinxError < StandardError attr_accessor :statement def self.new_from_mysql(error) case error.message when /parse error/, /query is non-computable/ replacement = ThinkingSphinx::ParseError.new(error.message) when /syntax error/ replacement = ThinkingSphinx::SyntaxError.new(error.message) when /query error/ replacement = ThinkingSphinx::QueryError.new(error.message) when /Can't connect to MySQL server/, /Communications link failure/, /Lost connection to MySQL server/ replacement = ThinkingSphinx::ConnectionError.new( "Error connecting to Sphinx via the MySQL protocol. #{error.message}" ) when /offset out of bounds/ replacement = ThinkingSphinx::OutOfBoundsError.new(error.message) else replacement = new(error.message) end replacement.set_backtrace error.backtrace replacement.statement = error.statement if error.respond_to?(:statement) replacement end end class ThinkingSphinx::ConnectionError < ThinkingSphinx::SphinxError end class ThinkingSphinx::QueryError < ThinkingSphinx::SphinxError end class ThinkingSphinx::QueryLengthError < ThinkingSphinx::SphinxError def message <<-MESSAGE The supplied SphinxQL statement is #{statement.length} characters long. The maximum allowed length is #{ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH}. If this error has been raised during real-time index population, it's probably due to overly large batches of records being processed at once. The default is 1000, but you can lower it on a per-environment basis in config/thinking_sphinx.yml: development: batch_size: 500 MESSAGE end end class ThinkingSphinx::SyntaxError < ThinkingSphinx::QueryError end class ThinkingSphinx::ParseError < ThinkingSphinx::QueryError end class ThinkingSphinx::OutOfBoundsError < ThinkingSphinx::QueryError end class ThinkingSphinx::QueryExecutionError < StandardError attr_accessor :statement end class ThinkingSphinx::MixedScopesError < StandardError end class ThinkingSphinx::NoIndicesError < StandardError end class ThinkingSphinx::MissingColumnError < StandardError end class ThinkingSphinx::PopulatedResultsError < StandardError end class ThinkingSphinx::DuplicateNameError < StandardError end class ThinkingSphinx::InvalidDatabaseAdapter < StandardError end class ThinkingSphinx::SphinxAlreadyRunning < StandardError end class ThinkingSphinx::UnknownDatabaseAdapter < StandardError end class ThinkingSphinx::UnknownAttributeType < StandardError end class ThinkingSphinx::TranscriptionError < StandardError attr_accessor :inner_exception, :instance, :property end class ThinkingSphinx::UnknownCommand < StandardError end thinking-sphinx-4.1.0/lib/thinking_sphinx/excerpter.rb000066400000000000000000000017541341132130100231320ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Excerpter DefaultOptions = { :before_match => '', :after_match => '', :chunk_separator => ' … ' # ellipsis } attr_accessor :index, :words, :options def initialize(index, words, options = {}) @index, @words = index, words @options = DefaultOptions.merge(options) @words = @options.delete(:words) if @options[:words] end def excerpt!(text) result = ThinkingSphinx::Connection.take do |connection| query = statement_for text ThinkingSphinx::Logger.log :query, query do connection.execute(query).first['snippet'] end end encoded? ? result : ThinkingSphinx::UTF8.encode(result) end private def statement_for(text) Riddle::Query.snippets(text, index, words, options) end def encoded? ThinkingSphinx::Configuration.instance.settings['utf8'].nil? || ThinkingSphinx::Configuration.instance.settings['utf8'] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/facet.rb000066400000000000000000000011151341132130100222020ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Facet attr_reader :name def initialize(name, properties) @name, @properties = name, properties end def filter_type use_field? ? :conditions : :with end def results_from(raw) raw.inject({}) { |hash, row| hash[row[group_column]] = row["sphinx_internal_count"] hash } end private def group_column @properties.any?(&:multi?) ? "sphinx_internal_group" : name end def use_field? @properties.any? { |property| property.type.nil? || property.type == :string } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/facet_search.rb000066400000000000000000000057141341132130100235400ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::FacetSearch include Enumerable attr_reader :options attr_accessor :query def initialize(query = nil, options = {}) query, options = nil, query if query.is_a?(Hash) @query, @options = query, options @hash = {} end def [](key) populate @hash[key] end def each(&block) populate @hash.each(&block) end def for(facet_values) filter_facets = facet_values.keys.collect { |key| facets.detect { |facet| facet.name == key.to_s } } ThinkingSphinx::Search.new query, options.merge( :indices => index_names_for(*filter_facets) ).merge(Filter.new(facets, facet_values).to_hash) end def populate return if @populated batch = ThinkingSphinx::BatchedSearch.new facets.each do |facet| batch.searches << ThinkingSphinx::Search.new(query, options_for(facet)) end batch.populate ThinkingSphinx::Middlewares::RAW_ONLY facets.each_with_index do |facet, index| @hash[facet.name.to_sym] = facet.results_from batch.searches[index].raw end @hash[:class] = @hash[:sphinx_internal_class] @populated = true end def populated? @populated end def to_hash populate @hash end private def configuration ThinkingSphinx::Configuration.instance end def facets @facets ||= properties.group_by(&:name).collect { |name, matches| ThinkingSphinx::Facet.new name, matches } end def properties properties = indices.collect(&:facets).flatten if options[:facets].present? properties = properties.select { |property| options[:facets].include? property.name.to_sym } end properties end def index_names_for(*facets) facet_names( indices.select do |index| facets.all? { |facet| facet_names(index.facets).include?(facet.name) } end ) end def facet_names(facets) facets.collect(&:name) end def indices @indices ||= configuration.index_set_class.new( options.slice(:classes, :indices) ) end def max_matches configuration.settings['max_matches'] || 1000 end def limit limit = options[:limit] || options[:per_page] || max_matches end def options_for(facet) options.merge( :select => [(options[:select] || '*'), "groupby() AS sphinx_internal_group", "id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count" ].join(', '), :group_by => facet.name, :indices => index_names_for(facet), :max_matches => max_matches, :limit => limit ) end class Filter def initialize(facets, hash) @facets, @hash = facets, hash end def to_hash @hash.keys.inject({}) { |options, key| type = @facets.detect { |facet| facet.name == key.to_s }.filter_type options[type] ||= {} options[type][key] = @hash[key] options } end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/float_formatter.rb000066400000000000000000000010621341132130100243110ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::FloatFormatter PATTERN = /(\d+)e\-(\d+)$/ def initialize(float) @float = float end def fixed return float.to_s unless exponent_present? ("%0.#{decimal_places}f" % float).gsub(/0+$/, '') end private attr_reader :float def exponent_decimal_places float.to_s[PATTERN, 1].length end def exponent_factor float.to_s[PATTERN, 2].to_i end def exponent_present? float.to_s['e'] end def decimal_places exponent_factor + exponent_decimal_places end end thinking-sphinx-4.1.0/lib/thinking_sphinx/frameworks.rb000066400000000000000000000004411341132130100233010ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Frameworks def self.current defined?(::Rails) ? ThinkingSphinx::Frameworks::Rails.new : ThinkingSphinx::Frameworks::Plain.new end end require 'thinking_sphinx/frameworks/plain' require 'thinking_sphinx/frameworks/rails' thinking-sphinx-4.1.0/lib/thinking_sphinx/frameworks/000077500000000000000000000000001341132130100227555ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/frameworks/plain.rb000066400000000000000000000003021341132130100244000ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Frameworks::Plain attr_accessor :environment, :root def initialize @environment = 'production' @root = Dir.pwd end end thinking-sphinx-4.1.0/lib/thinking_sphinx/frameworks/rails.rb000066400000000000000000000002221341132130100244100ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Frameworks::Rails def environment Rails.env end def root Rails.root end end thinking-sphinx-4.1.0/lib/thinking_sphinx/guard.rb000066400000000000000000000002651341132130100222270ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Guard # end require 'thinking_sphinx/guard/file' require 'thinking_sphinx/guard/files' require 'thinking_sphinx/guard/none' thinking-sphinx-4.1.0/lib/thinking_sphinx/guard/000077500000000000000000000000001341132130100216775ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/guard/file.rb000066400000000000000000000006331341132130100231450ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Guard::File attr_reader :name def initialize(name) @name = name end def lock FileUtils.touch path end def locked? File.exists? path end def path @path ||= File.join( ThinkingSphinx::Configuration.instance.indices_location, "ts-#{name}.tmp" ) end def unlock FileUtils.rm(path) if locked? end end thinking-sphinx-4.1.0/lib/thinking_sphinx/guard/files.rb000066400000000000000000000013501341132130100233250ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Guard::Files def self.call(names, &block) new(names).call(&block) end def initialize(names) @names = names end def call(&block) return if unlocked.empty? unlocked.each &:lock block.call unlocked.collect(&:name) rescue => error raise error ensure unlocked.each &:unlock end private attr_reader :names def log_lock(file) ThinkingSphinx::Logger.log :guard, "Guard file for index #{file.name} exists, not indexing: #{file.path}." end def unlocked @unlocked ||= names.collect { |name| ThinkingSphinx::Guard::File.new name }.reject { |file| log_lock file if file.locked? file.locked? } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/guard/none.rb000066400000000000000000000001771341132130100231700ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Guard::None def self.call(names, &block) block.call names end end thinking-sphinx-4.1.0/lib/thinking_sphinx/hooks/000077500000000000000000000000001341132130100217205ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/hooks/guard_presence.rb000066400000000000000000000015011341132130100252300ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Hooks::GuardPresence def self.call(configuration = nil, stream = STDERR) new(configuration, stream).call end def initialize(configuration = nil, stream = STDERR) @configuration = configuration || ThinkingSphinx::Configuration.instance @stream = stream end def call return if files.empty? stream.puts "WARNING: The following indexing guard files exist:" files.each do |file| stream.puts " * #{file}" end stream.puts <<-TXT These files indicate indexing is already happening. If that is not the case, these files should be deleted to ensure all indices can be processed. TXT end private attr_reader :configuration, :stream def files @files ||= Dir["#{configuration.indices_location}/ts-*.tmp"] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/index.rb000066400000000000000000000025161341132130100222350ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Index attr_reader :reference, :options, :block def self.define(reference, options = {}, &block) new(reference, options, &block).indices.each do |index| ThinkingSphinx::Configuration.instance.indices << index end end def initialize(reference, options, &block) defaults = ThinkingSphinx::Configuration.instance. settings['index_options'] || {} defaults.symbolize_keys! @reference, @options, @block = reference, defaults.merge(options), block end def indices options[:delta] ? delta_indices : [single_index] end private def index_class case options[:with] when :active_record ThinkingSphinx::ActiveRecord::Index when :real_time ThinkingSphinx::RealTime::Index else raise "Unknown index type: #{options[:with]}" end end def single_index index_class.new(reference, options).tap do |index| index.definition_block = block end end def delta_indices [false, true].collect do |delta| index_class.new( reference, options.merge(:delta? => delta, :delta_processor => processor) ).tap do |index| index.definition_block = block end end end def processor @processor ||= ThinkingSphinx::Deltas.processor_for options.delete(:delta) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/index_set.rb000066400000000000000000000036551341132130100231150ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::IndexSet include Enumerable def self.reference_name(klass) @cached_results ||= {} @cached_results[klass.name] ||= klass.name.underscore.to_sym end delegate :each, :empty?, :to => :indices def initialize(options = {}, configuration = nil) @options = options @index_names = options[:indices] || [] @configuration = configuration || ThinkingSphinx::Configuration.instance end def ancestors classes_and_ancestors - classes end def to_a indices end private attr_reader :configuration, :options def all_indices configuration.preload_indices configuration.indices end def classes options[:classes] || [] end def classes_specified? classes.any? || references_specified? end def classes_and_ancestors @classes_and_ancestors ||= mti_classes + sti_classes.collect { |model| model.ancestors.take_while { |klass| klass != ActiveRecord::Base }.select { |klass| klass.class == Class } }.flatten end def index_names options[:indices] || [] end def indices return all_indices.select { |index| index_names.include?(index.name) } if index_names.any? everything = classes_specified? ? indices_for_references : all_indices everything.reject &:distributed? end 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) } end def references_specified? options[:references] && options[:references].any? end def sti_classes classes.select { |klass| klass.column_names.include?(klass.inheritance_column) } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/indexing_strategies/000077500000000000000000000000001341132130100246345ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/indexing_strategies/all_at_once.rb000066400000000000000000000003041341132130100274160ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::IndexingStrategies::AllAtOnce def self.call(indices = [], &block) indices << '--all' if indices.empty? block.call indices end end thinking-sphinx-4.1.0/lib/thinking_sphinx/indexing_strategies/one_at_a_time.rb000066400000000000000000000006651341132130100277530ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::IndexingStrategies::OneAtATime def self.call(indices = [], &block) if indices.empty? configuration = ThinkingSphinx::Configuration.instance configuration.preload_indices indices = configuration.indices.select { |index| !(index.distributed? || index.type == 'rt') }.collect &:name end indices.each { |name| block.call [name] } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces.rb000066400000000000000000000003701341132130100232450ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Interfaces # end require 'thinking_sphinx/interfaces/base' require 'thinking_sphinx/interfaces/daemon' require 'thinking_sphinx/interfaces/real_time' require 'thinking_sphinx/interfaces/sql' thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces/000077500000000000000000000000001341132130100227205ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces/base.rb000066400000000000000000000004311341132130100241550ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Interfaces::Base include ThinkingSphinx::WithOutput private def command(command, extra_options = {}) ThinkingSphinx::Commander.call( command, configuration, options.merge(extra_options), stream ) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces/daemon.rb000066400000000000000000000011341341132130100245070ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Interfaces::Daemon < ThinkingSphinx::Interfaces::Base def start if command :running raise ThinkingSphinx::SphinxAlreadyRunning, 'searchd is already running' end command(options[:nodetach] ? :start_attached : :start_detached) end def status if command :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 command :stop end private delegate :controller, :to => :configuration end thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces/real_time.rb000066400000000000000000000016671341132130100252200ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Interfaces::RealTime < ThinkingSphinx::Interfaces::Base def initialize(configuration, options, stream = STDOUT) super configuration.preload_indices command :prepare end def clear command :clear_real_time, :indices => indices end def index return if indices.empty? if !command :running stream.puts <<-TXT The Sphinx daemon is not currently running. Real-time indices can only be populated by sending commands to a running daemon. TXT return end command :index_real_time, :indices => indices end private def index_names @index_names ||= options[:index_names] || [] end def indices @indices ||= begin indices = configuration.indices.select { |index| index.type == 'rt' } if index_names.any? indices.select! { |index| index_names.include? index.name } end indices end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/interfaces/sql.rb000066400000000000000000000023571341132130100240530ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Interfaces::SQL < ThinkingSphinx::Interfaces::Base def initialize(configuration, options, stream = STDOUT) super configuration.preload_indices command :prepare end def clear command :clear_sql, :indices => (filtered? ? filtered_indices : indices) 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? command :configure if reconfigure command :index_sql, :indices => (filtered? ? filtered_indices.collect(&:name) : nil) end def merge command :merge_and_update end private def filtered? index_names.any? end def filtered_indices indices.select { |index| index_names.include? index.name } end def index_names @index_names ||= options[:index_names] || [] end def indices @indices ||= configuration.indices.select do |index| index.type == 'plain' || index.type.blank? end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/logger.rb000066400000000000000000000003601341132130100224000ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Logger def self.log(notification, message, &block) ActiveSupport::Notifications.instrument( "#{notification}.thinking_sphinx", notification => message, &block ) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/masks.rb000066400000000000000000000004171341132130100222420ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Masks # end require 'thinking_sphinx/masks/group_enumerators_mask' require 'thinking_sphinx/masks/pagination_mask' require 'thinking_sphinx/masks/scopes_mask' require 'thinking_sphinx/masks/weight_enumerator_mask' thinking-sphinx-4.1.0/lib/thinking_sphinx/masks/000077500000000000000000000000001341132130100217135ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/masks/group_enumerators_mask.rb000066400000000000000000000013071341132130100270340ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Masks::GroupEnumeratorsMask def initialize(search) @search = search end def can_handle?(method) public_methods(false).include?(method) end def each_with_count(&block) @search.raw.each_with_index do |row, index| yield @search[index], row["sphinx_internal_count"] end end def each_with_group(&block) @search.raw.each_with_index do |row, index| yield @search[index], row["sphinx_internal_group"] end end def each_with_group_and_count(&block) @search.raw.each_with_index do |row, index| yield @search[index], row["sphinx_internal_group"], row["sphinx_internal_count"] end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/masks/pagination_mask.rb000066400000000000000000000021511341132130100254030ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Masks::PaginationMask def initialize(search) @search = search end def can_handle?(method) public_methods(false).include?(method) end def first_page? search.current_page == 1 end def last_page? next_page.nil? end def next_page search.current_page >= total_pages ? nil : search.current_page + 1 end def next_page? !next_page.nil? end def page(number) search.options[:page] = number search end def per(limit) search.options[:limit] = limit search end def previous_page search.current_page == 1 ? nil : search.current_page - 1 end alias_method :prev_page, :previous_page def total_entries search.meta['total_found'].to_i end alias_method :total_count, :total_entries alias_method :count, :total_entries def total_pages return 0 if search.meta['total'].nil? @total_pages ||= (search.meta['total'].to_i / search.per_page.to_f).ceil end alias_method :page_count, :total_pages alias_method :num_pages, :total_pages private attr_reader :search end thinking-sphinx-4.1.0/lib/thinking_sphinx/masks/scopes_mask.rb000066400000000000000000000023671341132130100245570ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Masks::ScopesMask def initialize(search) @search = search end def can_handle?(method) public_methods(false).include?(method) || can_apply_scope?(method) end def facets(query = nil, options = {}) search = ThinkingSphinx.facets query, options ThinkingSphinx::Search::Merger.new(search).merge!( @search.query, @search.options ) end def search(query = nil, options = {}) query, options = nil, query if query.is_a?(Hash) ThinkingSphinx::Search::Merger.new(@search).merge! query, options end def search_for_ids(query = nil, options = {}) query, options = nil, query if query.is_a?(Hash) search query, options.merge(:ids_only => true) end private def apply_scope(scope, *args) query, options = sphinx_scopes[scope].call(*args) search query, options end def can_apply_scope?(scope) @search.options[:classes].present? && @search.options[:classes].length == 1 && @search.options[:classes].first.respond_to?(:sphinx_scopes) && sphinx_scopes[scope].present? end def method_missing(method, *args, &block) apply_scope method, *args end def sphinx_scopes @search.options[:classes].first.sphinx_scopes end end thinking-sphinx-4.1.0/lib/thinking_sphinx/masks/weight_enumerator_mask.rb000066400000000000000000000005371341132130100270100ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Masks::WeightEnumeratorMask def initialize(search) @search = search end def can_handle?(method) public_methods(false).include?(method) end def each_with_weight(&block) @search.raw.each_with_index do |row, index| yield @search[index], row["weight()"] end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares.rb000066400000000000000000000016241341132130100234250ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Middlewares; end %w[ middleware active_record_translator geographer glazier ids_only inquirer sphinxql stale_id_checker stale_id_filter valid_options ].each do |middleware| require "thinking_sphinx/middlewares/#{middleware}" end module ThinkingSphinx::Middlewares def self.use(builder, middlewares) middlewares.each { |m| builder.use m } end BASE_MIDDLEWARES = [ValidOptions, SphinxQL, Geographer, Inquirer] DEFAULT = ::Middleware::Builder.new do use StaleIdFilter ThinkingSphinx::Middlewares.use self, BASE_MIDDLEWARES use ActiveRecordTranslator use StaleIdChecker use Glazier end RAW_ONLY = ::Middleware::Builder.new do ThinkingSphinx::Middlewares.use self, BASE_MIDDLEWARES end IDS_ONLY = ::Middleware::Builder.new do ThinkingSphinx::Middlewares.use self, BASE_MIDDLEWARES use IdsOnly end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/000077500000000000000000000000001341132130100230755ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/active_record_translator.rb000066400000000000000000000052521341132130100305100ustar00rootroot00000000000000# frozen_string_literal: true 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| Inner.new(context).call end app.call contexts end private class Inner def initialize(context) @context = context end def call results_for_models # load now to avoid segfaults context[:results] = if sql_options[:order] results_for_models.values.first else context[:results].collect { |row| result_for(row) } end end private attr_reader :context def ids_for_model(model_name) context[:results].collect { |row| row['sphinx_internal_id'] if row['sphinx_internal_class'] == 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_for(model) model = NO_MODEL unless model.respond_to?(:primary_key) @primary_keys ||= {} @primary_keys[model] ||= index_for(model).primary_key end def reset_memos @model_names = nil @results_for_models = nil end def result_for(row) results_for_models[row['sphinx_internal_class']].detect { |record| 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 model_sql_options = sql_options[name] || sql_options hash[name] = model_relation_with_sql_options(model.unscoped, model_sql_options).where( primary_key_for(model) => ids_for_model(name) ) hash end end def model_relation_with_sql_options(relation, model_sql_options) relation = relation.includes model_sql_options[:include] if model_sql_options[:include] relation = relation.joins model_sql_options[:joins] if model_sql_options[:joins] relation = relation.order model_sql_options[:order] if model_sql_options[:order] relation = relation.select model_sql_options[:select] if model_sql_options[:select] relation = relation.group model_sql_options[:group] if model_sql_options[:group] relation end def sql_options context.search.options[:sql] || {} end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/geographer.rb000066400000000000000000000040451341132130100255500ustar00rootroot00000000000000# frozen_string_literal: true require 'active_support/core_ext/module/delegation' class ThinkingSphinx::Middlewares::Geographer < ThinkingSphinx::Middlewares::Middleware def call(contexts) contexts.each do |context| Inner.new(context).call end app.call contexts end private class Inner def initialize(context) @context = context end def call return unless geo context[:sphinxql].prepend_values geodist_clause context[:panes] << ThinkingSphinx::Panes::DistancePane end private attr_reader :context delegate :geo, :latitude, :longitude, :to => :geolocation_attributes def fixed_format(float) ThinkingSphinx::FloatFormatter.new(float).fixed end def geolocation_attributes @geolocation_attributes ||= GeolocationAttributes.new(context) end def geodist_clause "GEODIST(#{fixed_format geo.first}, #{fixed_format geo.last}, #{latitude}, #{longitude}) AS geodist" end class GeolocationAttributes attr_accessor :latitude, :longitude def initialize(context) self.context = context self.latitude = latitude_attr if latitude_attr self.longitude = longitude_attr if longitude_attr end def geo search_context_options[:geo] end def latitude @latitude ||= names.detect { |name| %w[lat latitude].include?(name) } || 'lat' end def longitude @longitude ||= names.detect { |name| %w[lng longitude].include?(name) } || 'lng' end private attr_accessor :context def latitude_attr @latitude_attr ||= search_context_options[:latitude_attr] end def longitude_attr @longitude_attr ||= search_context_options[:longitude_attr] end def indices context[:indices] end def names @names ||= indices.collect(&:unique_attribute_names).flatten.uniq end def search_context_options @search_context_options ||= context.search.options end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/glazier.rb000066400000000000000000000014341341132130100250610ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::Glazier < ThinkingSphinx::Middlewares::Middleware def call(contexts) contexts.each do |context| Inner.new(context).call end app.call contexts end private class Inner def initialize(context) @context = context end def call return if context[:panes].empty? context[:results] = context[:results].collect { |result| ThinkingSphinx::Search::Glaze.new context, result, row_for(result), context[:panes] } end private attr_reader :context def row_for(result) context[:raw].detect { |row| row['sphinx_internal_class'] == result.class.name && row['sphinx_internal_id'] == result.id } end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/ids_only.rb000066400000000000000000000004721341132130100252450ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::IdsOnly < ThinkingSphinx::Middlewares::Middleware def call(contexts) contexts.each do |context| context[:results] = context[:results].collect { |row| row['sphinx_internal_id'] } end app.call contexts end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/inquirer.rb000066400000000000000000000025231341132130100252620ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::Inquirer < ThinkingSphinx::Middlewares::Middleware def call(contexts) @contexts = contexts @batch = nil ThinkingSphinx::Logger.log :query, combined_queries do batch.results end index = 0 contexts.each do |context| Inner.new(context).call batch.results[index], batch.results[index + 1] index += 2 end app.call contexts end private def batch @batch ||= begin batch = ThinkingSphinx::Search::BatchInquirer.new @contexts.each do |context| batch.append_query context[:sphinxql].to_sql batch.append_query Riddle::Query.meta end batch end end def combined_queries @contexts.collect { |context| context[:sphinxql].to_sql }.join('; ') end class Inner def initialize(context) @context = context end def call(raw_results, meta_results) context[:results] = raw_results.to_a context[:raw] = context[:results].dup context[:meta] = meta_results.inject({}) { |hash, row| hash[row['Variable_name']] = row['Value'] hash } total = context[:meta]['total_found'] ThinkingSphinx::Logger.log :message, "Found #{total} result#{'s' unless total == 1}" end private attr_reader :context end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/middleware.rb000066400000000000000000000002451341132130100255400ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::Middleware def initialize(app) @app = app end private attr_reader :app, :context end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/sphinxql.rb000066400000000000000000000145161341132130100252770ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::SphinxQL < ThinkingSphinx::Middlewares::Middleware SELECT_OPTIONS = [:agent_query_timeout, :boolean_simplify, :comment, :cutoff, :field_weights, :global_idf, :idf, :index_weights, :max_matches, :max_query_time, :max_predicted_time, :ranker, :retry_count, :retry_delay, :reverse_scan, :sort_method, :rand_seed] def call(contexts) contexts.each do |context| Inner.new(context).call end app.call contexts end private class Inner def initialize(context) @context = context end def call context[:indices] = indices context[:sphinxql] = statement end private attr_reader :context delegate :search, :configuration, :to => :context delegate :options, :to => :search delegate :settings, :to => :configuration def classes options[:classes] || [] end def classes_and_descendants classes + descendants end def classes_and_descendants_names classes_and_descendants.collect do |klass| name = klass.name name = %Q{"#{name}"} if name[/:/] name end end def classes_with_inheritance_column classes.select { |klass| klass.column_names.include?(klass.inheritance_column) } end def class_condition "(#{classes_and_descendants_names.join('|')})" end def class_condition_required? classes.any? && !indices_match_classes? end def constantize_inheritance_column(klass) values = klass.connection.select_values inheritance_column_select(klass) values.reject(&:blank?).each(&:constantize) end def descendants @descendants ||= options[:skip_sti] ? [] : descendants_from_tables end def descendants_from_tables classes_with_inheritance_column.collect do |klass| constantize_inheritance_column(klass) klass.descendants end.flatten end def indices_match_classes? indices.collect(&:reference).uniq.sort == classes.collect { |klass| ThinkingSphinx::IndexSet.reference_name(klass) }.sort end def inheritance_column_select(klass) <<-SQL SELECT DISTINCT #{klass.inheritance_column} FROM #{klass.table_name} SQL end def exclusive_filters @exclusive_filters ||= (options[:without] || {}).tap do |without| without[:sphinx_internal_id] = options[:without_ids] if options[:without_ids].present? end end def extended_query conditions = options[:conditions] || {} if class_condition_required? conditions[:sphinx_internal_class_name] = class_condition end @extended_query ||= ThinkingSphinx::Search::Query.new( context.search.query, conditions, options[:star] ).to_s end def group_attribute options[:group_by].to_s if options[:group_by] end def group_order_clause group_by = options[:order_group_by] group_by = "#{group_by} ASC" if group_by.is_a? Symbol group_by end def inclusive_filters (options[:with] || {}).merge({:sphinx_deleted => false}) end def index_names indices.collect(&:name) end def index_options indices.first.options end def indices @indices ||= begin set = configuration.index_set_class.new( options.slice(:classes, :indices) ) raise ThinkingSphinx::NoIndicesError if set.empty? set end end def order_clause order_by = options[:order] order_by = "#{order_by} ASC" if order_by.is_a? Symbol order_by end def select_options @select_options ||= SELECT_OPTIONS.inject({}) do |hash, key| hash[key] = settings[key.to_s] if settings.key? key.to_s hash[key] = index_options[key] if index_options.key? key hash[key] = options[key] if options.key? key hash end end def values options[:select] ||= ['*', "groupby() AS sphinx_internal_group", "id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count" ].join(', ') if group_attribute.present? options[:select] end def statement Statement.new(self).to_riddle_query_select end class Statement def initialize(report) self.report = report self.query = Riddle::Query::Select.new end def to_riddle_query_select filter_by_scopes query end protected attr_accessor :report, :query def filter_by_scopes scope_by_from scope_by_values scope_by_extended_query scope_by_inclusive_filters scope_by_with_all scope_by_exclusive_filters scope_by_without_all scope_by_order scope_by_group scope_by_pagination scope_by_options end def scope_by_from query.from *(index_names.collect { |index| "`#{index}`" }) end def scope_by_values query.values(values.present? ? values : '*') end def scope_by_extended_query query.matching extended_query if extended_query.present? end def scope_by_inclusive_filters query.where inclusive_filters if inclusive_filters.any? end def scope_by_with_all query.where_all options[:with_all] if options[:with_all] end def scope_by_exclusive_filters query.where_not exclusive_filters if exclusive_filters.any? end def scope_by_without_all query.where_not_all options[:without_all] if options[:without_all] end def scope_by_order query.order_by order_clause if order_clause.present? end def scope_by_group query.group_by group_attribute if group_attribute.present? query.group_best options[:group_best] if options[:group_best] query.order_within_group_by group_order_clause if group_order_clause.present? query.having options[:having] if options[:having] end def scope_by_pagination query.offset context.search.offset query.limit context.search.per_page end def scope_by_options query.with_options select_options if select_options.keys.any? end def method_missing(*args, &block) report.send *args, &block end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/stale_id_checker.rb000066400000000000000000000016511341132130100266750ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::StaleIdChecker < ThinkingSphinx::Middlewares::Middleware def call(contexts) contexts.each do |context| Inner.new(context).call end app.call contexts end private class Inner def initialize(context) @context = context end def call raise_exception if context[:results].any?(&:nil?) end private attr_reader :context def actual_ids context[:results].compact.collect(&:id) end def expected_ids context[:raw].collect { |row| row['sphinx_internal_id'].to_i } end def raise_exception raise ThinkingSphinx::Search::StaleIdsException.new(stale_ids, context) end def stale_ids # Currently only works with single-model queries. Has at no point done # otherwise, but such an improvement would be nice. expected_ids - actual_ids end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/stale_id_filter.rb000066400000000000000000000020031341132130100265460ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Middlewares::StaleIdFilter < ThinkingSphinx::Middlewares::Middleware def call(contexts) @context = contexts.first @stale_ids = [] @retries = stale_retries begin app.call contexts rescue ThinkingSphinx::Search::StaleIdsException => error raise error if @retries <= 0 append_stale_ids error.ids, error.context ThinkingSphinx::Logger.log :message, log_message @retries -= 1 and retry end end private def append_stale_ids(ids, context) @stale_ids |= ids context.search.options[:without_ids] ||= [] context.search.options[:without_ids] |= ids end def log_message 'Stale Ids (%s %s left): %s' % [ @retries, (@retries == 1 ? 'try' : 'tries'), @stale_ids.join(', ') ] end def stale_retries case context.search.options[:retry_stale] when nil, TrueClass 2 when FalseClass 0 else context.search.options[:retry_stale].to_i end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/middlewares/valid_options.rb000066400000000000000000000010331341132130100262710ustar00rootroot00000000000000# frozen_string_literal: true 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 :caution, "Unexpected search options: #{unknown.inspect}" end def invalid_keys(keys) keys - ThinkingSphinx::Search.valid_options end end thinking-sphinx-4.1.0/lib/thinking_sphinx/panes.rb000066400000000000000000000003751341132130100222350ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Panes # end require 'thinking_sphinx/panes/attributes_pane' require 'thinking_sphinx/panes/distance_pane' require 'thinking_sphinx/panes/excerpts_pane' require 'thinking_sphinx/panes/weight_pane' thinking-sphinx-4.1.0/lib/thinking_sphinx/panes/000077500000000000000000000000001341132130100217035ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/panes/attributes_pane.rb000066400000000000000000000002631341132130100254220ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Panes::AttributesPane def initialize(context, object, raw) @raw = raw end def sphinx_attributes @raw end end thinking-sphinx-4.1.0/lib/thinking_sphinx/panes/distance_pane.rb000066400000000000000000000003461341132130100250300ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Panes::DistancePane def initialize(context, object, raw) @raw = raw end def distance @raw['geodist'].to_f end def geodist @raw['geodist'].to_f end end thinking-sphinx-4.1.0/lib/thinking_sphinx/panes/excerpts_pane.rb000066400000000000000000000017271341132130100250770ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Panes::ExcerptsPane def initialize(context, object, raw) @context, @object = context, object end def excerpts @excerpt_glazing ||= Excerpts.new @object, excerpter end private def excerpter @excerpter ||= ThinkingSphinx::Excerpter.new( @context[:indices].first.name, excerpt_words, @context.search.options[:excerpts] || {} ) end def excerpt_words @excerpt_words ||= begin conditions = @context.search.options[:conditions] || {} ThinkingSphinx::Search::Query.new( ([@context.search.query] + conditions.values).compact.join(' '), {}, @context.search.options[:star] ).to_s end end class Excerpts def initialize(object, excerpter) @object, @excerpter = object, excerpter end private def method_missing(method, *args, &block) @excerpter.excerpt! @object.send(method, *args, &block).to_s end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/panes/weight_pane.rb000066400000000000000000000002601341132130100245200ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Panes::WeightPane def initialize(context, object, raw) @raw = raw end def weight @raw["weight()"] end end thinking-sphinx-4.1.0/lib/thinking_sphinx/query.rb000066400000000000000000000003411341132130100222650ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Query def self.escape(query) Riddle::Query.escape query end def self.wildcard(query, pattern = true) ThinkingSphinx::Wildcard.call query, pattern end end thinking-sphinx-4.1.0/lib/thinking_sphinx/railtie.rb000066400000000000000000000005241341132130100225540ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Railtie < Rails::Railtie initializer 'thinking_sphinx.initialisation' do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.send :include, ThinkingSphinx::ActiveRecord::Base end end rake_tasks do load File.expand_path('../tasks.rb', __FILE__) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/rake_interface.rb000066400000000000000000000013341341132130100240650ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RakeInterface DEFAULT_OPTIONS = {:verbose => true} def initialize(options = {}) @options = DEFAULT_OPTIONS.merge options @options[:verbose] = false if @options[:silent] end def configure ThinkingSphinx::Commander.call :configure, configuration, options end def daemon @daemon ||= ThinkingSphinx::Interfaces::Daemon.new configuration, options end def rt @rt ||= ThinkingSphinx::Interfaces::RealTime.new configuration, options end def sql @sql ||= ThinkingSphinx::Interfaces::SQL.new configuration, options end private attr_reader :options def configuration ThinkingSphinx::Configuration.instance end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time.rb000066400000000000000000000013131341132130100230610ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::RealTime module Callbacks # end def self.callback_for(reference, path = [], &block) Callbacks::RealTimeCallbacks.new reference.to_sym, path, &block end end require 'thinking_sphinx/real_time/property' require 'thinking_sphinx/real_time/attribute' require 'thinking_sphinx/real_time/field' require 'thinking_sphinx/real_time/index' require 'thinking_sphinx/real_time/interpreter' require 'thinking_sphinx/real_time/populator' require 'thinking_sphinx/real_time/transcribe_instance' require 'thinking_sphinx/real_time/transcriber' require 'thinking_sphinx/real_time/translator' require 'thinking_sphinx/real_time/callbacks/real_time_callbacks' thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/000077500000000000000000000000001341132130100225365ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/attribute.rb000066400000000000000000000006201341132130100250640ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Attribute < ThinkingSphinx::RealTime::Property def multi? @options[:multi] end def type @options[:type] end def translate(object) output = super || default_value json? ? output.to_json : output end private def default_value type == :string ? '' : 0 end def json? type == :json end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/callbacks/000077500000000000000000000000001341132130100244555ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb000066400000000000000000000023761341132130100307520ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks def initialize(reference, path = [], &block) @reference, @path, @block = reference, path, block end def after_commit(instance) persist_changes instance end def after_save(instance) persist_changes instance end private attr_reader :reference, :path, :block def callbacks_enabled? setting = configuration.settings['real_time_callbacks'] setting.nil? || setting end def configuration ThinkingSphinx::Configuration.instance end def indices configuration.indices_for_references reference end def objects_for(instance) if block results = block.call instance else results = path.inject(instance) { |object, method| object.send method } end Array results end def persist_changes(instance) return unless real_time_indices? && callbacks_enabled? real_time_indices.each do |index| objects_for(instance).each do |object| ThinkingSphinx::RealTime::Transcriber.new(index).copy object end end end def real_time_indices? real_time_indices.any? end def real_time_indices indices.select { |index| index.is_a? ThinkingSphinx::RealTime::Index } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/field.rb000066400000000000000000000003241341132130100241450ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Field < ThinkingSphinx::RealTime::Property include ThinkingSphinx::Core::Field def translate(object) Array(super || '').join(' ') end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/index.rb000066400000000000000000000034331341132130100241750ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex include ThinkingSphinx::Core::Index attr_writer :fields, :attributes, :conditions, :scope def initialize(reference, options = {}) @fields = [] @attributes = [] @conditions = [] super reference, options Template.new(self).apply end def add_attribute(attribute) @attributes << attribute end def add_field(field) @fields << field end def attributes interpret_definition! @attributes end def conditions interpret_definition! @conditions end def facets properties.select(&:facet?) end def fields interpret_definition! @fields end def scope @scope.nil? ? model : @scope.call end def unique_attribute_names attributes.collect(&:name) end private def append_unique_attribute(collection, attribute) collection << attribute.name unless collection.include?(attribute.name) end def collection_for(attribute) case attribute.type when :integer, :boolean attribute.multi? ? @rt_attr_multi : @rt_attr_uint when :string @rt_attr_string when :timestamp @rt_attr_timestamp when :float @rt_attr_float when :bigint attribute.multi? ? @rt_attr_multi_64 : @rt_attr_bigint when :json @rt_attr_json else raise "Unknown attribute type '#{attribute.type}'" end end def interpreter ThinkingSphinx::RealTime::Interpreter end def pre_render super @rt_field = fields.collect &:name attributes.each do |attribute| append_unique_attribute collection_for(attribute), attribute end end def properties fields + attributes end end require 'thinking_sphinx/real_time/index/template' thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/index/000077500000000000000000000000001341132130100236455ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/index/template.rb000066400000000000000000000016741341132130100260150ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Index::Template attr_reader :index def initialize(index) @index = index end def apply add_field class_column, :sphinx_internal_class_name add_attribute primary_key, :sphinx_internal_id, :bigint add_attribute class_column, :sphinx_internal_class, :string, :facet => true add_attribute 0, :sphinx_deleted, :integer end private def add_attribute(column, name, type, options = {}) index.add_attribute ThinkingSphinx::RealTime::Attribute.new( ThinkingSphinx::ActiveRecord::Column.new(*column), options.merge(:as => name, :type => type) ) end def add_field(column, name) index.add_field ThinkingSphinx::RealTime::Field.new( ThinkingSphinx::ActiveRecord::Column.new(*column), :as => name ) end def class_column [:class, :name] end def primary_key index.primary_key.to_sym end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/interpreter.rb000066400000000000000000000025011341132130100254240ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Interpreter < ::ThinkingSphinx::Core::Interpreter def has(*columns) options = columns.extract_options! @index.attributes += columns.collect { |column| ::ThinkingSphinx::RealTime::Attribute.new column, options } end def indexes(*columns) options = columns.extract_options! @index.fields += columns.collect { |column| ::ThinkingSphinx::RealTime::Field.new column, options } append_sortable_attributes columns, options if options[:sortable] end def scope(&block) @index.scope = block end def set_property(properties) properties.each do |key, value| @index.send("#{key}=", value) if @index.class.settings.include?(key) @index.options[key] = value if search_option?(key) end end def where(condition) @index.conditions << condition end private def append_sortable_attributes(columns, options) options = options.except(:sortable).merge(:type => :string) @index.attributes += columns.collect { |column| aliased_name = options[:as] aliased_name ||= column.__name.to_sym if column.respond_to?(:__name) aliased_name ||= column options[:as] = "#{aliased_name}_sort".to_sym ::ThinkingSphinx::RealTime::Attribute.new column, options } end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/populator.rb000066400000000000000000000016441341132130100251150ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Populator def self.populate(index) new(index).populate end def initialize(index) @index = index end def populate(&block) instrument 'start_populating' scope.find_in_batches(:batch_size => batch_size) do |instances| transcriber.copy *instances instrument 'populated', :instances => instances end instrument 'finish_populating' end private attr_reader :index delegate :controller, :batch_size, :to => :configuration delegate :scope, :to => :index def configuration ThinkingSphinx::Configuration.instance end def instrument(message, options = {}) ActiveSupport::Notifications.instrument( "#{message}.thinking_sphinx.real_time", options.merge(:index => index) ) end def transcriber @transcriber ||= ThinkingSphinx::RealTime::Transcriber.new index end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/property.rb000066400000000000000000000007431341132130100247530ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Property include ThinkingSphinx::Core::Property attr_reader :column, :options def initialize(column, options = {}) @options = options @column = column.respond_to?(:__name) ? column : ThinkingSphinx::ActiveRecord::Column.new(column) end def name (@options[:as] || @column.__name).to_s end def translate(object) ThinkingSphinx::RealTime::Translator.call(object, @column) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/transcribe_instance.rb000066400000000000000000000016471341132130100271130ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::TranscribeInstance def self.call(instance, index, properties) new(instance, index, properties).call end def initialize(instance, index, properties) @instance, @index, @properties = instance, index, properties end def call properties.each_with_object([document_id]) do |property, instance_values| begin instance_values << property.translate(instance) rescue StandardError => error raise_wrapper error, property end end end private attr_reader :instance, :index, :properties def document_id index.document_id_for_key instance.public_send(index.primary_key) end def raise_wrapper(error, property) wrapper = ThinkingSphinx::TranscriptionError.new wrapper.inner_exception = error wrapper.instance = instance wrapper.property = property raise wrapper end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/transcriber.rb000066400000000000000000000030531341132130100254020ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Transcriber def initialize(index) @index = index end def copy(*instances) items = instances.select { |instance| instance.persisted? && copy?(instance) } return unless items.present? values = [] items.each do |instance| begin values << ThinkingSphinx::RealTime::TranscribeInstance.call( instance, index, properties ) rescue ThinkingSphinx::TranscriptionError => error instrument 'error', :error => error end end insert = Riddle::Query::Insert.new index.name, columns, values sphinxql = insert.replace!.to_sql ThinkingSphinx::Logger.log :query, sphinxql do ThinkingSphinx::Connection.take do |connection| connection.execute sphinxql end end end private attr_reader :index def columns @columns ||= properties.each_with_object(['id']) do |property, columns| columns << property.name end end def copy?(instance) index.conditions.empty? || index.conditions.all? { |condition| case condition when Symbol instance.send(condition) when Proc condition.call instance else "Unexpected condition: #{condition}. Expecting Symbol or Proc." end } end def instrument(message, options = {}) ActiveSupport::Notifications.instrument( "#{message}.thinking_sphinx.real_time", options.merge(:index => index) ) end def properties @properties ||= index.fields + index.attributes end end thinking-sphinx-4.1.0/lib/thinking_sphinx/real_time/translator.rb000066400000000000000000000011761341132130100252610ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::RealTime::Translator def self.call(object, column) new(object, column).call end def initialize(object, column) @object, @column = object, column end def call return name unless name.is_a?(Symbol) return result unless result.is_a?(String) result.gsub("\u0000", '').force_encoding "UTF-8" end private attr_reader :object, :column def name @column.__name end def owner stack.inject(object) { |previous, node| previous.try node } end def result @result ||= owner.try name end def stack @column.__stack end end thinking-sphinx-4.1.0/lib/thinking_sphinx/scopes.rb000066400000000000000000000011751341132130100224220ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::Scopes extend ActiveSupport::Concern module ClassMethods def default_sphinx_scope(scope_name = nil) return @default_sphinx_scope unless scope_name @default_sphinx_scope = scope_name end def sphinx_scope(name, &block) sphinx_scopes[name] = block end def sphinx_scopes @sphinx_scopes ||= {} end private def method_missing(method, *args, &block) return super unless sphinx_scopes.keys.include?(method) query, options = sphinx_scopes[method].call(*args) search query, (options || {}) end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search.rb000066400000000000000000000076401341132130100223760ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search < Array CORE_METHODS = %w( == class class_eval extend frozen? id instance_eval instance_of? instance_values instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? kind_of? member? method methods nil? object_id respond_to? respond_to_missing? send should should_not type ) SAFE_METHODS = %w( partition private_methods protected_methods public_methods send class ) KNOWN_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 ] + ThinkingSphinx::Middlewares::SphinxQL::SELECT_OPTIONS ).uniq DEFAULT_MASKS = [ ThinkingSphinx::Masks::PaginationMask, ThinkingSphinx::Masks::ScopesMask, ThinkingSphinx::Masks::GroupEnumeratorsMask ] instance_methods.select { |method| method.to_s[/^__/].nil? && !CORE_METHODS.include?(method.to_s) }.each { |method| undef_method method } attr_reader :options attr_accessor :query def self.valid_options @valid_options end @valid_options = KNOWN_OPTIONS.dup def initialize(query = nil, options = {}) query, options = nil, query if query.is_a?(Hash) @query, @options = query, options populate if options[:populate] end def context @context ||= ThinkingSphinx::Search::Context.new self, ThinkingSphinx::Configuration.instance end def current_page options[:page] = 1 if options[:page].blank? options[:page].to_i end def marshal_dump populate [@populated, @query, @options, @context] end def marshal_load(array) @populated, @query, @options, @context = array end def masks @masks ||= @options[:masks] || DEFAULT_MASKS.clone end def meta populate context[:meta] end def offset @options[:offset] || ((current_page - 1) * per_page) end alias_method :offset_value, :offset def per_page(value = nil) @options[:limit] = value unless value.nil? @options[:limit] ||= (@options[:per_page] || 20) @options[:limit].to_i end alias_method :limit_value, :per_page def populate return self if @populated middleware.call [context] @populated = true self end def populated! @populated = true end def populated? @populated end def query_time meta['time'].to_f end def raw populate context[:raw] end def to_a populate context[:results].collect { |result| result.respond_to?(:unglazed) ? result.unglazed : result } end private def default_middleware options[:ids_only] ? ThinkingSphinx::Middlewares::IDS_ONLY : ThinkingSphinx::Middlewares::DEFAULT end def mask_stack @mask_stack ||= masks.collect { |klass| klass.new self } end def masks_respond_to?(method) mask_stack.any? { |mask| mask.can_handle? method } end def method_missing(method, *args, &block) mask_stack.each do |mask| return mask.send(method, *args, &block) if mask.can_handle?(method) end populate if !SAFE_METHODS.include?(method.to_s) context[:results].send(method, *args, &block) end def respond_to_missing?(method, include_private = false) super || masks_respond_to?(method) || results_respond_to?(method, include_private) end def middleware @options[:middleware] || default_middleware end def results_respond_to?(method, include_private = true) context[:results].respond_to?(method, include_private) end end require 'thinking_sphinx/search/batch_inquirer' require 'thinking_sphinx/search/context' require 'thinking_sphinx/search/glaze' require 'thinking_sphinx/search/merger' require 'thinking_sphinx/search/query' require 'thinking_sphinx/search/stale_ids_exception' thinking-sphinx-4.1.0/lib/thinking_sphinx/search/000077500000000000000000000000001341132130100220425ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/search/batch_inquirer.rb000066400000000000000000000006111341132130100253640ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search::BatchInquirer def initialize(&block) @queries = [] yield self if block_given? end def append_query(query) @queries << query end def results @results ||= begin @queries.freeze ThinkingSphinx::Connection.take do |connection| connection.query_all *@queries end end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search/context.rb000066400000000000000000000011351341132130100240530ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search::Context attr_reader :search, :configuration def initialize(search, configuration = nil) @search = search @configuration = configuration || ThinkingSphinx::Configuration.instance @memory = { :results => [], :panes => ThinkingSphinx::Configuration::Defaults::PANES.clone } end def [](key) @memory[key] end def []=(key, value) @memory[key] = value end def marshal_dump [@memory.except(:raw, :indices)] end def marshal_load(array) @memory = array.first end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search/glaze.rb000066400000000000000000000015441341132130100234750ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search::Glaze < BasicObject def initialize(context, object, raw = {}, pane_classes = []) @object, @raw = object, raw @panes = pane_classes.collect { |klass| klass.new context, object, @raw } end def ==(object) (@object == object) || super end def equal?(object) @object.equal? object end def respond_to?(method, include_private = false) @object.respond_to?(method, include_private) || @panes.any? { |pane| pane.respond_to?(method, include_private) } end def unglazed @object end private def method_missing(method, *args, &block) pane = @panes.detect { |pane| pane.respond_to?(method) } if @object.respond_to?(method) || pane.nil? @object.send(method, *args, &block) else pane.send(method, *args, &block) end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search/merger.rb000066400000000000000000000015421341132130100236520ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search::Merger attr_reader :search def initialize(search) @search = search end def merge!(query = nil, options = {}) if search.populated? raise ThinkingSphinx::PopulatedResultsError, 'This search request has already been made - you can no longer modify it.' end query, options = nil, query if query.is_a?(Hash) @search.query = query unless query.nil? options.each do |key, value| case key when :conditions, :with, :without, :with_all, :without_all @search.options[key] ||= {} @search.options[key].merge! value when :without_ids, :classes @search.options[key] ||= [] @search.options[key] += value @search.options[key].uniq! else @search.options[key] = value end end @search end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search/query.rb000066400000000000000000000014331341132130100235350ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true class ThinkingSphinx::Search::Query attr_reader :keywords, :conditions, :star def initialize(keywords = '', conditions = {}, star = false) @keywords, @conditions, @star = keywords, conditions, star end def to_s (star_keyword(keywords || '') + ' ' + conditions.keys.collect { |key| next if conditions[key].blank? "#{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' ThinkingSphinx::Query.wildcard keyword, star end end thinking-sphinx-4.1.0/lib/thinking_sphinx/search/stale_ids_exception.rb000066400000000000000000000004561341132130100264210ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Search::StaleIdsException < StandardError attr_reader :ids, :context def initialize(ids, context) @ids = ids @context = context end def message "Record IDs found by Sphinx but not by ActiveRecord : #{ids.join(', ')}" end end thinking-sphinx-4.1.0/lib/thinking_sphinx/settings.rb000066400000000000000000000050541341132130100227660ustar00rootroot00000000000000# frozen_string_literal: true require "pathname" class ThinkingSphinx::Settings ALWAYS_ABSOLUTE = %w[ socket ] FILE_KEYS = %w[ indices_location configuration_file bin_path log query_log pid_file binlog_path snippets_file_prefix sphinxql_state path stopwords wordforms exceptions global_idf rlp_context rlp_root rlp_environment plugin_dir lemmatizer_base mysql_ssl_cert mysql_ssl_key mysql_ssl_ca ].freeze DEFAULTS = { "configuration_file" => "config/ENVIRONMENT.sphinx.conf", "indices_location" => "db/sphinx/ENVIRONMENT", "pid_file" => "log/ENVIRONMENT.sphinx.pid", "log" => "log/ENVIRONMENT.searchd.log", "query_log" => "log/ENVIRONMENT.searchd.query.log", "binlog_path" => "tmp/binlog/ENVIRONMENT", "workers" => "threads" }.freeze def self.call(configuration) new(configuration).call end def initialize(configuration) @configuration = configuration end def call return defaults unless File.exists? file merged.inject({}) do |hash, (key, value)| if absolute_key?(key) hash[key] = absolute value else hash[key] = value end hash end end private attr_reader :configuration delegate :framework, :to => :configuration def absolute(relative) return relative if relative.nil? real_path File.absolute_path(relative, framework.root) end def absolute_key?(key) return true if ALWAYS_ABSOLUTE.include?(key) merged["absolute_paths"] && file_keys.include?(key) end def defaults DEFAULTS.inject({}) do |hash, (key, value)| value = value.gsub("ENVIRONMENT", framework.environment) if FILE_KEYS.include?(key) hash[key] = absolute value else hash[key] = value end hash end end def file @file ||= Pathname.new(framework.root).join "config", "thinking_sphinx.yml" end def file_keys @file_keys ||= FILE_KEYS + (original["file_keys"] || []) end def join(first, last) return first if last.nil? File.join first, last end def merged @merged ||= defaults.merge original end def original input = File.read file input = ERB.new(input).result if defined?(ERB) contents = YAML.load input contents && contents[framework.environment] || {} end def real_path(base, nonexistent = nil) if File.exist?(base) join File.realpath(base), nonexistent else components = File.split base real_path components.first, join(components.last, nonexistent) end end end thinking-sphinx-4.1.0/lib/thinking_sphinx/sinatra.rb000066400000000000000000000002231341132130100225600ustar00rootroot00000000000000# frozen_string_literal: true require 'thinking_sphinx' ActiveSupport.on_load :active_record do include ThinkingSphinx::ActiveRecord::Base end thinking-sphinx-4.1.0/lib/thinking_sphinx/subscribers/000077500000000000000000000000001341132130100231235ustar00rootroot00000000000000thinking-sphinx-4.1.0/lib/thinking_sphinx/subscribers/populator_subscriber.rb000066400000000000000000000021441341132130100277210ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Subscribers::PopulatorSubscriber def self.attach_to(namespace) subscriber = new subscriber.public_methods(false).each do |event| next if event == :call ActiveSupport::Notifications.subscribe( "#{event}.#{namespace}", subscriber ) end end def call(message, *args) send message.split('.').first, ActiveSupport::Notifications::Event.new(message, *args) end def error(event) error = event.payload[:error].inner_exception instance = event.payload[:error].instance puts <<-MESSAGE Error transcribing #{instance.class} #{instance.id}: #{error.message} MESSAGE end def start_populating(event) puts "Generating index files for #{event.payload[:index].name}" end def populated(event) print '.' * event.payload[:instances].length end def finish_populating(event) print "\n" end private delegate :output, :to => ThinkingSphinx delegate :puts, :print, :to => :output end ThinkingSphinx::Subscribers::PopulatorSubscriber.attach_to( 'thinking_sphinx.real_time' ) thinking-sphinx-4.1.0/lib/thinking_sphinx/tasks.rb000066400000000000000000000043611341132130100222530ustar00rootroot00000000000000# frozen_string_literal: true namespace :ts do desc 'Generate the Sphinx configuration file' task :configure => :environment do interface.configure end desc 'Generate the Sphinx configuration file and process all indices' task :index => ['ts:sql:index', 'ts:rt:index'] desc 'Clear out Sphinx files' task :clear => ['ts:sql:clear', 'ts:rt:clear'] desc "Merge all delta indices into their respective core indices" task :merge => ["ts:sql:merge"] desc 'Delete and regenerate Sphinx files, restart the daemon' task :rebuild => [ :stop, :clear, :configure, 'ts:sql:index', :start, 'ts:rt:index' ] desc 'Restart the Sphinx daemon' task :restart => [:stop, :start] desc 'Start the Sphinx daemon' task :start => :environment do interface.daemon.start end desc 'Stop the Sphinx daemon' task :stop => :environment do interface.daemon.stop end desc 'Determine whether Sphinx is running' task :status => :environment do 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 task :merge => :environment do interface.sql.merge 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.rake_interface.new( :verbose => Rake::FileUtilsExt.verbose_flag, :silent => Rake.application.options.silent, :nodetach => (ENV['NODETACH'] == 'true'), :index_names => ENV.fetch('INDEX_FILTER', '').split(',') ) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/test.rb000066400000000000000000000020421341132130100220770ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Test def self.init(suppress_delta_output = true) FileUtils.mkdir_p config.indices_location config.settings['quiet_deltas'] = suppress_delta_output end def self.start(options = {}) config.render_to_file config.controller.index if options[:index].nil? || options[:index] config.controller.start end def self.start_with_autostop autostop start end def self.stop config.controller.stop sleep(0.5) # Ensure Sphinx has shut down completely end def self.autostop Kernel.at_exit do ThinkingSphinx::Test.stop end end def self.run(&block) begin start yield ensure stop end end def self.clear [ config.indices_location, config.searchd.binlog_path ].each do |path| FileUtils.rm_r(path) if File.exists?(path) end end def self.config @config ||= ::ThinkingSphinx::Configuration.instance end def self.index(*indexes) config.controller.index *indexes end end thinking-sphinx-4.1.0/lib/thinking_sphinx/utf8.rb000066400000000000000000000004311341132130100220060ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::UTF8 attr_reader :string def self.encode(string) new(string).encode end def initialize(string) @string = string end def encode string.encode!('ISO-8859-1') string.force_encoding('UTF-8') end end thinking-sphinx-4.1.0/lib/thinking_sphinx/wildcard.rb000066400000000000000000000020131341132130100227070ustar00rootroot00000000000000# frozen_string_literal: true class ThinkingSphinx::Wildcard DEFAULT_TOKEN = /\p{Word}+/ def self.call(query, pattern = DEFAULT_TOKEN) new(query, pattern).call end def initialize(query, pattern = DEFAULT_TOKEN) @query = query || '' @pattern = pattern.is_a?(Regexp) ? pattern : DEFAULT_TOKEN end def call query.gsub(extended_pattern) do pre, proper, post = $`, $&, $' # E.g. "@foo", "/2", "~3", but not as part of a token pattern is_operator = pre.match(%r{@$}) || pre.match(%r{([^\\]+|\A)[~/]\Z}) || pre.match(%r{(\W|^)@\([^\)]*$}) # E.g. "foo bar", with quotes is_quote = proper[/^".*"$/] has_star = post[/\*$/] || pre[/^\*/] if is_operator || is_quote || has_star proper else "*#{proper}*" end end end private attr_reader :query, :pattern def extended_pattern Regexp.new( "(\"#{pattern}(.*?#{pattern})?\"|(?![!-])#{pattern})".encode('UTF-8') ) end end thinking-sphinx-4.1.0/lib/thinking_sphinx/with_output.rb000066400000000000000000000004421341132130100235150ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx::WithOutput def initialize(configuration, options = {}, stream = STDOUT) @configuration = configuration @options = options @stream = stream end private attr_reader :configuration, :options, :stream end thinking-sphinx-4.1.0/spec/000077500000000000000000000000001341132130100155555ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/acceptance/000077500000000000000000000000001341132130100176435ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/acceptance/association_scoping_spec.rb000066400000000000000000000044561341132130100252510ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Scoping association search calls by foreign keys', :live => true do describe 'for ActiveRecord indices' do it "limits results to those matching the foreign key" do pat = User.create :name => 'Pat' melbourne = Article.create :title => 'Guide to Melbourne', :user => pat paul = User.create :name => 'Paul' dublin = Article.create :title => 'Guide to Dublin', :user => paul index expect(pat.articles.search('Guide').to_a).to eq([melbourne]) end it "limits id-only results to those matching the foreign key" do pat = User.create :name => 'Pat' melbourne = Article.create :title => 'Guide to Melbourne', :user => pat paul = User.create :name => 'Paul' dublin = Article.create :title => 'Guide to Dublin', :user => paul index expect(pat.articles.search_for_ids('Guide').to_a).to eq([melbourne.id]) end end describe 'for real-time indices' do it "limits results to those matching the foreign key" do porsche = Manufacturer.create :name => 'Porsche' spyder = Car.create :name => '918 Spyder', :manufacturer => porsche audi = Manufacturer.create :name => 'Audi' r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi expect(porsche.cars.search('Spyder').to_a).to eq([spyder]) end it "limits id-only results to those matching the foreign key" do porsche = Manufacturer.create :name => 'Porsche' spyder = Car.create :name => '918 Spyder', :manufacturer => porsche audi = Manufacturer.create :name => 'Audi' r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi expect(porsche.cars.search_for_ids('Spyder').to_a).to eq([spyder.id]) end end describe 'with has_many :through associations' do it 'limits results to those matching the foreign key' do pancakes = Product.create :name => 'Low fat Pancakes' waffles = Product.create :name => 'Low fat Waffles' food = Category.create :name => 'food' flat = Category.create :name => 'flat' pancakes.categories << food pancakes.categories << flat waffles.categories << food expect(flat.products.search('Low').to_a).to eq([pancakes]) end end end thinking-sphinx-4.1.0/spec/acceptance/attribute_access_spec.rb000066400000000000000000000022741341132130100245330ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Accessing attributes directly via search results', :live => true do it "allows access to attribute values" do Book.create! :title => 'American Gods', :year => 2001 index search = Book.search('gods') search.context[:panes] << ThinkingSphinx::Panes::AttributesPane expect(search.first.sphinx_attributes['year']).to eq(2001) end it "provides direct access to the search weight/relevance scores" do Book.create! :title => 'American Gods', :year => 2001 index search = Book.search 'gods', :select => "*, weight()" search.context[:panes] << ThinkingSphinx::Panes::WeightPane expect(search.first.weight).to eq(2500) end it "can enumerate with the weight" do gods = Book.create! :title => 'American Gods', :year => 2001 index search = Book.search 'gods', :select => "*, weight()" search.masks << ThinkingSphinx::Masks::WeightEnumeratorMask expectations = [[gods, 2500]] search.each_with_weight do |result, weight| expectation = expectations.shift expect(result).to eq(expectation.first) expect(weight).to eq(expectation.last) end end end thinking-sphinx-4.1.0/spec/acceptance/attribute_updates_spec.rb000066400000000000000000000007571341132130100247430ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Update attributes automatically where possible', :live => true do it "updates boolean values" do article = Article.create :title => 'Pancakes', :published => false index expect(Article.search('pancakes', :with => {:published => true})).to be_empty article.published = true article.save expect(Article.search('pancakes', :with => {:published => true}).to_a) .to eq([article]) end end thinking-sphinx-4.1.0/spec/acceptance/batch_searching_spec.rb000066400000000000000000000012771341132130100243150ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Executing multiple searches in one Sphinx call', :live => true do it "returns results matching the given queries" do pancakes = Article.create! :title => 'Pancakes' waffles = Article.create! :title => 'Waffles' index batch = ThinkingSphinx::BatchedSearch.new batch.searches << Article.search('pancakes') batch.searches << Article.search('waffles') batch.populate expect(batch.searches.first).to include(pancakes) expect(batch.searches.first).not_to include(waffles) expect(batch.searches.last).to include(waffles) expect(batch.searches.last).not_to include(pancakes) end end thinking-sphinx-4.1.0/spec/acceptance/big_integers_spec.rb000066400000000000000000000033561341132130100236520ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe '64 bit integer support' do it "ensures all internal id attributes are big ints if one is" do large_index = ThinkingSphinx::ActiveRecord::Index.new(:tweet) large_index.definition_block = Proc.new { indexes text } small_index = ThinkingSphinx::ActiveRecord::Index.new(:article) small_index.definition_block = Proc.new { indexes title } real_time_index = ThinkingSphinx::RealTime::Index.new(:product) real_time_index.definition_block = Proc.new { indexes name } ThinkingSphinx::Configuration::ConsistentIds.new( [small_index, large_index, real_time_index] ).reconcile expect(large_index.sources.first.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' }.type).to eq(:bigint) expect(small_index.sources.first.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' }.type).to eq(:bigint) expect(real_time_index.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' }.type).to eq(:bigint) end end describe '64 bit document ids', :live => true do context 'with ActiveRecord' do it 'handles large 32 bit integers with an offset multiplier' do user = User.create! :name => 'Pat' user.update_column :id, 980190962 index expect(User.search('pat').to_a).to eq([user]) end end context 'with Real-Time' do it 'handles large 32 bit integers with an offset multiplier' do product = Product.create! :name => "Widget" product.update_attributes :id => 980190962 expect( Product.search('widget', :indices => ['product_core']).to_a ).to eq([product]) end end end thinking-sphinx-4.1.0/spec/acceptance/connection_spec.rb000066400000000000000000000013511341132130100233410ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' RSpec.describe 'Connections', :live => true do 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 expect { ThinkingSphinx::Connection.take do |connection| connection.execute query.insert(-3, 'a' * difference) end }.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 + 5)) end }.to raise_error(ThinkingSphinx::QueryLengthError) end end thinking-sphinx-4.1.0/spec/acceptance/excerpts_spec.rb000066400000000000000000000027421341132130100230440ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true require 'acceptance/spec_helper' describe 'Accessing excerpts for methods on a search result', :live => true do it "returns excerpts for a given method" do Book.create! :title => 'American Gods', :year => 2001 index search = Book.search('gods') search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane expect(search.first.excerpts.title). to eq('American Gods') end it "handles UTF-8 text for excerpts" do Book.create! :title => 'Война и миръ', :year => 1869 index search = Book.search 'миръ' search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane expect(search.first.excerpts.title). to eq('Война и миръ') end if ENV['SPHINX_VERSION'].try :[], /2.2.\d/ it "does not include class names in excerpts" do Book.create! :title => 'The Graveyard Book' index search = Book.search('graveyard') search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane expect(search.first.excerpts.title). to eq('The Graveyard Book') end it "respects the star option with queries" do Article.create! :title => 'Something' index search = Article.search('thin', :star => true) search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane expect(search.first.excerpts.title). to eq('Something') end end thinking-sphinx-4.1.0/spec/acceptance/facets_spec.rb000066400000000000000000000071401341132130100224510ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true require 'acceptance/spec_helper' describe 'Faceted searching', :live => true do it "provides facet breakdowns across marked integer attributes" do blue = Colour.create! :name => 'blue' red = Colour.create! :name => 'red' green = Colour.create! :name => 'green' Tee.create! :colour => blue Tee.create! :colour => blue Tee.create! :colour => red Tee.create! :colour => green Tee.create! :colour => green Tee.create! :colour => green index expect(Tee.facets.to_hash[:colour_id]).to eq({ blue.id => 2, red.id => 1, green.id => 3 }) end it "provides facet breakdowns across classes" do Tee.create! Tee.create! City.create! Article.create! index expect(ThinkingSphinx.facets.to_hash[:class]).to eq({ 'Tee' => 2, 'City' => 1, 'Article' => 1 }) end it "handles field facets" do Book.create! :title => 'American Gods', :author => 'Neil Gaiman' Book.create! :title => 'Anansi Boys', :author => 'Neil Gaiman' Book.create! :title => 'Snuff', :author => 'Terry Pratchett' Book.create! :title => '1Q84', :author => '村上 春樹' index expect(Book.facets.to_hash[:author]).to eq({ 'Neil Gaiman' => 2, 'Terry Pratchett' => 1, '村上 春樹' => 1 }) end it "handles MVA facets" do pancakes = Tag.create! :name => 'pancakes' waffles = Tag.create! :name => 'waffles' user = User.create! Tagging.create! :article => Article.create!(:user => user), :tag => pancakes Tagging.create! :article => Article.create!(:user => user), :tag => waffles user = User.create! Tagging.create! :article => Article.create!(:user => user), :tag => pancakes index expect(User.facets.to_hash[:tag_ids]).to eq({ pancakes.id => 2, waffles.id => 1 }) end it "can filter on integer facet results" do blue = Colour.create! :name => 'blue' red = Colour.create! :name => 'red' b1 = Tee.create! :colour => blue b2 = Tee.create! :colour => blue r1 = Tee.create! :colour => red index expect(Tee.facets.for(:colour_id => blue.id).to_a).to eq([b1, b2]) end it "can filter on MVA facet results" do pancakes = Tag.create! :name => 'pancakes' waffles = Tag.create! :name => 'waffles' u1 = User.create! Tagging.create! :article => Article.create!(:user => u1), :tag => pancakes Tagging.create! :article => Article.create!(:user => u1), :tag => waffles u2 = User.create! Tagging.create! :article => Article.create!(:user => u2), :tag => pancakes index expect(User.facets.for(:tag_ids => waffles.id).to_a).to eq([u1]) end it "can filter on string facet results" do gods = Book.create! :title => 'American Gods', :author => 'Neil Gaiman' boys = Book.create! :title => 'Anansi Boys', :author => 'Neil Gaiman' snuff = Book.create! :title => 'Snuff', :author => 'Terry Pratchett' index expect(Book.facets.for(:author => 'Neil Gaiman').to_a).to eq([gods, boys]) end it "allows enumeration" do blue = Colour.create! :name => 'blue' red = Colour.create! :name => 'red' b1 = Tee.create! :colour => blue b2 = Tee.create! :colour => blue r1 = Tee.create! :colour => red index calls = 0 expectations = [ [:sphinx_internal_class, {'Tee' => 3}], [:colour_id, {blue.id => 2, red.id => 1}], [:class, {'Tee' => 3}] ] Tee.facets.each do |facet, hash| expect(facet).to eq(expectations[calls].first) expect(hash).to eq(expectations[calls].last) calls += 1 end end end thinking-sphinx-4.1.0/spec/acceptance/geosearching_spec.rb000066400000000000000000000044101341132130100236370ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching by latitude and longitude', :live => true do it "orders by distance" do mel = City.create :name => 'Melbourne', :lat => -0.6599720, :lng => 2.530082 syd = City.create :name => 'Sydney', :lat => -0.5909679, :lng => 2.639131 bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index expect(City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC'). to_a).to eq([syd, mel, bri]) end it "filters by distance" do mel = City.create :name => 'Melbourne', :lat => -0.6599720, :lng => 2.530082 syd = City.create :name => 'Sydney', :lat => -0.5909679, :lng => 2.639131 bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index expect(City.search( :geo => [-0.616241, 2.602712], :with => {:geodist => 0.0..470_000.0} ).to_a).to eq([mel, syd]) end it "provides the distance for each search result" do mel = City.create :name => 'Melbourne', :lat => -0.6599720, :lng => 2.530082 syd = City.create :name => 'Sydney', :lat => -0.5909679, :lng => 2.639131 bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index cities = City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC') if ENV.fetch('SPHINX_VERSION', '2.1.2').to_f > 2.1 expected = {:mysql => 249907.171875, :postgresql => 249912.03125} else expected = {:mysql => 250326.906250, :postgresql => 250331.234375} end if ActiveRecord::Base.configurations['test']['adapter'][/postgres/] expect(cities.first.geodist).to eq(expected[:postgresql]) else # mysql expect(cities.first.geodist).to eq(expected[:mysql]) end end it "handles custom select clauses that refer to the distance" do mel = City.create :name => 'Melbourne', :lat => -0.6599720, :lng => 2.530082 syd = City.create :name => 'Sydney', :lat => -0.5909679, :lng => 2.639131 bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index expect(City.search( :geo => [-0.616241, 2.602712], :with => {:geodist => 0.0..470_000.0}, :select => "*, geodist as custom_weight" ).to_a).to eq([mel, syd]) end end thinking-sphinx-4.1.0/spec/acceptance/grouping_by_attributes_spec.rb000066400000000000000000000046741341132130100260070ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Grouping search results by attributes', :live => true do it "groups by the provided attribute" do snuff = Book.create! :title => 'Snuff', :year => 2011 earth = Book.create! :title => 'The Long Earth', :year => 2012 dodger = Book.create! :title => 'Dodger', :year => 2012 index expect(Book.search(:group_by => :year).to_a).to eq([snuff, earth]) end it "allows sorting within the group" do snuff = Book.create! :title => 'Snuff', :year => 2011 earth = Book.create! :title => 'The Long Earth', :year => 2012 dodger = Book.create! :title => 'Dodger', :year => 2012 index expect(Book.search(:group_by => :year, :order_group_by => 'title ASC').to_a). to eq([snuff, dodger]) end it "allows enumerating by count" do snuff = Book.create! :title => 'Snuff', :year => 2011 earth = Book.create! :title => 'The Long Earth', :year => 2012 dodger = Book.create! :title => 'Dodger', :year => 2012 index expectations = [[snuff, 1], [earth, 2]] Book.search(:group_by => :year).each_with_count do |book, count| expectation = expectations.shift expect(book).to eq(expectation.first) expect(count).to eq(expectation.last) end end it "allows enumerating by group" do snuff = Book.create! :title => 'Snuff', :year => 2011 earth = Book.create! :title => 'The Long Earth', :year => 2012 dodger = Book.create! :title => 'Dodger', :year => 2012 index expectations = [[snuff, 2011], [earth, 2012]] Book.search(:group_by => :year).each_with_group do |book, group| expectation = expectations.shift expect(book).to eq(expectation.first) expect(group).to eq(expectation.last) end end it "allows enumerating by group and count" do snuff = Book.create! :title => 'Snuff', :year => 2011 earth = Book.create! :title => 'The Long Earth', :year => 2012 dodger = Book.create! :title => 'Dodger', :year => 2012 index expectations = [[snuff, 2011, 1], [earth, 2012, 2]] search = Book.search(:group_by => :year) search.each_with_group_and_count do |book, group, count| expectation = expectations.shift expect(book).to eq(expectation[0]) expect(group).to eq(expectation[1]) expect(count).to eq(expectation[2]) end end end thinking-sphinx-4.1.0/spec/acceptance/index_options_spec.rb000066400000000000000000000076161341132130100240760ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Index options' do let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) } %w( infix prefix ).each do |type| context "all fields are #{type}ed" do before :each do index.definition_block = Proc.new { indexes title set_property "min_#{type}_len".to_sym => 3 } index.render end it "keeps #{type}_fields blank" do expect(index.send("#{type}_fields")).to be_nil end it "sets min_#{type}_len" do expect(index.send("min_#{type}_len")).to eq(3) end end context "some fields are #{type}ed" do before :each do index.definition_block = Proc.new { indexes title, "#{type}es".to_sym => true indexes content set_property "min_#{type}_len".to_sym => 3 } index.render end it "#{type}_fields should contain the field" do expect(index.send("#{type}_fields")).to eq('title') end it "sets min_#{type}_len" do expect(index.send("min_#{type}_len")).to eq(3) end end end context "multiple source definitions" do before :each do index.definition_block = Proc.new { define_source do indexes title end define_source do indexes title, content end } index.render end it "stores each source definition" do expect(index.sources.length).to eq(2) end it "treats each source as separate" do expect(index.sources.first.fields.length).to eq(2) expect(index.sources.last.fields.length).to eq(3) end end context 'wordcount fields and attributes' do before :each do index.definition_block = Proc.new { indexes title, :wordcount => true has content, :type => :wordcount } index.render end it "declares wordcount fields" do expect(index.sources.first.sql_field_str2wordcount).to eq(['title']) end it "declares wordcount attributes" do expect(index.sources.first.sql_attr_str2wordcount).to eq(['content']) end end context 'respecting source options' do before :each do index.definition_block = Proc.new { indexes title set_property :sql_range_step => 5 set_property :disable_range? => true set_property :sql_query_pre => ["DO STUFF"] } index.render end it "allows for core source settings" do expect(index.sources.first.sql_range_step).to eq(5) end it "allows for source options" do expect(index.sources.first.disable_range?).to be_truthy end it "respects sql_query_pre values" do expect(index.sources.first.sql_query_pre).to include("DO STUFF") end end context 'respecting index options over core configuration' do before :each do ThinkingSphinx::Configuration.instance.settings['min_infix_len'] = 2 ThinkingSphinx::Configuration.instance.settings['sql_range_step'] = 2 index.definition_block = Proc.new { indexes title set_property :min_infix_len => 1 set_property :sql_range_step => 20 } index.render end after :each do ThinkingSphinx::Configuration.instance.settings.delete 'min_infix_len' ThinkingSphinx::Configuration.instance.settings.delete 'sql_range_step' end it "prioritises index-level options over YAML options" do expect(index.min_infix_len).to eq(1) end it "prioritises index-level source options" do expect(index.sources.first.sql_range_step).to eq(20) end it "keeps index-level options prioritised when rendered again" do index.render expect(index.min_infix_len).to eq(1) end it "keeps index-level options prioritised when rendered again" do index.render expect(index.sources.first.sql_range_step).to eq(20) end end end thinking-sphinx-4.1.0/spec/acceptance/indexing_spec.rb000066400000000000000000000020721341132130100230100ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Indexing', :live => true do it "does not index files where the temp file exists" do path = Rails.root.join('db/sphinx/test/ts-article_core.tmp') FileUtils.mkdir_p Rails.root.join('db/sphinx/test') FileUtils.touch path article = Article.create! :title => 'Pancakes' index 'article_core' expect(Article.search).to be_empty FileUtils.rm path end it "indexes files when other indices are already being processed" do path = Rails.root.join('db/sphinx/test/ts-book_core.tmp') FileUtils.mkdir_p Rails.root.join('db/sphinx/test') FileUtils.touch path article = Article.create! :title => 'Pancakes' index 'article_core' expect(Article.search).not_to be_empty FileUtils.rm path end it "cleans up temp files even when an exception is raised" do FileUtils.mkdir_p Rails.root.join('db/sphinx/test') index 'article_core' file = Rails.root.join('db/sphinx/test/ts-article_core.tmp') expect(File.exist?(file)).to be_falsey end end thinking-sphinx-4.1.0/spec/acceptance/merging_spec.rb000066400000000000000000000042201341132130100226300ustar00rootroot00000000000000# frozen_string_literal: true require "acceptance/spec_helper" describe "Merging deltas", :live => true do it "merges in new records" do guards = Book.create( :title => "Guards! Guards!", :author => "Terry Pratchett" ) sleep 0.25 expect( Book.search("Terry Pratchett", :indices => ["book_delta"]).to_a ).to eq([guards]) expect( Book.search("Terry Pratchett", :indices => ["book_core"]).to_a ).to be_empty merge guards.reload expect( Book.search("Terry Pratchett", :indices => ["book_core"]).to_a ).to eq([guards]) expect(guards.delta).to eq(false) end it "merges in changed records" do race = Book.create( :title => "The Hate Space", :author => "Maxine Beneba Clarke" ) index expect( Book.search("Space", :indices => ["book_core"]).to_a ).to eq([race]) race.reload.update_attributes :title => "The Hate Race" sleep 0.25 expect( Book.search("Race", :indices => ["book_delta"]).to_a ).to eq([race]) expect( Book.search("Race", :indices => ["book_core"]).to_a ).to be_empty merge race.reload expect( Book.search("Race", :indices => ["book_core"]).to_a ).to eq([race]) expect( Book.search("Race", :indices => ["book_delta"]).to_a ).to eq([race]) expect( Book.search("Space", :indices => ["book_core"]).to_a ).to be_empty expect(race.delta).to eq(false) end it "maintains existing records" do race = Book.create( :title => "The Hate Race", :author => "Maxine Beneba Clarke" ) index soil = Book.create( :title => "Foreign Soil", :author => "Maxine Beneba Clarke" ) sleep 0.25 expect( Book.search("Soil", :indices => ["book_delta"]).to_a ).to eq([soil]) expect( Book.search("Soil", :indices => ["book_core"]).to_a ).to be_empty expect( Book.search("Race", :indices => ["book_core"]).to_a ).to eq([race]) merge expect( Book.search("Soil", :indices => ["book_core"]).to_a ).to eq([soil]) expect( Book.search("Race", :indices => ["book_core"]).to_a ).to eq([race]) end end thinking-sphinx-4.1.0/spec/acceptance/paginating_search_results_spec.rb000066400000000000000000000012351341132130100264320ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Paginating search results', :live => true do it "tracks how many results there are in total" do 21.times { |number| Article.create :title => "Article #{number}" } index expect(Article.search.total_entries).to eq(21) end it "paginates the result set by default" do 21.times { |number| Article.create :title => "Article #{number}" } index expect(Article.search.length).to eq(20) end it "tracks the number of pages" do 21.times { |number| Article.create :title => "Article #{number}" } index expect(Article.search.total_pages).to eq(2) end end thinking-sphinx-4.1.0/spec/acceptance/real_time_updates_spec.rb000066400000000000000000000016661341132130100247010ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Updates to records in real-time indices', :live => true do it "handles fields with unicode nulls" do product = Product.create! :name => "Widget \u0000" expect(Product.search.first).to eq(product) end unless ENV['DATABASE'] == 'postgresql' it "handles attributes for sortable fields accordingly" do product = Product.create! :name => 'Red Fish' product.update_attributes :name => 'Blue Fish' expect(Product.search('blue fish', :indices => ['product_core']).to_a). to eq([product]) end it "handles inserts and updates for namespaced models" do person = Admin::Person.create :name => 'Death' expect(Admin::Person.search('Death').to_a).to eq([person]) person.update_attributes :name => 'Mort' expect(Admin::Person.search('Death').to_a).to be_empty expect(Admin::Person.search('Mort').to_a).to eq([person]) end end thinking-sphinx-4.1.0/spec/acceptance/remove_deleted_records_spec.rb000066400000000000000000000034451341132130100257140ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Hiding deleted records from search results', :live => true do it "does not return deleted records" do pancakes = Article.create! :title => 'Pancakes' index expect(Article.search('pancakes')).not_to be_empty pancakes.destroy expect(Article.search('pancakes')).to be_empty end it "will catch stale records deleted without callbacks being fired" do pancakes = Article.create! :title => 'Pancakes' index expect(Article.search('pancakes')).not_to be_empty Article.connection.execute "DELETE FROM articles WHERE id = #{pancakes.id}" expect(Article.search('pancakes')).to be_empty end it "removes records from real-time index results" do product = Product.create! :name => 'Shiny' expect(Product.search('Shiny', :indices => ['product_core']).to_a). to eq([product]) product.destroy expect(Product.search_for_ids('Shiny', :indices => ['product_core'])). to be_empty end it "does not remove real-time results when callbacks are disabled" do original = ThinkingSphinx::Configuration.instance. settings['real_time_callbacks'] product = Product.create! :name => 'Shiny' expect(Product.search('Shiny', :indices => ['product_core']).to_a). to eq([product]) ThinkingSphinx::Configuration.instance. settings['real_time_callbacks'] = false product.destroy expect(Product.search_for_ids('Shiny', :indices => ['product_core'])). not_to be_empty ThinkingSphinx::Configuration.instance. settings['real_time_callbacks'] = original end it "deletes STI child classes from parent indices" do duck = Bird.create :name => 'Duck' index duck.destroy expect(Bird.search_for_ids('duck')).to be_empty end end thinking-sphinx-4.1.0/spec/acceptance/search_counts_spec.rb000066400000000000000000000007671341132130100240540ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Get search result counts', :live => true do it "returns counts for a single model" do 4.times { |i| Article.create :title => "Article #{i}" } index expect(Article.search_count).to eq(4) end it "returns counts across all models" do 3.times { |i| Article.create :title => "Article #{i}" } 2.times { |i| Book.create :title => "Book #{i}" } index expect(ThinkingSphinx.count).to eq(5) end end thinking-sphinx-4.1.0/spec/acceptance/search_for_just_ids_spec.rb000066400000000000000000000011441341132130100252210ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching for just instance Ids', :live => true do it "returns just the instance ids" do pancakes = Article.create! :title => 'Pancakes' waffles = Article.create! :title => 'Waffles' index expect(Article.search_for_ids('pancakes').to_a).to eq([pancakes.id]) end it "works across the global context" do article = Article.create! :title => 'Pancakes' book = Book.create! :title => 'American Gods' index expect(ThinkingSphinx.search_for_ids.to_a).to match_array([article.id, book.id]) end end thinking-sphinx-4.1.0/spec/acceptance/searching_across_models_spec.rb000066400000000000000000000021501341132130100260600ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching across models', :live => true do it "returns results" do article = Article.create! :title => 'Pancakes' index expect(ThinkingSphinx.search.first).to eq(article) end it "returns results matching the given query" do pancakes = Article.create! :title => 'Pancakes' waffles = Article.create! :title => 'Waffles' index articles = ThinkingSphinx.search 'pancakes' expect(articles).to include(pancakes) expect(articles).not_to include(waffles) end it "handles results from different models" do article = Article.create! :title => 'Pancakes' book = Book.create! :title => 'American Gods' index expect(ThinkingSphinx.search.to_a).to match_array([article, book]) end it "filters by multiple classes" do article = Article.create! :title => 'Pancakes' book = Book.create! :title => 'American Gods' user = User.create! :name => 'Pat' index expect(ThinkingSphinx.search(:classes => [User, Article]).to_a). to match_array([article, user]) end end thinking-sphinx-4.1.0/spec/acceptance/searching_across_schemas_spec.rb000066400000000000000000000027021341132130100262230ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' multi_schema = MultiSchema.new describe 'Searching across PostgreSQL schemas', :live => true do before :each do ThinkingSphinx::Configuration.instance.index_set_class = MultiSchema::IndexSet end after :each do ThinkingSphinx::Configuration.instance.index_set_class = nil multi_schema.switch :public end it 'can distinguish between objects with the same primary key' do multi_schema.switch :public jekyll = Product.create name: 'Doctor Jekyll' expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([jekyll]) expect(Product.search(:retry_stale => false).to_a).to eq([jekyll]) multi_schema.switch :thinking_sphinx hyde = Product.create name: 'Mister Hyde' expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([]) expect(Product.search('Hyde', :retry_stale => false).to_a).to eq([hyde]) expect(Product.search(:retry_stale => false).to_a).to eq([hyde]) multi_schema.switch :public expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([jekyll]) expect(Product.search(:retry_stale => false).to_a).to eq([jekyll]) expect(Product.search('Hyde', :retry_stale => false).to_a).to eq([]) expect(Product.search( :middleware => ThinkingSphinx::Middlewares::RAW_ONLY, :indices => ['product_core', 'product_two_core'] ).to_a.length).to eq(2) end end if multi_schema.active? thinking-sphinx-4.1.0/spec/acceptance/searching_on_fields_spec.rb000066400000000000000000000036101341132130100251670ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching on fields', :live => true do it "limits results by field" do pancakes = Article.create! :title => 'Pancakes' waffles = Article.create! :title => 'Waffles', :content => 'Different to pancakes - and not quite as tasty.' index articles = Article.search :conditions => {:title => 'pancakes'} expect(articles).to include(pancakes) expect(articles).not_to include(waffles) end it "limits results for a field from an association" do user = User.create! :name => 'Pat' pancakes = Article.create! :title => 'Pancakes', :user => user index expect(Article.search(:conditions => {:user => 'pat'}).first).to eq(pancakes) end it "returns results with matches from grouped fields" do user = User.create! :name => 'Pat' pancakes = Article.create! :title => 'Pancakes', :user => user waffles = Article.create! :title => 'Waffles', :user => user index expect(Article.search('waffles', :conditions => {:title => 'pancakes'}).to_a). to eq([pancakes]) end it "returns results with matches from concatenated columns in a field" do book = Book.create! :title => 'Night Watch', :author => 'Terry Pratchett' index expect(Book.search(:conditions => {:info => 'Night Pratchett'}).to_a). to eq([book]) end it "handles NULLs in concatenated fields" do book = Book.create! :title => 'Night Watch' index expect(Book.search(:conditions => {:info => 'Night Watch'}).to_a).to eq([book]) end it "returns results with matches from file fields" do file_path = Rails.root.join('tmp', 'caption.txt') File.open(file_path, 'w') { |file| file.print 'Cyberpunk at its best' } book = Book.create! :title => 'Accelerando', :blurb_file => file_path.to_s index expect(Book.search('cyberpunk').to_a).to eq([book]) end end thinking-sphinx-4.1.0/spec/acceptance/searching_with_filters_spec.rb000066400000000000000000000125661341132130100257420ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching with filters', :live => true do it "limits results by single value boolean filters" do pancakes = Article.create! :title => 'Pancakes', :published => true waffles = Article.create! :title => 'Waffles', :published => false index expect(Article.search(:with => {:published => true}).to_a).to eq([pancakes]) end it "limits results by an array of values" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.search(:with => {:year => [2001, 2005]}).to_a).to eq([gods, boys]) end it "limits results by a ranged filter" do gods = Book.create! :title => 'American Gods' boys = Book.create! :title => 'Anansi Boys' grave = Book.create! :title => 'The Graveyard Book' gods.update_column :created_at, 5.days.ago boys.update_column :created_at, 3.days.ago grave.update_column :created_at, 1.day.ago index expect(Book.search(:with => {:created_at => 6.days.ago..2.days.ago}).to_a). to eq([gods, boys]) end it "limits results by exclusive filters on single values" do pancakes = Article.create! :title => 'Pancakes', :published => true waffles = Article.create! :title => 'Waffles', :published => false index expect(Article.search(:without => {:published => true}).to_a).to eq([waffles]) end it "limits results by exclusive filters on arrays of values" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.search(:without => {:year => [2001, 2005]}).to_a).to eq([grave]) end it "limits results by ranged filters on timestamp MVAs" do pancakes = Article.create :title => 'Pancakes' waffles = Article.create :title => 'Waffles' food = Tag.create :name => 'food' flat = Tag.create :name => 'flat' Tagging.create(:tag => food, :article => pancakes). update_column :created_at, 5.days.ago Tagging.create :tag => flat, :article => pancakes Tagging.create(:tag => food, :article => waffles). update_column :created_at, 3.days.ago index expect(Article.search( :with => {:taggings_at => 1.days.ago..1.day.from_now} ).to_a).to eq([pancakes]) end it "takes into account local timezones for timestamps" do pancakes = Article.create :title => 'Pancakes' waffles = Article.create :title => 'Waffles' food = Tag.create :name => 'food' flat = Tag.create :name => 'flat' Tagging.create(:tag => food, :article => pancakes). update_column :created_at, 5.minutes.ago Tagging.create :tag => flat, :article => pancakes Tagging.create(:tag => food, :article => waffles). update_column :created_at, 3.minute.ago index expect(Article.search( :with => {:taggings_at => 2.minutes.ago..Time.zone.now} ).to_a).to eq([pancakes]) end it "limits results with MVAs having all of the given values" do pancakes = Article.create :title => 'Pancakes' waffles = Article.create :title => 'Waffles' food = Tag.create :name => 'food' flat = Tag.create :name => 'flat' Tagging.create :tag => food, :article => pancakes Tagging.create :tag => flat, :article => pancakes Tagging.create :tag => food, :article => waffles index articles = Article.search :with_all => {:tag_ids => [food.id, flat.id]} expect(articles.to_a).to eq([pancakes]) end it "limits results with MVAs that don't contain all the given values" do # Matching results may have some of the given values, but cannot have all # of them. Certainly an edge case. skip "SphinxQL doesn't yet support OR in its WHERE clause" pancakes = Article.create :title => 'Pancakes' waffles = Article.create :title => 'Waffles' food = Tag.create :name => 'food' flat = Tag.create :name => 'flat' Tagging.create :tag => food, :article => pancakes Tagging.create :tag => flat, :article => pancakes Tagging.create :tag => food, :article => waffles index articles = Article.search :without_all => {:tag_ids => [food.id, flat.id]} expect(articles.to_a).to eq([waffles]) end it "limits results on real-time indices with multi-value integer attributes" do pancakes = Product.create :name => 'Pancakes' waffles = Product.create :name => 'Waffles' food = Category.create :name => 'food' flat = Category.create :name => 'flat' pancakes.categories << food pancakes.categories << flat waffles.categories << food products = Product.search :with => {:category_ids => [flat.id]} expect(products.to_a).to eq([pancakes]) end it 'searches with real-time JSON attributes' do pancakes = Product.create :name => 'Pancakes', :options => {'lemon' => 1, 'sugar' => 1, :number => 3} waffles = Product.create :name => 'Waffles', :options => {'chocolate' => 1, 'sugar' => 1, :number => 1} products = Product.search :with => {"options.lemon" => 1} expect(products.to_a).to eq([pancakes]) products = Product.search :with => {"options.sugar" => 1} expect(products.to_a).to eq([pancakes, waffles]) end if JSONColumn.call end thinking-sphinx-4.1.0/spec/acceptance/searching_with_sti_spec.rb000066400000000000000000000043411341132130100250610ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching across STI models', :live => true do it "returns super- and sub-class results" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' index expect(Animal.search(:indices => ['animal_core']).to_a).to eq([platypus, duck]) end it "limits results based on subclasses" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' index expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck]) end it "returns results for deeper subclasses when searching on their parents" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' emu = FlightlessBird.create :name => 'Emu' index expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck, emu]) end it "returns results for deeper subclasses" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' emu = FlightlessBird.create :name => 'Emu' index expect(FlightlessBird.search(:indices => ['animal_core']).to_a).to eq([emu]) end it "filters out sibling subclasses" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' otter = Mammal.create :name => 'Otter' index expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck]) end it "obeys :classes if supplied" do platypus = Animal.create :name => 'Platypus' duck = Bird.create :name => 'Duck' emu = FlightlessBird.create :name => 'Emu' index expect(Bird.search( :indices => ['animal_core'], :skip_sti => true, :classes => [Bird, FlightlessBird] ).to_a).to eq([duck, emu]) end it 'finds root objects when type is blank' do animal = Animal.create :name => 'Animal', type: '' index expect(Animal.search(:indices => ['animal_core']).to_a).to eq([animal]) end it 'allows for indices on mid-hierarchy classes' do duck = Bird.create :name => 'Duck' emu = FlightlessBird.create :name => 'Emu' index expect(Bird.search(:indices => ['bird_core']).to_a).to eq([duck, emu]) end end thinking-sphinx-4.1.0/spec/acceptance/searching_within_a_model_spec.rb000066400000000000000000000053301341132130100262100ustar00rootroot00000000000000# encoding: UTF-8 # frozen_string_literal: true require 'acceptance/spec_helper' describe 'Searching within a model', :live => true do it "returns results" do article = Article.create! :title => 'Pancakes' index expect(Article.search.first).to eq(article) end it "returns results matching the given query" do pancakes = Article.create! :title => 'Pancakes' waffles = Article.create! :title => 'Waffles' index articles = Article.search 'pancakes' expect(articles).to include(pancakes) expect(articles).not_to include(waffles) end it "handles unicode characters" do istanbul = City.create! :name => 'İstanbul' index expect(City.search('İstanbul').to_a).to eq([istanbul]) end it "will star provided queries on request" do article = Article.create! :title => 'Pancakes' index expect(Article.search('cake', :star => true).first).to eq(article) end it "allows for searching on specific indices" do article = Article.create :title => 'Pancakes' index articles = Article.search('pancake', :indices => ['stemmed_article_core']) expect(articles.to_a).to eq([article]) end it "allows for searching on distributed indices" do article = Article.create :title => 'Pancakes' index articles = Article.search('pancake', :indices => ['article']) expect(articles.to_a).to eq([article]) end it "can search on namespaced models" do person = Admin::Person.create :name => 'James Bond' index expect(Admin::Person.search('Bond').to_a).to eq([person]) end it "raises an error if searching through an ActiveRecord scope" do expect { City.ordered.search }.to raise_error(ThinkingSphinx::MixedScopesError) end it "does not raise an error when searching with a default ActiveRecord scope" do expect { User.search }.not_to raise_error end it "raises an error when searching with default and applied AR scopes" do expect { User.recent.search }.to raise_error(ThinkingSphinx::MixedScopesError) end it "raises an error if the model has no indices defined" do expect { 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(:indices => ['album_core', 'album_delta']).first). to eq(album) expect(Album.search(:indices => ['album_real_core']).first). to eq(album) end end describe 'Searching within a model with a realtime index', :live => true do it "returns results" do product = Product.create! :name => 'Widget' expect(Product.search.first).to eq(product) end end thinking-sphinx-4.1.0/spec/acceptance/sorting_search_results_spec.rb000066400000000000000000000034601341132130100260000ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Sorting search results', :live => true do it "sorts by a given clause" do gods = Book.create! :title => 'American Gods', :year => 2001 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 boys = Book.create! :title => 'Anansi Boys', :year => 2005 index expect(Book.search(:order => 'year ASC').to_a).to eq([gods, boys, grave]) end it "sorts by a given attribute in ascending order" do gods = Book.create! :title => 'American Gods', :year => 2001 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 boys = Book.create! :title => 'Anansi Boys', :year => 2005 index expect(Book.search(:order => :year).to_a).to eq([gods, boys, grave]) end it "sorts by a given sortable field" do gods = Book.create! :title => 'American Gods', :year => 2001 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 boys = Book.create! :title => 'Anansi Boys', :year => 2005 index expect(Book.search(:order => :title).to_a).to eq([gods, boys, grave]) end it "sorts by a given sortable field with real-time indices" do widgets = Product.create! :name => 'Widgets' gadgets = Product.create! :name => 'Gadgets' expect(Product.search(:order => "name_sort ASC").to_a).to eq([gadgets, widgets]) end it "can sort with a provided expression" do gods = Book.create! :title => 'American Gods', :year => 2001 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 boys = Book.create! :title => 'Anansi Boys', :year => 2005 index expect(Book.search( :select => '*, year MOD 2004 as mod_year', :order => 'mod_year ASC' ).to_a).to eq([boys, grave, gods]) end end thinking-sphinx-4.1.0/spec/acceptance/spec_helper.rb000066400000000000000000000002411341132130100224560ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' root = File.expand_path File.dirname(__FILE__) Dir["#{root}/support/**/*.rb"].each { |file| require file } thinking-sphinx-4.1.0/spec/acceptance/specifying_sql_spec.rb000066400000000000000000000441621341132130100242300ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'specifying SQL for index definitions' do it "renders the SQL with the join" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title join user } index.render expect(index.sources.first.sql_query).to match(/LEFT OUTER JOIN .users./) end it "handles deep joins" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title join user.articles } index.render query = index.sources.first.sql_query expect(query).to match(/LEFT OUTER JOIN .users./) expect(query).to match(/LEFT OUTER JOIN .articles./) end it "handles has-many :through joins" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes tags.name } index.render query = index.sources.first.sql_query expect(query).to match(/LEFT OUTER JOIN .taggings./) expect(query).to match(/LEFT OUTER JOIN .tags./) end it "handles custom join SQL statements" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title join "INNER JOIN foo ON foo.x = bar.y" } index.render query = index.sources.first.sql_query expect(query).to match(/INNER JOIN foo ON foo.x = bar.y/) end it "handles GROUP BY clauses" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title group_by 'lat' } index.render query = index.sources.first.sql_query expect(query).to match(/GROUP BY .articles.\..id., .?articles.?\..title., .?articles.?\..id., lat/) end it "handles WHERE clauses" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title where "title != 'secret'" } index.render query = index.sources.first.sql_query expect(query).to match(/WHERE .+title != 'secret'.+ GROUP BY/) end it "handles manual MVA declarations" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title has "taggings.tag_ids", :as => :tag_ids, :type => :integer, :multi => true } index.render expect(index.sources.first.sql_attr_multi).to eq(['uint tag_ids from field']) end it "provides the sanitize_sql helper within the index definition block" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title where sanitize_sql(["title != ?", 'secret']) } index.render query = index.sources.first.sql_query expect(query).to match(/WHERE .+title != 'secret'.+ GROUP BY/) end it "escapes new lines in SQL snippets" do index = ThinkingSphinx::ActiveRecord::Index.new(:article) index.definition_block = Proc.new { indexes title has <<-SQL, as: :custom_attribute, type: :integer ARRAY_AGG( CONCAT( something ) ) SQL } index.render query = index.sources.first.sql_query expect(query).to match(/\\\n/) end it "joins each polymorphic relation" do index = ThinkingSphinx::ActiveRecord::Index.new(:event) index.definition_block = Proc.new { indexes eventable.title, :as => :title polymorphs eventable, :to => %w(Article Book) } index.render query = index.sources.first.sql_query expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) expect(query).to match(/LEFT OUTER JOIN .books. ON .books.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Book'/) expect(query).to match(/.articles.\..title., .books.\..title./) end if ActiveRecord::VERSION::MAJOR > 3 it "concatenates references that have column" do index = ThinkingSphinx::ActiveRecord::Index.new(:event) index.definition_block = Proc.new { indexes eventable.title, :as => :title polymorphs eventable, :to => %w(Article User) } index.render query = index.sources.first.sql_query expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) expect(query).not_to match(/articles\..title., users\..title./) expect(query).to match(/.articles.\..title./) end if ActiveRecord::VERSION::MAJOR > 3 it "respects deeper associations through polymorphic joins" do index = ThinkingSphinx::ActiveRecord::Index.new(:event) index.definition_block = Proc.new { indexes eventable.user.name, :as => :user_name polymorphs eventable, :to => %w(Article Book) } index.render query = index.sources.first.sql_query expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) 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 } it "generates an appropriate SQL query for an MVA" do index.definition_block = Proc.new { indexes title has taggings.tag_id, :as => :tag_ids, :source => :query } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) 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 it "generates a SQL query with joins when appropriate for MVAs" do index.definition_block = Proc.new { indexes title has taggings.tag.id, :as => :tag_ids, :source => :query } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) 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 it "respects has_many :through joins for MVA queries" do index.definition_block = Proc.new { indexes title has tags.id, :as => :tag_ids, :source => :query } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) 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 it "can handle multiple joins for MVA queries" do index = ThinkingSphinx::ActiveRecord::Index.new(:user) index.definition_block = Proc.new { indexes name has articles.tags.id, :as => :tag_ids, :source => :query } index.render source = index.sources.first attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) 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 it "can handle simple HABTM joins for MVA queries" do index = ThinkingSphinx::ActiveRecord::Index.new(:book) index.definition_block = Proc.new { indexes title has genres.id, :as => :genre_ids, :source => :query } index.render source = index.sources.first attribute = source.sql_attr_multi.detect { |attribute| attribute[/genre_ids/] } declaration, query = attribute.split(/;\s+/) 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 it "generates an appropriate range SQL queries for an MVA" do index.definition_block = Proc.new { indexes title has taggings.tag_id, :as => :tag_ids, :source => :ranged_query } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query, range = attribute.split(/;\s+/) 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 it "generates a SQL query with joins when appropriate for MVAs" do index.definition_block = Proc.new { indexes title has taggings.tag.id, :as => :tag_ids, :source => :ranged_query } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query, range = attribute.split(/;\s+/) 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 it "can handle ranged queries for simple HABTM joins for MVA queries" do index = ThinkingSphinx::ActiveRecord::Index.new(:book) index.definition_block = Proc.new { indexes title has genres.id, :as => :genre_ids, :source => :ranged_query } index.render source = index.sources.first attribute = source.sql_attr_multi.detect { |attribute| attribute[/genre_ids/] } declaration, query, range = attribute.split(/;\s+/) 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 it "respects custom SQL snippets as the query value" do index.definition_block = Proc.new { indexes title has 'My Custom SQL Query', :as => :tag_ids, :source => :query, :type => :integer, :multi => true } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) expect(declaration).to eq('uint tag_ids from query') expect(query).to eq('My Custom SQL Query') end it "respects custom SQL snippets as the ranged query value" do index.definition_block = Proc.new { indexes title has 'My Custom SQL Query; And a Range', :as => :tag_ids, :source => :ranged_query, :type => :integer, :multi => true } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query, range = attribute.split(/;\s+/) expect(declaration).to eq('uint tag_ids from ranged-query') expect(query).to eq('My Custom SQL Query') expect(range).to eq('And a Range') end it "escapes new lines in custom SQL snippets" do index.definition_block = Proc.new { indexes title has <<-SQL, :as => :tag_ids, :source => :query, :type => :integer, :multi => true My Custom SQL Query SQL } index.render attribute = source.sql_attr_multi.detect { |attribute| attribute[/tag_ids/] } declaration, query = attribute.split(/;\s+/) expect(declaration).to eq('uint tag_ids from query') expect(query).to eq("My Custom\\\nSQL Query") end end describe 'separate queries for field' do let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) } let(:count) { ThinkingSphinx::Configuration.instance.indices.count } let(:source) { index.sources.first } it "generates a SQL query with joins when appropriate for MVF" do index.definition_block = Proc.new { indexes taggings.tag.name, :as => :tags, :source => :query } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) expect(declaration).to eq('tags from query') expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) end it "respects has_many :through joins for MVF queries" do index.definition_block = Proc.new { indexes tags.name, :as => :tags, :source => :query } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) expect(declaration).to eq('tags from query') expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) end it "can handle multiple joins for MVF queries" do index = ThinkingSphinx::ActiveRecord::Index.new(:user) index.definition_block = Proc.new { indexes articles.tags.name, :as => :tags, :source => :query } index.render source = index.sources.first field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) expect(declaration).to eq('tags from query') expect(query).to match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.articles.\..user_id. IS NOT NULL\)\s? ORDER BY .articles.\..user_id. ASC\s?$/) end it "generates a SQL query with joins when appropriate for MVFs" do index.definition_block = Proc.new { indexes taggings.tag.name, :as => :tags, :source => :ranged_query } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query, range = field.split(/;\s+/) expect(declaration).to eq('tags from ranged-query') expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. 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\)\s? ORDER BY .taggings.\..article_id. ASC$/) expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) end it "respects custom SQL snippets as the query value" do index.definition_block = Proc.new { indexes 'My Custom SQL Query', :as => :tags, :source => :query } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) expect(declaration).to eq('tags from query') expect(query).to eq('My Custom SQL Query') end it "respects custom SQL snippets as the ranged query value" do index.definition_block = Proc.new { indexes 'My Custom SQL Query; And a Range', :as => :tags, :source => :ranged_query } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query, range = field.split(/;\s+/) expect(declaration).to eq('tags from ranged-query') expect(query).to eq('My Custom SQL Query') expect(range).to eq('And a Range') end it "escapes new lines in custom SQL snippets" do index.definition_block = Proc.new { indexes <<-SQL, :as => :tags, :source => :query My Custom SQL Query SQL } index.render field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) expect(declaration).to eq('tags from query') expect(query).to eq("My Custom\\\nSQL Query") end end thinking-sphinx-4.1.0/spec/acceptance/sphinx_scopes_spec.rb000066400000000000000000000051641341132130100240750ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Sphinx scopes', :live => true do it "allows calling sphinx scopes from models" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.by_year(2009).to_a).to eq([grave]) end it "allows scopes to return both query and options" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.by_query_and_year('Graveyard', 2009).to_a).to eq([grave]) end it "allows chaining of scopes" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.by_year(2001..2005).ordered.to_a).to eq([boys, gods]) end it "allows chaining of scopes that include queries" do gods = Book.create! :title => 'American Gods', :year => 2001 boys = Book.create! :title => 'Anansi Boys', :year => 2005 grave = Book.create! :title => 'The Graveyard Book', :year => 2009 index expect(Book.by_year(2001).by_query_and_year('Graveyard', 2009).to_a). to eq([grave]) end it "allows further search calls on scopes" do gaiman = Book.create! :title => 'American Gods' pratchett = Book.create! :title => 'Small Gods' index expect(Book.by_query('Gods').search('Small').to_a).to eq([pratchett]) end it "allows facet calls on scopes" do Book.create! :title => 'American Gods', :author => 'Neil Gaiman' Book.create! :title => 'Anansi Boys', :author => 'Neil Gaiman' Book.create! :title => 'Small Gods', :author => 'Terry Pratchett' index expect(Book.by_query('Gods').facets.to_hash[:author]).to eq({ 'Neil Gaiman' => 1, 'Terry Pratchett' => 1 }) end it "allows accessing counts on scopes" do Book.create! :title => 'American Gods' Book.create! :title => 'Anansi Boys' Book.create! :title => 'Small Gods' Book.create! :title => 'Night Watch' index expect(Book.by_query('gods').count).to eq(2) end it 'raises an exception when trying to modify a populated request' do request = Book.by_query('gods') request.count expect { request.search('foo') }.to raise_error( ThinkingSphinx::PopulatedResultsError ) end end thinking-sphinx-4.1.0/spec/acceptance/sql_deltas_spec.rb000066400000000000000000000032721341132130100233410ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'SQL delta indexing', :live => true do it "automatically indexes new records" do guards = Book.create( :title => 'Guards! Guards!', :author => 'Terry Pratchett' ) index expect(Book.search('Terry Pratchett').to_a).to eq([guards]) men = Book.create( :title => 'Men At Arms', :author => 'Terry Pratchett' ) sleep 0.25 expect(Book.search('Terry Pratchett').to_a).to eq([guards, men]) end it "automatically indexes updated records" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index expect(Book.search('Harry').to_a).to eq([book]) book.reload.update_attributes(:author => 'Terry Pratchett') sleep 0.25 expect(Book.search('Terry').to_a).to eq([book]) end it "does not match on old values" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index expect(Book.search('Harry').to_a).to eq([book]) book.reload.update_attributes(:author => 'Terry Pratchett') sleep 0.25 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' ) sleep 0.25 expect(Book.search('Gaiman').to_a).to eq([book]) end end thinking-sphinx-4.1.0/spec/acceptance/support/000077500000000000000000000000001341132130100213575ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/acceptance/support/database_cleaner.rb000066400000000000000000000004251341132130100251420ustar00rootroot00000000000000# frozen_string_literal: true RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation end config.after(:each) do |example| if example.example_group_instance.class.metadata[:live] DatabaseCleaner.clean end end end thinking-sphinx-4.1.0/spec/acceptance/support/sphinx_controller.rb000066400000000000000000000024541341132130100254650ustar00rootroot00000000000000# frozen_string_literal: true class SphinxController def initialize config.searchd.mysql41 = 9307 end def setup FileUtils.mkdir_p config.indices_location config.controller.bin_path = ENV['SPHINX_BIN'] || '' config.render_to_file && index ThinkingSphinx::Configuration.reset ActiveSupport::Dependencies.loaded.each do |path| $LOADED_FEATURES.delete "#{path}.rb" end ActiveSupport::Dependencies.clear config.searchd.mysql41 = 9307 config.settings['quiet_deltas'] = true config.settings['attribute_updates'] = true config.controller.bin_path = ENV['SPHINX_BIN'] || '' end def start config.controller.start rescue Riddle::CommandFailedError => error puts <<-TXT The Sphinx start command failed: Command: #{error.command_result.command} Status: #{error.command_result.status} Output: #{error.command_result.output} TXT raise error end def stop while config.controller.running? do config.controller.stop sleep(0.1) end end def index(*indices) ThinkingSphinx::Commander.call :index_sql, config, :indices => indices end def merge ThinkingSphinx::Commander.call(:merge_and_update, config, {}) end private def config ThinkingSphinx::Configuration.instance end end thinking-sphinx-4.1.0/spec/acceptance/support/sphinx_helpers.rb000066400000000000000000000015451341132130100247440ustar00rootroot00000000000000# frozen_string_literal: true module SphinxHelpers def sphinx @sphinx ||= SphinxController.new end def index(*indices) sleep 0.5 if ENV['TRAVIS'] yield if block_given? sphinx.index *indices sleep 0.25 sleep 0.5 if ENV['TRAVIS'] end def merge sleep 0.5 if ENV['TRAVIS'] sleep 0.5 sphinx.merge sleep 1.5 sleep 0.5 if ENV['TRAVIS'] end end RSpec.configure do |config| config.include SphinxHelpers config.before :all do |group| FileUtils.rm_rf ThinkingSphinx::Configuration.instance.indices_location FileUtils.rm_rf ThinkingSphinx::Configuration.instance.searchd.binlog_path sphinx.setup && sphinx.start if group.class.metadata[:live] end config.after :all do |group| sphinx.stop if group.class.metadata[:live] end config.after :suite do SphinxController.new.stop end end thinking-sphinx-4.1.0/spec/acceptance/suspended_deltas_spec.rb000066400000000000000000000027741341132130100245420ustar00rootroot00000000000000# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Suspend deltas for a given action', :live => true do it "does not update the delta indices until after the block is finished" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend :book do book.reload.update_attributes(:author => 'Terry Pratchett') sleep 0.25 expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 expect(Book.search('Terry').to_a).to eq([book]) end it "returns core records even though they are no longer valid" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend :book do book.reload.update_attributes(:author => 'Terry Pratchett') sleep 0.25 expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 expect(Book.search('Harry').to_a).to eq([book]) end it "marks core records as deleted" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend_and_update :book do book.reload.update_attributes(:author => 'Terry Pratchett') sleep 0.25 expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 expect(Book.search('Harry').to_a).to be_empty end end thinking-sphinx-4.1.0/spec/fixtures/000077500000000000000000000000001341132130100174265ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/fixtures/database.yml000066400000000000000000000001031341132130100217070ustar00rootroot00000000000000username: root password: host: localhost database: thinking_sphinx thinking-sphinx-4.1.0/spec/internal/000077500000000000000000000000001341132130100173715ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/app/000077500000000000000000000000001341132130100201515ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/app/indices/000077500000000000000000000000001341132130100215675ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/app/indices/admin_person_index.rb000066400000000000000000000003561341132130100257650ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define 'admin/person', :with => :active_record do indexes name end ThinkingSphinx::Index.define 'admin/person', :with => :real_time, :name => 'admin_person_rt' do indexes name end thinking-sphinx-4.1.0/spec/internal/app/indices/album_index.rb000066400000000000000000000004621341132130100244050ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :album, :with => :active_record, :primary_key => :integer_id, :delta => true do indexes name, artist end ThinkingSphinx::Index.define :album, :with => :real_time, :primary_key => :integer_id, :name => :album_real do indexes name, artist end thinking-sphinx-4.1.0/spec/internal/app/indices/animal_index.rb000066400000000000000000000001631341132130100245440ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :animal, :with => :active_record do indexes name end thinking-sphinx-4.1.0/spec/internal/app/indices/article_index.rb000066400000000000000000000013341341132130100247270ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :article, :with => :active_record do indexes title, content indexes user.name, :as => :user indexes user.articles.title, :as => :related_titles has published, user_id has taggings.tag_id, :as => :tag_ids, :source => :query has taggings.created_at, :as => :taggings_at, :type => :timestamp set_property :min_infix_len => 4 set_property :enable_star => true end ThinkingSphinx::Index.define :article, :with => :active_record, :name => 'stemmed_article' do indexes title has published, user_id has taggings.tag_id, :as => :tag_ids has taggings.created_at, :as => :taggings_at, :type => :timestamp set_property :morphology => 'stem_en' end thinking-sphinx-4.1.0/spec/internal/app/indices/bird_index.rb000066400000000000000000000002001341132130100242130ustar00rootroot00000000000000# frozen_string_literal: true FlightlessBird ThinkingSphinx::Index.define :bird, :with => :active_record do indexes name end thinking-sphinx-4.1.0/spec/internal/app/indices/book_index.rb000066400000000000000000000004641341132130100242410ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :book, :with => :active_record, :delta => true do indexes title, :sortable => true indexes author, :facet => true indexes [title, author], :as => :info indexes blurb_file, :file => true has year has created_at, :type => :timestamp end thinking-sphinx-4.1.0/spec/internal/app/indices/car_index.rb000066400000000000000000000002511341132130100240460ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :car, :with => :real_time do indexes name, :sortable => true has manufacturer_id, :type => :integer end thinking-sphinx-4.1.0/spec/internal/app/indices/city_index.rb000066400000000000000000000004161341132130100242540ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :city, :with => :active_record do indexes name has lat, lng set_property :charset_table => '0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F, U+0130' set_property :utf8? => true end thinking-sphinx-4.1.0/spec/internal/app/indices/product_index.rb000066400000000000000000000011521341132130100247620ustar00rootroot00000000000000# frozen_string_literal: true multi_schema = MultiSchema.new ThinkingSphinx::Index.define :product, :with => :real_time do indexes name, :sortable => true has category_ids, :type => :integer, :multi => true has options, :type => :json if JSONColumn.call end if multi_schema.active? ThinkingSphinx::Index.define(:product, :name => :product_two, :offset_as => :product_two, :with => :real_time ) do indexes name, prefixes: true set_property min_prefix_len: 1, dict: :keywords scope do multi_schema.switch :thinking_sphinx User end end multi_schema.switch :public end thinking-sphinx-4.1.0/spec/internal/app/indices/tee_index.rb000066400000000000000000000002251341132130100240570ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :tee, :with => :active_record do index colour.name has colour_id, :facet => true end thinking-sphinx-4.1.0/spec/internal/app/indices/user_index.rb000066400000000000000000000003341341132130100242610ustar00rootroot00000000000000# frozen_string_literal: true ThinkingSphinx::Index.define :user, :with => :active_record do indexes name has articles.taggings.tag_id, :as => :tag_ids, :facet => true set_property :big_document_ids => true end thinking-sphinx-4.1.0/spec/internal/app/models/000077500000000000000000000000001341132130100214345ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/app/models/admin/000077500000000000000000000000001341132130100225245ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/app/models/admin/person.rb000066400000000000000000000002631341132130100243600ustar00rootroot00000000000000# frozen_string_literal: true class Admin::Person < ActiveRecord::Base self.table_name = 'admin_people' after_save ThinkingSphinx::RealTime.callback_for('admin/person') end thinking-sphinx-4.1.0/spec/internal/app/models/album.rb000066400000000000000000000010411341132130100230550ustar00rootroot00000000000000# frozen_string_literal: true class Album < ActiveRecord::Base self.primary_key = :id before_validation :set_id, :on => :create before_validation :set_integer_id, :on => :create after_save ThinkingSphinx::RealTime.callback_for(:album) 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 thinking-sphinx-4.1.0/spec/internal/app/models/animal.rb000066400000000000000000000001051341132130100232160ustar00rootroot00000000000000# frozen_string_literal: true class Animal < ActiveRecord::Base end thinking-sphinx-4.1.0/spec/internal/app/models/article.rb000066400000000000000000000002261341132130100234040ustar00rootroot00000000000000# frozen_string_literal: true class Article < ActiveRecord::Base belongs_to :user has_many :taggings has_many :tags, :through => :taggings end thinking-sphinx-4.1.0/spec/internal/app/models/bird.rb000066400000000000000000000000671341132130100227040ustar00rootroot00000000000000# frozen_string_literal: true class Bird < Animal end thinking-sphinx-4.1.0/spec/internal/app/models/book.rb000066400000000000000000000006221341132130100227130ustar00rootroot00000000000000# frozen_string_literal: true class Book < ActiveRecord::Base include ThinkingSphinx::Scopes has_and_belongs_to_many :genres sphinx_scope(:by_query) { |query| query } sphinx_scope(:by_year) do |year| {:with => {:year => year}} end sphinx_scope(:by_query_and_year) do |query, year| [query, {:with => {:year =>year}}] end sphinx_scope(:ordered) { {:order => 'year DESC'} } end thinking-sphinx-4.1.0/spec/internal/app/models/car.rb000066400000000000000000000002271341132130100225270ustar00rootroot00000000000000# frozen_string_literal: true class Car < ActiveRecord::Base belongs_to :manufacturer after_save ThinkingSphinx::RealTime.callback_for(:car) end thinking-sphinx-4.1.0/spec/internal/app/models/categorisation.rb000066400000000000000000000004441341132130100247760ustar00rootroot00000000000000# frozen_string_literal: true class Categorisation < ActiveRecord::Base belongs_to :category belongs_to :product after_commit :update_product private def update_product product.reload ThinkingSphinx::RealTime.callback_for(:product, [:product]).after_save self end end thinking-sphinx-4.1.0/spec/internal/app/models/category.rb000066400000000000000000000002261341132130100235760ustar00rootroot00000000000000# frozen_string_literal: true class Category < ActiveRecord::Base has_many :categorisations has_many :products, :through => :categorisations end thinking-sphinx-4.1.0/spec/internal/app/models/city.rb000066400000000000000000000001551341132130100227320ustar00rootroot00000000000000# frozen_string_literal: true class City < ActiveRecord::Base scope :ordered, lambda { order(:name) } end thinking-sphinx-4.1.0/spec/internal/app/models/colour.rb000066400000000000000000000001261341132130100232630ustar00rootroot00000000000000# frozen_string_literal: true class Colour < ActiveRecord::Base has_many :tees end thinking-sphinx-4.1.0/spec/internal/app/models/event.rb000066400000000000000000000001621341132130100231010ustar00rootroot00000000000000# frozen_string_literal: true class Event < ActiveRecord::Base belongs_to :eventable, :polymorphic => true end thinking-sphinx-4.1.0/spec/internal/app/models/flightless_bird.rb000066400000000000000000000000771341132130100251310ustar00rootroot00000000000000# frozen_string_literal: true class FlightlessBird < Bird end thinking-sphinx-4.1.0/spec/internal/app/models/genre.rb000066400000000000000000000001101341132130100230510ustar00rootroot00000000000000# frozen_string_literal: true class Genre < ActiveRecord::Base # end thinking-sphinx-4.1.0/spec/internal/app/models/hardcover.rb000066400000000000000000000000761341132130100237410ustar00rootroot00000000000000# frozen_string_literal: true class Hardcover < Book # end thinking-sphinx-4.1.0/spec/internal/app/models/mammal.rb000066400000000000000000000000711341132130100232230ustar00rootroot00000000000000# frozen_string_literal: true class Mammal < Animal end thinking-sphinx-4.1.0/spec/internal/app/models/manufacturer.rb000066400000000000000000000001341341132130100244530ustar00rootroot00000000000000# frozen_string_literal: true class Manufacturer < ActiveRecord::Base has_many :cars end thinking-sphinx-4.1.0/spec/internal/app/models/product.rb000066400000000000000000000003251341132130100234410ustar00rootroot00000000000000# frozen_string_literal: true class Product < ActiveRecord::Base has_many :categorisations has_many :categories, :through => :categorisations after_save ThinkingSphinx::RealTime.callback_for(:product) end thinking-sphinx-4.1.0/spec/internal/app/models/tag.rb000066400000000000000000000002031341132130100225270ustar00rootroot00000000000000# frozen_string_literal: true class Tag < ActiveRecord::Base has_many :taggings has_many :articles, :through => :taggings end thinking-sphinx-4.1.0/spec/internal/app/models/tagging.rb000066400000000000000000000001561341132130100234030ustar00rootroot00000000000000# frozen_string_literal: true class Tagging < ActiveRecord::Base belongs_to :tag belongs_to :article end thinking-sphinx-4.1.0/spec/internal/app/models/tee.rb000066400000000000000000000001271341132130100225360ustar00rootroot00000000000000# frozen_string_literal: true class Tee < ActiveRecord::Base belongs_to :colour end thinking-sphinx-4.1.0/spec/internal/app/models/tweet.rb000066400000000000000000000001351341132130100231100ustar00rootroot00000000000000# frozen_string_literal: true class Tweet < ActiveRecord::Base self.primary_key = :id end thinking-sphinx-4.1.0/spec/internal/app/models/user.rb000066400000000000000000000002701341132130100227360ustar00rootroot00000000000000# frozen_string_literal: true class User < ActiveRecord::Base has_many :articles default_scope { order(:id) } scope :recent, lambda { where('created_at > ?', 1.week.ago) } end thinking-sphinx-4.1.0/spec/internal/config/000077500000000000000000000000001341132130100206365ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/config/database.yml000066400000000000000000000003241341132130100231240ustar00rootroot00000000000000test: adapter: <%= ENV['DATABASE'] || 'mysql2' %> database: thinking_sphinx username: <%= ENV['DATABASE'] == 'postgresql' ? ENV['USER'] : 'root' %> min_messages: warning encoding: utf8 thinking-sphinx-4.1.0/spec/internal/db/000077500000000000000000000000001341132130100177565ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/db/schema.rb000066400000000000000000000047571341132130100215600ustar00rootroot00000000000000# frozen_string_literal: true ActiveRecord::Schema.define do create_table(:admin_people, :force => true) do |t| t.string :name 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 end create_table(:articles, :force => true) do |t| t.string :title t.text :content t.boolean :published t.integer :user_id t.timestamps null: false end create_table(:manufacturers, :force => true) do |t| t.string :name end create_table(:cars, :force => true) do |t| t.integer :manufacturer_id t.string :name end create_table(:books, :force => true) do |t| t.string :title t.string :author t.integer :year t.string :blurb_file t.boolean :delta, :default => true, :null => false t.string :type, :default => 'Book', :null => false t.timestamps null: false end create_table(:books_genres, :force => true, :id => false) do |t| t.integer :book_id t.integer :genre_id end create_table(:categories, :force => true) do |t| t.string :name end create_table(:categorisations, :force => true) do |t| t.integer :category_id t.integer :product_id end create_table(:cities, :force => true) do |t| t.string :name t.float :lat t.float :lng end create_table(:colours, :force => true) do |t| t.string :name t.timestamps null: false end create_table(:events, :force => true) do |t| t.string :eventable_type t.integer :eventable_id end create_table(:genres, :force => true) do |t| t.string :name end create_table(:products, :force => true) do |t| t.string :name t.json :options if ::JSONColumn.call end create_table(:taggings, :force => true) do |t| t.integer :tag_id t.integer :article_id t.timestamps null: false end create_table(:tags, :force => true) do |t| t.string :name t.timestamps null: false end create_table(:tees, :force => true) do |t| t.integer :colour_id t.timestamps null: false end create_table(:tweets, :force => true, :id => false) do |t| t.column :id, :bigint, :null => false t.string :text t.timestamps null: false end create_table(:users, :force => true) do |t| t.string :name t.timestamps null: false end end thinking-sphinx-4.1.0/spec/internal/tmp/000077500000000000000000000000001341132130100201715ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/internal/tmp/.gitkeep000066400000000000000000000000001341132130100216100ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/spec_helper.rb000066400000000000000000000012271341132130100203750ustar00rootroot00000000000000# frozen_string_literal: true require 'rubygems' require 'bundler' Bundler.require :default, :development root = File.expand_path File.dirname(__FILE__) require "#{root}/support/multi_schema" require "#{root}/support/json_column" require "#{root}/support/mysql" require 'thinking_sphinx/railtie' Combustion.initialize! :active_record MultiSchema.new.create 'thinking_sphinx' require "#{root}/support/sphinx_yaml_helpers" RSpec.configure do |config| # 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 thinking-sphinx-4.1.0/spec/support/000077500000000000000000000000001341132130100172715ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/support/json_column.rb000066400000000000000000000013711341132130100221460ustar00rootroot00000000000000# frozen_string_literal: true class JSONColumn include ActiveRecord::ConnectionAdapters def self.call new.call end def call ruby? && postgresql? && column? end private def column? ( ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQLAdapter) && PostgreSQLAdapter.constants.include?(:TableDefinition) && PostgreSQLAdapter::TableDefinition.instance_methods.include?(:json) ) || ( ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQL) && PostgreSQL.constants.include?(:ColumnMethods) && PostgreSQL::ColumnMethods.instance_methods.include?(:json) ) end def postgresql? ENV['DATABASE'] == 'postgresql' end def ruby? RUBY_PLATFORM != 'java' end end thinking-sphinx-4.1.0/spec/support/multi_schema.rb000066400000000000000000000017121341132130100222710ustar00rootroot00000000000000# frozen_string_literal: true class MultiSchema def active? ENV['DATABASE'] == 'postgresql' end def create(schema_name) return unless active? unless connection.schema_exists? schema_name connection.execute %Q{CREATE SCHEMA "#{schema_name}"} end switch schema_name load Rails.root.join('db', 'schema.rb') end def current connection.schema_search_path end def switch(schema_name) connection.schema_search_path = %Q{"#{schema_name}"} connection.clear_query_cache end private def connection ActiveRecord::Base.connection end class IndexSet < ThinkingSphinx::IndexSet private def indices return super if index_names.any? prefixed = !multi_schema.current.include?('public') super.select { |index| prefixed ? index.name[/_two_core$/] : index.name[/_two_core$/].nil? } end def multi_schema @multi_schema ||= MultiSchema.new end end end thinking-sphinx-4.1.0/spec/support/mysql.rb000066400000000000000000000013421341132130100207630ustar00rootroot00000000000000# frozen_string_literal: true # New versions of MySQL don't allow NULL values for primary keys, but old # versions of Rails do. To use both at the same time, we need to update Rails' # default primary key type to no longer have a default NULL value. # class PatchAdapter def call return unless using_mysql? && using_rails_pre_4_1? require 'active_record/connection_adapters/abstract_mysql_adapter' ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter:: NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY" end def using_mysql? ENV.fetch('DATABASE', 'mysql2') == 'mysql2' end def using_rails_pre_4_1? ActiveRecord::VERSION::STRING.to_f < 4.1 end end PatchAdapter.new.call thinking-sphinx-4.1.0/spec/support/sphinx_yaml_helpers.rb000066400000000000000000000003761341132130100237010ustar00rootroot00000000000000# frozen_string_literal: true module SphinxYamlHelpers def write_configuration(hash) allow(File).to receive_messages :read => {'test' => hash}.to_yaml, :exists? => true end end RSpec.configure do |config| config.include SphinxYamlHelpers end thinking-sphinx-4.1.0/spec/thinking_sphinx/000077500000000000000000000000001341132130100207615ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/000077500000000000000000000000001341132130100235725ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/association_spec.rb000066400000000000000000000006331341132130100274470ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Association do let(:association) { ThinkingSphinx::ActiveRecord::Association.new column } let(:column) { double('column', :__stack => [:users], :__name => :post) } describe '#stack' do it "returns the column's stack and name" do expect(association.stack).to eq([:users, :post]) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/attribute/000077500000000000000000000000001341132130100255755ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/attribute/type_spec.rb000066400000000000000000000107651341132130100301260ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord class Attribute; end end end require 'thinking_sphinx/errors' require 'thinking_sphinx/active_record/attribute/type' describe ThinkingSphinx::ActiveRecord::Attribute::Type do let(:type) { ThinkingSphinx::ActiveRecord::Attribute::Type.new attribute, model } let(:attribute) { double('attribute', :columns => [column], :options => {}) } let(:model) { double('model', :columns => [db_column]) } let(:column) { double('column', :__name => :created_at, :string? => false, :__stack => []) } let(:db_column) { double('column', :name => 'created_at', :type => :integer) } describe '#multi?' do let(:association) { double('association', :klass => double) } before :each do column.__stack << :foo allow(model).to receive(:reflect_on_association).and_return(association) end it "returns true if there are has_many associations" do allow(association).to receive(:macro).and_return(:has_many) expect(type).to be_multi end it "returns true if there are has_and_belongs_to_many associations" do allow(association).to receive(:macro).and_return(:has_and_belongs_to_many) expect(type).to be_multi end it "returns false if there are no associations" do column.__stack.clear expect(type).not_to be_multi end it "returns false if there are only belongs_to associations" do allow(association).to receive(:macro).and_return(:belongs_to) expect(type).not_to be_multi end it "returns false if there are only has_one associations" do allow(association).to receive(:macro).and_return(:has_one) expect(type).not_to be_multi end it "returns true if deeper associations have many" do column.__stack << :bar deep_association = double(:klass => double, :macro => :has_many) allow(association).to receive(:macro).and_return(:belongs_to) allow(association).to receive(:klass).and_return( double(:reflect_on_association => deep_association) ) expect(type).to be_multi end it "respects the provided setting" do attribute.options[:multi] = true expect(type).to be_multi end end describe '#type' do it "returns the type option provided" do attribute.options[:type] = :datetime expect(type.type).to eq(:datetime) end it "detects integer types from the database" do allow(db_column).to receive_messages(:type => :integer, :sql_type => 'integer(11)') expect(type.type).to eq(:integer) end it "detects boolean types from the database" do allow(db_column).to receive_messages(:type => :boolean) expect(type.type).to eq(:boolean) end it "detects datetime types from the database as timestamps" do allow(db_column).to receive_messages(:type => :datetime) expect(type.type).to eq(:timestamp) end it "detects date types from the database as timestamps" do allow(db_column).to receive_messages(:type => :date) expect(type.type).to eq(:timestamp) end it "detects string types from the database" do allow(db_column).to receive_messages(:type => :string) expect(type.type).to eq(:string) end it "detects text types from the database as strings" do allow(db_column).to receive_messages(:type => :text) expect(type.type).to eq(:string) end it "detects float types from the database" do allow(db_column).to receive_messages(:type => :float) expect(type.type).to eq(:float) end it "detects decimal types from the database as floats" do allow(db_column).to receive_messages(:type => :decimal) expect(type.type).to eq(:float) end it "detects big ints as big ints" do allow(db_column).to receive_messages :type => :bigint expect(type.type).to eq(:bigint) end it "detects large integers as big ints" do allow(db_column).to receive_messages :type => :integer, :sql_type => 'bigint(20)' expect(type.type).to eq(:bigint) end it "detects JSON" do allow(db_column).to receive_messages :type => :json expect(type.type).to eq(:json) end it "respects provided type setting" do attribute.options[:type] = :timestamp expect(type.type).to eq(:timestamp) end it 'raises an error if the database column does not exist' do model.columns.clear expect { type.type }.to raise_error(ThinkingSphinx::MissingColumnError) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/base_spec.rb000066400000000000000000000076061341132130100260540ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Base do let(:model) { Class.new(ActiveRecord::Base) do include ThinkingSphinx::ActiveRecord::Base def self.name; 'Model'; end end } let(:sub_model) { Class.new(model) do def self.name; 'SubModel'; end end } describe '.facets' do it "returns a new search object" do expect(model.facets).to be_a(ThinkingSphinx::FacetSearch) end it "passes through arguments to the search object" do expect(model.facets('pancakes').query).to eq('pancakes') end it "scopes the search to a given model" do expect(model.facets('pancakes').options[:classes]).to eq([model]) end it "merges the :classes option with the model" do expect(model.facets('pancakes', :classes => [sub_model]). options[:classes]).to eq([sub_model, model]) end it "applies the default scope if there is one" do allow(model).to receive_messages :default_sphinx_scope => :default, :sphinx_scopes => {:default => Proc.new { {:order => :created_at} }} expect(model.facets.options[:order]).to eq(:created_at) end it "does not apply a default scope if one is not set" do allow(model).to receive_messages :default_sphinx_scope => nil, :default => {:order => :created_at} expect(model.facets.options[:order]).to be_nil end end describe '.search' do let(:stack) { double('stack', :call => true) } before :each do stub_const 'ThinkingSphinx::Middlewares::DEFAULT', stack end it "returns a new search object" do expect(model.search).to be_a(ThinkingSphinx::Search) end it "passes through arguments to the search object" do expect(model.search('pancakes').query).to eq('pancakes') end it "scopes the search to a given model" do expect(model.search('pancakes').options[:classes]).to eq([model]) end it "passes through options to the search object" do expect(model.search('pancakes', populate: true). options[:populate]).to be_truthy end it "should automatically populate when :populate is set to true" do expect(stack).to receive(:call).and_return(true) model.search('pancakes', populate: true) end it "merges the :classes option with the model" do expect(model.search('pancakes', :classes => [sub_model]). options[:classes]).to eq([sub_model, model]) end it "respects provided middleware" do expect(model.search(:middleware => ThinkingSphinx::Middlewares::RAW_ONLY). options[:middleware]).to eq(ThinkingSphinx::Middlewares::RAW_ONLY) end it "respects provided masks" do expect(model.search(:masks => [ThinkingSphinx::Masks::PaginationMask]). masks).to eq([ThinkingSphinx::Masks::PaginationMask]) end it "applies the default scope if there is one" do allow(model).to receive_messages :default_sphinx_scope => :default, :sphinx_scopes => {:default => Proc.new { {:order => :created_at} }} expect(model.search.options[:order]).to eq(:created_at) end it "does not apply a default scope if one is not set" do allow(model).to receive_messages :default_sphinx_scope => nil, :default => {:order => :created_at} expect(model.search.options[:order]).to be_nil end end describe '.search_count' do let(:search) { double('search', :options => {}, :total_entries => 12, :populated? => false) } before :each do allow(ThinkingSphinx).to receive_messages :search => search allow(FileUtils).to receive_messages :mkdir_p => true end it "returns the search object's total entries count" do expect(model.search_count).to eq(search.total_entries) end it "scopes the search to a given model" do model.search_count expect(search.options[:classes]).to eq([model]) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/callbacks/000077500000000000000000000000001341132130100255115ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb000066400000000000000000000075131341132130100323170ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks do let(:callbacks) { ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks.new instance } let(:instance) { double('instance', :delta? => true) } describe '.after_destroy' do let(:callbacks) { double('callbacks', :after_destroy => nil) } before :each do allow(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). to receive_messages :new => callbacks end it "builds an object from the instance" do expect(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). to receive(:new).with(instance).and_return(callbacks) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_destroy(instance) end it "invokes after_destroy on the object" do expect(callbacks).to receive(:after_destroy) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_destroy(instance) end end describe '#after_destroy' do let(:index_set) { double 'index set', :to_a => [index] } let(:index) { double('index', :name => 'foo_core', :document_id_for_key => 14, :type => 'plain', :distributed? => false) } let(:instance) { double('instance', :id => 7, :new_record? => false) } before :each do allow(ThinkingSphinx::IndexSet).to receive_messages :new => index_set end it "performs the deletion for the index and instance" do expect(ThinkingSphinx::Deletion).to receive(:perform).with(index, 7) callbacks.after_destroy end it "doesn't do anything if the instance is a new record" do allow(instance).to receive_messages :new_record? => true expect(ThinkingSphinx::Deletion).not_to receive(:perform) callbacks.after_destroy end it 'does nothing if callbacks are suspended' do ThinkingSphinx::Callbacks.suspend! expect(ThinkingSphinx::Deletion).not_to receive(:perform) callbacks.after_destroy ThinkingSphinx::Callbacks.resume! end end describe '.after_rollback' do let(:callbacks) { double('callbacks', :after_rollback => nil) } before :each do allow(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). to receive_messages :new => callbacks end it "builds an object from the instance" do expect(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). to receive(:new).with(instance).and_return(callbacks) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_rollback(instance) end it "invokes after_rollback on the object" do expect(callbacks).to receive(:after_rollback) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_rollback(instance) end end describe '#after_rollback' do let(:index_set) { double 'index set', :to_a => [index] } let(:index) { double('index', :name => 'foo_core', :document_id_for_key => 14, :type => 'plain', :distributed? => false) } let(:instance) { double('instance', :id => 7, :new_record? => false) } before :each do allow(ThinkingSphinx::IndexSet).to receive_messages :new => index_set end it "performs the deletion for the index and instance" do expect(ThinkingSphinx::Deletion).to receive(:perform).with(index, 7) callbacks.after_rollback end it "doesn't do anything if the instance is a new record" do allow(instance).to receive_messages :new_record? => true expect(ThinkingSphinx::Deletion).not_to receive(:perform) callbacks.after_rollback end it 'does nothing if callbacks are suspended' do ThinkingSphinx::Callbacks.suspend! expect(ThinkingSphinx::Deletion).not_to receive(:perform) callbacks.after_rollback ThinkingSphinx::Callbacks.resume! end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb000066400000000000000000000116171341132130100321460ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks do let(:callbacks) { ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks.new instance } let(:instance) { double('instance', :delta? => true) } let(:config) { double('config') } let(:processor) { double('processor', :toggled? => true, :index => true, :delete => true) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end [:after_commit, :before_save].each do |callback| describe ".#{callback}" do let(:callbacks) { double('callbacks', callback => nil) } before :each do allow(ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks). to receive_messages :new => callbacks end it "builds an object from the instance" do expect(ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks). to receive(:new).with(instance).and_return(callbacks) ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. send(callback, instance) end it "invokes #{callback} on the object" do expect(callbacks).to receive(callback) ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. send(callback, instance) end end end describe '#after_commit' do let(:index) { double('index', :delta? => false, :delta_processor => processor, :type => 'plain') } before :each do allow(config).to receive_messages :index_set_class => double(:new => [index]) end context 'without delta indices' do it "does not fire a delta index when no delta indices" do expect(processor).not_to receive(:index) callbacks.after_commit end it "does not delete the instance from any index" do expect(processor).not_to receive(:delete) callbacks.after_commit end end context 'with delta indices' do let(:core_index) { double('index', :delta? => false, :name => 'foo_core', :delta_processor => processor, :type => 'plain') } let(:delta_index) { double('index', :delta? => true, :name => 'foo_delta', :delta_processor => processor, :type => 'plain') } before :each do allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => false allow(config).to receive_messages :index_set_class => double( :new => [core_index, delta_index] ) end it "only indexes delta indices" do expect(processor).to receive(:index).with(delta_index) callbacks.after_commit end it "does not process delta indices when deltas are suspended" do allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => true expect(processor).not_to receive(:index) callbacks.after_commit end it "deletes the instance from the core index" do expect(processor).to receive(:delete).with(core_index, instance) callbacks.after_commit end it "does not index if model's delta flag is not true" do allow(processor).to receive_messages :toggled? => false expect(processor).not_to receive(:index) callbacks.after_commit end it "does not delete if model's delta flag is not true" do allow(processor).to receive_messages :toggled? => false expect(processor).not_to receive(:delete) callbacks.after_commit end it "does not delete when deltas are suspended" do allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => true expect(processor).not_to receive(:delete) callbacks.after_commit end end end describe '#before_save' do let(:index) { double('index', :delta? => true, :delta_processor => processor, :type => 'plain') } before :each do allow(config).to receive_messages :index_set_class => double(:new => [index]) allow(instance).to receive_messages( :changed? => true, :new_record? => false ) end it "sets delta to true if there are delta indices" do expect(processor).to receive(:toggle).with(instance) callbacks.before_save end it "does not try to set delta to true if there are no delta indices" do allow(index).to receive_messages :delta? => false expect(processor).not_to receive(:toggle) callbacks.before_save end it "does not try to set delta to true if the instance is unchanged" do allow(instance).to receive_messages :changed? => false expect(processor).not_to receive(:toggle) callbacks.before_save end it "does set delta to true if the instance is unchanged but new" do allow(instance).to receive_messages( :changed? => false, :new_record? => true ) expect(processor).to receive(:toggle) callbacks.before_save end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb000066400000000000000000000057111341132130100323350ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module ActiveRecord module Callbacks; end end end require 'active_support/core_ext/string/inflections' require 'thinking_sphinx/callbacks' require 'thinking_sphinx/errors' require 'thinking_sphinx/active_record/callbacks/update_callbacks' describe ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks do describe '#after_update' do let(:callbacks) { ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks.new instance } let(:instance) { double('instance', :class => klass, :id => 2) } let(:klass) { double(:name => 'Article') } let(:configuration) { double('configuration', :settings => {'attribute_updates' => true}, :indices_for_references => [index]) } let(:connection) { double('connection', :execute => '') } let(:index) { double 'index', :name => 'article_core', :sources => [source], :document_id_for_key => 3, :distributed? => false, :type => 'plain'} let(:source) { double('source', :attributes => []) } before :each do stub_const 'ThinkingSphinx::Configuration', double(:instance => configuration) stub_const 'ThinkingSphinx::Connection', double stub_const 'Riddle::Query', double(:update => 'SphinxQL') allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) source.attributes.replace([ double(:name => 'foo', :updateable? => true, :columns => [double(:__name => 'foo_column')]), double(:name => 'bar', :updateable? => true, :value_for => 7, :columns => [double(:__name => 'bar_column')]), double(:name => 'baz', :updateable? => false) ]) 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 configuration.settings['attribute_updates'] = false expect(connection).not_to receive(:execute) callbacks.after_update end it "builds an update query with only updateable attributes that have changed" do expect(Riddle::Query).to receive(:update). with('article_core', 3, 'bar' => 7).and_return('SphinxQL') callbacks.after_update end it "sends the update query through to Sphinx" do expect(connection).to receive(:execute).with('SphinxQL') callbacks.after_update end it "doesn't care if the update fails at Sphinx's end" do allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) expect { callbacks.after_update }.not_to raise_error end it 'does nothing if callbacks are suspended' do ThinkingSphinx::Callbacks.suspend! expect(connection).not_to receive(:execute) callbacks.after_update ThinkingSphinx::Callbacks.resume! end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/column_spec.rb000066400000000000000000000040471341132130100264330ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Column do describe '#__name' do it "returns the top item" do column = ThinkingSphinx::ActiveRecord::Column.new(:content) expect(column.__name).to eq(:content) end end describe '#__replace' do let(:base) { [:a, :b] } let(:replacements) { [[:a, :c], [:a, :d]] } it "returns itself when it's a string column" do column = ThinkingSphinx::ActiveRecord::Column.new('foo') expect(column.__replace(base, replacements).collect(&:__path)). to eq([['foo']]) end it "returns itself when the base of the stack does not match" do column = ThinkingSphinx::ActiveRecord::Column.new(:b, :c) expect(column.__replace(base, replacements).collect(&:__path)). to eq([[:b, :c]]) end it "returns an array of new columns " do column = ThinkingSphinx::ActiveRecord::Column.new(:a, :b, :e) expect(column.__replace(base, replacements).collect(&:__path)). to eq([[:a, :c, :e], [:a, :d, :e]]) end end describe '#__stack' do it "returns all but the top item" do column = ThinkingSphinx::ActiveRecord::Column.new(:users, :posts, :id) expect(column.__stack).to eq([:users, :posts]) end end describe '#method_missing' do let(:column) { ThinkingSphinx::ActiveRecord::Column.new(:user) } it "shifts the current name to the stack" do column.email expect(column.__stack).to eq([:user]) end it "adds the new method call as the name" do column.email expect(column.__name).to eq(:email) end it "returns itself" do expect(column.email).to eq(column) end end describe '#string?' do it "is true when the name is a string" do column = ThinkingSphinx::ActiveRecord::Column.new('content') expect(column).to be_a_string end it "is false when the name is a symbol" do column = ThinkingSphinx::ActiveRecord::Column.new(:content) expect(column).not_to be_a_string end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/column_sql_presenter_spec.rb000066400000000000000000000025151341132130100313770ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::ColumnSQLPresenter do describe '#with_table' do let(:model) { double 'Model' } let(:column) { double 'Column', :__name => 'column_name', :__stack => [], :string? => false } let(:adapter) { double 'Adapter' } let(:associations) { double 'Associations' } let(:path) { double 'Path', :model => double(:column_names => ['column_name']) } let(:presenter) { ThinkingSphinx::ActiveRecord::ColumnSQLPresenter.new( model, column, adapter, associations ) } before do stub_const 'Joiner::Path', double(:new => path) allow(adapter).to receive(:quote) { |arg| "`#{arg}`" } end context "when there's no explicit db name" do before { allow(associations).to receive_messages(:alias_for => 'table_name') } it 'returns quoted table and column names' do expect(presenter.with_table).to eq('`table_name`.`column_name`') end end context 'when an eplicit db name is provided' do before { allow(associations).to receive_messages(:alias_for => 'db_name.table_name') } it 'returns properly quoted table name with column name' do expect(presenter.with_table).to eq('`db_name`.`table_name`.`column_name`') end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/database_adapters/000077500000000000000000000000001341132130100272215ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/database_adapters/abstract_adapter_spec.rb000066400000000000000000000016621341132130100340700ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter do let(:adapter) { ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter.new model } let(:model) { double('model', :connection => connection) } let(:connection) { double('connection') } describe '#quote' do it "uses the model's connection to quote columns" do expect(connection).to receive(:quote_column_name).with('foo') adapter.quote 'foo' end it "returns the quoted value" do allow(connection).to receive_messages :quote_column_name => '"foo"' expect(adapter.quote('foo')).to eq('"foo"') end end describe '#quoted_table_name' do it "passes the method through to the model" do expect(model).to receive(:quoted_table_name).and_return('"articles"') expect(adapter.quoted_table_name).to eq('"articles"') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb000066400000000000000000000030461341132130100334300ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter do let(:adapter) { ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter.new(model) } let(:model) { double('model') } it "returns 1 for true" do expect(adapter.boolean_value(true)).to eq(1) end it "returns 0 for false" do expect(adapter.boolean_value(false)).to eq(0) end describe '#cast_to_string' do it "casts the clause to characters" do expect(adapter.cast_to_string('foo')).to eq("CAST(foo AS char)") end end describe '#cast_to_timestamp' do it "converts to unix timestamps" do expect(adapter.cast_to_timestamp('created_at')). to eq('UNIX_TIMESTAMP(created_at)') end end describe '#concatenate' do it "concatenates with the given separator" do expect(adapter.concatenate('foo, bar, baz', ',')). to eq("CONCAT_WS(',', foo, bar, baz)") end end describe '#convert_nulls' do it "translates arguments to an IFNULL SQL call" do expect(adapter.convert_nulls('id', 5)).to eq('IFNULL(id, 5)') end end describe '#convert_blank' do it "translates arguments to a COALESCE NULLIF SQL call" do expect(adapter.convert_blank('id', 5)).to eq("COALESCE(NULLIF(id, ''), 5)") end end describe '#group_concatenate' do it "group concatenates the clause with the given separator" do expect(adapter.group_concatenate('foo', ',')). to eq("GROUP_CONCAT(DISTINCT foo SEPARATOR ',')") end end end postgresql_adapter_spec.rb000066400000000000000000000036531341132130100344130ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/database_adapters# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter do let(:adapter) { ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter.new(model) } let(:model) { double('model') } describe '#boolean_value' do it "returns 'TRUE' for true" do expect(adapter.boolean_value(true)).to eq('TRUE') end it "returns 'FALSE' for false" do expect(adapter.boolean_value(false)).to eq('FALSE') end end describe '#cast_to_string' do it "casts the clause to characters" do expect(adapter.cast_to_string('foo')).to eq('foo::varchar') end end describe '#cast_to_timestamp' do it "converts to int unix timestamps" do expect(adapter.cast_to_timestamp('created_at')). to eq('extract(epoch from created_at)::int') end it "converts to bigint unix timestamps" do ThinkingSphinx::Configuration.instance.settings['64bit_timestamps'] = true expect(adapter.cast_to_timestamp('created_at')). to eq('extract(epoch from created_at)::bigint') end end describe '#concatenate' do it "concatenates with the given separator" do expect(adapter.concatenate('foo, bar, baz', ',')). to eq("COALESCE(foo, '') || ',' || COALESCE(bar, '') || ',' || COALESCE(baz, '')") end end describe '#convert_nulls' do it "translates arguments to a COALESCE SQL call" do expect(adapter.convert_nulls('id', 5)).to eq('COALESCE(id, 5)') end end describe '#convert_blank' do it "translates arguments to a COALESCE NULLIF SQL call" do expect(adapter.convert_blank('id', 5)).to eq("COALESCE(NULLIF(id, ''), 5)") end end describe '#group_concatenate' do it "group concatenates the clause with the given separator" do expect(adapter.group_concatenate('foo', ',')). to eq("array_to_string(array_agg(DISTINCT foo), ',')") end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/database_adapters_spec.rb000066400000000000000000000117761341132130100305740ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters do let(:model) { double('model') } describe '.adapter_for' do it "returns a MysqlAdapter object for :mysql" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). to receive_messages(:adapter_type_for => :mysql) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). to be_a( ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter ) end it "returns a PostgreSQLAdapter object for :postgresql" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). to receive_messages(:adapter_type_for => :postgresql) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). to be_a( ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter ) end it "instantiates using the default adapter if one is provided" do adapter_class = double('adapter class') adapter_instance = double('adapter instance') ThinkingSphinx::ActiveRecord::DatabaseAdapters.default = adapter_class allow(adapter_class).to receive_messages(:new => adapter_instance) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). to eq(adapter_instance) ThinkingSphinx::ActiveRecord::DatabaseAdapters.default = nil end it "raises an exception for other responses" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). to receive_messages(:adapter_type_for => :sqlite) expect { ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model) }.to raise_error(ThinkingSphinx::InvalidDatabaseAdapter) end end describe '.adapter_type_for' do let(:klass) { double('connection class') } let(:connection) { double('connection', :class => klass) } let(:model) { double('model', :connection => connection) } it "translates a normal MySQL adapter" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::MysqlAdapter') expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:mysql) end it "translates a MySQL2 adapter" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::Mysql2Adapter') expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:mysql) end it "translates a normal PostgreSQL adapter" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter') expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:postgresql) end it "translates a JDBC MySQL adapter to MySQL" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') allow(connection).to receive_messages(:config => {:adapter => 'jdbcmysql'}) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:mysql) end it "translates a JDBC PostgreSQL adapter to PostgreSQL" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') allow(connection).to receive_messages(:config => {:adapter => 'jdbcpostgresql'}) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:postgresql) end it "translates a JDBC adapter with MySQL connection string to MySQL" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') allow(connection).to receive_messages(:config => {:adapter => 'jdbc', :url => 'jdbc:mysql://127.0.0.1:3306/sphinx'}) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:mysql) end it "translates a JDBC adapter with PostgresSQL connection string to PostgresSQL" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') allow(connection).to receive_messages(:config => {:adapter => 'jdbc', :url => 'jdbc:postgresql://127.0.0.1:3306/sphinx'}) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq(:postgresql) end it "returns other JDBC adapters without translation" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') allow(connection).to receive_messages(:config => {:adapter => 'jdbcmssql'}) expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)).to eq('jdbcmssql') end it "returns other unknown adapters without translation" do allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::FooAdapter') expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. adapter_type_for(model)). to eq('ActiveRecord::ConnectionAdapters::FooAdapter') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/field_spec.rb000066400000000000000000000025131341132130100262150ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Field do let(:field) { ThinkingSphinx::ActiveRecord::Field.new model, column } let(:column) { double('column', :__name => :title, :__stack => [], :string? => false) } let(:model) { double('model') } before :each do allow(column).to receive_messages :to_a => [column] end describe '#columns' do it 'returns the provided Column object' do expect(field.columns).to eq([column]) end it 'translates symbols to Column objects' do expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new).with(:title). and_return(column) ThinkingSphinx::ActiveRecord::Field.new model, :title end end describe '#file?' do it "defaults to false" do expect(field).not_to be_file end it "is true if file option is set" do field = ThinkingSphinx::ActiveRecord::Field.new model, column, :file => true expect(field).to be_file end end describe '#with_attribute?' do it "defaults to false" do expect(field).not_to be_with_attribute end it "is true if the field is sortable" do field = ThinkingSphinx::ActiveRecord::Field.new model, column, :sortable => true expect(field).to be_with_attribute end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/filter_reflection_spec.rb000066400000000000000000000140671341132130100306400ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::FilterReflection do describe '.call' do let(:reflection) { double('Reflection', :macro => :has_some, :options => options, :active_record => double, :name => 'baz', :foreign_type => :foo_type, :class => original_klass) } let(:options) { {:polymorphic => true} } let(:filtered_reflection) { double 'filtered reflection' } let(:original_klass) { double } let(:subclass) { double :include => true } before :each do allow(reflection.active_record).to receive_message_chain(:connection, :quote_column_name). and_return('"foo_type"') if ActiveRecord::VERSION::STRING.to_f < 5.2 allow(original_klass).to receive(:new).and_return(filtered_reflection) else allow(Class).to receive(:new).with(original_klass).and_return(subclass) allow(subclass).to receive(:new).and_return(filtered_reflection) end end class ArgumentsWrapper attr_reader :macro, :name, :scope, :options, :parent def initialize(*arguments) if ActiveRecord::VERSION::STRING.to_f < 4.0 @macro, @name, @options, @parent = arguments elsif ActiveRecord::VERSION::STRING.to_f < 4.2 @macro, @name, @scope, @options, @parent = arguments else @name, @scope, @options, @parent = arguments end end end def reflection_klass ActiveRecord::VERSION::STRING.to_f < 5.2 ? original_klass : subclass end def expected_reflection_arguments expect(reflection_klass).to receive(:new) do |*arguments| yield ArgumentsWrapper.new(*arguments) end end it "uses the existing reflection's macro" do expect(reflection_klass).to receive(:new) do |macro, *args| expect(macro).to eq(:has_some) end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end if ActiveRecord::VERSION::STRING.to_f < 4.2 it "uses the supplied name" do expected_reflection_arguments do |wrapper| expect(wrapper.name).to eq('foo_bar') end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "uses the existing reflection's parent" do expected_reflection_arguments do |wrapper| expect(wrapper.parent).to eq(reflection.active_record) end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "removes the polymorphic setting from the options" do expected_reflection_arguments do |wrapper| expect(wrapper.options[:polymorphic]).to be_nil end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "adds the class name option" do expected_reflection_arguments do |wrapper| expect(wrapper.options[:class_name]).to eq('Bar') end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "sets the foreign key if necessary" do expected_reflection_arguments do |wrapper| expect(wrapper.options[:foreign_key]).to eq('baz_id') end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "respects supplied foreign keys" do options[:foreign_key] = 'qux_id' expected_reflection_arguments do |wrapper| expect(wrapper.options[:foreign_key]).to eq('qux_id') end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end if ActiveRecord::VERSION::STRING.to_f < 4.0 it "sets conditions if there are none" do expect(reflection_klass).to receive(:new) do |macro, name, options, parent| expect(options[:conditions]).to eq("::ts_join_alias::.\"foo_type\" = 'Bar'") end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "appends to the conditions array" do options[:conditions] = ['existing'] expect(reflection_klass).to receive(:new) do |macro, name, options, parent| expect(options[:conditions]).to eq(['existing', "::ts_join_alias::.\"foo_type\" = 'Bar'"]) end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "extends the conditions hash" do options[:conditions] = {:x => :y} expect(reflection_klass).to receive(:new) do |macro, name, options, parent| expect(options[:conditions]).to eq({:x => :y, :foo_type => 'Bar'}) end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end it "appends to the conditions string" do options[:conditions] = 'existing' expect(reflection_klass).to receive(:new) do |macro, name, options, parent| expect(options[:conditions]).to eq("existing AND ::ts_join_alias::.\"foo_type\" = 'Bar'") end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end else it "does not add a conditions option" do expected_reflection_arguments do |wrapper| expect(wrapper.options.keys).not_to include(:conditions) end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end end it "includes custom behaviour in the subclass" do expect(subclass).to receive(:include).with(ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection::JoinConstraint) ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) end if ActiveRecord::VERSION::STRING.to_f > 5.1 it "returns the new reflection" do expect(ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' )).to eq(filtered_reflection) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/index_spec.rb000066400000000000000000000142331341132130100262430ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Index do let(:index) { ThinkingSphinx::ActiveRecord::Index.new :user } let(:config) { double('config', :settings => {}, :indices_location => 'location', :next_offset => 8) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end describe '#append_source' do let(:model) { double('model', :primary_key => :id, :table_exists? => true) } let(:source) { double('source') } before :each do allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) allow(ThinkingSphinx::ActiveRecord::SQLSource).to receive_messages :new => source allow(config).to receive_messages :next_offset => 17 end it "adds a source to the index" do expect(index.sources).to receive(:<<).with(source) index.append_source end it "creates the source with the index's offset" do expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:offset => 17)).and_return(source) index.append_source end it "returns the new source" do expect(index.append_source).to eq(source) end it "defaults to the model's primary key" do allow(model).to receive_messages :primary_key => :sphinx_id expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :sphinx_id)). and_return(source) index.append_source end it "uses a custom column when set" do allow(model).to receive_messages :primary_key => :sphinx_id expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :custom_sphinx_id)). and_return(source) index = ThinkingSphinx::ActiveRecord::Index.new:user, :primary_key => :custom_sphinx_id index.append_source end it "defaults to id if no primary key is set" do allow(model).to receive_messages :primary_key => nil expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :id)). and_return(source) index.append_source end end describe '#delta?' do it "defaults to false" do expect(index).not_to be_delta end it "reflects the delta? option" do index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta? => true expect(index).to be_delta end end describe '#delta_processor' do it "creates an instance of the delta processor option" do processor = double('processor') processor_class = double('processor class', :new => processor) index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta_processor => processor_class expect(index.delta_processor).to eq(processor) end end describe '#docinfo' do it "defaults to extern" do expect(index.docinfo).to eq(:extern) end it "can be disabled" do config.settings["skip_docinfo"] = true expect(index.docinfo).to be_nil end end describe '#document_id_for_key' do it "calculates the document id based on offset and number of indices" do allow(config).to receive_message_chain(:indices, :count).and_return(5) allow(config).to receive_messages :next_offset => 7 expect(index.document_id_for_key(123)).to eq(622) end end describe '#interpret_definition!' do let(:block) { double('block') } before :each do index.definition_block = block end it "interprets the definition block" do expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:translate!). with(index, block) index.interpret_definition! end it "only interprets the definition block once" do expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:translate!). once index.interpret_definition! index.interpret_definition! end end describe '#model' do let(:model) { double('model') } it "translates symbol references to model class" do allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) expect(index.model).to eq(model) end it "memoizes the result" do expect(ActiveSupport::Inflector).to receive(:constantize).with('User').once. and_return(model) index.model index.model end end describe '#morphology' do context 'with a render' do before :each do allow(FileUtils).to receive_messages :mkdir_p => true end it "defaults to nil" do begin index.render rescue Riddle::Configuration::ConfigurationError end expect(index.morphology).to be_nil end it "reads from the settings file if provided" do config.settings['morphology'] = 'stem_en' begin index.render rescue Riddle::Configuration::ConfigurationError end expect(index.morphology).to eq('stem_en') end end end describe '#name' do it "uses the core suffix by default" do index = ThinkingSphinx::ActiveRecord::Index.new :user expect(index.name).to eq('user_core') end it "uses the delta suffix when delta? is true" do index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta? => true expect(index.name).to eq('user_delta') end end describe '#offset' do before :each do allow(config).to receive_messages :next_offset => 4 end it "uses the next offset value from the configuration" do expect(index.offset).to eq(4) end it "uses the reference to get a unique offset" do expect(config).to receive(:next_offset).with(:user).and_return(2) index.offset end end describe '#render' do before :each do allow(FileUtils).to receive_messages :mkdir_p => true end it "interprets the provided definition" do expect(index).to receive(:interpret_definition!).at_least(:once) begin index.render rescue Riddle::Configuration::ConfigurationError # Ignoring underlying validation error. end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/interpreter_spec.rb000066400000000000000000000221671341132130100275040ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Interpreter do let(:instance) { ThinkingSphinx::ActiveRecord::Interpreter.new index, block } let(:model) { double('model') } let(:index) { double('index', :append_source => source, :options => {}) } let(:source) { Struct.new(:attributes, :fields, :associations, :groupings, :conditions). new([], [], [], [], []) } let(:block) { Proc.new { } } before :each do allow(ThinkingSphinx::ActiveRecord::SQLSource).to receive_messages :new => source allow(source).to receive_messages :model => model end describe '.translate!' do let(:instance) { double('interpreter', :translate! => true) } it "creates a new interpreter instance with the given block and index" do expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:new). with(index, block).and_return(instance) ThinkingSphinx::ActiveRecord::Interpreter.translate! index, block end it "calls translate! on the instance" do allow(ThinkingSphinx::ActiveRecord::Interpreter).to receive_messages(:new => instance) expect(instance).to receive(:translate!) ThinkingSphinx::ActiveRecord::Interpreter.translate! index, block end end describe '#group_by' do it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.group_by 'lat' end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.group_by 'lat' instance.group_by 'lng' end it "appends a new grouping statement to the source" do instance.group_by 'lat' expect(source.groupings).to include('lat') end end describe '#has' do let(:column) { double('column') } let(:attribute) { double('attribute') } before :each do allow(ThinkingSphinx::ActiveRecord::Attribute).to receive_messages :new => attribute end it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.has column end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.has column instance.has column end it "creates a new attribute with the provided column" do expect(ThinkingSphinx::ActiveRecord::Attribute).to receive(:new). with(model, column, {}).and_return(attribute) instance.has column end it "passes through options to the attribute" do expect(ThinkingSphinx::ActiveRecord::Attribute).to receive(:new). with(model, column, :as => :other_name).and_return(attribute) instance.has column, :as => :other_name end it "adds an attribute to the source" do instance.has column expect(source.attributes).to include(attribute) end it "adds multiple attributes when passed multiple columns" do instance.has column, column expect(source.attributes.select { |saved_attribute| saved_attribute == attribute }.length).to eq(2) end end describe '#indexes' do let(:column) { double('column') } let(:field) { double('field') } before :each do allow(ThinkingSphinx::ActiveRecord::Field).to receive_messages :new => field end it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.indexes column end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.indexes column instance.indexes column end it "creates a new field with the provided column" do expect(ThinkingSphinx::ActiveRecord::Field).to receive(:new). with(model, column, {}).and_return(field) instance.indexes column end it "passes through options to the field" do expect(ThinkingSphinx::ActiveRecord::Field).to receive(:new). with(model, column, :as => :other_name).and_return(field) instance.indexes column, :as => :other_name end it "adds a field to the source" do instance.indexes column expect(source.fields).to include(field) end it "adds multiple fields when passed multiple columns" do instance.indexes column, column expect(source.fields.select { |saved_field| saved_field == field }.length).to eq(2) end end describe '#join' do let(:column) { double('column') } let(:association) { double('association') } before :each do allow(ThinkingSphinx::ActiveRecord::Association).to receive_messages :new => association end it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.join column end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.join column instance.join column end it "creates a new association with the provided column" do expect(ThinkingSphinx::ActiveRecord::Association).to receive(:new). with(column).and_return(association) instance.join column end it "adds an association to the source" do instance.join column expect(source.associations).to include(association) end it "adds multiple fields when passed multiple columns" do instance.join column, column expect(source.associations.select { |saved_assoc| saved_assoc == association }.length).to eq(2) end end describe '#method_missing' do let(:column) { double('column') } before :each do allow(ThinkingSphinx::ActiveRecord::Column).to receive_messages(:new => column) end it "returns a new column for the given method" do expect(instance.id).to eq(column) end it "should initialise the column with the method name and arguments" do expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new). with(:users, :posts, :subject).and_return(column) instance.users(:posts, :subject) end end describe '#set_database' do before :each do allow(source).to receive_messages :set_database_settings => true stub_const 'ActiveRecord::Base', double(:configurations => {'other' => {'baz' => 'qux'}}) end it "sends through a hash if provided" do expect(source).to receive(:set_database_settings).with(:foo => :bar) instance.set_database :foo => :bar end it "finds the environment settings if given a string key" do expect(source).to receive(:set_database_settings).with(:baz => 'qux') instance.set_database 'other' end it "finds the environment settings if given a symbol key" do expect(source).to receive(:set_database_settings).with(:baz => 'qux') instance.set_database :other end end describe '#set_property' do before :each do allow(index.class).to receive_messages :settings => [:morphology] allow(source.class).to receive_messages :settings => [:mysql_ssl_cert] end it 'saves other settings as index options' do instance.set_property :field_weights => {:name => 10} expect(index.options[:field_weights]).to eq({:name => 10}) end context 'index settings' do it "sets the provided setting" do expect(index).to receive(:morphology=).with('stem_en') instance.set_property :morphology => 'stem_en' end end context 'source settings' do before :each do allow(source).to receive_messages :mysql_ssl_cert= => true end it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.set_property :mysql_ssl_cert => 'private.cert' end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.set_property :mysql_ssl_cert => 'private.cert' instance.set_property :mysql_ssl_cert => 'private.cert' end it "sets the provided setting" do expect(source).to receive(:mysql_ssl_cert=).with('private.cert') instance.set_property :mysql_ssl_cert => 'private.cert' end end end describe '#translate!' do it "returns the block evaluated within the context of the interpreter" do block = Proc.new { __id__ } interpreter = ThinkingSphinx::ActiveRecord::Interpreter.new index, block expect(interpreter.translate!). to eq(interpreter.__id__) end end describe '#where' do it "adds a source to the index" do expect(index).to receive(:append_source).and_return(source) instance.where 'id > 100' end it "only adds a single source for the given context" do expect(index).to receive(:append_source).once.and_return(source) instance.where 'id > 100' instance.where 'id < 150' end it "appends a new grouping statement to the source" do instance.where 'id > 100' expect(source.conditions).to include('id > 100') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/polymorpher_spec.rb000066400000000000000000000057341341132130100275220ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Polymorpher do let(:polymorpher) { ThinkingSphinx::ActiveRecord::Polymorpher.new source, column, class_names } let(:source) { double 'Source', :model => outer, :fields => [field], :attributes => [attribute] } let(:column) { double 'Column', :__name => :foo, :__stack => [:a, :b], :__path => [:a, :b, :foo] } let(:class_names) { %w( Article Animal ) } let(:field) { double :rebase => true } let(:attribute) { double :rebase => true } let(:outer) { double( :reflect_on_association => double(:klass => inner)) } let(:inner) { double( :reflect_on_association => double(:klass => model)) } let(:model) { double 'Model', :reflections => {}, :reflect_on_association => reflection } let(:reflection) { double 'Polymorphic Reflection' } describe '#morph!' do let(:article_reflection) { double 'Article Reflection' } let(:animal_reflection) { double 'Animal Reflection' } before :each do allow(ThinkingSphinx::ActiveRecord::FilterReflection). to receive(:call). and_return(article_reflection, animal_reflection) allow(model).to receive(:reflect_on_association) do |name| name == :foo ? reflection : nil end if ActiveRecord::Reflection.respond_to?(:add_reflection) allow(ActiveRecord::Reflection).to receive :add_reflection end end it "creates a new reflection for each class" do allow(ThinkingSphinx::ActiveRecord::FilterReflection). to receive(:call).and_call_original expect(ThinkingSphinx::ActiveRecord::FilterReflection). to receive(:call). with(reflection, :foo_article, 'Article'). and_return(article_reflection) expect(ThinkingSphinx::ActiveRecord::FilterReflection). to receive(:call). with(reflection, :foo_animal, 'Animal'). and_return(animal_reflection) polymorpher.morph! end it "adds the new reflections to the end-of-stack model" do if ActiveRecord::Reflection.respond_to?(:add_reflection) expect(ActiveRecord::Reflection).to receive(:add_reflection). with(model, :foo_article, article_reflection) expect(ActiveRecord::Reflection).to receive(:add_reflection). with(model, :foo_animal, animal_reflection) polymorpher.morph! else polymorpher.morph! expect(model.reflections[:foo_article]).to eq(article_reflection) expect(model.reflections[:foo_animal]).to eq(animal_reflection) end end it "rebases each field" do expect(field).to receive(:rebase).with([:a, :b, :foo], :to => [[:a, :b, :foo_article], [:a, :b, :foo_animal]]) polymorpher.morph! end it "rebases each attribute" do expect(attribute).to receive(:rebase).with([:a, :b, :foo], :to => [[:a, :b, :foo_article], [:a, :b, :foo_animal]]) polymorpher.morph! end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb000066400000000000000000000217141341132130100317700ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::PropertySQLPresenter do let(:adapter) { double 'adapter' } let(:associations) { double 'associations', :alias_for => 'articles' } let(:model) { double :column_names => ['title', 'created_at'] } let(:path) { double :aggregate? => false, :model => model } before :each do allow(adapter).to receive(:quote) { |column| column } stub_const 'Joiner::Path', double(:new => path) end context 'with a field' do let(:presenter) { ThinkingSphinx::ActiveRecord::PropertySQLPresenter.new( field, adapter, associations ) } let(:field) { double('field', :name => 'title', :columns => [column], :type => nil, :multi? => false, :source_type => nil, :model => double) } let(:column) { double('column', :string? => false, :__stack => [], :__name => 'title') } describe '#to_group' do it "returns the column name as a string" do expect(presenter.to_group).to eq('articles.title') end it "gets the column's table alias from the associations object" do allow(column).to receive_messages(:__stack => [:users, :posts]) expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_group end it "returns nil if the property is an aggregate" do allow(path).to receive_messages :aggregate? => true expect(presenter.to_group).to be_nil end it "returns nil if the field is sourced via a separate query" do allow(field).to receive_messages :source_type => 'query' expect(presenter.to_group).to be_nil end end describe '#to_select' do it "returns the column name as a string" do expect(presenter.to_select).to eq('articles.title AS title') end it "gets the column's table alias from the associations object" do allow(column).to receive_messages(:__stack => [:users, :posts]) expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_select end it "returns the column name with an alias when provided" do allow(field).to receive_messages(:name => :subject) expect(presenter.to_select).to eq('articles.title AS subject') end it "groups and concatenates aggregated columns" do allow(adapter).to receive :group_concatenate do |clause, separator| "GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')" end allow(path).to receive_messages :aggregate? => true expect(presenter.to_select). to eq("GROUP_CONCAT(articles.title SEPARATOR ' ') AS title") end it "concatenates multiple columns" do allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end allow(field).to receive_messages(:columns => [column, column]) expect(presenter.to_select). to eq("CONCAT_WS(' ', articles.title, articles.title) AS title") end it "does not include columns that don't exist" do allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end allow(field).to receive_messages(:columns => [column, double('column', :string? => false, :__stack => [], :__name => 'body')]) expect(presenter.to_select). to eq("CONCAT_WS(' ', articles.title) AS title") end it "returns nil for query sourced fields" do allow(field).to receive_messages :source_type => :query expect(presenter.to_select).to be_nil end it "returns nil for ranged query sourced fields" do allow(field).to receive_messages :source_type => :ranged_query expect(presenter.to_select).to be_nil end end end context 'with an attribute' do let(:presenter) { ThinkingSphinx::ActiveRecord::PropertySQLPresenter.new( attribute, adapter, associations ) } let(:attribute) { double('attribute', :name => 'created_at', :columns => [column], :type => :integer, :multi? => false, :source_type => nil, :model => double) } let(:column) { double('column', :string? => false, :__stack => [], :__name => 'created_at') } before :each do allow(adapter).to receive :cast_to_timestamp do |clause| "UNIX_TIMESTAMP(#{clause})" end end describe '#to_group' do it "returns the column name as a string" do expect(presenter.to_group).to eq('articles.created_at') end it "gets the column's table alias from the associations object" do allow(column).to receive_messages(:__stack => [:users, :posts]) expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_group end it "returns nil if the column is a string" do allow(column).to receive_messages(:string? => true) expect(presenter.to_group).to be_nil end it "returns nil if the property is an aggregate" do allow(path).to receive_messages :aggregate? => true expect(presenter.to_group).to be_nil end it "returns nil if the attribute is sourced via a separate query" do allow(attribute).to receive_messages :source_type => 'query' expect(presenter.to_group).to be_nil end end describe '#to_select' do it "returns the column name as a string" do expect(presenter.to_select).to eq('articles.created_at AS created_at') end it "gets the column's table alias from the associations object" do allow(column).to receive_messages(:__stack => [:users, :posts]) expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_select end it "returns the column name with an alias when provided" do allow(attribute).to receive_messages(:name => :creation_timestamp) expect(presenter.to_select). to eq('articles.created_at AS creation_timestamp') end it "ensures datetime attributes are converted to timestamps" do allow(attribute).to receive_messages :type => :timestamp expect(presenter.to_select). to eq('UNIX_TIMESTAMP(articles.created_at) AS created_at') end it "does not include columns that don't exist" do allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end allow(attribute).to receive_messages(:columns => [column, double('column', :string? => false, :__stack => [], :__name => 'updated_at')]) expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(articles.created_at AS varchar)) AS created_at") end it "casts and concatenates multiple columns for attributes" do allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end allow(attribute).to receive_messages(:columns => [column, column]) expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(articles.created_at AS varchar), CAST(articles.created_at AS varchar)) AS created_at") end it "double-casts and concatenates multiple columns for timestamp attributes" do allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end allow(attribute).to receive_messages :columns => [column, column], :type => :timestamp expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar), CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar)) AS created_at") end it "does not split attribute clause for timestamp casting if it looks like a function call" do allow(column).to receive_messages :__name => "COALESCE(articles.updated_at, articles.created_at)", :string? => true allow(attribute).to receive_messages :name => 'mod_date', :columns => [column], :type => :timestamp expect(presenter.to_select).to eq("UNIX_TIMESTAMP(COALESCE(articles.updated_at, articles.created_at)) AS mod_date") end it "returns nil for query sourced attributes" do allow(attribute).to receive_messages :source_type => :query expect(presenter.to_select).to be_nil end it "returns nil for ranged query sourced attributes" do allow(attribute).to receive_messages :source_type => :ranged_query expect(presenter.to_select).to be_nil end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/sql_builder_spec.rb000066400000000000000000000457711341132130100274540ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::SQLBuilder do let(:source) { double('source', :model => model, :offset => 3, :fields => [], :attributes => [], :disable_range? => false, :delta_processor => nil, :conditions => [], :groupings => [], :adapter => adapter, :associations => [], :primary_key => :id, :options => {}, :properties => []) } let(:model) { double('model', :connection => connection, :descends_from_active_record? => true, :column_names => [], :inheritance_column => 'type', :unscoped => relation, :quoted_table_name => '`users`', :name => 'User') } let(:connection) { double('connection') } let(:relation) { double('relation') } let(:config) { double('config', :indices => indices, :settings => {}) } let(:indices) { double('indices', :count => 5) } let(:presenter) { double('presenter', :to_select => '`name` AS `name`', :to_group => '`name`') } let(:adapter) { double('adapter', :time_zone_query_pre => ['SET TIME ZONE']) } let(:associations) { double('associations', :join_values => []) } let(:builder) { ThinkingSphinx::ActiveRecord::SQLBuilder.new source } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config allow(ThinkingSphinx::ActiveRecord::PropertySQLPresenter).to receive_messages :new => presenter allow(Joiner::Joins).to receive_messages :new => associations allow(relation).to receive_messages :select => relation, :where => relation, :group => relation, :order => relation, :joins => relation, :to_sql => '' allow(connection).to receive(:quote_column_name) { |column| "`#{column}`"} end describe 'sql_query' do before :each do allow(source).to receive_messages :type => 'mysql' end it "adds source associations to the joins of the query" do source.associations << double('association', :stack => [:user, :posts], :string? => false) expect(associations).to receive(:add_join_to).with([:user, :posts]) builder.sql_query end it "adds string joins directly to the relation" do source.associations << double('association', :to_s => 'my string', :string? => true) expect(relation).to receive(:joins).with(['my string']).and_return(relation) builder.sql_query end context 'MySQL adapter' do before :each do allow(source).to receive_messages :type => 'mysql' end it "returns the relation's query" do allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' expect(builder.sql_query).to eq('SELECT * FROM people') end it "ensures results aren't from cache" do expect(relation).to receive(:select) do |string| expect(string).to match(/^SQL_NO_CACHE /) relation end builder.sql_query end it "adds the document id using the offset and index count" do expect(relation).to receive(:select) do |string| expect(string).to match(/`users`.`id` \* 5 \+ 3 AS `id`/) relation end builder.sql_query end it "adds each field to the SELECT clause" do source.fields << double('field') expect(relation).to receive(:select) do |string| expect(string).to match(/`name` AS `name`/) relation end builder.sql_query end it "adds each attribute to the SELECT clause" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_select => '`created_at` AS `created_at`') expect(relation).to receive(:select) do |string| expect(string).to match(/`created_at` AS `created_at`/) relation end builder.sql_query end it "limits results to a set range" do expect(relation).to receive(:where) do |string| expect(string).to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end builder.sql_query end it "shouldn't limit results to a range if ranges are disabled" do allow(source).to receive_messages :disable_range? => true expect(relation).to receive(:where) do |string| expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end builder.sql_query end it "adds source conditions" do source.conditions << 'created_at > NOW()' expect(relation).to receive(:where) do |string| expect(string).to match(/created_at > NOW()/) relation end builder.sql_query end it "groups by the primary key" do expect(relation).to receive(:group) do |string| expect(string).to match(/`users`.`id`/) relation end builder.sql_query end it "groups each field" do source.fields << double('field') expect(relation).to receive(:group) do |string| expect(string).to match(/`name`/) relation end builder.sql_query end it "groups each attribute" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_group => '`created_at`') expect(relation).to receive(:group) do |string| expect(string).to match(/`created_at`/) relation end builder.sql_query end it "groups by source groupings" do source.groupings << '`latitude`' expect(relation).to receive(:group) do |string| expect(string).to match(/`latitude`/) relation end builder.sql_query end it "orders by NULL" do expect(relation).to receive(:order).with('NULL').and_return(relation) builder.sql_query end context 'STI model' do before :each do model.column_names << 'type' allow(model).to receive_messages :descends_from_active_record? => false allow(model).to receive_messages :store_full_sti_class => true end it "groups by the inheritance column" do expect(relation).to receive(:group) do |string| expect(string).to match(/`users`.`type`/) relation end builder.sql_query end context 'with a custom inheritance column' do before :each do model.column_names << 'custom_type' allow(model).to receive_messages :inheritance_column => 'custom_type' end it "groups by the right column" do expect(relation).to receive(:group) do |string| expect(string).to match(/`users`.`custom_type`/) relation end builder.sql_query end end end context 'with a delta processor' do let(:processor) { double('processor') } before :each do allow(source).to receive_messages :delta_processor => processor allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do expect(processor).to receive(:clause).with(true).and_return('`delta` = 1') expect(relation).to receive(:where) do |string| expect(string).to match(/`delta` = 1/) relation end builder.sql_query end end end context 'PostgreSQL adapter' do let(:presenter) { double('presenter', :to_select => '"name" AS "name"', :to_group => '"name"') } before :each do allow(source).to receive_messages :type => 'pgsql' allow(model).to receive_messages :quoted_table_name => '"users"' allow(connection).to receive(:quote_column_name) { |column| "\"#{column}\""} end it "returns the relation's query" do allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' expect(builder.sql_query).to eq('SELECT * FROM people') end it "adds the document id using the offset and index count" do expect(relation).to receive(:select) do |string| expect(string).to match(/"users"."id" \* 5 \+ 3 AS "id"/) relation end builder.sql_query end it "adds each field to the SELECT clause" do source.fields << double('field') expect(relation).to receive(:select) do |string| expect(string).to match(/"name" AS "name"/) relation end builder.sql_query end it "adds each attribute to the SELECT clause" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_select => '"created_at" AS "created_at"') expect(relation).to receive(:select) do |string| expect(string).to match(/"created_at" AS "created_at"/) relation end builder.sql_query end it "limits results to a set range" do expect(relation).to receive(:where) do |string| expect(string).to match(/"users"."id" BETWEEN \$start AND \$end/) relation end builder.sql_query end it "shouldn't limit results to a range if ranges are disabled" do allow(source).to receive_messages :disable_range? => true expect(relation).to receive(:where) do |string| expect(string).not_to match(/"users"."id" BETWEEN \$start AND \$end/) relation end builder.sql_query end it "adds source conditions" do source.conditions << 'created_at > NOW()' expect(relation).to receive(:where) do |string| expect(string).to match(/created_at > NOW()/) relation end builder.sql_query end it "groups by the primary key" do expect(relation).to receive(:group) do |string| expect(string).to match(/"users"."id"/) relation end builder.sql_query end it "groups each field" do source.fields << double('field') expect(relation).to receive(:group) do |string| expect(string).to match(/"name"/) relation end builder.sql_query end it "groups each attribute" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_group => '"created_at"') expect(relation).to receive(:group) do |string| expect(string).to match(/"created_at"/) relation end builder.sql_query end it "groups by source groupings" do source.groupings << '"latitude"' expect(relation).to receive(:group) do |string| expect(string).to match(/"latitude"/) relation end builder.sql_query end it "has no ORDER clause" do expect(relation).not_to receive(:order) builder.sql_query end context 'group by shortcut' do before :each do source.options[:minimal_group_by?] = true end it "groups by the primary key" do expect(relation).to receive(:group) do |string| expect(string).to match(/"users"."id"/) relation end builder.sql_query end it "does not group by fields" do source.fields << double('field') expect(relation).to receive(:group) do |string| expect(string).not_to match(/"name"/) relation end builder.sql_query end it "does not group by attributes" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_group => '"created_at"') expect(relation).to receive(:group) do |string| expect(string).not_to match(/"created_at"/) relation end builder.sql_query end it "groups by source groupings" do source.groupings << '"latitude"' expect(relation).to receive(:group) do |string| expect(string).to match(/"latitude"/) relation end builder.sql_query end end context 'group by shortcut in global configuration' do before :each do config.settings['minimal_group_by'] = true end it "groups by the primary key" do expect(relation).to receive(:group) do |string| expect(string).to match(/"users"."id"/) relation end builder.sql_query end it "does not group by fields" do source.fields << double('field') expect(relation).to receive(:group) do |string| expect(string).not_to match(/"name"/) relation end builder.sql_query end it "does not group by attributes" do source.attributes << double('attribute') allow(presenter).to receive_messages(:to_group => '"created_at"') expect(relation).to receive(:group) do |string| expect(string).not_to match(/"created_at"/) relation end builder.sql_query end it "groups by source groupings" do source.groupings << '"latitude"' expect(relation).to receive(:group) do |string| expect(string).to match(/"latitude"/) relation end builder.sql_query end end context 'STI model' do before :each do model.column_names << 'type' allow(model).to receive_messages :descends_from_active_record? => false allow(model).to receive_messages :store_full_sti_class => true end it "groups by the inheritance column" do expect(relation).to receive(:group) do |string| expect(string).to match(/"users"."type"/) relation end builder.sql_query end context 'with a custom inheritance column' do before :each do model.column_names << 'custom_type' allow(model).to receive_messages :inheritance_column => 'custom_type' end it "groups by the right column" do expect(relation).to receive(:group) do |string| expect(string).to match(/"users"."custom_type"/) relation end builder.sql_query end end end context 'with a delta processor' do let(:processor) { double('processor') } before :each do allow(source).to receive_messages :delta_processor => processor allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do expect(processor).to receive(:clause).with(true).and_return('"delta" = 1') expect(relation).to receive(:where) do |string| expect(string).to match(/"delta" = 1/) relation end builder.sql_query end end end end describe 'sql_query_pre' do let(:processor) { double('processor', :reset_query => 'RESET DELTAS') } before :each do allow(source).to receive_messages :options => {}, :delta_processor => nil, :delta? => false allow(adapter).to receive_messages :utf8_query_pre => ['SET UTF8'] end it "adds a reset delta query if there is a delta processor and this is the core source" do allow(source).to receive_messages :delta_processor => processor expect(builder.sql_query_pre).to include('RESET DELTAS') end it "does not add a reset query if there is no delta processor" do expect(builder.sql_query_pre).not_to include('RESET DELTAS') end it "does not add a reset query if this is a delta source" do allow(source).to receive_messages :delta_processor => processor allow(source).to receive_messages :delta? => true expect(builder.sql_query_pre).not_to include('RESET DELTAS') end it "sets the group_concat_max_len value if set" do source.options[:group_concat_max_len] = 123 expect(builder.sql_query_pre). to include('SET SESSION group_concat_max_len = 123') end it "does not set the group_concat_max_len if not provided" do source.options[:group_concat_max_len] = nil expect(builder.sql_query_pre.select { |sql| sql[/SET SESSION group_concat_max_len/] }).to be_empty end it "sets the connection to use UTF-8 if required" do source.options[:utf8?] = true expect(builder.sql_query_pre).to include('SET UTF8') end it "does not set the connection to use UTF-8 if not required" do source.options[:utf8?] = false expect(builder.sql_query_pre).not_to include('SET UTF8') end it "adds a time-zone query by default" do expect(builder.sql_query_pre).to include('SET TIME ZONE') end it "does not add a time-zone query if requested" do config.settings['skip_time_zone'] = true expect(builder.sql_query_pre).to_not include('SET TIME ZONE') end end describe 'sql_query_range' do before :each do allow(adapter).to receive(:convert_nulls) { |string, default| "ISNULL(#{string}, #{default})" } end it "returns the relation's query" do allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' expect(builder.sql_query_range).to eq('SELECT * FROM people') end it "returns nil if ranges are disabled" do allow(source).to receive_messages :disable_range? => true expect(builder.sql_query_range).to be_nil end it "selects the minimum primary key value, allowing for nulls" do expect(relation).to receive(:select) do |string| expect(string).to match(/ISNULL\(MIN\(`users`.`id`\), 1\)/) relation end builder.sql_query_range end it "selects the maximum primary key value, allowing for nulls" do expect(relation).to receive(:select) do |string| expect(string).to match(/ISNULL\(MAX\(`users`.`id`\), 1\)/) relation end builder.sql_query_range end it "shouldn't limit results to a range" do expect(relation).to receive(:where) do |string| expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end builder.sql_query_range end it "does not add source conditions" do source.conditions << 'created_at > NOW()' expect(relation).to receive(:where) do |string| expect(string).not_to match(/created_at > NOW()/) relation end builder.sql_query_range end context 'with a delta processor' do let(:processor) { double('processor') } before :each do allow(source).to receive_messages :delta_processor => processor allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do expect(processor).to receive(:clause).with(true).and_return('`delta` = 1') expect(relation).to receive(:where) do |string| expect(string).to match(/`delta` = 1/) relation end builder.sql_query_range end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/active_record/sql_source_spec.rb000066400000000000000000000364701341132130100273220ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::ActiveRecord::SQLSource do let(:model) { double('model', :connection => connection, :name => 'User', :column_names => [], :inheritance_column => 'type', :primary_key => :id) } let(:connection) { double('connection', :instance_variable_get => db_config) } let(:db_config) { {:host => 'localhost', :user => 'root', :database => 'default'} } let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new(model, :position => 3, :primary_key => model.primary_key || :id ) } let(:adapter) { double('adapter') } before :each do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). to receive_messages(:=== => true) allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). to receive_messages(:adapter_for => adapter) end describe '#adapter' do it "returns a database adapter for the model" do expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters). to receive(:adapter_for).with(model).and_return(adapter) expect(source.adapter).to eq(adapter) end end describe '#attributes' do it "has the internal id attribute by default" do expect(source.attributes.collect(&:name)).to include('sphinx_internal_id') end it "has the class name attribute by default" do expect(source.attributes.collect(&:name)).to include('sphinx_internal_class') end it "has the internal deleted attribute by default" do expect(source.attributes.collect(&:name)).to include('sphinx_deleted') end it "marks the internal class attribute as a facet" do expect(source.attributes.detect { |attribute| attribute.name == 'sphinx_internal_class' }.options[:facet]).to be_truthy end end describe '#delta_processor' do let(:processor_class) { double('processor class', :try => processor) } let(:processor) { double('processor') } let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new model, :delta_processor => processor_class, :primary_key => model.primary_key || :id } let(:source_with_options) { ThinkingSphinx::ActiveRecord::SQLSource.new model, :delta_processor => processor_class, :delta_options => { :opt_key => :opt_value }, :primary_key => model.primary_key || :id } it "loads the processor with the adapter" do expect(processor_class).to receive(:try).with(:new, adapter, {}). and_return processor source.delta_processor end it "returns the given processor" do expect(source.delta_processor).to eq(processor) end it "passes given options to the processor" do expect(processor_class).to receive(:try).with(:new, adapter, {:opt_key => :opt_value}) source_with_options.delta_processor end end describe '#delta?' do it "returns the given delta setting" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, :delta? => true, :primary_key => model.primary_key || :id expect(source).to be_a_delta end end describe '#disable_range?' do it "returns the given range setting" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, :disable_range? => true, :primary_key => model.primary_key || :id expect(source.disable_range?).to be_truthy end end describe '#fields' do it "has the internal class field by default" do expect(source.fields.collect(&:name)). to include('sphinx_internal_class_name') end it "sets the sphinx class field to use a string of the class name" do expect(source.fields.detect { |field| field.name == 'sphinx_internal_class_name' }.columns.first.__name).to eq("'User'") end it "uses the inheritance column if it exists for the sphinx class field" do allow(adapter).to receive_messages :quoted_table_name => '"users"', :quote => '"type"' allow(adapter).to receive(:convert_blank) { |clause, default| "coalesce(nullif(#{clause}, ''), #{default})" } allow(model).to receive_messages :column_names => ['type'], :sti_name => 'User' expect(source.fields.detect { |field| field.name == 'sphinx_internal_class_name' }.columns.first.__name). to eq("coalesce(nullif(\"users\".\"type\", ''), 'User')") end end describe '#name' do it "defaults to the model name downcased with the given position" do expect(source.name).to eq('user_3') end it "allows for custom names, but adds the position suffix" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, :name => 'people', :position => 2, :primary_key => model.primary_key || :id expect(source.name).to eq('people_2') end end describe '#offset' do it "returns the given offset" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, :offset => 12, :primary_key => model.primary_key || :id expect(source.offset).to eq(12) end end describe '#options' do it "defaults to having utf8? set to false" do expect(source.options[:utf8?]).to be_falsey end it "sets utf8? to true if the database encoding is utf8" do db_config[:encoding] = 'utf8' 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') } let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new(model, :position => 3, :primary_key => :custom_key) } let(:template) { ThinkingSphinx::ActiveRecord::SQLSource::Template.new(source) } it 'template should allow primary key from options' do template.apply template.source.attributes.collect(&:columns) == :custom_key end end end describe '#render' do let(:builder) { double('builder', :sql_query_pre => [], :sql_query_post_index => [], :sql_query => 'query', :sql_query_range => 'range', :sql_query_info => 'info') } let(:config) { double('config', :settings => {}) } let(:presenter) { double('presenter', :collection_type => :uint) } let(:template) { double('template', :apply => true) } before :each do allow(ThinkingSphinx::ActiveRecord::SQLBuilder).to receive_messages :new => builder allow(ThinkingSphinx::ActiveRecord::Attribute::SphinxPresenter).to receive_messages :new => presenter allow(ThinkingSphinx::ActiveRecord::SQLSource::Template).to receive_messages :new => template allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "uses the builder's sql_query value" do allow(builder).to receive_messages :sql_query => 'select * from table' source.render expect(source.sql_query).to eq('select * from table') end it "uses the builder's sql_query_range value" do allow(builder).to receive_messages :sql_query_range => 'select 0, 10 from table' source.render expect(source.sql_query_range).to eq('select 0, 10 from table') end it "appends the builder's sql_query_pre value" do allow(builder).to receive_messages :sql_query_pre => ['Change Setting'] source.render expect(source.sql_query_pre).to eq(['Change Setting']) end it "adds fields with attributes to sql_field_string" do source.fields << double('field', :name => 'title', :source_type => nil, :with_attribute? => true, :file? => false, :wordcount? => false) source.render expect(source.sql_field_string).to include('title') end it "adds any joined or file fields" do source.fields << double('field', :name => 'title', :file? => true, :with_attribute? => false, :wordcount? => false, :source_type => nil) source.render expect(source.sql_file_field).to include('title') end it "adds wordcounted fields to sql_field_str2wordcount" do source.fields << double('field', :name => 'title', :source_type => nil, :with_attribute? => false, :file? => false, :wordcount? => true) source.render expect(source.sql_field_str2wordcount).to include('title') end it "adds any joined fields" do allow(ThinkingSphinx::ActiveRecord::PropertyQuery).to receive_messages( :new => double(:to_s => 'query for title') ) source.fields << double('field', :name => 'title', :source_type => :query, :with_attribute? => false, :file? => false, :wordcount? => false) source.render expect(source.sql_joined_field).to include('query for title') end it "adds integer attributes to sql_attr_uint" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'count', :collection_type => :uint source.render expect(source.sql_attr_uint).to include('count') end it "adds boolean attributes to sql_attr_bool" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'published', :collection_type => :bool source.render expect(source.sql_attr_bool).to include('published') end it "adds string attributes to sql_attr_string" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'name', :collection_type => :string source.render expect(source.sql_attr_string).to include('name') end it "adds timestamp attributes to sql_attr_timestamp" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'created_at', :collection_type => :timestamp source.render expect(source.sql_attr_timestamp).to include('created_at') end it "adds float attributes to sql_attr_float" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'rating', :collection_type => :float source.render expect(source.sql_attr_float).to include('rating') end it "adds bigint attributes to sql_attr_bigint" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'super_id', :collection_type => :bigint source.render expect(source.sql_attr_bigint).to include('super_id') end it "adds ordinal strings to sql_attr_str2ordinal" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'name', :collection_type => :str2ordinal source.render expect(source.sql_attr_str2ordinal).to include('name') end it "adds multi-value attributes to sql_attr_multi" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'uint tag_ids from field', :collection_type => :multi source.render expect(source.sql_attr_multi).to include('uint tag_ids from field') end it "adds word count attributes to sql_attr_str2wordcount" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'name', :collection_type => :str2wordcount source.render expect(source.sql_attr_str2wordcount).to include('name') end it "adds json attributes to sql_attr_json" do source.attributes << double('attribute') allow(presenter).to receive_messages :declaration => 'json', :collection_type => :json source.render expect(source.sql_attr_json).to include('json') end it "adds relevant settings from thinking_sphinx.yml" do config.settings['mysql_ssl_cert'] = 'foo.cert' config.settings['morphology'] = 'stem_en' # should be ignored source.render expect(source.mysql_ssl_cert).to eq('foo.cert') end end describe '#set_database_settings' do it "sets the sql_host setting from the model's database settings" do source.set_database_settings :host => '12.34.56.78' expect(source.sql_host).to eq('12.34.56.78') end it "defaults sql_host to localhost if the model has no host" do source.set_database_settings :host => nil expect(source.sql_host).to eq('localhost') end it "sets the sql_user setting from the model's database settings" do source.set_database_settings :username => 'pat' expect(source.sql_user).to eq('pat') end it "uses the user setting if username is not set in the model" do source.set_database_settings :username => nil, :user => 'pat' expect(source.sql_user).to eq('pat') end it "sets the sql_pass setting from the model's database settings" do source.set_database_settings :password => 'swordfish' expect(source.sql_pass).to eq('swordfish') end it "escapes hashes in the password for sql_pass" do source.set_database_settings :password => 'sword#fish' expect(source.sql_pass).to eq('sword\#fish') end it "sets the sql_db setting from the model's database settings" do source.set_database_settings :database => 'rails_app' expect(source.sql_db).to eq('rails_app') end it "sets the sql_port setting from the model's database settings" do source.set_database_settings :port => 5432 expect(source.sql_port).to eq(5432) end it "sets the sql_sock setting from the model's database settings" do source.set_database_settings :socket => '/unix/socket' expect(source.sql_sock).to eq('/unix/socket') end it "sets the mysql_ssl_cert from the model's database settings" do source.set_database_settings :sslcert => '/path/to/cert.pem' expect(source.mysql_ssl_cert).to eq '/path/to/cert.pem' end it "sets the mysql_ssl_key from the model's database settings" do source.set_database_settings :sslkey => '/path/to/key.pem' expect(source.mysql_ssl_key).to eq '/path/to/key.pem' end it "sets the mysql_ssl_ca from the model's database settings" do source.set_database_settings :sslca => '/path/to/ca.pem' expect(source.mysql_ssl_ca).to eq '/path/to/ca.pem' end end describe '#type' do it "is mysql when using the MySQL Adapter" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). to receive_messages(:=== => true) allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). to receive_messages(:=== => false) expect(source.type).to eq('mysql') end it "is pgsql when using the PostgreSQL Adapter" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). to receive_messages(:=== => false) allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). to receive_messages(:=== => true) expect(source.type).to eq('pgsql') end it "raises an exception for any other adapter" do allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). to receive_messages(:=== => false) allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). to receive_messages(:=== => false) expect { source.type }.to raise_error( ThinkingSphinx::UnknownDatabaseAdapter ) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/attribute_types_spec.rb000066400000000000000000000022631341132130100255520ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/000077500000000000000000000000001341132130100225625ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/clear_real_time_spec.rb000066400000000000000000000030041341132130100272250ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::ClearRealTime do let(:command) { ThinkingSphinx::Commands::ClearRealTime.new( configuration, {:indices => [users_index, parts_index]}, stream ) } let(:configuration) { double 'configuration', :searchd => double(:binlog_path => '/path/to/binlog') } let(:stream) { double :puts => nil } let(:users_index) { double :path => '/path/to/my/index/users', :render => true } let(:parts_index) { double :path => '/path/to/my/index/parts', :render => true } before :each do 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 :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([]) command.call end it "removes the directory for the binlog files" do expect(FileUtils).to receive(:rm_r).with('/path/to/binlog') command.call 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') command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/clear_sql_spec.rb000066400000000000000000000034111341132130100260650ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::ClearSQL do let(:command) { ThinkingSphinx::Commands::ClearSQL.new( configuration, {:indices => [users_index, parts_index]}, stream ) } let(:configuration) { double 'configuration', :preload_indices => true, :render => true, :indices => [users_index, parts_index], :indices_location => '/path/to/indices' } let(:stream) { double :puts => nil } 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') } before :each do 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(Dir).to receive(:[]).with('/path/to/indices/ts-*.tmp'). and_return(['/path/to/indices/ts-foo.tmp']) allow(FileUtils).to receive_messages :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([]) command.call 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') command.call end it "removes any indexing guard files" do expect(FileUtils).to receive(:rm_r).with(["/path/to/indices/ts-foo.tmp"]) command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/configure_spec.rb000066400000000000000000000014111341132130100260770ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/index_real_time_spec.rb000066400000000000000000000017521341132130100272560ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::IndexRealTime do let(:command) { ThinkingSphinx::Commands::IndexRealTime.new( configuration, {:indices => [users_index, parts_index]}, stream ) } let(:configuration) { double 'configuration', :controller => controller } let(:controller) { double 'controller', :rotate => nil } let(:stream) { double :puts => nil } let(:users_index) { double(name: 'users') } let(:parts_index) { double(name: 'parts') } before :each do allow(ThinkingSphinx::RealTime::Populator).to receive(:populate) 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) command.call end it "rotates the daemon for each index" do expect(controller).to receive(:rotate).twice command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/index_sql_spec.rb000066400000000000000000000045631341132130100261170ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::IndexSQL do let(:command) { ThinkingSphinx::Commands::IndexSQL.new( configuration, {:verbose => true}, stream ) } let(:configuration) { double 'configuration', :controller => controller, :indexing_strategy => indexing_strategy, :guarding_strategy => guarding_strategy } let(:controller) { double 'controller', :index => true } let(:stream) { double :puts => nil } let(:indexing_strategy) { Proc.new { |names, &block| block.call names } } let(:guarding_strategy) { Proc.new { |names, &block| block.call names } } before :each do allow(ThinkingSphinx).to receive_messages :before_index_hooks => [] end it "calls all registered hooks" do called = false ThinkingSphinx.before_index_hooks << Proc.new { called = true } command.call expect(called).to eq(true) end 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::IndexSQL.new( configuration, {:verbose => false}, stream ) expect(controller).to receive(:index).with(:verbose => false) command.call end it "ignores a nil indices filter" do command = ThinkingSphinx::Commands::IndexSQL.new( configuration, {:verbose => false, :indices => nil}, stream ) expect(controller).to receive(:index).with(:verbose => false) command.call end it "ignores an empty indices filter" do command = ThinkingSphinx::Commands::IndexSQL.new( configuration, {:verbose => false, :indices => []}, stream ) expect(controller).to receive(:index).with(:verbose => false) command.call end it "uses filtered index names" do command = ThinkingSphinx::Commands::IndexSQL.new( configuration, {:verbose => false, :indices => ['foo_bar']}, stream ) expect(controller).to receive(:index).with('foo_bar', :verbose => false) command.call end it "does not call hooks when filtering by index" do called = false ThinkingSphinx.before_index_hooks << Proc.new { called = true } ThinkingSphinx::Commands::IndexSQL.new( configuration, {:verbose => false, :indices => ['foo_bar']}, stream ).call expect(called).to eq(false) end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/merge_and_update_spec.rb000066400000000000000000000077101341132130100274110ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::MergeAndUpdate do let(:command) { ThinkingSphinx::Commands::MergeAndUpdate.new( configuration, {}, stream ) } let(:configuration) { double "configuration", :preload_indices => nil, :render => "", :indices => [core_index_a, delta_index_a, rt_index, plain_index, core_index_b, delta_index_b] } let(:stream) { double :puts => nil } let(:commander) { double :call => true } let(:core_index_a) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => false, :name => "index_a_core", :model => model_a, :path => "index_a_core" } let(:delta_index_a) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => true, :name => "index_a_delta", :path => "index_a_delta" } let(:core_index_b) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => false, :name => "index_b_core", :model => model_b, :path => "index_b_core" } let(:delta_index_b) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => true, :name => "index_b_delta", :path => "index_b_delta" } let(:rt_index) { double "index", :type => "rt", :name => "rt_index" } let(:plain_index) { double "index", :type => "plain", :name => "plain_index", :options => {:delta_processor => nil} } let(:model_a) { double "model", :where => where_a } let(:model_b) { double "model", :where => where_b } let(:where_a) { double "where", :update_all => nil } let(:where_b) { double "where", :update_all => nil } before :each do stub_const 'ThinkingSphinx::Commander', commander end it "merges core/delta pairs" do expect(commander).to receive(:call).with( :merge, configuration, hash_including( :core_index => core_index_a, :delta_index => delta_index_a, :filters => {:sphinx_deleted => 0} ), stream ) expect(commander).to receive(:call).with( :merge, configuration, hash_including( :core_index => core_index_b, :delta_index => delta_index_b, :filters => {:sphinx_deleted => 0} ), stream ) command.call end it "unflags delta records" do expect(model_a).to receive(:where).with(:delta => true).and_return(where_a) expect(where_a).to receive(:update_all).with(:delta => false) expect(model_b).to receive(:where).with(:delta => true).and_return(where_b) expect(where_b).to receive(:update_all).with(:delta => false) command.call end it "ignores real-time indices" do expect(commander).to_not receive(:call).with( :merge, configuration, hash_including(:core_index => rt_index), stream ) expect(commander).to_not receive(:call).with( :merge, configuration, hash_including(:delta_index => rt_index), stream ) command.call end it "ignores non-delta SQL indices" do expect(commander).to_not receive(:call).with( :merge, configuration, hash_including(:core_index => plain_index), stream ) expect(commander).to_not receive(:call).with( :merge, configuration, hash_including(:delta_index => plain_index), stream ) command.call end context "with index name filter" do let(:command) { ThinkingSphinx::Commands::MergeAndUpdate.new( configuration, {:index_names => ["index_a"]}, stream ) } it "only processes matching indices" do expect(commander).to receive(:call).with( :merge, configuration, hash_including( :core_index => core_index_a, :delta_index => delta_index_a, :filters => {:sphinx_deleted => 0} ), stream ) expect(commander).to_not receive(:call).with( :merge, configuration, hash_including( :core_index => core_index_b, :delta_index => delta_index_b, :filters => {:sphinx_deleted => 0} ), stream ) command.call end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/merge_spec.rb000066400000000000000000000025621341132130100252250ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::Merge do let(:command) { ThinkingSphinx::Commands::Merge.new( configuration, {:core_index => core_index, :delta_index => delta_index, :filters => {:sphinx_deleted => 0}}, stream ) } let(:configuration) { double "configuration", :controller => controller } let(:stream) { double :puts => nil } let(:controller) { double "controller", :merge => nil } let(:core_index) { double "index", :path => "index_a_core", :name => "index_a_core" } let(:delta_index) { double "index", :path => "index_a_delta", :name => "index_a_delta" } before :each do allow(File).to receive(:exist?).and_return(true) end it "merges core/delta pairs" do expect(controller).to receive(:merge).with( "index_a_core", "index_a_delta", :filters => {:sphinx_deleted => 0}, :verbose => nil ) command.call end it "does not merge if just the core does not exist" do allow(File).to receive(:exist?).with("index_a_core.spi").and_return(false) expect(controller).to_not receive(:merge) command.call end it "does not merge if just the delta does not exist" do allow(File).to receive(:exist?).with("index_a_delta.spi").and_return(false) expect(controller).to_not receive(:merge) command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/prepare_spec.rb000066400000000000000000000011031341132130100255520ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Commands::Prepare do let(:command) { ThinkingSphinx::Commands::Prepare.new( configuration, {}, stream ) } let(:configuration) { double 'configuration', :indices_location => '/path/to/indices' } let(:stream) { double :puts => nil } before :each do allow(FileUtils).to receive_messages :mkdir_p => true end it "creates the directory for the index files" do expect(FileUtils).to receive(:mkdir_p).with('/path/to/indices') command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/start_detached_spec.rb000066400000000000000000000031571341132130100271050ustar00rootroot00000000000000# frozen_string_literal: true 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 thinking-sphinx-4.1.0/spec/thinking_sphinx/commands/stop_spec.rb000066400000000000000000000033701341132130100251110ustar00rootroot00000000000000# frozen_string_literal: true 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 } let(:commander) { double :call => nil } before :each do stub_const 'ThinkingSphinx::Commander', commander allow(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(true, true, false) end it "prints a message if the daemon is not already running" do allow(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(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(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(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(commander).to receive(:call). with(:running, configuration, {}, stream). and_return(true, true, true, false) expect(controller).to receive(:stop).twice command.call end end thinking-sphinx-4.1.0/spec/thinking_sphinx/configuration/000077500000000000000000000000001341132130100236305ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/configuration/minimum_fields_spec.rb000066400000000000000000000036271341132130100302000ustar00rootroot00000000000000# frozen_string_literal: true 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', :table_exists? => true } let(:model_b) { double :inheritance_column => 'type', :table_exists? => true } 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 'removes the class name fields when models have no tables' do allow(model_a).to receive(:table_exists?).and_return(false) allow(model_b).to receive(:table_exists?).and_return(false) 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 thinking-sphinx-4.1.0/spec/thinking_sphinx/configuration_spec.rb000066400000000000000000000403011341132130100251650ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Configuration do let(:config) { ThinkingSphinx::Configuration.instance } after :each do ThinkingSphinx::Configuration.reset end describe '.instance' do it "returns an instance of ThinkingSphinx::Configuration" do expect(ThinkingSphinx::Configuration.instance). to be_a(ThinkingSphinx::Configuration) end it "memoizes the instance" do config = double('configuration') expect(ThinkingSphinx::Configuration).to receive(:new).once.and_return(config) ThinkingSphinx::Configuration.instance ThinkingSphinx::Configuration.instance end end describe '.reset' do after :each do config.framework = ThinkingSphinx::Frameworks.current end it 'does not cache settings after reset' do allow(File).to receive_messages :exists? => true allow(File).to receive_messages :read => { 'test' => {'foo' => 'bugs'}, 'production' => {'foo' => 'bar'} }.to_yaml ThinkingSphinx::Configuration.reset # Grab a new copy of the instance. config = ThinkingSphinx::Configuration.instance expect(config.settings['foo']).to eq('bugs') config.framework = double :environment => 'production', :root => Pathname.new(__FILE__).join('..', '..', 'internal') expect(config.settings['foo']).to eq('bar') end end describe '#configuration_file' do it "uses the Rails environment in the configuration file name" do expect(config.configuration_file). to eq(File.join(Rails.root, 'config', 'test.sphinx.conf')) end it "respects provided settings" do write_configuration 'configuration_file' => '/path/to/foo.conf' expect(config.configuration_file).to eq('/path/to/foo.conf') end end describe '#controller' do it "returns an instance of Riddle::Controller" do expect(config.controller).to be_a(Riddle::Controller) end it "memoizes the instance" do expect(Riddle::Controller).to receive(:new).once. and_return(double('controller')) config.controller config.controller end it "sets the bin path from the thinking_sphinx.yml file" do write_configuration('bin_path' => '/foo/bar/bin/') expect(config.controller.bin_path).to eq('/foo/bar/bin/') end it "appends a backslash to the bin_path if appropriate" do write_configuration('bin_path' => '/foo/bar/bin') expect(config.controller.bin_path).to eq('/foo/bar/bin/') end end describe '#index_paths' do it "uses app/indices in the Rails app by default" do expect(config.index_paths).to include(File.join(Rails.root, 'app', 'indices')) end it "uses app/indices in the Rails engines" do engine = double :engine, { :paths => { 'app/indices' => double(:path, { :existent => '/engine/app/indices' } ) } } engine_class = double :instance => engine expect(Rails::Engine).to receive(:subclasses).and_return([ engine_class ]) expect(config.index_paths).to include('/engine/app/indices') end end describe '#indices_location' do it "stores index files in db/sphinx/ENVIRONMENT" do expect(config.indices_location). to eq(File.join(Rails.root, 'db', 'sphinx', 'test')) end it "respects provided settings" do write_configuration 'indices_location' => '/my/index/files' expect(config.indices_location).to eq('/my/index/files') end it "respects relative paths" do write_configuration 'indices_location' => 'my/index/files' expect(config.indices_location).to eq('my/index/files') end it "translates relative paths to absolute if config requests it" do write_configuration( 'indices_location' => 'my/index/files', 'absolute_paths' => true ) expect(config.indices_location).to eq( File.join(config.framework.root, 'my/index/files') ) end it "respects paths that are already absolute" do write_configuration( 'indices_location' => '/my/index/files', 'absolute_paths' => true ) expect(config.indices_location).to eq('/my/index/files') end it "translates linked directories" do write_configuration( 'indices_location' => 'mine/index/files', 'absolute_paths' => true ) framework = ThinkingSphinx::Frameworks.current local_path = File.join framework.root, "mine" linked_path = File.join framework.root, "my" FileUtils.mkdir_p linked_path `ln -s #{linked_path} #{local_path}` expect(config.indices_location).to eq( File.join(config.framework.root, "my/index/files") ) FileUtils.rm local_path FileUtils.rmdir linked_path end end describe '#initialize' do before :each do FileUtils.rm_rf Rails.root.join('log') end it "sets the daemon pid file within log for the Rails app" do expect(config.searchd.pid_file). to eq(File.join(Rails.root, 'log', 'test.sphinx.pid')) end it "sets the daemon log within log for the Rails app" do expect(config.searchd.log). to eq(File.join(Rails.root, 'log', 'test.searchd.log')) end it "sets the query log within log for the Rails app" do expect(config.searchd.query_log). to eq(File.join(Rails.root, 'log', 'test.searchd.query.log')) end it "sets indexer settings if within thinking_sphinx.yml" do write_configuration 'mem_limit' => '128M' expect(config.indexer.mem_limit).to eq('128M') end it "sets searchd settings if within thinking_sphinx.yml" do write_configuration 'workers' => 'none' expect(config.searchd.workers).to eq('none') end it 'adds settings to indexer without common section' do write_configuration 'lemmatizer_base' => 'foo' expect(config.indexer.lemmatizer_base).to eq('foo') end it 'adds settings to common section if requested' do write_configuration 'lemmatizer_base' => 'foo', 'common_sphinx_configuration' => true expect(config.common.lemmatizer_base).to eq('foo') end end describe '#next_offset' do let(:reference) { double('reference') } it "starts at 0" do expect(config.next_offset(reference)).to eq(0) end it "increments for each new reference" do expect(config.next_offset(double('reference'))).to eq(0) expect(config.next_offset(double('reference'))).to eq(1) expect(config.next_offset(double('reference'))).to eq(2) end it "doesn't increment for recorded references" do expect(config.next_offset(reference)).to eq(0) expect(config.next_offset(reference)).to eq(0) end end describe '#preload_indices' do let(:distributor) { double :reconcile => true } before :each do stub_const 'ThinkingSphinx::Configuration::DistributedIndices', double(:new => distributor) end it "searches each index path for ruby files" do config.index_paths.replace ['/path/to/indices', '/path/to/other/indices'] expect(Dir).to receive(:[]).with('/path/to/indices/**/*.rb').once. and_return([]) expect(Dir).to receive(:[]).with('/path/to/other/indices/**/*.rb').once. and_return([]) config.preload_indices end it "loads each file returned" do config.index_paths.replace ['/path/to/indices'] allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/foo_index.rb').once expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/bar_index.rb').once config.preload_indices end it "does not double-load indices" do config.index_paths.replace ['/path/to/indices'] allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/foo_index.rb').once expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/bar_index.rb').once config.preload_indices config.preload_indices end it 'adds distributed indices' do expect(distributor).to receive(:reconcile) config.preload_indices end it 'does not add distributed indices if disabled' do write_configuration('distributed_indices' => false) expect(distributor).not_to receive(:reconcile) config.preload_indices end end describe '#render' do before :each do allow(config.searchd).to receive_messages :render => 'searchd { }' end it "searches each index path for ruby files" do config.index_paths.replace ['/path/to/indices', '/path/to/other/indices'] expect(Dir).to receive(:[]).with('/path/to/indices/**/*.rb').once. and_return([]) expect(Dir).to receive(:[]).with('/path/to/other/indices/**/*.rb').once. and_return([]) config.render end it "loads each file returned" do config.index_paths.replace ['/path/to/indices'] allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/foo_index.rb').once expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/bar_index.rb').once config.render end it "does not double-load indices" do config.index_paths.replace ['/path/to/indices'] allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/foo_index.rb').once expect(ActiveSupport::Dependencies).to receive(:require_or_load). with('/path/to/indices/bar_index.rb').once config.preload_indices config.preload_indices end end describe '#render_to_file' do let(:file) { double('file') } let(:output) { config.render } before :each do allow(config.searchd).to receive_messages :render => 'searchd { }' end it "writes the rendered configuration to the file" do config.configuration_file = '/path/to/file.config' expect(config).to receive(:open).with('/path/to/file.config', 'w'). and_yield(file) expect(file).to receive(:write).with(output) config.render_to_file end it "creates a directory at the binlog_path" do allow(FileUtils).to receive_messages :mkdir_p => true allow(config).to receive_messages :searchd => double(:binlog_path => '/path/to/binlog') expect(FileUtils).to receive(:mkdir_p).with('/path/to/binlog') config.render_to_file end it "skips creating a directory when the binlog_path is blank" do allow(FileUtils).to receive_messages :mkdir_p => true allow(config).to receive_messages :searchd => double(:binlog_path => '') expect(FileUtils).not_to receive(:mkdir_p) config.render_to_file end end describe '#searchd' do describe '#address' do it "defaults to 127.0.0.1" do expect(config.searchd.address).to eq('127.0.0.1') end it "respects the address setting" do write_configuration('address' => '10.11.12.13') expect(config.searchd.address).to eq('10.11.12.13') end end describe '#log' do it "defaults to an environment-specific file" do expect(config.searchd.log).to eq( File.join(config.framework.root, "log/test.searchd.log") ) end it "translates linked directories" do framework = ThinkingSphinx::Frameworks.current log_path = File.join framework.root, "log" linked_path = File.join framework.root, "logging" log_exists = File.exist? log_path FileUtils.mv log_path, "#{log_path}-tmp" if log_exists FileUtils.mkdir_p linked_path `ln -s #{linked_path} #{log_path}` expect(config.searchd.log).to eq( File.join(config.framework.root, "logging/test.searchd.log") ) FileUtils.rm log_path FileUtils.rmdir linked_path FileUtils.mv "#{log_path}-tmp", log_path if log_exists end unless RUBY_PLATFORM == "java" end describe '#mysql41' do it "defaults to 9306" do expect(config.searchd.mysql41).to eq(9306) end it "respects the port setting" do write_configuration('port' => 9313) expect(config.searchd.mysql41).to eq(9313) end it "respects the mysql41 setting" do write_configuration('mysql41' => 9307) expect(config.searchd.mysql41).to eq(9307) end end describe "#socket" do it "does not set anything by default" do expect(config.searchd.socket).to be_nil end it "ignores unspecified address and port when socket is set" do write_configuration("socket" => "/my/socket") expect(config.searchd.socket).to eq("/my/socket:mysql41") expect(config.searchd.address).to be_nil expect(config.searchd.mysql41).to be_nil end it "allows address and socket settings" do write_configuration("socket" => "/my/socket", "address" => "1.1.1.1") expect(config.searchd.socket).to eq("/my/socket:mysql41") expect(config.searchd.address).to eq("1.1.1.1") expect(config.searchd.mysql41).to eq(9306) end it "allows mysql41 and socket settings" do write_configuration("socket" => "/my/socket", "mysql41" => 9307) expect(config.searchd.socket).to eq("/my/socket:mysql41") expect(config.searchd.address).to eq("127.0.0.1") expect(config.searchd.mysql41).to eq(9307) end it "allows port and socket settings" do write_configuration("socket" => "/my/socket", "port" => 9307) expect(config.searchd.socket).to eq("/my/socket:mysql41") expect(config.searchd.address).to eq("127.0.0.1") expect(config.searchd.mysql41).to eq(9307) end it "allows address, mysql41 and socket settings" do write_configuration( "socket" => "/my/socket", "address" => "1.2.3.4", "mysql41" => 9307 ) expect(config.searchd.socket).to eq("/my/socket:mysql41") expect(config.searchd.address).to eq("1.2.3.4") expect(config.searchd.mysql41).to eq(9307) end end end describe '#settings' do context 'YAML file exists' do before :each do allow(File).to receive_messages :exists? => true end it "reads from the YAML file" do expect(File).to receive(:read).and_return('') config.settings end it "uses the settings for the given environment" do allow(File).to receive_messages :read => { 'test' => {'foo' => 'bar'}, 'staging' => {'baz' => 'qux'} }.to_yaml allow(Rails).to receive_messages :env => 'staging' expect(config.settings['baz']).to eq('qux') end it "remembers the file contents" do expect(File).to receive(:read).and_return('') config.settings config.settings end it "returns the default hash when no settings for the environment exist" do allow(File).to receive_messages :read => {'test' => {'foo' => 'bar'}}.to_yaml allow(Rails).to receive_messages :env => 'staging' expect(config.settings.class).to eq(Hash) end end context 'YAML file does not exist' do before :each do allow(File).to receive_messages :exists? => false end it "does not read the file" do expect(File).not_to receive(:read) config.settings end it "returns a hash" do expect(config.settings.class).to eq(Hash) end end end describe '#version' do it "defaults to 2.2.11" do expect(config.version).to eq('2.2.11') end it "respects supplied YAML versions" do write_configuration 'version' => '2.0.4' expect(config.version).to eq('2.0.4') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/connection_spec.rb000066400000000000000000000043461341132130100244660ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Connection do describe '.take' do let(:pool) { double 'pool' } let(:connection) { double 'connection', :base_error => StandardError } let(:error) { ThinkingSphinx::QueryExecutionError.new 'failed' } let(:translated_error) { ThinkingSphinx::SphinxError.new } before :each do allow(ThinkingSphinx::Connection).to receive_messages :pool => pool allow(ThinkingSphinx::SphinxError).to receive_messages :new_from_mysql => translated_error allow(pool).to receive(:take).and_yield(connection) error.statement = 'SELECT * FROM article_core' translated_error.statement = 'SELECT * FROM article_core' end it "yields a connection from the pool" do ThinkingSphinx::Connection.take do |c| expect(c).to eq(connection) end end it "retries errors once" do tries = 0 expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 2 end }.not_to raise_error end it "retries errors twice" do tries = 0 expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 3 end }.not_to raise_error end it "raises a translated error if it fails three times" do tries = 0 expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 4 end }.to raise_error(ThinkingSphinx::SphinxError) end [ThinkingSphinx::SyntaxError, ThinkingSphinx::ParseError].each do |klass| context klass.name do let(:translated_error) { klass.new } it "raises the error" do expect { ThinkingSphinx::Connection.take { |c| raise error } }.to raise_error(klass) end it "does not yield the connection more than once" do yields = 0 begin ThinkingSphinx::Connection.take do |c| yields += 1 raise error end rescue klass # end expect(yields).to eq(1) end end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/deletion_spec.rb000066400000000000000000000031751341132130100241310ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Deletion do describe '.perform' do let(:connection) { double('connection', :execute => nil) } let(:index) { double('index', :name => 'foo_core', :document_id_for_key => 14, :type => 'plain', :distributed? => false) } before :each do allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) allow(Riddle::Query).to receive_messages :update => 'UPDATE STATEMENT' end context 'index is SQL-backed' do it "updates the deleted flag to false" do expect(connection).to receive(:execute). with('UPDATE foo_core SET sphinx_deleted = 1 WHERE id IN (14)') ThinkingSphinx::Deletion.perform index, 7 end it "doesn't care about Sphinx errors" do allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) expect { ThinkingSphinx::Deletion.perform index, 7 }.not_to raise_error end end context "index is real-time" do before :each do allow(index).to receive_messages :type => 'rt' end it "deletes the record to false" do expect(connection).to receive(:execute). with('DELETE FROM foo_core WHERE id = 14') ThinkingSphinx::Deletion.perform index, 7 end it "doesn't care about Sphinx errors" do allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) expect { ThinkingSphinx::Deletion.perform index, 7 }.not_to raise_error end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/deltas/000077500000000000000000000000001341132130100222355ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/deltas/default_delta_spec.rb000066400000000000000000000070061341132130100263740ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Deltas::DefaultDelta do let(:delta) { ThinkingSphinx::Deltas::DefaultDelta.new adapter } let(:adapter) { double('adapter', :quoted_table_name => 'articles', :quote => 'delta') } describe '#clause' do context 'for a delta source' do before :each do allow(adapter).to receive_messages :boolean_value => 't' end it "limits results to those flagged as deltas" do expect(delta.clause(true)).to eq("articles.delta = t") end end end describe '#delete' do let(:connection) { double('connection', :execute => nil) } let(:index) { double('index', :name => 'foo_core', :document_id_for_instance => 14) } let(:instance) { double('instance', :id => 7) } before :each do allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) allow(Riddle::Query).to receive_messages :update => 'UPDATE STATEMENT' end it "updates the deleted flag to false" do expect(connection).to receive(:execute).with('UPDATE STATEMENT') delta.delete index, instance end it "builds the update query for the given index" do expect(Riddle::Query).to receive(:update). with('foo_core', anything, anything).and_return('') delta.delete index, instance end it "builds the update query for the sphinx document id" do expect(Riddle::Query).to receive(:update). with(anything, 14, anything).and_return('') delta.delete index, instance end it "builds the update query for setting sphinx_deleted to true" do expect(Riddle::Query).to receive(:update). with(anything, anything, :sphinx_deleted => true).and_return('') delta.delete index, instance end it "doesn't care about Sphinx errors" do allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) expect { delta.delete index, instance }.not_to raise_error end end describe '#index' do let(:config) { double('config', :controller => controller, :settings => {}) } let(:controller) { double('controller') } let(:commander) { double('commander', :call => true) } before :each do stub_const 'ThinkingSphinx::Commander', commander allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "indexes the given index" do expect(commander).to receive(:call).with( :index_sql, config, :indices => ['foo_delta'], :verbose => false ) delta.index double('index', :name => 'foo_delta') end end describe '#reset_query' do it "updates the table to set delta flags to false" do allow(adapter).to receive(:boolean_value) { |value| value ? 't' : 'f' } expect(delta.reset_query). to eq('UPDATE articles SET delta = f WHERE delta = t') end end describe '#toggle' do let(:instance) { double('instance') } it "sets instance's delta flag to true" do expect(instance).to receive(:delta=).with(true) delta.toggle(instance) end end describe '#toggled?' do let(:instance) { double('instance') } it "returns the delta flag value when true" do allow(instance).to receive_messages :delta? => true expect(delta.toggled?(instance)).to be_truthy end it "returns the delta flag value when false" do allow(instance).to receive_messages :delta? => false expect(delta.toggled?(instance)).to be_falsey end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/deltas_spec.rb000066400000000000000000000041751341132130100236030ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Deltas do describe '.processor_for' do it "returns the default processor class when given true" do expect(ThinkingSphinx::Deltas.processor_for(true)). to eq(ThinkingSphinx::Deltas::DefaultDelta) end it "returns the class when given one" do klass = Class.new expect(ThinkingSphinx::Deltas.processor_for(klass)).to eq(klass) end it "instantiates a class from the name as a string" do expect(ThinkingSphinx::Deltas. processor_for('ThinkingSphinx::Deltas::DefaultDelta')). to eq(ThinkingSphinx::Deltas::DefaultDelta) end end describe '.suspend' do let(:config) { double('config', :indices_for_references => [core_index, delta_index]) } let(:core_index) { double('index', :name => 'user_core', :delta_processor => processor, :delta? => false) } let(:delta_index) { double('index', :name => 'user_core', :delta_processor => processor, :delta? => true) } let(:processor) { double('processor', :index => true) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "executes the given block" do variable = :foo ThinkingSphinx::Deltas.suspend :user do variable = :bar end expect(variable).to eq(:bar) end it "suspends deltas within the block" do ThinkingSphinx::Deltas.suspend :user do expect(ThinkingSphinx::Deltas).to be_suspended end end it "removes the suspension after the block" do ThinkingSphinx::Deltas.suspend :user do # end expect(ThinkingSphinx::Deltas).not_to be_suspended end it "processes the delta indices for the given reference" do expect(processor).to receive(:index).with(delta_index) ThinkingSphinx::Deltas.suspend :user do # end end it "does not process the core indices for the given reference" do expect(processor).not_to receive(:index).with(core_index) ThinkingSphinx::Deltas.suspend :user do # end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/errors_spec.rb000066400000000000000000000071301341132130100236350ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::SphinxError do describe '.new_from_mysql' do let(:error) { double 'error', :message => 'index foo: unknown error', :backtrace => ['foo', 'bar'] } it "translates syntax errors" do allow(error).to receive_messages :message => 'index foo: syntax error: something is wrong' expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::SyntaxError) end it "translates parse errors" do allow(error).to receive_messages :message => 'index foo: parse error: something is wrong' expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). 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' expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::QueryError) end it "translates connection errors" do allow(error).to receive_messages :message => "Can't connect to MySQL server on '127.0.0.1' (61)" expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::ConnectionError) allow(error).to receive_messages :message => "Communications link failure" expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::ConnectionError) allow(error).to receive_messages :message => "Lost connection to MySQL server" expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::ConnectionError) end it 'translates out-of-bounds errors' do allow(error).to receive_messages :message => "offset out of bounds (offset=1001, max_matches=1000)" expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::OutOfBoundsError) end it 'prefixes the connection error message' do allow(error).to receive_messages :message => "Can't connect to MySQL server on '127.0.0.1' (61)" expect(ThinkingSphinx::SphinxError.new_from_mysql(error).message). to eq("Error connecting to Sphinx via the MySQL protocol. Can't connect to MySQL server on '127.0.0.1' (61)") end it "translates jdbc connection errors" do allow(error).to receive_messages :message => "Communications link failure" expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::ConnectionError) end it 'prefixes the jdbc connection error message' do allow(error).to receive_messages :message => "Communications link failure" expect(ThinkingSphinx::SphinxError.new_from_mysql(error).message). to eq("Error connecting to Sphinx via the MySQL protocol. Communications link failure") end it "defaults to sphinx errors" do allow(error).to receive_messages :message => 'index foo: unknown error: something is wrong' expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). to be_a(ThinkingSphinx::SphinxError) end it "keeps the original error's backtrace" do allow(error).to receive_messages :message => 'index foo: unknown error: something is wrong' expect(ThinkingSphinx::SphinxError.new_from_mysql(error). backtrace).to eq(error.backtrace) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/excerpter_spec.rb000066400000000000000000000032461341132130100243260ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Excerpter do let(:excerpter) { ThinkingSphinx::Excerpter.new('index', 'all words') } let(:connection) { double('connection', :execute => [{'snippet' => 'some highlighted words'}]) } before :each do allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) allow(Riddle::Query).to receive_messages :snippets => 'CALL SNIPPETS' end describe '#excerpt!' do it "generates a snippets call" do expect(Riddle::Query).to receive(:snippets). with('all of the words', 'index', 'all words', ThinkingSphinx::Excerpter::DefaultOptions). and_return('CALL SNIPPETS') excerpter.excerpt!('all of the words') end it "respects the provided options" do excerpter = ThinkingSphinx::Excerpter.new('index', 'all words', :before_match => '', :chunk_separator => ' -- ') expect(Riddle::Query).to receive(:snippets). with('all of the words', 'index', 'all words', :before_match => '', :after_match => '', :chunk_separator => ' -- '). and_return('CALL SNIPPETS') excerpter.excerpt!('all of the words') end it "sends the snippets call to Sphinx" do expect(connection).to receive(:execute).with('CALL SNIPPETS'). and_return([{'snippet' => ''}]) excerpter.excerpt!('all of the words') end it "returns the first value returned by Sphinx" do allow(connection).to receive_messages :execute => [{'snippet' => 'some highlighted words'}] expect(excerpter.excerpt!('all of the words')).to eq('some highlighted words') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/facet_search_spec.rb000066400000000000000000000075631341132130100247420ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::FacetSearch do let(:facet_search) { ThinkingSphinx::FacetSearch.new '', {} } let(:batch) { double('batch', :searches => [], :populate => true) } let(:index_set) { [] } let(:index) { double('index', :facets => [property_a, property_b], :name => 'foo_core') } let(:property_a) { double('property', :name => 'price_bracket', :multi? => false) } let(:property_b) { double('property', :name => 'category_id', :multi? => false) } let(:configuration) { double 'configuration', :settings => {}, :index_set_class => double(:new => index_set) } before :each do stub_const 'ThinkingSphinx::BatchedSearch', double(:new => batch) stub_const 'ThinkingSphinx::Search', DumbSearch stub_const 'ThinkingSphinx::Middlewares::RAW_ONLY', double stub_const 'ThinkingSphinx::Configuration', double(:instance => configuration) index_set << index << double('index', :facets => [], :name => 'bar_core') end DumbSearch = ::Struct.new(:query, :options) do def raw [{ 'sphinx_internal_class' => 'Foo', 'price_bracket' => 3, 'tag_ids' => '1,2', 'category_id' => 11, "sphinx_internal_count" => 5, "sphinx_internal_group" => 2 }] end end describe '#[]' do it "populates facet results" do expect(facet_search[:price_bracket]).to eq({3 => 5}) end end describe '#populate' do it "queries on each facet with a grouped search in a batch" do facet_search.populate expect(batch.searches.detect { |search| search.options[:group_by] == 'price_bracket' }).not_to be_nil end it "limits query for a facet to just indices that have that facet" do facet_search.populate expect(batch.searches.detect { |search| search.options[:indices] == ['foo_core'] }).not_to be_nil end it "limits facets to the specified set" do facet_search.options[:facets] = [:category_id] facet_search.populate expect(batch.searches.collect { |search| search.options[:group_by] }).to eq(['category_id']) end it "aliases the class facet from sphinx_internal_class" do allow(property_a).to receive_messages :name => 'sphinx_internal_class' facet_search.populate expect(facet_search[:class]).to eq({'Foo' => 5}) end it "uses the @groupby value for MVAs" do allow(property_a).to receive_messages :name => 'tag_ids', :multi? => true facet_search.populate expect(facet_search[:tag_ids]).to eq({2 => 5}) end [:max_matches, :limit].each do |setting| it "sets #{setting} in each search" do facet_search.populate batch.searches.each { |search| expect(search.options[setting]).to eq(1000) } end it "respects configured max_matches values for #{setting}" do configuration.settings['max_matches'] = 1234 facet_search.populate batch.searches.each { |search| expect(search.options[setting]).to eq(1234) } end end [:limit, :per_page].each do |setting| it "respects #{setting} option if set" do facet_search = ThinkingSphinx::FacetSearch.new '', {setting => 42} facet_search.populate batch.searches.each { |search| expect(search.options[setting]).to eq(42) } end it "allows separate #{setting} and max_matches settings to support pagination" do configuration.settings['max_matches'] = 500 facet_search = ThinkingSphinx::FacetSearch.new '', {setting => 10} facet_search.populate batch.searches.each do |search| expect(search.options[setting]).to eq(10) expect(search.options[:max_matches]).to eq(500) end end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/hooks/000077500000000000000000000000001341132130100221045ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/hooks/guard_presence_spec.rb000066400000000000000000000014201341132130100264260ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe ThinkingSphinx::Hooks::GuardPresence do let(:subject) do ThinkingSphinx::Hooks::GuardPresence.new configuration, stream end let(:configuration) { double "configuration", :indices_location => "/path" } let(:stream) { double "stream", :puts => nil } describe "#call" do it "outputs nothing if no guard files exist" do allow(Dir).to receive(:[]).with('/path/ts-*.tmp').and_return([]) expect(stream).not_to receive(:puts) subject.call end it "outputs a warning if a guard file exists" do allow(Dir).to receive(:[]).with('/path/ts-*.tmp'). and_return(['/path/ts-foo.tmp']) expect(stream).to receive(:puts) subject.call end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/index_set_spec.rb000066400000000000000000000073641341132130100243140ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx; end require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/module/delegation' require 'thinking_sphinx/index_set' describe ThinkingSphinx::IndexSet do let(:set) { ThinkingSphinx::IndexSet.new options, configuration } let(:configuration) { double('configuration', :preload_indices => true, :indices => []) } let(:ar_base) { double('ActiveRecord::Base') } let(:options) { {} } before :each do stub_const 'ActiveRecord::Base', ar_base end def class_double(name, methods = {}, *superclasses) klass = double 'class', :name => name, :class => Class allow(klass).to receive_messages( :ancestors => ([klass] + superclasses + [ar_base]), :inheritance_column => :type ) allow(klass).to receive_messages(methods) klass end describe '#to_a' do it "ensures the indices are loaded" do expect(configuration).to receive(:preload_indices) set.to_a end it "returns all non-distributed indices when no models or indices are specified" do article_core = double 'index', :name => 'article_core', :distributed? => false user_core = double 'index', :name => 'user_core', :distributed? => false distributed = double 'index', :name => 'user', :distributed? => true configuration.indices.replace [article_core, user_core, distributed] expect(set.to_a).to eq([article_core, user_core]) end it "uses indices for the given classes" do configuration.indices.replace [ double(:reference => :article, :distributed? => false), double(:reference => :opinion_article, :distributed? => false), double(:reference => :page, :distributed? => false) ] options[:classes] = [class_double('Article', :column_names => [])] expect(set.to_a.length).to eq(1) end 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) ] 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') configuration.indices.replace [article_core, user_core] options[:indices] = ['article_core'] expect(set.to_a).to eq([article_core]) end it "selects from the full index set those with matching references" do configuration.indices.replace [ double('index', :reference => :article, :distributed? => false), double('index', :reference => :book, :distributed? => false), double('index', :reference => :page, :distributed? => false) ] options[:references] = [:book, :article] expect(set.to_a.length).to eq(2) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/index_spec.rb000066400000000000000000000107171341132130100234350ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Index do let(:configuration) { Struct.new(:indices, :settings).new([], {}) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => configuration end describe '.define' do let(:index) { double('index', :definition_block= => nil) } context 'with ActiveRecord' do before :each do allow(ThinkingSphinx::ActiveRecord::Index).to receive_messages :new => index end it "creates an ActiveRecord index" do expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). with(:user, :with => :active_record).and_return index ThinkingSphinx::Index.define(:user, :with => :active_record) end it "returns the ActiveRecord index" do expect(ThinkingSphinx::Index.define(:user, :with => :active_record)). to eq([index]) end it "adds the index to the collection of indices" do ThinkingSphinx::Index.define(:user, :with => :active_record) expect(configuration.indices).to include(index) end it "sets the block in the index" do expect(index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :active_record) do indexes name end end context 'with a delta' do let(:delta_index) { double('delta index', :definition_block= => nil) } let(:processor) { double('delta processor') } before :each do allow(ThinkingSphinx::Deltas).to receive_messages :processor_for => processor allow(ThinkingSphinx::ActiveRecord::Index).to receive(:new). and_return(index, delta_index) end it "creates two indices with delta settings" do allow(ThinkingSphinx::ActiveRecord::Index).to receive(:new).and_call_original expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). with(:user, hash_including(:delta? => false, :delta_processor => processor) ).once. and_return index expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). with(:user, hash_including(:delta? => true, :delta_processor => processor) ).once. and_return delta_index ThinkingSphinx::Index.define :user, :with => :active_record, :delta => true end it "appends both indices to the collection" do ThinkingSphinx::Index.define :user, :with => :active_record, :delta => true expect(configuration.indices).to include(index) expect(configuration.indices).to include(delta_index) end it "sets the block in the index" do expect(index).to receive(:definition_block=).with instance_of(Proc) expect(delta_index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :active_record, :delta => true) do indexes name end end end end context 'with Real-Time' do before :each do allow(ThinkingSphinx::RealTime::Index).to receive_messages :new => index end it "creates a real-time index" do expect(ThinkingSphinx::RealTime::Index).to receive(:new). with(:user, :with => :real_time).and_return index ThinkingSphinx::Index.define(:user, :with => :real_time) end it "returns the ActiveRecord index" do expect(ThinkingSphinx::Index.define(:user, :with => :real_time)). to eq([index]) end it "adds the index to the collection of indices" do ThinkingSphinx::Index.define(:user, :with => :real_time) expect(configuration.indices).to include(index) end it "sets the block in the index" do expect(index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :real_time) do indexes name end end end end describe '#initialize' do it "is fine with no defaults from settings" do expect(ThinkingSphinx::Index.new(:user, {}).options).to eq({}) end it "respects defaults from settings" do configuration.settings['index_options'] = {'delta' => true} expect(ThinkingSphinx::Index.new(:user, {}).options).to eq({:delta => true}) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/interfaces/000077500000000000000000000000001341132130100231045ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/interfaces/daemon_spec.rb000066400000000000000000000031641341132130100257120ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Interfaces::Daemon do let(:configuration) { double 'configuration' } let(:stream) { double 'stream', :puts => true } let(:commander) { double :call => nil } let(:interface) { ThinkingSphinx::Interfaces::Daemon.new(configuration, {}, stream) } before :each do stub_const 'ThinkingSphinx::Commander', commander allow(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(false) end describe '#start' do it "starts the daemon" do expect(commander).to receive(:call).with( :start_detached, configuration, {}, stream ) interface.start end it "raises an error if the daemon is already running" do allow(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(true) expect { interface.start }.to raise_error(ThinkingSphinx::SphinxAlreadyRunning) end end describe '#status' do it "reports when the daemon is running" do allow(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(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(commander).to receive(:call). with(:running, configuration, {}, stream).and_return(false) expect(stream).to receive(:puts). with('The Sphinx daemon searchd is not currently running.') interface.status end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/interfaces/real_time_spec.rb000066400000000000000000000060231341132130100264050ustar00rootroot00000000000000# frozen_string_literal: true 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(:commander) { double :call => true } let(:stream) { double :puts => nil } before :each do stub_const "ThinkingSphinx::Commander", commander end 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] ) end it 'prepares the indices' do expect(commander).to receive(:call).with( :prepare, configuration, {}, stream ) interface.clear end it 'invokes the clear command' do expect(commander).to receive(:call).with( :clear_real_time, configuration, {:indices => [users_index, parts_index]}, stream ) interface.clear end context "with options[:index_names]" do let(:interface) { ThinkingSphinx::Interfaces::RealTime.new( configuration, {:index_names => ['users']}, stream ) } it "removes each file for real-time indices that match :index_filter" do expect(commander).to receive(:call).with( :clear_real_time, configuration, {:index_names => ['users'], :indices => [users_index]}, stream ) 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] ) end it 'invokes the index command with real-time indices' do expect(commander).to receive(:call).with( :index_real_time, configuration, {:indices => [users_index, parts_index]}, stream ) interface.index end context "with options[:index_names]" do let(:interface) { ThinkingSphinx::Interfaces::RealTime.new( configuration, {:index_names => ['users']}, stream ) } it 'invokes the index command for matching indices' do expect(commander).to receive(:call).with( :index_real_time, configuration, {:index_names => ['users'], :indices => [users_index]}, stream ) interface.index end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/interfaces/sql_spec.rb000066400000000000000000000065231341132130100252500ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::Interfaces::SQL do let(:interface) { ThinkingSphinx::Interfaces::SQL.new( configuration, {:verbose => true}, stream ) } let(:commander) { double :call => true } let(:configuration) { double 'configuration', :preload_indices => true, :render => true, :indices => [double(:index, :type => 'plain')] } let(:stream) { double :puts => nil } before :each do stub_const 'ThinkingSphinx::Commander', commander end describe '#clear' do let(:users_index) { double(:type => 'plain') } let(:parts_index) { double(:type => 'plain') } let(:rt_index) { double(:type => 'rt') } before :each do allow(configuration).to receive(:indices). and_return([users_index, parts_index, rt_index]) end it "invokes the clear_sql command" do expect(commander).to receive(:call).with( :clear_sql, configuration, {:verbose => true, :indices => [users_index, parts_index]}, stream ) interface.clear end end describe '#index' do it "invokes the prepare command" do expect(commander).to receive(:call).with( :prepare, configuration, {:verbose => true}, stream ) interface.index end it "renders the configuration to a file by default" do expect(commander).to receive(:call).with( :configure, configuration, {:verbose => true}, stream ) interface.index end it "does not render the configuration if requested" do expect(commander).not_to receive(:call).with( :configure, configuration, {:verbose => true}, stream ) interface.index false end it "executes the index command" do expect(commander).to receive(:call).with( :index_sql, configuration, {:verbose => true, :indices => nil}, stream ) interface.index end context "with options[:index_names]" do let(:users_index) { double(:name => 'users', :type => 'plain') } let(:parts_index) { double(:name => 'parts', :type => 'plain') } let(:rt_index) { double(:type => 'rt') } let(:interface) { ThinkingSphinx::Interfaces::SQL.new( configuration, {:index_names => ['users']}, stream ) } before :each do allow(configuration).to receive(:indices). and_return([users_index, parts_index, rt_index]) end it 'invokes the index command for matching indices' do expect(commander).to receive(:call).with( :index_sql, configuration, {:index_names => ['users'], :indices => ['users']}, stream ) interface.index end end end describe '#merge' do it "invokes the merge command" do expect(commander).to receive(:call).with( :merge_and_update, configuration, {:verbose => true}, stream ) interface.merge end context "with options[:index_names]" do let(:interface) { ThinkingSphinx::Interfaces::SQL.new( configuration, {:index_names => ['users']}, stream ) } it 'invokes the merge command with the index_names option' do expect(commander).to receive(:call).with( :merge_and_update, configuration, {:index_names => ['users']}, stream ) interface.merge end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/masks/000077500000000000000000000000001341132130100220775ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/masks/pagination_mask_spec.rb000066400000000000000000000055351341132130100266120ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Masks; end end require 'active_support/core_ext/object/blank' require 'thinking_sphinx/masks/pagination_mask' describe ThinkingSphinx::Masks::PaginationMask do let(:search) { double('search', :options => {}, :meta => {}, :per_page => 20, :current_page => 1) } let(:mask) { ThinkingSphinx::Masks::PaginationMask.new search } describe '#first_page?' do it "returns true when on the first page" do expect(mask).to be_first_page end it "returns false on other pages" do allow(search).to receive_messages :current_page => 2 expect(mask).not_to be_first_page end end describe '#last_page?' do before :each do search.meta['total'] = '44' end it "is true when there's no more pages" do allow(search).to receive_messages :current_page => 3 expect(mask).to be_last_page end it "is false when there's still more pages" do expect(mask).not_to be_last_page end end describe '#next_page' do before :each do search.meta['total'] = '44' end it "should return one more than the current page" do expect(mask.next_page).to eq(2) end it "should return nil if on the last page" do allow(search).to receive_messages :current_page => 3 expect(mask.next_page).to be_nil end end describe '#next_page?' do before :each do search.meta['total'] = '44' end it "is true when there is a second page" do expect(mask.next_page?).to be_truthy end it "is false when there's no more pages" do allow(search).to receive_messages :current_page => 3 expect(mask.next_page?).to be_falsey end end describe '#previous_page' do before :each do search.meta['total'] = '44' end it "should return one less than the current page" do allow(search).to receive_messages :current_page => 2 expect(mask.previous_page).to eq(1) end it "should return nil if on the first page" do expect(mask.previous_page).to be_nil end end describe '#total_entries' do before :each do search.meta['total_found'] = '12' end it "returns the total found from the search request metadata" do expect(mask.total_entries).to eq(12) end end describe '#total_pages' do before :each do search.meta['total'] = '40' search.meta['total_found'] = '44' end it "uses the total available from the search request metadata" do expect(mask.total_pages).to eq(2) end it "should allow for custom per_page values" do allow(search).to receive_messages :per_page => 40 expect(mask.total_pages).to eq(1) end it "should return 0 if there is no index and therefore no results" do search.meta.clear expect(mask.total_pages).to eq(0) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/masks/scopes_mask_spec.rb000066400000000000000000000067151341132130100257560ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Masks; end end require 'thinking_sphinx/masks/scopes_mask' describe ThinkingSphinx::Masks::ScopesMask do let(:search) { double('search', :options => {}, :per_page => 20, :populated? => false) } let(:mask) { ThinkingSphinx::Masks::ScopesMask.new search } before :each do allow(FileUtils).to receive_messages :mkdir_p => true end describe '#search' do it "replaces the query if one is supplied" do expect(search).to receive(:query=).with('bar') mask.search('bar') end it "keeps the existing query when only options are offered" do expect(search).not_to receive(:query=) mask.search :with => {:foo => :bar} end it "merges conditions" do search.options[:conditions] = {:foo => 'bar'} mask.search :conditions => {:baz => 'qux'} expect(search.options[:conditions]).to eq({:foo => 'bar', :baz => 'qux'}) end it "merges filters" do search.options[:with] = {:foo => :bar} mask.search :with => {:baz => :qux} expect(search.options[:with]).to eq({:foo => :bar, :baz => :qux}) end it "merges exclusive filters" do search.options[:without] = {:foo => :bar} mask.search :without => {:baz => :qux} expect(search.options[:without]).to eq({:foo => :bar, :baz => :qux}) end it "appends excluded ids" do search.options[:without_ids] = [1, 3] mask.search :without_ids => [5, 7] expect(search.options[:without_ids]).to eq([1, 3, 5, 7]) end it "replaces the retry_stale option" do search.options[:retry_stale] = true mask.search :retry_stale => 6 expect(search.options[:retry_stale]).to eq(6) end it "returns the original search object" do expect(mask.search.object_id).to eq(search.object_id) end end describe '#search_for_ids' do it "replaces the query if one is supplied" do expect(search).to receive(:query=).with('bar') mask.search_for_ids('bar') end it "keeps the existing query when only options are offered" do expect(search).not_to receive(:query=) mask.search_for_ids :with => {:foo => :bar} end it "merges conditions" do search.options[:conditions] = {:foo => 'bar'} mask.search_for_ids :conditions => {:baz => 'qux'} expect(search.options[:conditions]).to eq({:foo => 'bar', :baz => 'qux'}) end it "merges filters" do search.options[:with] = {:foo => :bar} mask.search_for_ids :with => {:baz => :qux} expect(search.options[:with]).to eq({:foo => :bar, :baz => :qux}) end it "merges exclusive filters" do search.options[:without] = {:foo => :bar} mask.search_for_ids :without => {:baz => :qux} expect(search.options[:without]).to eq({:foo => :bar, :baz => :qux}) end it "appends excluded ids" do search.options[:without_ids] = [1, 3] mask.search_for_ids :without_ids => [5, 7] expect(search.options[:without_ids]).to eq([1, 3, 5, 7]) end it "replaces the retry_stale option" do search.options[:retry_stale] = true mask.search_for_ids :retry_stale => 6 expect(search.options[:retry_stale]).to eq(6) end it "adds the ids_only option" do mask.search_for_ids expect(search.options[:ids_only]).to be_truthy end it "returns the original search object" do expect(mask.search_for_ids.object_id).to eq(search.object_id) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/000077500000000000000000000000001341132130100232615ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb000066400000000000000000000141471341132130100317110ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end class Search; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/active_record_translator' require 'thinking_sphinx/search/stale_ids_exception' describe ThinkingSphinx::Middlewares::ActiveRecordTranslator do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::ActiveRecordTranslator.new app } let(:context) { {:raw => [], :results => [] } } let(:model) { double('model', :primary_key => :id) } let(:search) { double('search', :options => {}) } let(:configuration) { double('configuration', :settings => {:primary_key => :id}) } def raw_result(id, model_name) {'sphinx_internal_id' => id, 'sphinx_internal_class' => model_name} end describe '#call' do before :each do allow(context).to receive_messages :search => search allow(context).to receive_messages :configuration => configuration allow(model).to receive_messages :unscoped => model end it "translates records to ActiveRecord objects" do model_name = double('article', :constantize => model) instance = double('instance', :id => 24) allow(model).to receive_messages :where => [instance] context[:results] << raw_result(24, model_name) middleware.call [context] expect(context[:results]).to eq([instance]) end it "only queries the model once for the given search results" do model_name = double('article', :constantize => model) instance_a = double('instance', :id => 24) instance_b = double('instance', :id => 42) context[:results] << raw_result(24, model_name) context[:results] << raw_result(42, model_name) expect(model).to receive(:where).once.and_return([instance_a, instance_b]) middleware.call [context] end it "handles multiple models" do article_model = double('article model', :primary_key => :id) article_name = double('article name', :constantize => article_model) article = double('article instance', :id => 24) user_model = double('user model', :primary_key => :id) user_name = double('user name', :constantize => user_model) user = double('user instance', :id => 12) allow(article_model).to receive_messages :unscoped => article_model allow(user_model).to receive_messages :unscoped => user_model context[:results] << raw_result(24, article_name) context[:results] << raw_result(12, user_name) expect(article_model).to receive(:where).once.and_return([article]) expect(user_model).to receive(:where).once.and_return([user]) middleware.call [context] end it "sorts the results according to Sphinx order, not database order" do model_name = double('article', :constantize => model) instance_1 = double('instance 1', :id => 1) instance_2 = double('instance 2', :id => 2) context[:results] << raw_result(2, model_name) context[:results] << raw_result(1, model_name) allow(model).to receive_messages(:where => [instance_1, instance_2]) middleware.call [context] expect(context[:results]).to eq([instance_2, instance_1]) end it "returns objects in database order if a SQL order clause is supplied" do model_name = double('article', :constantize => model) instance_1 = double('instance 1', :id => 1) instance_2 = double('instance 2', :id => 2) context[:results] << raw_result(2, model_name) context[:results] << raw_result(1, model_name) allow(model).to receive_messages(:order => model, :where => [instance_1, instance_2]) search.options[:sql] = {:order => 'name DESC'} middleware.call [context] expect(context[:results]).to eq([instance_1, instance_2]) end it "handles model without primary key" do no_primary_key_model = double('no primary key model') allow(no_primary_key_model).to receive_messages :unscoped => no_primary_key_model model_name = double('article', :constantize => no_primary_key_model) instance = double('instance', :id => 1) allow(no_primary_key_model).to receive_messages :where => [instance] context[:results] << raw_result(1, model_name) middleware.call [context] end context 'SQL options' do let(:relation) { double('relation', :where => []) } let(:model_name) { double('article', :constantize => model) } before :each do allow(model).to receive_messages :unscoped => relation context[:results] << raw_result(1, model_name) end it "passes through SQL include options to the relation" do search.options[:sql] = {:include => :association} expect(relation).to receive(:includes).with(:association). and_return(relation) middleware.call [context] end it "passes through SQL join options to the relation" do search.options[:sql] = {:joins => :association} expect(relation).to receive(:joins).with(:association).and_return(relation) middleware.call [context] end it "passes through SQL order options to the relation" do search.options[:sql] = {:order => 'name DESC'} expect(relation).to receive(:order).with('name DESC').and_return(relation) middleware.call [context] end it "passes through SQL select options to the relation" do search.options[:sql] = {:select => :column} expect(relation).to receive(:select).with(:column).and_return(relation) middleware.call [context] end it "passes through SQL group options to the relation" do search.options[:sql] = {:group => :column} expect(relation).to receive(:group).with(:column).and_return(relation) middleware.call [context] end it "passes through SQL options defined by model to the relation" do search.options[:sql] = {model_name => {:joins => :association}} expect(relation).to receive(:joins).with(:association).and_return(relation) middleware.call [context] end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/geographer_spec.rb000066400000000000000000000061161341132130100267470ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/geographer' require 'thinking_sphinx/float_formatter' describe ThinkingSphinx::Middlewares::Geographer do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::Geographer.new app } let(:context) { {:sphinxql => sphinx_sql, :indices => [], :panes => []} } let(:sphinx_sql) { double('sphinx_sql') } let(:search) { double('search', :options => {}) } before :each do stub_const 'ThinkingSphinx::Panes::DistancePane', double allow(context).to receive_messages :search => search end describe '#call' do context 'no geodistance location provided' do before :each do search.options[:geo] = nil end it "doesn't add anything if :geo is nil" do expect(sphinx_sql).not_to receive(:prepend_values) middleware.call [context] end end context 'geodistance location provided' do before :each do search.options[:geo] = [0.1, 0.2] end it "adds the geodist function when given a :geo option" do expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, lat, lng) AS geodist'). and_return(sphinx_sql) middleware.call [context] end it "adds the distance pane" do allow(sphinx_sql).to receive_messages :prepend_values => sphinx_sql middleware.call [context] expect(context[:panes]).to include(ThinkingSphinx::Panes::DistancePane) end it "respects :latitude_attr and :longitude_attr options" do search.options[:latitude_attr] = 'side_to_side' search.options[:longitude_attr] = 'up_or_down' expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, side_to_side, up_or_down) AS geodist'). and_return(sphinx_sql) middleware.call [context] end it "uses latitude if any index has that but not lat as an attribute" do context[:indices] << double('index', :unique_attribute_names => ['latitude'], :name => 'an_index') expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, latitude, lng) AS geodist'). and_return(sphinx_sql) middleware.call [context] end it "uses latitude if any index has that but not lat as an attribute" do context[:indices] << double('index', :unique_attribute_names => ['longitude'], :name => 'an_index') expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, lat, longitude) AS geodist'). and_return(sphinx_sql) middleware.call [context] end it "handles very small values" do search.options[:geo] = [0.0000001, 0.00000000002] expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.0000001, 0.00000000002, lat, lng) AS geodist'). and_return(sphinx_sql) middleware.call [context] end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/glazier_spec.rb000066400000000000000000000035131341132130100262570ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/glazier' describe ThinkingSphinx::Middlewares::Glazier do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::Glazier.new app } let(:context) { {:results => [result], :indices => [index], :meta => {}, :raw => [raw_result], :panes => []} } let(:result) { double('result', :id => 10, :class => double(:name => 'Article')) } let(:index) { double('index', :name => 'foo_core') } let(:search) { double('search', :options => {}) } let(:glazed_result) { double('glazed result') } let(:raw_result) { {'sphinx_internal_class' => 'Article', 'sphinx_internal_id' => 10} } describe '#call' do before :each do stub_const 'ThinkingSphinx::Search::Glaze', double(:new => glazed_result) allow(context).to receive_messages :search => search end context 'No panes provided' do before :each do context[:panes].clear end it "leaves the results as they are" do middleware.call [context] expect(context[:results]).to eq([result]) end end context 'Panes provided' do let(:pane_class) { double('pane class') } before :each do context[:panes] << pane_class end it "replaces each result with a glazed version" do middleware.call [context] expect(context[:results]).to eq([glazed_result]) end it "creates a glazed result for each result" do expect(ThinkingSphinx::Search::Glaze).to receive(:new). with(context, result, raw_result, [pane_class]). and_return(glazed_result) middleware.call [context] end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/inquirer_spec.rb000066400000000000000000000040731341132130100264620ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/inquirer' describe ThinkingSphinx::Middlewares::Inquirer do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::Inquirer.new app } let(:context) { {:sphinxql => sphinx_sql} } let(:sphinx_sql) { double('sphinx_sql', :to_sql => 'SELECT * FROM index') } let(:batch_inquirer) { double('batcher', :append_query => true, :results => [[:raw], [{'Variable_name' => 'meta', 'Value' => 'value'}]]) } before :each do batch_class = double allow(batch_class).to receive(:new).and_return(batch_inquirer) stub_const 'Riddle::Query', double(:meta => 'SHOW META') stub_const 'ThinkingSphinx::Search::BatchInquirer', batch_class end describe '#call' do it "passes through the SphinxQL from a Riddle::Query::Select object" do expect(batch_inquirer).to receive(:append_query).with('SELECT * FROM index') expect(batch_inquirer).to receive(:append_query).with('SHOW META') middleware.call [context] end it "sets up the raw results" do middleware.call [context] expect(context[:raw]).to eq([:raw]) end it "sets up the meta results as a hash" do middleware.call [context] expect(context[:meta]).to eq({'meta' => 'value'}) end it "uses the raw values as the initial results" do middleware.call [context] expect(context[:results]).to eq([:raw]) end context "with mysql2 result" do class FakeResult include Enumerable def each; [{"fake" => "value"}].each { |m| yield m }; end end let(:batch_inquirer) { double('batcher', :append_query => true, :results => [ FakeResult.new, [{'Variable_name' => 'meta', 'Value' => 'value'}] ]) } it "converts the results into an array" do middleware.call [context] expect(context[:results]).to be_a Array end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/sphinxql_spec.rb000066400000000000000000000313711341132130100264730ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end end module ActiveRecord class Base; end end class SphinxQLSubclass end require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/inflections' require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/sphinxql' require 'thinking_sphinx/errors' describe ThinkingSphinx::Middlewares::SphinxQL do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::SphinxQL.new app } let(:context) { {} } let(:search) { double('search', :query => '', :options => {}, :offset => 0, :per_page => 5) } let(:index_set) { [double(:name => 'article_core', :options => {})] } let(:sphinx_sql) { double('sphinx_sql', :from => true, :offset => true, :limit => true, :where => true, :matching => true, :values => true) } let(:query) { double('query') } let(:configuration) { double('configuration', :settings => {}, index_set_class: set_class) } let(:set_class) { double(:new => index_set) } before :each do stub_const 'Riddle::Query::Select', double(:new => sphinx_sql) stub_const 'ThinkingSphinx::Search::Query', double(:new => query) allow(context).to receive_messages :search => search, :configuration => configuration end describe '#call' do it "uses the indexes for the FROM clause" do index_set.replace [ double('index', :name => 'article_core', :options => {}), double('index', :name => 'user_core', :options => {}) ] expect(sphinx_sql).to receive(:from).with('`article_core`', '`user_core`'). and_return(sphinx_sql) middleware.call [context] end it "finds index objects for the given models and indices options" do klass = double(:column_names => [], :inheritance_column => 'type', :name => 'User') search.options[:classes] = [klass] search.options[:indices] = ['user_core'] allow(index_set.first).to receive_messages :reference => :user expect(set_class).to receive(:new). with(:classes => [klass], :indices => ['user_core']). and_return(index_set) middleware.call [context] end it "raises an exception if there's no matching indices" do index_set.clear expect { middleware.call [context] }.to raise_error(ThinkingSphinx::NoIndicesError) end it "generates a Sphinx query from the provided keyword and conditions" do allow(search).to receive_messages :query => 'tasty' search.options[:conditions] = {:title => 'pancakes'} expect(ThinkingSphinx::Search::Query).to receive(:new). with('tasty', {:title => 'pancakes'}, anything).and_return(query) middleware.call [context] end it "matches on the generated query" do allow(query).to receive_messages :to_s => 'waffles' expect(sphinx_sql).to receive(:matching).with('waffles') middleware.call [context] end it "requests a starred query if the :star option is set to true" do search.options[:star] = true expect(ThinkingSphinx::Search::Query).to receive(:new). with(anything, anything, true).and_return(query) middleware.call [context] end it "doesn't append a field condition by default" do expect(ThinkingSphinx::Search::Query).to receive(:new) do |query, conditions, star| expect(conditions[:sphinx_internal_class_name]).to be_nil query end middleware.call [context] end it "doesn't append a field condition if all classes match index references" do model = double('model', :connection => double, :ancestors => [ActiveRecord::Base], :name => 'Animal') allow(index_set.first).to receive_messages :reference => :animal search.options[:classes] = [model] expect(ThinkingSphinx::Search::Query).to receive(:new) do |query, conditions, star| expect(conditions[:sphinx_internal_class_name]).to be_nil query end middleware.call [context] end it "appends field conditions for the class when searching on subclasses" do db_connection = double('db connection', :select_values => [], :schema_cache => double('cache', :table_exists? => false)) supermodel = Class.new(ActiveRecord::Base) do def self.name; 'Cat'; end def self.inheritance_column; 'type'; end end allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] allow(index_set.first).to receive_messages :reference => :cat search.options[:classes] = [submodel] expect(ThinkingSphinx::Search::Query).to receive(:new).with(anything, hash_including(:sphinx_internal_class_name => '(Lion)'), anything). and_return(query) middleware.call [context] end it "quotes namespaced models in the class name condition" do db_connection = double('db connection', :select_values => [], :schema_cache => double('cache', :table_exists? => false)) supermodel = Class.new(ActiveRecord::Base) do def self.name; 'Animals::Cat'; end def self.inheritance_column; 'type'; end end allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Animals::Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] allow(index_set.first).to receive_messages :reference => :"animals/cat" search.options[:classes] = [submodel] expect(ThinkingSphinx::Search::Query).to receive(:new).with(anything, hash_including(:sphinx_internal_class_name => '("Animals::Lion")'), anything). and_return(query) middleware.call [context] end it "does not query the database for subclasses if :skip_sti is set to true" do model = double('model', :connection => double, :ancestors => [ActiveRecord::Base], :name => 'Animal') allow(index_set.first).to receive_messages :reference => :animal search.options[:classes] = [model] search.options[:skip_sti] = true expect(model.connection).not_to receive(:select_values) middleware.call [context] end it "ignores blank subclasses" do db_connection = double('db connection', :select_values => [''], :schema_cache => double('cache', :table_exists? => false)) supermodel = Class.new(ActiveRecord::Base) do def self.name; 'Cat'; end def self.inheritance_column; 'type'; end end allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] allow(index_set.first).to receive_messages :reference => :cat search.options[:classes] = [submodel] expect { middleware.call [context] }.to_not raise_error end it "filters out deleted values by default" do expect(sphinx_sql).to receive(:where).with(:sphinx_deleted => false). and_return(sphinx_sql) middleware.call [context] end it "appends boolean attribute filters to the query" do search.options[:with] = {:visible => true} expect(sphinx_sql).to receive(:where).with(hash_including(:visible => true)). and_return(sphinx_sql) middleware.call [context] end it "appends exclusive filters to the query" do search.options[:without] = {:tag_ids => [2, 4, 8]} expect(sphinx_sql).to receive(:where_not). with(hash_including(:tag_ids => [2, 4, 8])).and_return(sphinx_sql) middleware.call [context] end it "appends the without_ids option as an exclusive filter" do search.options[:without_ids] = [1, 4, 9] expect(sphinx_sql).to receive(:where_not). with(hash_including(:sphinx_internal_id => [1, 4, 9])). and_return(sphinx_sql) middleware.call [context] end it "appends MVA matches with all values" do search.options[:with_all] = {:tag_ids => [1, 7]} expect(sphinx_sql).to receive(:where_all). with(:tag_ids => [1, 7]).and_return(sphinx_sql) middleware.call [context] end it "appends MVA matches without all of the given values" do search.options[:without_all] = {:tag_ids => [1, 7]} expect(sphinx_sql).to receive(:where_not_all). with(:tag_ids => [1, 7]).and_return(sphinx_sql) middleware.call [context] end it "appends order clauses to the query" do search.options[:order] = 'created_at ASC' expect(sphinx_sql).to receive(:order_by).with('created_at ASC'). and_return(sphinx_sql) middleware.call [context] end it "presumes attributes given as symbols should be sorted ascendingly" do search.options[:order] = :updated_at expect(sphinx_sql).to receive(:order_by).with('updated_at ASC'). and_return(sphinx_sql) middleware.call [context] end it "appends a group by clause to the query" do search.options[:group_by] = :foreign_id allow(search).to receive_messages :masks => [] allow(sphinx_sql).to receive_messages :values => sphinx_sql expect(sphinx_sql).to receive(:group_by).with('foreign_id'). and_return(sphinx_sql) middleware.call [context] end it "appends a sort within group clause to the query" do search.options[:order_group_by] = :title expect(sphinx_sql).to receive(:order_within_group_by).with('title ASC'). and_return(sphinx_sql) middleware.call [context] end it "uses the provided offset" do allow(search).to receive_messages :offset => 50 expect(sphinx_sql).to receive(:offset).with(50).and_return(sphinx_sql) middleware.call [context] end it "uses the provided limit" do allow(search).to receive_messages :per_page => 24 expect(sphinx_sql).to receive(:limit).with(24).and_return(sphinx_sql) middleware.call [context] end it "adds the provided select statement" do search.options[:select] = 'foo as bar' expect(sphinx_sql).to receive(:values).with('foo as bar'). and_return(sphinx_sql) middleware.call [context] end it "adds the provided group-best count" do search.options[:group_best] = 5 expect(sphinx_sql).to receive(:group_best).with(5).and_return(sphinx_sql) middleware.call [context] end it "adds the provided having clause" do search.options[:having] = 'foo > 1' expect(sphinx_sql).to receive(:having).with('foo > 1').and_return(sphinx_sql) middleware.call [context] end it "uses any provided field weights" do search.options[:field_weights] = {:title => 3} expect(sphinx_sql).to receive(:with_options) do |options| expect(options[:field_weights]).to eq({:title => 3}) sphinx_sql end middleware.call [context] end it "uses index-defined field weights if they're available" do index_set.first.options[:field_weights] = {:title => 3} expect(sphinx_sql).to receive(:with_options).with( hash_including(:field_weights => {:title => 3}) ).and_return(sphinx_sql) middleware.call [context] end it "uses index-defined max matches if it's available" do index_set.first.options[:max_matches] = 100 expect(sphinx_sql).to receive(:with_options).with( hash_including(:max_matches => 100) ).and_return(sphinx_sql) middleware.call [context] end it "uses configuration-level max matches if set" do configuration.settings['max_matches'] = 120 expect(sphinx_sql).to receive(:with_options).with( hash_including(:max_matches => 120) ).and_return(sphinx_sql) middleware.call [context] end it "uses any given ranker option" do search.options[:ranker] = 'proximity' expect(sphinx_sql).to receive(:with_options) do |options| expect(options[:ranker]).to eq('proximity') sphinx_sql end middleware.call [context] end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb000066400000000000000000000027331341132130100300750ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end class Search; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/stale_id_checker' require 'thinking_sphinx/search/stale_ids_exception' describe ThinkingSphinx::Middlewares::StaleIdChecker do let(:app) { double('app') } let(:middleware) { ThinkingSphinx::Middlewares::StaleIdChecker.new app } let(:context) { {:raw => [], :results => []} } let(:model) { double('model') } def raw_result(id, model_name) {'sphinx_internal_id' => id, 'sphinx_internal_class' => model_name} end describe '#call' do it 'passes the call on if there are no nil results' do context[:raw] << raw_result(24, 'Article') context[:raw] << raw_result(42, 'Article') context[:results] << double('instance', :id => 24) context[:results] << double('instance', :id => 42) expect(app).to receive(:call) middleware.call [context] end it "raises a stale id exception if ActiveRecord doesn't return ids" do context[:raw] << raw_result(24, 'Article') context[:raw] << raw_result(42, 'Article') context[:results] << double('instance', :id => 24) context[:results] << nil expect { middleware.call [context] }.to raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| expect(err.ids).to eq([42]) expect(err.context).to eq(context) } end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb000066400000000000000000000066011341132130100277540ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Middlewares; end class Search; end end require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/stale_id_filter' require 'thinking_sphinx/search/stale_ids_exception' describe ThinkingSphinx::Middlewares::StaleIdFilter do let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::StaleIdFilter.new app } let(:context) { {:raw => [], :results => []} } let(:search) { double('search', :options => {}) } describe '#call' do before :each do allow(context).to receive_messages :search => search end context 'one stale ids exception' do before :each do allow(app).to receive(:call) do @calls ||= 0 @calls += 1 raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 end end it "appends the ids to the without_ids filter" do middleware.call [context] expect(search.options[:without_ids]).to eq([12]) end it "respects existing without_ids filters" do search.options[:without_ids] = [11] middleware.call [context] expect(search.options[:without_ids]).to eq([11, 12]) end end context 'two stale ids exceptions' do before :each do allow(app).to receive(:call) do @calls ||= 0 @calls += 1 raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 raise ThinkingSphinx::Search::StaleIdsException.new([13], context) if @calls == 2 end end it "appends the ids to the without_ids filter" do middleware.call [context] expect(search.options[:without_ids]).to eq([12, 13]) end it "respects existing without_ids filters" do search.options[:without_ids] = [11] middleware.call [context] expect(search.options[:without_ids]).to eq([11, 12, 13]) end end context 'three stale ids exceptions' do before :each do allow(app).to receive(:call) do @calls ||= 0 @calls += 1 raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 raise ThinkingSphinx::Search::StaleIdsException.new([13], context) if @calls == 2 raise ThinkingSphinx::Search::StaleIdsException.new([14], context) if @calls == 3 end end it "raises the final stale ids exceptions" do expect { middleware.call [context] }.to raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| expect(err.ids).to eq([14]) } end end context 'stale ids exceptions with multiple contexts' do let(:context2) { {:raw => [], :results => []} } let(:search2) { double('search2', :options => {}) } before :each do allow(context2).to receive_messages :search => search2 allow(app).to receive(:call) do @calls ||= 0 @calls += 1 raise ThinkingSphinx::Search::StaleIdsException.new([12], context2) if @calls == 1 end end it "appends the ids to the without_ids filter in the correct context" do middleware.call [context, context2] expect(search.options[:without_ids]).to eq(nil) expect(search2.options[:without_ids]).to eq([12]) end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/middlewares/valid_options_spec.rb000066400000000000000000000022451341132130100274750ustar00rootroot00000000000000# frozen_string_literal: true 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(:caution, "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 thinking-sphinx-4.1.0/spec/thinking_sphinx/panes/000077500000000000000000000000001341132130100220675ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/panes/attributes_pane_spec.rb000066400000000000000000000010571341132130100266220ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Panes; end end require 'thinking_sphinx/panes/attributes_pane' describe ThinkingSphinx::Panes::AttributesPane do let(:pane) { ThinkingSphinx::Panes::AttributesPane.new context, object, raw } let(:context) { double('context') } let(:object) { double('object') } let(:raw) { {} } describe '#sphinx_attributes' do it "returns the object's sphinx attributes by default" do raw['foo'] = 24 expect(pane.sphinx_attributes).to eq({'foo' => 24}) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/panes/distance_pane_spec.rb000066400000000000000000000017351341132130100262310ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Panes; end end require 'thinking_sphinx/panes/distance_pane' describe ThinkingSphinx::Panes::DistancePane do let(:pane) { ThinkingSphinx::Panes::DistancePane.new context, object, raw } let(:context) { double('context') } let(:object) { double('object') } let(:raw) { {} } describe '#distance' do it "returns the object's geodistance attribute by default" do raw['geodist'] = 123.45 expect(pane.distance).to eq(123.45) end it "converts string geodistances to floats" do raw['geodist'] = '123.450' expect(pane.distance).to eq(123.45) end end describe '#geodist' do it "returns the object's geodistance attribute by default" do raw['geodist'] = 123.45 expect(pane.geodist).to eq(123.45) end it "converts string geodistances to floats" do raw['geodist'] = '123.450' expect(pane.geodist).to eq(123.45) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/panes/excerpts_pane_spec.rb000066400000000000000000000031071341132130100262670ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Panes; end end require 'thinking_sphinx/panes/excerpts_pane' describe ThinkingSphinx::Panes::ExcerptsPane do let(:pane) { ThinkingSphinx::Panes::ExcerptsPane.new context, object, raw } let(:context) { {:indices => [double(:name => 'foo_core')]} } let(:object) { double('object') } let(:raw) { {} } let(:search) { double('search', :query => 'foo', :options => {}) } before :each do allow(context).to receive_messages :search => search end describe '#excerpts' do let(:excerpter) { double('excerpter') } let(:excerpts) { double('excerpts object') } before :each do stub_const 'ThinkingSphinx::Excerpter', double(:new => excerpter) allow(ThinkingSphinx::Panes::ExcerptsPane::Excerpts).to receive_messages :new => excerpts end it "returns an excerpt glazing" do expect(pane.excerpts).to eq(excerpts) end it "creates an excerpter with the first index and the query and conditions values" do context[:indices] = [double(:name => 'alpha'), double(:name => 'beta')] context.search.options[:conditions] = {:baz => 'bar'} expect(ThinkingSphinx::Excerpter).to receive(:new). with('alpha', 'foo bar', anything).and_return(excerpter) pane.excerpts end it "passes through excerpts options" do search.options[:excerpts] = {:before_match => 'foo'} expect(ThinkingSphinx::Excerpter).to receive(:new). with(anything, anything, :before_match => 'foo').and_return(excerpter) pane.excerpts end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/panes/weight_pane_spec.rb000066400000000000000000000007721341132130100257260ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx module Panes; end end require 'thinking_sphinx/panes/weight_pane' describe ThinkingSphinx::Panes::WeightPane do let(:pane) { ThinkingSphinx::Panes::WeightPane.new context, object, raw } let(:context) { double('context') } let(:object) { double('object') } let(:raw) { {} } describe '#weight' do it "returns the object's weight by default" do raw["weight()"] = 101 expect(pane.weight).to eq(101) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/rake_interface_spec.rb000066400000000000000000000016551341132130100252710ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RakeInterface do let(:interface) { ThinkingSphinx::RakeInterface.new } let(:commander) { double :call => nil } before :each do stub_const 'ThinkingSphinx::Commander', commander end describe '#configure' do it 'sends the configure command' do expect(commander).to receive(:call). with(:configure, anything, {:verbose => true}) interface.configure end end describe '#daemon' do it 'returns a daemon interface' do expect(interface.daemon.class).to eq(ThinkingSphinx::Interfaces::Daemon) end end describe '#rt' do it 'returns a real-time interface' do expect(interface.rt.class).to eq(ThinkingSphinx::Interfaces::RealTime) end end describe '#sql' do it 'returns an SQL interface' do expect(interface.sql.class).to eq(ThinkingSphinx::Interfaces::SQL) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/000077500000000000000000000000001341132130100227225ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/attribute_spec.rb000066400000000000000000000037031341132130100262670ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Attribute do let(:attribute) { ThinkingSphinx::RealTime::Attribute.new column } let(:column) { double('column', :__name => :created_at, :__stack => []) } describe '#name' do it "uses the provided option by default" do attribute = ThinkingSphinx::RealTime::Attribute.new column, :as => :foo expect(attribute.name).to eq('foo') end it "falls back to the column's name" do expect(attribute.name).to eq('created_at') end end describe '#translate' do let(:klass) { Struct.new(:name, :parent) } let(:object) { klass.new 'the object name', parent } let(:parent) { klass.new 'the parent name', nil } it "returns the column's name if it's a string" do allow(column).to receive_messages :__name => 'value' expect(attribute.translate(object)).to eq('value') end it "returns the column's name if it's an integer" do allow(column).to receive_messages :__name => 404 expect(attribute.translate(object)).to eq(404) end it "returns the object's method matching the column's name" do allow(object).to receive_messages :created_at => 'a time' expect(attribute.translate(object)).to eq('a time') end it "uses the column's stack to navigate through the object tree" do allow(column).to receive_messages :__name => :name, :__stack => [:parent] expect(attribute.translate(object)).to eq('the parent name') end it "returns zero if any element in the object tree is nil" do allow(column).to receive_messages :__name => :name, :__stack => [:parent] object.parent = nil expect(attribute.translate(object)).to be_zero end end describe '#type' do it "returns the given type option" do attribute = ThinkingSphinx::RealTime::Attribute.new column, :type => :string expect(attribute.type).to eq(:string) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/callbacks/000077500000000000000000000000001341132130100246415ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb000066400000000000000000000204711341132130100321440ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do let(:callbacks) { ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new :article } let(:instance) { double('instance', :id => 12, :persisted? => true) } let(:config) { double('config', :indices_for_references => [index], :settings => {}) } let(:index) { double('index', :name => 'my_index', :is_a? => true, :document_id_for_key => 123, :fields => [], :attributes => [], :conditions => [], :primary_key => :id) } let(:connection) { double('connection', :execute => true) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config allow(ThinkingSphinx::Connection).to receive_message_chain(:pool, :take).and_yield connection end describe '#after_save, #after_commit' do let(:insert) { double('insert', :to_sql => 'REPLACE INTO my_index') } let(:time) { 1.day.ago } let(:field) { double('field', :name => 'name', :translate => 'Foo') } let(:attribute) { double('attribute', :name => 'created_at', :translate => time) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config allow(Riddle::Query::Insert).to receive_messages :new => insert allow(insert).to receive_messages :replace! => insert allow(index).to receive_messages :fields => [field], :attributes => [attribute] end it "creates an insert statement with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new). with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "switches the insert to a replace statement" do expect(insert).to receive(:replace!).and_return(insert) callbacks.after_save instance end it "sends the insert through to the server" do expect(connection).to receive(:execute).with('REPLACE INTO my_index') callbacks.after_save instance end it "creates an insert statement with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new). with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_commit instance end it "switches the insert to a replace statement" do expect(insert).to receive(:replace!).and_return(insert) callbacks.after_commit instance end it "sends the insert through to the server" do expect(connection).to receive(:execute).with('REPLACE INTO my_index') callbacks.after_commit instance end context 'with a given path' do let(:callbacks) { ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new( :article, [:user] ) } let(:instance) { double('instance', :id => 12, :user => user) } let(:user) { double('user', :id => 13, :persisted? => true) } it "creates an insert statement with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new). with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for the user object" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) callbacks.after_save instance end it "translates values for the user object" do expect(field).to receive(:translate).with(user).and_return('Foo') callbacks.after_save instance end it "creates an insert statement with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new). with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_commit instance end it "gets the document id for the user object" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) callbacks.after_commit instance end it "translates values for the user object" do expect(field).to receive(:translate).with(user).and_return('Foo') callbacks.after_commit instance end end context 'with a path returning multiple objects' do let(:callbacks) { ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new( :article, [:readers] ) } let(:instance) { double('instance', :id => 12, :readers => [user_a, user_b]) } let(:user_a) { double('user', :id => 13, :persisted? => true) } let(:user_b) { double('user', :id => 14, :persisted? => true) } it "creates insert statements with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new).twice. with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for each reader" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_save instance end it "translates values for each reader" do expect(field).to receive(:translate).with(user_a).and_return('Foo') expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_save instance end it "creates insert statements with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new).twice. with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_commit instance end it "gets the document id for each reader" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_commit instance end it "translates values for each reader" do expect(field).to receive(:translate).with(user_a).and_return('Foo') expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_commit instance end end context 'with a block instead of a path' do let(:callbacks) { ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new( :article ) { |object| object.readers } } let(:instance) { double('instance', :id => 12, :readers => [user_a, user_b]) } let(:user_a) { double('user', :id => 13, :persisted? => true) } let(:user_b) { double('user', :id => 14, :persisted? => true) } it "creates insert statements with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new).twice. with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for each reader" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_save instance end it "translates values for each reader" do expect(field).to receive(:translate).with(user_a).and_return('Foo') expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_save instance end it "creates insert statements with all fields and attributes" do expect(Riddle::Query::Insert).to receive(:new).twice. with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_commit instance end it "gets the document id for each reader" do expect(index).to receive(:document_id_for_key).with(13).and_return(123) expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_commit instance end it "translates values for each reader" do expect(field).to receive(:translate).with(user_a).and_return('Foo') expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_commit instance end end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/field_spec.rb000066400000000000000000000040521341132130100253450ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Field do let(:field) { ThinkingSphinx::RealTime::Field.new column } let(:column) { double('column', :__name => :created_at, :__stack => []) } describe '#column' do it 'returns the provided Column object' do expect(field.column).to eq(column) end it 'translates symbols to Column objects' do expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new).with(:title). and_return(column) ThinkingSphinx::RealTime::Field.new :title end end describe '#name' do it "uses the provided option by default" do field = ThinkingSphinx::RealTime::Field.new column, :as => :foo expect(field.name).to eq('foo') end it "falls back to the column's name" do expect(field.name).to eq('created_at') end end describe '#translate' do let(:klass) { Struct.new(:name, :parent) } let(:object) { klass.new 'the object name', parent } let(:parent) { klass.new 'the parent name', nil } it "returns the column's name if it's a string" do allow(column).to receive_messages :__name => 'value' expect(field.translate(object)).to eq('value') end it "returns the column's name as a string if it's an integer" do allow(column).to receive_messages :__name => 404 expect(field.translate(object)).to eq('404') end it "returns the object's method matching the column's name" do allow(object).to receive_messages :created_at => 'a time' expect(field.translate(object)).to eq('a time') end it "uses the column's stack to navigate through the object tree" do allow(column).to receive_messages :__name => :name, :__stack => [:parent] expect(field.translate(object)).to eq('the parent name') end it "returns a blank string if any element in the object tree is nil" do allow(column).to receive_messages :__name => :name, :__stack => [:parent] object.parent = nil expect(field.translate(object)).to eq('') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/index_spec.rb000066400000000000000000000114001341132130100253640ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Index do let(:index) { ThinkingSphinx::RealTime::Index.new :user } let(:config) { double('config', :settings => {}, :indices_location => 'location', :next_offset => 8) } before :each do allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end describe '#attributes' do it "has the internal id attribute by default" do expect(index.attributes.collect(&:name)).to include('sphinx_internal_id') end it "has the class name attribute by default" do expect(index.attributes.collect(&:name)).to include('sphinx_internal_class') end it "has the internal deleted attribute by default" do expect(index.attributes.collect(&:name)).to include('sphinx_deleted') end end describe '#delta?' do it "always returns false" do expect(index).not_to be_delta end end describe '#docinfo' do it "defaults to extern" do expect(index.docinfo).to eq(:extern) end it "can be disabled" do config.settings["skip_docinfo"] = true expect(index.docinfo).to be_nil end end describe '#document_id_for_key' do it "calculates the document id based on offset and number of indices" do allow(config).to receive_message_chain(:indices, :count).and_return(5) allow(config).to receive_messages :next_offset => 7 expect(index.document_id_for_key(123)).to eq(622) end end describe '#fields' do it "has the internal class field by default" do expect(index.fields.collect(&:name)).to include('sphinx_internal_class_name') end end describe '#interpret_definition!' do let(:block) { double('block') } before :each do index.definition_block = block end it "interprets the definition block" do expect(ThinkingSphinx::RealTime::Interpreter).to receive(:translate!). with(index, block) index.interpret_definition! end it "only interprets the definition block once" do expect(ThinkingSphinx::RealTime::Interpreter).to receive(:translate!). once index.interpret_definition! index.interpret_definition! end end describe '#model' do let(:model) { double('model', :primary_key => :id) } it "translates symbol references to model class" do allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) expect(index.model).to eq(model) end it "memoizes the result" do expect(ActiveSupport::Inflector).to receive(:constantize).with('User').once. and_return(model) index.model index.model end end describe '#morphology' do before :each do skip end context 'with a render' do it "defaults to nil" do begin index.render rescue Riddle::Configuration::ConfigurationError end expect(index.morphology).to be_nil end it "reads from the settings file if provided" do config.settings['morphology'] = 'stem_en' begin index.render rescue Riddle::Configuration::ConfigurationError end expect(index.morphology).to eq('stem_en') end end end describe '#name' do it "always uses the core suffix" do index = ThinkingSphinx::RealTime::Index.new :user expect(index.name).to eq('user_core') end end describe '#offset' do before :each do allow(config).to receive_messages :next_offset => 4 end it "uses the next offset value from the configuration" do expect(index.offset).to eq(4) end it "uses the reference to get a unique offset" do expect(config).to receive(:next_offset).with(:user).and_return(2) index.offset end end describe '#render' do before :each do allow(FileUtils).to receive_messages :mkdir_p => true end it "interprets the provided definition" do expect(index).to receive(:interpret_definition!).at_least(:once) begin index.render rescue Riddle::Configuration::ConfigurationError # Ignoring underlying validation error. end end end describe '#scope' do let(:model) { double('model', :primary_key => :id) } it "returns the model by default" do allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) expect(index.scope).to eq(model) end it "returns the evaluated scope if provided" do index.scope = lambda { :foo } expect(index.scope).to eq(:foo) end end describe '#unique_attribute_names' do it "returns all attribute names" do expect(index.unique_attribute_names).to eq([ 'sphinx_internal_id', 'sphinx_internal_class', 'sphinx_deleted' ]) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/interpreter_spec.rb000066400000000000000000000133261341132130100266310ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Interpreter do let(:instance) { ThinkingSphinx::RealTime::Interpreter.new index, block } let(:model) { double('model') } let(:index) { Struct.new(:attributes, :fields, :options).new([], [], {}) } let(:block) { Proc.new { } } describe '.translate!' do let(:instance) { double('interpreter', :translate! => true) } it "creates a new interpreter instance with the given block and index" do expect(ThinkingSphinx::RealTime::Interpreter).to receive(:new). with(index, block).and_return(instance) ThinkingSphinx::RealTime::Interpreter.translate! index, block end it "calls translate! on the instance" do allow(ThinkingSphinx::RealTime::Interpreter).to receive_messages(:new => instance) expect(instance).to receive(:translate!) ThinkingSphinx::RealTime::Interpreter.translate! index, block end end describe '#has' do let(:column) { double('column') } let(:attribute) { double('attribute') } before :each do allow(ThinkingSphinx::RealTime::Attribute).to receive_messages :new => attribute end it "creates a new attribute with the provided column" do expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(column, {}).and_return(attribute) instance.has column end it "passes through options to the attribute" do expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(column, :as => :other_name).and_return(attribute) instance.has column, :as => :other_name end it "adds an attribute to the index" do instance.has column expect(index.attributes).to include(attribute) end it "adds multiple attributes when passed multiple columns" do instance.has column, column expect(index.attributes.select { |saved_attribute| saved_attribute == attribute }.length).to eq(2) end end describe '#indexes' do let(:column) { double('column') } let(:field) { double('field') } before :each do allow(ThinkingSphinx::RealTime::Field).to receive_messages :new => field end it "creates a new field with the provided column" do expect(ThinkingSphinx::RealTime::Field).to receive(:new). with(column, {}).and_return(field) instance.indexes column end it "passes through options to the field" do expect(ThinkingSphinx::RealTime::Field).to receive(:new). with(column, :as => :other_name).and_return(field) instance.indexes column, :as => :other_name end it "adds a field to the index" do instance.indexes column expect(index.fields).to include(field) end it "adds multiple fields when passed multiple columns" do instance.indexes column, column expect(index.fields.select { |saved_field| saved_field == field }.length).to eq(2) end context 'sortable' do let(:attribute) { double('attribute') } before :each do allow(ThinkingSphinx::RealTime::Attribute).to receive_messages :new => attribute allow(column).to receive_messages :__name => :col end it "adds the _sort suffix to the field's name" do expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(column, :as => :col_sort, :type => :string). and_return(attribute) instance.indexes column, :sortable => true end it "respects given aliases" do expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(column, :as => :other_sort, :type => :string). and_return(attribute) instance.indexes column, :sortable => true, :as => :other end it "respects symbols instead of columns" do expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(:title, :as => :title_sort, :type => :string). and_return(attribute) instance.indexes :title, :sortable => true end it "adds an attribute to the index" do instance.indexes column, :sortable => true expect(index.attributes).to include(attribute) end end end describe '#method_missing' do let(:column) { double('column') } before :each do allow(ThinkingSphinx::ActiveRecord::Column).to receive_messages(:new => column) end it "returns a new column for the given method" do expect(instance.id).to eq(column) end it "should initialise the column with the method name and arguments" do expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new). with(:users, :posts, :subject).and_return(column) instance.users(:posts, :subject) end end describe '#scope' do it "passes the scope block through to the index" do expect(index).to receive(:scope=).with(instance_of(Proc)) instance.scope { :foo } end end describe '#set_property' do before :each do allow(index.class).to receive_messages :settings => [:morphology] end it 'saves other settings as index options' do instance.set_property :field_weights => {:name => 10} expect(index.options[:field_weights]).to eq({:name => 10}) end context 'index settings' do it "sets the provided setting" do expect(index).to receive(:morphology=).with('stem_en') instance.set_property :morphology => 'stem_en' end end end describe '#translate!' do it "returns the block evaluated within the context of the interpreter" do block = Proc.new { __id__ } interpreter = ThinkingSphinx::RealTime::Interpreter.new index, block expect(interpreter.translate!). to eq(interpreter.__id__) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/transcribe_instance_spec.rb000066400000000000000000000022051341132130100303000ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::RealTime::TranscribeInstance do let(:subject) do ThinkingSphinx::RealTime::TranscribeInstance.call( instance, index, [property_a, property_b, property_c] ) end let(:instance) { double :id => 43 } let(:index) { double :document_id_for_key => 46, :primary_key => :id } let(:property_a) { double :translate => 'A' } let(:property_b) { double :translate => 'B' } let(:property_c) { double :translate => 'C' } it 'returns an array of each translated property, and the document id' do expect(subject).to eq([46, 'A', 'B', 'C']) end it 'raises an error if something goes wrong' do allow(property_b).to receive(:translate).and_raise(StandardError) expect { subject }.to raise_error(ThinkingSphinx::TranscriptionError) end it 'notes the instance and property in the wrapper error' do allow(property_b).to receive(:translate).and_raise(StandardError) expect { subject }.to raise_error do |wrapper| expect(wrapper.instance).to eq(instance) expect(wrapper.property).to eq(property_b) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/transcriber_spec.rb000066400000000000000000000065031341132130100266030ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ThinkingSphinx::RealTime::Transcriber do let(:subject) { ThinkingSphinx::RealTime::Transcriber.new index } let(:index) { double 'index', :name => 'foo_core', :conditions => [], :fields => [double(:name => 'field_a'), double(:name => 'field_b')], :attributes => [double(:name => 'attr_a'), double(:name => 'attr_b')] } let(:insert) { double :replace! => replace } let(:replace) { double :to_sql => 'REPLACE QUERY' } let(:connection) { double :execute => true } let(:instance_a) { double :id => 48, :persisted? => true } let(:instance_b) { double :id => 49, :persisted? => true } let(:properties_a) { double } let(:properties_b) { double } before :each do allow(Riddle::Query::Insert).to receive(:new).and_return(insert) allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). with(instance_a, index, anything).and_return(properties_a) allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). with(instance_b, index, anything).and_return(properties_b) end it "generates a SphinxQL command" do expect(Riddle::Query::Insert).to receive(:new).with( 'foo_core', ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], [properties_a, properties_b] ) subject.copy instance_a, instance_b end it "executes the SphinxQL command" do expect(connection).to receive(:execute).with('REPLACE QUERY') subject.copy instance_a, instance_b end it "skips instances that aren't in the database" do allow(instance_a).to receive(:persisted?).and_return(false) expect(Riddle::Query::Insert).to receive(:new).with( 'foo_core', ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], [properties_b] ) subject.copy instance_a, instance_b end it "skips instances that fail a symbol condition" do index.conditions << :ok? allow(instance_a).to receive(:ok?).and_return(true) allow(instance_b).to receive(:ok?).and_return(false) expect(Riddle::Query::Insert).to receive(:new).with( 'foo_core', ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], [properties_a] ) subject.copy instance_a, instance_b end it "skips instances that fail a Proc condition" do index.conditions << Proc.new { |instance| instance.ok? } allow(instance_a).to receive(:ok?).and_return(true) allow(instance_b).to receive(:ok?).and_return(false) expect(Riddle::Query::Insert).to receive(:new).with( 'foo_core', ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], [properties_a] ) subject.copy instance_a, instance_b end it "skips instances that throw an error while transcribing values" do error = ThinkingSphinx::TranscriptionError.new error.instance = instance_a error.inner_exception = StandardError.new allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). with(instance_a, index, anything). and_raise(error) allow(ThinkingSphinx.output).to receive(:puts).and_return(nil) expect(Riddle::Query::Insert).to receive(:new).with( 'foo_core', ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], [properties_b] ) subject.copy instance_a, instance_b end end thinking-sphinx-4.1.0/spec/thinking_sphinx/real_time/translator_spec.rb000066400000000000000000000007721341132130100264600ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::RealTime::Translator do let(:subject) { ThinkingSphinx::RealTime::Translator.call object, column } let(:object) { double } let(:column) { double :__stack => [], :__name => :title } it "converts non-UTF-8 strings to UTF-8" do allow(object).to receive(:title). and_return "hello".dup.force_encoding("ASCII-8BIT") expect(subject).to eq("hello") expect(subject.encoding.name).to eq("UTF-8") end end thinking-sphinx-4.1.0/spec/thinking_sphinx/scopes_spec.rb000066400000000000000000000022331341132130100236140ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Scopes do let(:model) { Class.new do include ThinkingSphinx::Scopes def self.search(query = nil, options = {}) ThinkingSphinx::Search.new(query, options) end end } describe '#method_missing' do before :each do model.sphinx_scopes[:foo] = Proc.new { {:with => {:foo => :bar}} } end it "creates new search" do expect(model.foo.class).to eq(ThinkingSphinx::Search) end it "passes block result to constructor" do expect(model.foo.options[:with]).to eq({:foo => :bar}) end it "passes non-scopes through to the standard method error call" do expect { model.bar }.to raise_error(NoMethodError) end end describe '#sphinx_scope' do it "saves the given block with a name" do model.sphinx_scope(:foo) { 27 } expect(model.sphinx_scopes[:foo].call).to eq(27) end end describe '#default_sphinx_scope' do it "gets and sets the default scope depending on the argument" do model.default_sphinx_scope :foo expect(model.default_sphinx_scope).to eq(:foo) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/search/000077500000000000000000000000001341132130100222265ustar00rootroot00000000000000thinking-sphinx-4.1.0/spec/thinking_sphinx/search/glaze_spec.rb000066400000000000000000000042701341132130100246720ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx class Search; end end require 'thinking_sphinx/search/glaze' describe ThinkingSphinx::Search::Glaze do let(:glaze) { ThinkingSphinx::Search::Glaze.new context, object, raw, [] } let(:object) { double('object') } let(:raw) { {} } let(:context) { {} } describe '#!=' do it "is true for objects that don't match" do expect(glaze != double('foo')).to be_truthy end it "is false when the underlying object is a match" do expect(glaze != object).to be_falsey end end describe '#method_missing' do let(:glaze) { ThinkingSphinx::Search::Glaze.new context, object, raw, [klass, klass] } let(:klass) { double('pane class') } let(:pane_one) { double('pane one', :foo => 'one') } let(:pane_two) { double('pane two', :foo => 'two', :bar => 'two') } before :each do allow(klass).to receive(:new).and_return(pane_one, pane_two) end it "respects objects existing methods" do allow(object).to receive_messages :foo => 'original' expect(glaze.foo).to eq('original') end it "uses the first pane that responds to the method" do expect(glaze.foo).to eq('one') expect(glaze.bar).to eq('two') end it "raises the method missing error otherwise" do allow(object).to receive_messages :respond_to? => false allow(object).to receive(:baz).and_raise(NoMethodError) expect { glaze.baz }.to raise_error(NoMethodError) end end describe '#respond_to?' do it "responds to underlying object methods" do allow(object).to receive_messages :foo => true expect(glaze.respond_to?(:foo)).to be_truthy end it "responds to underlying pane methods" do pane = double('Pane Class', :new => double('pane', :bar => true)) glaze = ThinkingSphinx::Search::Glaze.new context, object, raw, [pane] expect(glaze.respond_to?(:bar)).to be_truthy end it "does not to respond to methods that don't exist" do expect(glaze.respond_to?(:something)).to be_falsey end end describe '#unglazed' do it "returns the original object" do expect(glaze.unglazed).to eq(object) end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/search/query_spec.rb000066400000000000000000000050321341132130100247320ustar00rootroot00000000000000# frozen_string_literal: true module ThinkingSphinx class Search; end end require 'active_support/core_ext/object/blank' require './lib/thinking_sphinx/search/query' describe ThinkingSphinx::Search::Query do before :each do stub_const 'ThinkingSphinx::Query', double(wildcard: '') end describe '#to_s' do it "passes through the keyword as provided" do query = ThinkingSphinx::Search::Query.new 'pancakes' expect(query.to_s).to eq('pancakes') end it "pairs fields and keywords for given conditions" do query = ThinkingSphinx::Search::Query.new '', :title => 'pancakes' expect(query.to_s).to eq('@title pancakes') end it "combines both keywords and conditions" do query = ThinkingSphinx::Search::Query.new 'tasty', :title => 'pancakes' expect(query.to_s).to eq('tasty @title pancakes') end it "automatically stars keywords if requested" do expect(ThinkingSphinx::Query).to receive(:wildcard).with('cake', true). and_return('*cake*') ThinkingSphinx::Search::Query.new('cake', {}, true).to_s end it "automatically stars condition keywords if requested" do expect(ThinkingSphinx::Query).to receive(:wildcard).with('pan', true). and_return('*pan*') ThinkingSphinx::Search::Query.new('', {:title => 'pan'}, true).to_s end it "does not star the sphinx_internal_class field keyword" do query = ThinkingSphinx::Search::Query.new '', {:sphinx_internal_class_name => 'article'}, true expect(query.to_s).to eq('@sphinx_internal_class_name article') end it "handles null values by removing them from the conditions hash" do query = ThinkingSphinx::Search::Query.new '', :title => nil expect(query.to_s).to eq('') end it "handles empty string values by removing them from the conditions hash" do query = ThinkingSphinx::Search::Query.new '', :title => '' expect(query.to_s).to eq('') end it "handles nil queries" do query = ThinkingSphinx::Search::Query.new nil, {} expect(query.to_s).to eq('') end it "allows mixing of blank and non-blank conditions" do query = ThinkingSphinx::Search::Query.new 'tasty', :title => 'pancakes', :ingredients => nil 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 thinking-sphinx-4.1.0/spec/thinking_sphinx/search_spec.rb000066400000000000000000000124511341132130100235700ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx::Search do let(:search) { ThinkingSphinx::Search.new } let(:context) { {:results => []} } let(:stack) { double('stack', :call => true) } let(:pagination_mask_methods) do [:first_page?, :last_page?, :next_page, :next_page?, :page, :per, :previous_page, :total_entries, :total_count, :count, :total_pages, :page_count, :num_pages] end let(:scopes_mask_methods) do [:facets, :search, :search_for_ids] end let(:group_enumerator_mask_methods) do [:each_with_count, :each_with_group, :each_with_group_and_count] end before :each do allow(ThinkingSphinx::Search::Context).to receive_messages :new => context stub_const 'ThinkingSphinx::Middlewares::DEFAULT', stack end describe '#current_page' do it "should return 1 by default" do expect(search.current_page).to eq(1) end it "should handle string page values" do expect(ThinkingSphinx::Search.new(:page => '2').current_page).to eq(2) end it "should handle empty string page values" do expect(ThinkingSphinx::Search.new(:page => '').current_page).to eq(1) end it "should return the requested page" do expect(ThinkingSphinx::Search.new(:page => 10).current_page).to eq(10) end end describe '#empty?' do it "returns false if there is anything in the data set" do context[:results] << double expect(search).not_to be_empty end it "returns true if the data set is empty" do context[:results].clear expect(search).to be_empty end end describe '#initialize' do it "lazily loads by default" do expect(stack).not_to receive(:call) ThinkingSphinx::Search.new end it "should automatically populate when :populate is set to true" do expect(stack).to receive(:call).and_return(true) ThinkingSphinx::Search.new(:populate => true) end end describe '#offset' do it "should default to 0" do expect(search.offset).to eq(0) end it "should increase by the per_page value for each page in" do expect(ThinkingSphinx::Search.new(:per_page => 25, :page => 2).offset). to eq(25) end it "should prioritise explicit :offset over calculated if given" do expect(ThinkingSphinx::Search.new(:offset => 5).offset).to eq(5) end end describe '#page' do it "sets the current page" do search.page(3) expect(search.current_page).to eq(3) end it "returns the search object" do expect(search.page(2)).to eq(search) end end describe '#per' do it "sets the current per_page value" do search.per(29) expect(search.per_page).to eq(29) end it "returns the search object" do expect(search.per(29)).to eq(search) end end describe '#per_page' do it "defaults to 20" do expect(search.per_page).to eq(20) end it "is set as part of the search options" do expect(ThinkingSphinx::Search.new(:per_page => 10).per_page).to eq(10) end it "should prioritise :limit over :per_page if given" do expect(ThinkingSphinx::Search.new(:per_page => 30, :limit => 40).per_page). to eq(40) end it "should allow for string arguments" do expect(ThinkingSphinx::Search.new(:per_page => '10').per_page).to eq(10) end it "allows setting of the per_page value" do search.per_page(24) expect(search.per_page).to eq(24) end end describe '#populate' do it "runs the middleware" do expect(stack).to receive(:call).with([context]).and_return(true) search.populate end it "does not retrieve results twice" do expect(stack).to receive(:call).with([context]).once.and_return(true) search.populate search.populate end end describe '#respond_to?' do it "should respond to Array methods" do expect(search.respond_to?(:each)).to be_truthy end it "should respond to Search methods" do expect(search.respond_to?(:per_page)).to be_truthy end it "should return true for methods delegated to pagination mask by method_missing" do pagination_mask_methods.each do |method| expect(search).to respond_to method end end it "should return true for methods delegated to scopes mask by method_missing" do scopes_mask_methods.each do |method| expect(search).to respond_to method end end it "should return true for methods delegated to group enumerators mask by method_missing" do group_enumerator_mask_methods.each do |method| expect(search).to respond_to method end end end describe '#to_a' do it "returns each of the standard ActiveRecord objects" do unglazed = double('unglazed instance') glazed = double('glazed instance', :unglazed => unglazed) context[:results] << glazed expect(search.to_a.first.__id__).to eq(unglazed.__id__) end end it "correctly handles access to methods delegated to masks through 'method' call" do [ pagination_mask_methods, scopes_mask_methods, group_enumerator_mask_methods ].flatten.each do |method| expect { search.method method }.to_not raise_exception end end end thinking-sphinx-4.1.0/spec/thinking_sphinx/wildcard_spec.rb000066400000000000000000000030721341132130100241130ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true module ThinkingSphinx; end require './lib/thinking_sphinx/wildcard' describe ThinkingSphinx::Wildcard do describe '.call' do it "does not star quorum operators" do expect(ThinkingSphinx::Wildcard.call("foo/3")).to eq("*foo*/3") end it "does not star proximity operators or quoted strings" do expect(ThinkingSphinx::Wildcard.call(%q{"hello world"~3})). to eq(%q{"hello world"~3}) end it "treats slashes as a separator when starring" do expect(ThinkingSphinx::Wildcard.call("a\\/c")).to eq("*a*\\/*c*") end it "separates escaping from the end of words" do expect(ThinkingSphinx::Wildcard.call("\\(913\\)")).to eq("\\(*913*\\)") end it "ignores escaped slashes" do expect(ThinkingSphinx::Wildcard.call("\\/\\/pan")).to eq("\\/\\/*pan*") end it "does not star manually provided field tags" do expect(ThinkingSphinx::Wildcard.call("@title pan")).to eq("@title *pan*") end it 'does not star multiple field tags' do expect(ThinkingSphinx::Wildcard.call("@title pan @tags food")). to eq("@title *pan* @tags *food*") end it "does not star manually provided arrays of field tags" do expect(ThinkingSphinx::Wildcard.call("@(title, body) pan")). to eq("@(title, body) *pan*") end it "handles nil queries" do expect(ThinkingSphinx::Wildcard.call(nil)).to eq('') end it "handles unicode values" do expect(ThinkingSphinx::Wildcard.call('älytön')).to eq('*älytön*') end end end thinking-sphinx-4.1.0/spec/thinking_sphinx_spec.rb000066400000000000000000000022101341132130100223130ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ThinkingSphinx do describe '.count' do let(:search) { double('search', :total_entries => 23, :populated? => false, :options => {}) } before :each do allow(ThinkingSphinx::Search).to receive_messages :new => search end it "returns the total entries of the search object" do expect(ThinkingSphinx.count).to eq(search.total_entries) end it "passes through the given query and options" do expect(ThinkingSphinx::Search).to receive(:new).with('foo', :bar => :baz). and_return(search) ThinkingSphinx.count('foo', :bar => :baz) end end describe '.search' do let(:search) { double('search') } before :each do allow(ThinkingSphinx::Search).to receive_messages :new => search end it "returns a new search object" do expect(ThinkingSphinx.search).to eq(search) end it "passes through the given query and options" do expect(ThinkingSphinx::Search).to receive(:new).with('foo', :bar => :baz). and_return(search) ThinkingSphinx.search('foo', :bar => :baz) end end end thinking-sphinx-4.1.0/thinking-sphinx.gemspec000066400000000000000000000027361341132130100213220ustar00rootroot00000000000000# frozen_string_literal: true # -*- encoding: utf-8 -*- $:.push File.expand_path('../lib', __FILE__) Gem::Specification.new do |s| s.name = 'thinking-sphinx' s.version = '4.1.0' s.platform = Gem::Platform::RUBY s.authors = ["Pat Allan"] s.email = ["pat@freelancing-gods.com"] s.homepage = 'https://pat.github.io/thinking-sphinx/' s.summary = 'A smart wrapper over Sphinx for ActiveRecord' s.description = %Q{An intelligent layer for ActiveRecord (via Rails and Sinatra) for the Sphinx full-text search tool.} s.license = 'MIT' s.rubyforge_project = 'thinking-sphinx' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ['lib'] s.add_runtime_dependency 'activerecord', '>= 3.1.0' s.add_runtime_dependency 'builder', '>= 2.1.2' s.add_runtime_dependency 'joiner', '>= 0.2.0' s.add_runtime_dependency 'middleware', '>= 0.1.0' s.add_runtime_dependency 'innertube', '>= 1.0.2' s.add_runtime_dependency 'riddle', '~> 2.3' s.add_development_dependency 'appraisal', '~> 1.0.2' s.add_development_dependency 'combustion', '~> 0.8.0' s.add_development_dependency 'database_cleaner', '~> 1.6.0' s.add_development_dependency 'rspec', '~> 3.7.0' s.add_development_dependency 'rspec-retry', '~> 0.5.6' end