├── .gitignore ├── .travis.yml ├── .yardopts ├── Gemfile ├── MIT-LICENSE ├── README.markdown ├── Rakefile ├── examples ├── rails-application-template.rb └── tire-dsl.rb ├── lib ├── tire.rb └── tire │ ├── alias.rb │ ├── configuration.rb │ ├── count.rb │ ├── delete_by_query.rb │ ├── dsl.rb │ ├── http │ ├── client.rb │ ├── clients │ │ ├── curb.rb │ │ └── faraday.rb │ └── response.rb │ ├── index.rb │ ├── logger.rb │ ├── model │ ├── callbacks.rb │ ├── import.rb │ ├── indexing.rb │ ├── naming.rb │ ├── percolate.rb │ ├── persistence.rb │ ├── persistence │ │ ├── attributes.rb │ │ ├── finders.rb │ │ └── storage.rb │ ├── search.rb │ └── suggest.rb │ ├── multi_search.rb │ ├── results │ ├── collection.rb │ ├── item.rb │ ├── pagination.rb │ └── suggestions.rb │ ├── rubyext │ ├── hash.rb │ ├── ruby_1_8.rb │ ├── symbol.rb │ └── uri_escape.rb │ ├── search.rb │ ├── search │ ├── facet.rb │ ├── filter.rb │ ├── highlight.rb │ ├── queries │ │ ├── custom_filters_score.rb │ │ └── match.rb │ ├── query.rb │ ├── scan.rb │ ├── script_field.rb │ └── sort.rb │ ├── suggest.rb │ ├── suggest │ └── suggestion.rb │ ├── tasks.rb │ ├── utils.rb │ └── version.rb ├── test ├── fixtures │ └── articles │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ └── 5.json ├── integration │ ├── active_model_indexing_test.rb │ ├── active_model_searchable_test.rb │ ├── active_record_searchable_test.rb │ ├── boolean_queries_test.rb │ ├── boosting_queries_test.rb │ ├── bulk_test.rb │ ├── constant_score_queries_test.rb │ ├── count_test.rb │ ├── custom_filters_score_queries_test.rb │ ├── custom_score_queries_test.rb │ ├── delete_by_query_test.rb │ ├── dis_max_queries_test.rb │ ├── dsl_search_test.rb │ ├── explanation_test.rb │ ├── facets_test.rb │ ├── filtered_queries_test.rb │ ├── filters_test.rb │ ├── fuzzy_queries_test.rb │ ├── highlight_test.rb │ ├── index_aliases_test.rb │ ├── index_mapping_test.rb │ ├── index_store_test.rb │ ├── index_update_document_test.rb │ ├── match_query_test.rb │ ├── mongoid_searchable_test.rb │ ├── multi_search_test.rb │ ├── nested_query_test.rb │ ├── percolator_test.rb │ ├── persistent_model_test.rb │ ├── prefix_query_test.rb │ ├── query_return_version_test.rb │ ├── query_string_test.rb │ ├── range_queries_test.rb │ ├── reindex_test.rb │ ├── results_test.rb │ ├── scan_test.rb │ ├── script_fields_test.rb │ ├── search_response_test.rb │ ├── sort_test.rb │ └── suggest_test.rb ├── models │ ├── active_model_article.rb │ ├── active_model_article_with_callbacks.rb │ ├── active_model_article_with_custom_document_type.rb │ ├── active_model_article_with_custom_index_name.rb │ ├── active_record_models.rb │ ├── article.rb │ ├── mongoid_models.rb │ ├── persistent_article.rb │ ├── persistent_article_in_index.rb │ ├── persistent_article_in_namespace.rb │ ├── persistent_article_with_casting.rb │ ├── persistent_article_with_defaults.rb │ ├── persistent_article_with_percolation.rb │ ├── persistent_article_with_strict_mapping.rb │ ├── persistent_articles_with_custom_index_name.rb │ ├── supermodel_article.rb │ └── validated_model.rb ├── test_helper.rb └── unit │ ├── active_model_lint_test.rb │ ├── configuration_test.rb │ ├── count_test.rb │ ├── http_client_test.rb │ ├── http_response_test.rb │ ├── index_alias_test.rb │ ├── index_test.rb │ ├── logger_test.rb │ ├── model_callbacks_test.rb │ ├── model_import_test.rb │ ├── model_initialization_test.rb │ ├── model_persistence_test.rb │ ├── model_search_test.rb │ ├── multi_search_test.rb │ ├── results_collection_test.rb │ ├── results_item_test.rb │ ├── rubyext_test.rb │ ├── search_facet_test.rb │ ├── search_filter_test.rb │ ├── search_highlight_test.rb │ ├── search_query_test.rb │ ├── search_scan_test.rb │ ├── search_script_field_test.rb │ ├── search_sort_test.rb │ ├── search_test.rb │ ├── suggest_test.rb │ └── tire_test.rb └── tire.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | rdoc/ 6 | doc/ 7 | .yardoc/ 8 | coverage/ 9 | scratch/ 10 | examples/*.html 11 | *.log 12 | .rvmrc 13 | .rbenv-version 14 | tmp/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------- 2 | # Configuration file for http://travis-ci.org/#!/karmi/tire 3 | # --------------------------------------------------------- 4 | 5 | language: ruby 6 | 7 | rvm: 8 | - 1.8.7 9 | - 1.9.3 10 | - 2.0.0 11 | - jruby-19mode 12 | 13 | env: 14 | - TEST_COMMAND="rake test:unit" 15 | - TEST_COMMAND="rake test:integration" 16 | 17 | script: "bundle exec $TEST_COMMAND" 18 | 19 | services: 20 | - elasticsearch 21 | - redis 22 | - mongodb 23 | 24 | matrix: 25 | exclude: 26 | - rvm: 1.8.7 27 | env: TEST_COMMAND="rake test:integration" 28 | 29 | notifications: 30 | disable: true 31 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "pry" 4 | 5 | # Specify your gem's dependencies in tire.gemspec 6 | gemspec 7 | 8 | platform :jruby do 9 | gem "jdbc-sqlite3" 10 | gem "activerecord-jdbcsqlite3-adapter" 11 | gem "json" if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' 12 | end 13 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Karel Minarik 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | task :default => :test 5 | 6 | require 'rake/testtask' 7 | Rake::TestTask.new(:test) do |test| 8 | test.libs << 'lib' << 'test' 9 | test.test_files = FileList['test/unit/*_test.rb', 'test/integration/*_test.rb'] 10 | test.verbose = true 11 | # test.warning = true 12 | end 13 | 14 | namespace :test do 15 | Rake::TestTask.new(:unit) do |test| 16 | test.libs << 'lib' << 'test' 17 | test.test_files = FileList["test/unit/*_test.rb"] 18 | test.verbose = true 19 | end 20 | Rake::TestTask.new(:integration) do |test| 21 | test.libs << 'lib' << 'test' 22 | test.test_files = FileList["test/integration/*_test.rb"] 23 | test.verbose = true 24 | end 25 | end 26 | 27 | namespace :web do 28 | 29 | desc "Update the Github website" 30 | task :update => :generate do 31 | current_branch = `git branch --no-color`. 32 | split("\n"). 33 | select { |line| line =~ /^\* / }. 34 | first.to_s. 35 | gsub(/\* (.*)/, '\1') 36 | (puts "Unable to determine current branch"; exit(1) ) unless current_branch 37 | system "git checkout web" 38 | system "cp examples/tire-dsl.html index.html" 39 | system "git add index.html && git co -m 'Updated Tire website'" 40 | system "git push origin web:gh-pages -f" 41 | system "git checkout #{current_branch}" 42 | end 43 | 44 | desc "Generate the Rocco documentation page" 45 | task :generate do 46 | system "rocco examples/tire-dsl.rb" 47 | html = File.read('examples/tire-dsl.html').gsub!(/>tire\-dsl\.rbtire.rb<') 48 | File.open('examples/tire-dsl.html', 'w') { |f| f.write html } 49 | system "open examples/tire-dsl.html" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/tire.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | require 'multi_json' 3 | require 'active_model' 4 | require 'hashr' 5 | require 'cgi' 6 | 7 | require 'active_support/core_ext/object/to_param' 8 | require 'active_support/core_ext/object/to_query' 9 | require 'active_support/core_ext/hash/except' 10 | require 'active_support/json' 11 | 12 | # Ruby 1.8 compatibility 13 | require 'tire/rubyext/ruby_1_8' if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' 14 | 15 | require 'tire/version' 16 | require 'tire/rubyext/hash' 17 | require 'tire/rubyext/symbol' 18 | require 'tire/utils' 19 | require 'tire/logger' 20 | require 'tire/configuration' 21 | require 'tire/http/response' 22 | require 'tire/http/client' 23 | require 'tire/search' 24 | require 'tire/search/query' 25 | require 'tire/search/queries/match' 26 | require 'tire/search/queries/custom_filters_score' 27 | require 'tire/search/sort' 28 | require 'tire/search/facet' 29 | require 'tire/search/filter' 30 | require 'tire/search/highlight' 31 | require 'tire/search/scan' 32 | require 'tire/search/script_field' 33 | require 'tire/suggest' 34 | require 'tire/suggest/suggestion' 35 | require 'tire/delete_by_query' 36 | require 'tire/multi_search' 37 | require 'tire/count' 38 | require 'tire/results/pagination' 39 | require 'tire/results/collection' 40 | require 'tire/results/item' 41 | require 'tire/results/suggestions' 42 | require 'tire/index' 43 | require 'tire/alias' 44 | require 'tire/dsl' 45 | require 'tire/model/naming' 46 | require 'tire/model/callbacks' 47 | require 'tire/model/percolate' 48 | require 'tire/model/indexing' 49 | require 'tire/model/import' 50 | require 'tire/model/suggest' 51 | require 'tire/model/search' 52 | require 'tire/model/persistence/finders' 53 | require 'tire/model/persistence/attributes' 54 | require 'tire/model/persistence/storage' 55 | require 'tire/model/persistence' 56 | require 'tire/tasks' 57 | 58 | module Tire 59 | extend DSL 60 | 61 | def warn(message) 62 | line = caller.detect { |line| line !~ %r|lib\/tire\/| }.sub(/:in .*/, '') 63 | STDERR.puts "", "\e[31m[DEPRECATION WARNING] #{message}", "(Called from #{line})", "\e[0m" 64 | end 65 | module_function :warn 66 | end 67 | -------------------------------------------------------------------------------- /lib/tire/configuration.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | 3 | class Configuration 4 | 5 | def self.url(value=nil) 6 | @url = (value ? value.to_s.gsub(%r|/*$|, '') : nil) || @url || ENV['ELASTICSEARCH_URL'] || "http://localhost:9200" 7 | end 8 | 9 | def self.client(klass=nil) 10 | @client = klass || @client || HTTP::Client::RestClient 11 | end 12 | 13 | def self.wrapper(klass=nil) 14 | @wrapper = klass || @wrapper || Results::Item 15 | end 16 | 17 | def self.logger(device=nil, options={}) 18 | return @logger = Logger.new(device, options) if device 19 | @logger || nil 20 | end 21 | 22 | def self.pretty(value=nil, options={}) 23 | if value === false 24 | return @pretty = false 25 | else 26 | @pretty.nil? ? true : @pretty 27 | end 28 | end 29 | 30 | def self.reset(*properties) 31 | reset_variables = properties.empty? ? instance_variables : instance_variables.map { |p| p.to_s} & \ 32 | properties.map { |p| "@#{p}" } 33 | reset_variables.each { |v| instance_variable_set(v.to_sym, nil) } 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/tire/count.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | class CountRequestFailed < StandardError; end 4 | 5 | class Count 6 | 7 | attr_reader :indices, :types, :query, :response, :json 8 | 9 | def initialize(indices=nil, options={}, &block) 10 | @indices = Array(indices) 11 | @types = Array(options.delete(:type)).map { |type| Utils.escape(type) } 12 | @options = options 13 | 14 | @path = ['/', @indices.join(','), @types.join(','), '_count'].compact.join('/').squeeze('/') 15 | 16 | if block_given? 17 | @query = Query.new 18 | block.arity < 1 ? @query.instance_eval(&block) : block.call(@query) 19 | end 20 | end 21 | 22 | def url 23 | Configuration.url + @path 24 | end 25 | 26 | def params 27 | options = @options.except(:wrapper) 28 | options.empty? ? '' : '?' + options.to_param 29 | end 30 | 31 | def perform 32 | @response = Configuration.client.get self.url + self.params, self.to_json 33 | if @response.failure? 34 | STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n" 35 | raise CountRequestFailed, @response.to_s 36 | end 37 | @json = MultiJson.decode(@response.body) 38 | @value = @json['count'] 39 | return self 40 | ensure 41 | logged 42 | end 43 | 44 | def value 45 | @value || (perform and return @value) 46 | end 47 | 48 | def to_json(options={}) 49 | @query.to_json if @query 50 | end 51 | 52 | def to_curl 53 | if to_json 54 | to_json_escaped = to_json.gsub("'",'\u0027') 55 | %Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '#{to_json_escaped}'| 56 | else 57 | %Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty'| 58 | end 59 | end 60 | 61 | def logged(endpoint='_count') 62 | if Configuration.logger 63 | 64 | Configuration.logger.log_request endpoint, indices, to_curl 65 | 66 | code = @response.code rescue nil 67 | 68 | if Configuration.logger.level.to_s == 'debug' 69 | body = if @json 70 | MultiJson.encode( @json, :pretty => Configuration.pretty) 71 | else 72 | MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue '' 73 | end 74 | else 75 | body = '' 76 | end 77 | 78 | Configuration.logger.log_response code || 'N/A', 'N/A', body || 'N/A' 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/tire/delete_by_query.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | class DeleteByQuery 3 | class DeleteByQueryRequestFailed < StandardError; end 4 | 5 | attr_reader :indices, :types, :query, :response, :json 6 | 7 | def initialize(indices=nil, options={}, &block) 8 | @indices = Array(indices) 9 | @types = Array(options[:type]).flatten 10 | @options = options 11 | 12 | if block_given? 13 | @query = Search::Query.new 14 | block.arity < 1 ? @query.instance_eval(&block) : block.call(@query) 15 | else 16 | raise "no query given for #{self.class}" 17 | end 18 | end 19 | 20 | def perform 21 | @response = Configuration.client.delete url 22 | if @response.failure? 23 | STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n" 24 | raise DeleteByQueryRequestFailed, @response.to_s 25 | end 26 | @json = MultiJson.decode(@response.body) 27 | true 28 | ensure 29 | logged 30 | end 31 | 32 | private 33 | 34 | def path 35 | [ 36 | '/', 37 | indices.join(','), 38 | types.map { |type| Utils.escape(type) }.join(','), 39 | '_query', 40 | ].compact.join('/') 41 | end 42 | 43 | def url 44 | "#{Configuration.url}#{path}/?source=#{Utils.escape(to_json)}" 45 | end 46 | 47 | def to_json(options={}) 48 | query.to_json 49 | end 50 | 51 | def to_curl 52 | %Q|curl -X DELETE '#{url}'| 53 | end 54 | 55 | def logged(endpoint='_query') 56 | if Configuration.logger 57 | 58 | Configuration.logger.log_request endpoint, indices, to_curl 59 | 60 | code = response.code rescue nil 61 | 62 | if Configuration.logger.level.to_s == 'debug' 63 | body = if json 64 | MultiJson.encode(json, :pretty => Configuration.pretty) 65 | else 66 | MultiJson.encode(MultiJson.load(response.body), :pretty => Configuration.pretty) rescue '' 67 | end 68 | else 69 | body = '' 70 | end 71 | 72 | Configuration.logger.log_response code || 'N/A', 'N/A', body || 'N/A' 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/tire/dsl.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module DSL 3 | 4 | def configure(&block) 5 | Configuration.class_eval(&block) 6 | end 7 | 8 | def search(indices=nil, params={}, &block) 9 | if block_given? 10 | Search::Search.new(indices, params, &block) 11 | else 12 | raise ArgumentError, "Please pass a Ruby Hash or an object with `to_hash` method, not #{params.class}" \ 13 | unless params.respond_to?(:to_hash) 14 | 15 | params = params.to_hash 16 | 17 | if payload = params.delete(:payload) 18 | options = params 19 | else 20 | payload = params 21 | end 22 | 23 | # Extract URL parameters from payload 24 | # 25 | search_params = %w| search_type routing scroll from size timeout | 26 | 27 | search_options = search_params.inject({}) do |sum,item| 28 | if param = (payload.delete(item) || payload.delete(item.to_sym)) 29 | sum[item.to_sym] = param 30 | end 31 | sum 32 | end 33 | 34 | search_options.update(options) if options && !options.empty? 35 | search_options.update(:payload => payload) unless payload.empty? 36 | Search::Search.new(indices, search_options) 37 | end 38 | end 39 | 40 | # Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html) 41 | # request. 42 | # 43 | # s = Tire.multi_search 'clients' do 44 | # search :names do 45 | # query { match :name, 'carpenter' } 46 | # end 47 | # search :counts, search_type: 'count' do 48 | # query { match [:name, :street, :occupation], 'carpenter' } 49 | # end 50 | # search :vip, index: 'clients-vip' do 51 | # query { string "last_name:carpenter" } 52 | # end 53 | # search() { query {all} } 54 | # end 55 | # 56 | # The DSL allows you to perform multiple searches and get corresponding results 57 | # in a single HTTP request, saving network roundtrips. 58 | # 59 | # Use the `search` method in the block to define a search request with the 60 | # regular Tire's DSL (`query`, `facet`, etc). 61 | # 62 | # You can pass options such as `search_type`, `routing`, etc., 63 | # as well as a different `index` and/or `type` to individual searches. 64 | # 65 | # You can give single searches names, to be able to refer to them later. 66 | # 67 | # The results are returned as an enumerable collection of {Tire::Results::Collection} instances. 68 | # 69 | # You may simply iterate over them with `each`: 70 | # 71 | # s.results.each do |results| 72 | # puts results.map(&:name) 73 | # end 74 | # 75 | # To iterate over named results, use the `each_pair` method: 76 | # 77 | # s.results.each_pair do |name,results| 78 | # puts "Search #{name} got #{results.size} results" 79 | # end 80 | # 81 | # You can get a specific named result: 82 | # 83 | # search.results[:vip] 84 | # 85 | # You can mix & match named and non-named searches in the definition; the non-named 86 | # searches will be zero-based numbered, so you can refer to them: 87 | # 88 | # search.results[3] # Results for the last query 89 | # 90 | # To log the multi-search request, use the standard `to_curl` method (or set up a logger): 91 | # 92 | # print search.to_curl 93 | # 94 | def multi_search(indices=nil, options={}, &block) 95 | Search::Multi::Search.new(indices, options, &block) 96 | rescue Exception => error 97 | STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n" 98 | raise 99 | ensure 100 | end 101 | alias :multisearch :multi_search 102 | alias :msearch :multi_search 103 | 104 | def count(indices=nil, options={}, &block) 105 | Search::Count.new(indices, options, &block).value 106 | end 107 | 108 | def delete(indices=nil, options={}, &block) 109 | DeleteByQuery.new(indices, options, &block).perform 110 | end 111 | 112 | def index(name, &block) 113 | Index.new(name, &block) 114 | end 115 | 116 | def scan(names, options={}, &block) 117 | Search::Scan.new(names, options, &block) 118 | end 119 | 120 | def suggest(indices=nil, options={}, &block) 121 | Suggest::Suggest.new(indices, options, &block) 122 | end 123 | 124 | def aliases 125 | Alias.all 126 | end 127 | 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/tire/http/client.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | 3 | module HTTP 4 | 5 | module Client 6 | 7 | class RestClient 8 | ConnectionExceptions = [::RestClient::ServerBrokeConnection, ::RestClient::RequestTimeout] 9 | 10 | def self.get(url, data=nil) 11 | perform ::RestClient::Request.new(:method => :get, :url => url, :payload => data).execute 12 | rescue *ConnectionExceptions 13 | raise 14 | rescue ::RestClient::Exception => e 15 | Response.new e.http_body, e.http_code 16 | end 17 | 18 | def self.post(url, data) 19 | perform ::RestClient.post(url, data) 20 | rescue *ConnectionExceptions 21 | raise 22 | rescue ::RestClient::Exception => e 23 | Response.new e.http_body, e.http_code 24 | end 25 | 26 | def self.put(url, data) 27 | perform ::RestClient.put(url, data) 28 | rescue *ConnectionExceptions 29 | raise 30 | rescue ::RestClient::Exception => e 31 | Response.new e.http_body, e.http_code 32 | end 33 | 34 | def self.delete(url) 35 | perform ::RestClient.delete(url) 36 | rescue *ConnectionExceptions 37 | raise 38 | rescue ::RestClient::Exception => e 39 | Response.new e.http_body, e.http_code 40 | end 41 | 42 | def self.head(url) 43 | perform ::RestClient.head(url) 44 | rescue *ConnectionExceptions 45 | raise 46 | rescue ::RestClient::Exception => e 47 | Response.new e.http_body, e.http_code 48 | end 49 | 50 | def self.__host_unreachable_exceptions 51 | [Errno::ECONNREFUSED, Errno::ETIMEDOUT, ::RestClient::ServerBrokeConnection, ::RestClient::RequestTimeout, SocketError] 52 | end 53 | 54 | private 55 | 56 | def self.perform(response) 57 | Response.new response.body, response.code, response.headers 58 | end 59 | 60 | end 61 | 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/tire/http/clients/curb.rb: -------------------------------------------------------------------------------- 1 | require 'curb' 2 | 3 | module Tire 4 | 5 | module HTTP 6 | 7 | module Client 8 | 9 | class Curb 10 | def self.client 11 | Thread.current[:client] ||= begin 12 | client = ::Curl::Easy.new 13 | client.resolve_mode = :ipv4 14 | # client.verbose = true 15 | client 16 | end 17 | end 18 | 19 | def self.get(url, data=nil) 20 | client.url = url 21 | 22 | # FIXME: Curb cannot post bodies with GET requests? 23 | # Roy Fielding seems to approve: 24 | # 25 | if data 26 | client.post_body = data 27 | client.http_post 28 | else 29 | client.http_get 30 | end 31 | Response.new client.body_str, client.response_code 32 | end 33 | 34 | def self.post(url, data) 35 | client.url = url 36 | client.post_body = data 37 | client.http_post 38 | Response.new client.body_str, client.response_code 39 | end 40 | 41 | # NOTE: newrelic_rpm breaks Curl::Easy#http_put 42 | # https://github.com/newrelic/rpm/blob/master/lib/new_relic/agent/instrumentation/curb.rb#L49 43 | # 44 | def self.put(url, data) 45 | method = client.respond_to?(:http_put_without_newrelic) ? :http_put_without_newrelic : :http_put 46 | client.url = url 47 | client.send method, data 48 | Response.new client.body_str, client.response_code 49 | end 50 | 51 | def self.delete(url) 52 | client.url = url 53 | client.http_delete 54 | Response.new client.body_str, client.response_code 55 | end 56 | 57 | def self.head(url) 58 | client.url = url 59 | client.http_head 60 | Response.new client.body_str, client.response_code 61 | end 62 | 63 | def self.__host_unreachable_exceptions 64 | [::Curl::Err::HostResolutionError, ::Curl::Err::ConnectionFailedError] 65 | end 66 | 67 | end 68 | 69 | end 70 | 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/tire/http/clients/faraday.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | # A Faraday-based HTTP client, which allows you to choose a HTTP client. 4 | # 5 | # See 6 | # 7 | # NOTE: Tire will switch to Faraday for the HTTP abstraction layer. This client is a temporary solution. 8 | # 9 | # Example: 10 | # -------- 11 | # 12 | # require 'typhoeus' 13 | # require 'tire/http/clients/faraday' 14 | # 15 | # Tire.configure do |config| 16 | # 17 | # # Unless specified, tire will use Faraday.default_adapter and no middleware 18 | # Tire::HTTP::Client::Faraday.faraday_middleware = Proc.new do |builder| 19 | # builder.adapter :typhoeus 20 | # end 21 | # 22 | # config.client(Tire::HTTP::Client::Faraday) 23 | # 24 | # end 25 | # 26 | # 27 | module Tire 28 | module HTTP 29 | module Client 30 | class Faraday 31 | 32 | # Default middleware stack. 33 | DEFAULT_MIDDLEWARE = Proc.new do |builder| 34 | builder.adapter ::Faraday.default_adapter 35 | end 36 | 37 | class << self 38 | # A customized stack of Faraday middleware that will be used to make each request. 39 | attr_accessor :faraday_middleware 40 | 41 | def get(url, data = nil) 42 | request(:get, url, data) 43 | end 44 | 45 | def post(url, data) 46 | request(:post, url, data) 47 | end 48 | 49 | def put(url, data) 50 | request(:put, url, data) 51 | end 52 | 53 | def delete(url, data = nil) 54 | request(:delete, url, data) 55 | end 56 | 57 | def head(url) 58 | request(:head, url) 59 | end 60 | 61 | def __host_unreachable_exceptions 62 | [::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError] 63 | end 64 | 65 | private 66 | def request(method, url, data = nil) 67 | conn = ::Faraday.new( &(faraday_middleware || DEFAULT_MIDDLEWARE) ) 68 | response = conn.run_request(method, url, data, nil) 69 | Response.new(response.body, response.status, response.headers) 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/tire/http/response.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | 3 | module HTTP 4 | 5 | class Response 6 | attr_reader :body, :code, :headers 7 | 8 | def initialize(body, code, headers={}) 9 | @body, @code, @headers = body, code.to_i, headers 10 | end 11 | 12 | def success? 13 | code > 0 && code < 400 14 | end 15 | 16 | def failure? 17 | ! success? 18 | end 19 | 20 | def to_s 21 | [code, body].join(' : ') 22 | end 23 | end 24 | 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/tire/logger.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | class Logger 3 | 4 | def initialize(device, options={}) 5 | @device = if device.respond_to?(:write) 6 | device 7 | else 8 | File.open(device, 'a') 9 | end 10 | @device.sync = true if @device.respond_to?(:sync) 11 | @options = options 12 | # at_exit { @device.close unless @device.closed? } if @device.respond_to?(:closed?) && @device.respond_to?(:close) 13 | end 14 | 15 | def level 16 | @options[:level] || 'info' 17 | end 18 | 19 | def write(message) 20 | @device.write message 21 | end 22 | 23 | def log_request(endpoint, params=nil, curl='') 24 | # 2001-02-12 18:20:42:32 [_search] (articles,users) 25 | # 26 | # curl -X POST .... 27 | # 28 | content = "# #{time}" 29 | content << " [#{endpoint}]" 30 | content << " (#{params.inspect})" if params 31 | content << "\n#\n" 32 | content << curl 33 | content << "\n\n" 34 | write content 35 | end 36 | 37 | def log_response(status, took=nil, json='') 38 | # 2001-02-12 18:20:42:32 [200] (4 msec) 39 | # 40 | # { 41 | # "took" : 4, 42 | # "hits" : [...] 43 | # ... 44 | # } 45 | # 46 | content = "# #{time}" 47 | content << " [#{status}]" 48 | content << " (#{took} msec)" if took 49 | content << "\n#\n" unless json.to_s !~ /\S/ 50 | json.to_s.each_line { |line| content << "# #{line}" } unless json.to_s !~ /\S/ 51 | content << "\n\n" 52 | write content 53 | end 54 | 55 | def time 56 | Time.now.strftime('%Y-%m-%d %H:%M:%S:%L') 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/tire/model/callbacks.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | # Main module containing the infrastructure for automatic updating 5 | # of the _Elasticsearch_ index on model instance create, update or delete. 6 | # 7 | # Include it in your model: `include Tire::Model::Callbacks` 8 | # 9 | # The model must respond to `after_save` and `after_destroy` callbacks 10 | # (ActiveModel and ActiveRecord models do so, by default). 11 | # 12 | module Callbacks 13 | 14 | # A hook triggered by the `include Tire::Model::Callbacks` statement in the model. 15 | # 16 | def self.included(base) 17 | 18 | # Update index on model instance change or destroy. 19 | # 20 | if base.respond_to?(:after_save) && base.respond_to?(:after_destroy) 21 | base.send :after_save, lambda { tire.update_index } 22 | base.send :after_destroy, lambda { tire.update_index } 23 | end 24 | 25 | # Add neccessary infrastructure for the model, when missing in 26 | # some half-baked ActiveModel implementations. 27 | # 28 | if base.respond_to?(:before_destroy) && !base.instance_methods.map(&:to_sym).include?(:destroyed?) 29 | base.class_eval do 30 | before_destroy { @destroyed = true } 31 | def destroyed?; !!@destroyed; end 32 | end 33 | end 34 | 35 | end 36 | 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tire/model/import.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | # Provides support for efficient and effective importing of large recordsets into Elasticsearch. 5 | # 6 | # Tire will use dedicated strategies for fetching records in batches when ActiveRecord or Mongoid models are detected. 7 | # 8 | # Two dedicated strategies for popular pagination libraries are also provided: WillPaginate and Kaminari. 9 | # These could be used in situations where your model is neither ActiveRecord nor Mongoid based. 10 | # 11 | # You can implement your own custom strategy and pass it via the `:strategy` option. 12 | # 13 | # Note, that it's always possible to use the `Tire::Index#import` method directly. 14 | # 15 | # @note See `Tire::Import::Strategy`. 16 | # 17 | module Import 18 | 19 | module ClassMethods 20 | def import options={}, &block 21 | strategy = Strategy.from_class(klass, options) 22 | strategy.import &block 23 | end 24 | end 25 | 26 | # Importing strategies for common persistence frameworks (ActiveModel, Mongoid), as well as 27 | # pagination libraries (WillPaginate, Kaminari), or a custom strategy. 28 | # 29 | module Strategy 30 | def self.from_class(klass, options={}) 31 | return const_get(options[:strategy]).new(klass, options) if options[:strategy] 32 | 33 | case 34 | when defined?(::ActiveRecord) && klass.ancestors.include?(::ActiveRecord::Base) 35 | ActiveRecord.new klass, options 36 | when defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) 37 | Mongoid.new klass, options 38 | when defined?(Kaminari) && klass.respond_to?(:page) 39 | Kaminari.new klass, options 40 | when defined?(WillPaginate) && klass.respond_to?(:paginate) 41 | WillPaginate.new klass, options 42 | else 43 | Default.new klass, options 44 | end 45 | end 46 | 47 | module Base 48 | attr_reader :klass, :options, :index 49 | def initialize(klass, options={}) 50 | @klass = klass 51 | @options = {:per_page => 1000}.update(options) 52 | @index = options[:index] ? Tire::Index.new(options.delete(:index)) : klass.tire.index 53 | end 54 | end 55 | 56 | class ActiveRecord 57 | include Base 58 | def import &block 59 | klass.find_in_batches(:batch_size => options[:per_page]) do |batch| 60 | index.import batch, options, &block 61 | end 62 | self 63 | end 64 | end 65 | 66 | class Mongoid 67 | include Base 68 | def import &block 69 | items = [] 70 | klass.all.each do |item| 71 | items << item 72 | if items.length % options[:per_page] == 0 73 | index.import items, options, &block 74 | items = [] 75 | end 76 | end 77 | index.import items, options, &block unless items.empty? 78 | self 79 | end 80 | end 81 | 82 | class Kaminari 83 | include Base 84 | def import &block 85 | current = 0 86 | page = 1 87 | while current < klass.count 88 | items = klass.page(page).per(options[:per_page]) 89 | index.import items, options, &block 90 | current = current + items.size 91 | page += 1 92 | end 93 | self 94 | end 95 | end 96 | 97 | class WillPaginate 98 | include Base 99 | def import &block 100 | current = 0 101 | page = 1 102 | while current < klass.count 103 | items = klass.paginate(:page => page, :per_page => options[:per_page]) 104 | index.import items, options, &block 105 | current += items.size 106 | page += 1 107 | end 108 | self 109 | end 110 | end 111 | 112 | class Default 113 | include Base 114 | def import &block 115 | index.import klass, options.update(:method => 'paginate'), &block 116 | self 117 | end 118 | end 119 | end 120 | 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/tire/model/naming.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | # Contains logic for getting and setting the index name and document type for this model. 5 | # 6 | module Naming 7 | 8 | module ClassMethods 9 | 10 | # Get or set the index name for this model, based on arguments. 11 | # 12 | # By default, uses ActiveSupport inflection, so a class named `Article` 13 | # will be stored in the `articles` index. 14 | # 15 | # To get the index name: 16 | # 17 | # Article.index_name 18 | # 19 | # To set the index name: 20 | # 21 | # Article.index_name 'my-custom-name' 22 | # 23 | # You can also use a block for defining the index name, 24 | # which is evaluated in the class context: 25 | # 26 | # Article.index_name { "articles-#{Time.now.year}" } 27 | # 28 | # Article.index_name { "articles-#{Rails.env}" } 29 | # 30 | def index_name name=nil, &block 31 | @index_name = name if name 32 | @index_name = block if block_given? 33 | # TODO: Try to get index_name from ancestor classes 34 | @index_name || [index_prefix, klass.model_name.plural].compact.join('_') 35 | end 36 | 37 | # Set or get index prefix for all models or for a specific model. 38 | # 39 | # To set the prefix for all models (preferably in an initializer inside Rails): 40 | # 41 | # Tire::Model::Search.index_prefix Rails.env 42 | # 43 | # To set the prefix for specific model: 44 | # 45 | # class Article 46 | # # ... 47 | # index_prefix 'my_prefix' 48 | # end 49 | # 50 | # TODO: Maybe this would be more sane with ActiveSupport extensions such as `class_attribute`? 51 | # 52 | @@__index_prefix__ = nil 53 | def index_prefix(*args) 54 | # Uses class or instance variable depending on the context 55 | if args.size > 0 56 | value = args.pop 57 | self.is_a?(Module) ? ( @@__index_prefix__ = value ) : ( @__index_prefix__ = value ) 58 | end 59 | self.is_a?(Module) ? ( @@__index_prefix__ || nil ) : ( @__index_prefix__ || @@__index_prefix__ || nil ) 60 | end 61 | extend self 62 | 63 | # Get or set the document type for this model, based on arguments. 64 | # 65 | # By default, uses ActiveSupport inflection, so a class named `Article` 66 | # will be stored as the `article` type. 67 | # 68 | # To get the document type: 69 | # 70 | # Article.document_type 71 | # 72 | # To set the document type: 73 | # 74 | # Article.document_type 'my-custom-type' 75 | # 76 | def document_type name=nil 77 | @document_type = name if name 78 | @document_type || klass.model_name.to_s.underscore 79 | end 80 | end 81 | 82 | module InstanceMethods 83 | 84 | # Proxy to class method `index_name`. 85 | # 86 | def index_name 87 | instance.class.tire.index_name 88 | end 89 | 90 | # Proxy to instance method `document_type`. 91 | # 92 | def document_type 93 | instance.class.tire.document_type 94 | end 95 | end 96 | 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/tire/model/percolate.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | # Contains support for the [percolation](http://www.elasticsearch.org/guide/reference/api/percolate.html) 5 | # feature of _Elasticsearch_. 6 | # 7 | module Percolate 8 | 9 | module ClassMethods 10 | 11 | # Set up the percolation when documents are being added to the index. 12 | # 13 | # Usage: 14 | # 15 | # class Article 16 | # # ... 17 | # percolate! 18 | # end 19 | # 20 | # First, you have to register a percolator query: 21 | # 22 | # Article.index.register_percolator_query('fail') { |query| query.string 'fail' } 23 | # 24 | # Then, when you update the index, matching queries are returned in the `matches` property: 25 | # 26 | # p Article.create(:title => 'This is a FAIL!').matches 27 | # 28 | # 29 | # You may pass a pattern to filter which percolator queries will be executed. 30 | # 31 | # See for more information. 32 | # 33 | def percolate!(pattern=true) 34 | @_percolator = pattern 35 | self 36 | end 37 | 38 | # A callback method for intercepting percolator matches. 39 | # 40 | # Usage: 41 | # 42 | # class Article 43 | # # ... 44 | # on_percolate do 45 | # puts "Article title “#{title}” matches queries: #{matches.inspect}" unless matches.empty? 46 | # end 47 | # end 48 | # 49 | # Based on the response received in `matches`, you may choose to fire notifications, 50 | # increment counters, send out e-mail alerts, etc. 51 | # 52 | def on_percolate(pattern=true,&block) 53 | percolate!(pattern) 54 | klass.after_update_elasticsearch_index(block) 55 | end 56 | 57 | # Returns the status or pattern of percolator for this class. 58 | # 59 | def percolator 60 | @_percolator 61 | end 62 | end 63 | 64 | module InstanceMethods 65 | 66 | # Run this document against registered percolator queries, without indexing it. 67 | # 68 | # First, register a percolator query: 69 | # 70 | # Article.index.register_percolator_query('fail') { |query| query.string 'fail' } 71 | # 72 | # Then, you may query the percolator endpoint with: 73 | # 74 | # p Article.new(:title => 'This is a FAIL!').percolate 75 | # 76 | # Optionally, you may pass a block to filter which percolator queries will be executed. 77 | # 78 | # See for more information. 79 | def percolate(&block) 80 | index.percolate instance, block 81 | end 82 | 83 | # Mark this instance for percolation when adding it to the index. 84 | # 85 | def percolate=(pattern) 86 | @_percolator = pattern 87 | end 88 | 89 | # Returns the status or pattern of percolator for this instance. 90 | # 91 | def percolator 92 | @_percolator || instance.class.tire.percolator || nil 93 | end 94 | end 95 | 96 | end 97 | 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/tire/model/persistence.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | # Allows to use _Elasticsearch_ as a primary database (storage). 5 | # 6 | # Contains all the `Tire::Model::Search` features and provides 7 | # an [_ActiveModel_](http://rubygems.org/gems/activemodel)-compatible 8 | # interface for persistance. 9 | # 10 | # Usage: 11 | # 12 | # class Article 13 | # include Tire::Model::Persistence 14 | # 15 | # property :title 16 | # end 17 | # 18 | # Article.create :id => 1, :title => 'One' 19 | # 20 | # article = Article.find 21 | # 22 | # article.destroy 23 | # 24 | module Persistence 25 | 26 | def self.included(base) 27 | 28 | base.class_eval do 29 | include ActiveModel::AttributeMethods 30 | include ActiveModel::Validations 31 | include ActiveModel::Serialization 32 | include ActiveModel::Serializers::JSON 33 | include ActiveModel::Naming 34 | include ActiveModel::Conversion 35 | 36 | extend ActiveModel::Callbacks 37 | define_model_callbacks :save, :destroy 38 | 39 | extend Persistence::Finders::ClassMethods 40 | extend Persistence::Attributes::ClassMethods 41 | include Persistence::Attributes::InstanceMethods 42 | include Persistence::Storage 43 | 44 | include Tire::Model::Search 45 | 46 | ['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each do |attr| 47 | define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value } 48 | define_method("#{attr}") { @attributes[attr] } 49 | end 50 | 51 | def self.search(*args, &block) 52 | args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash 53 | args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash } 54 | 55 | self.tire.search(*args, &block) 56 | end 57 | 58 | def self.multi_search(*args, &block) 59 | args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash 60 | args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash } 61 | 62 | self.tire.multi_search(*args, &block) 63 | end 64 | 65 | end 66 | 67 | end 68 | 69 | end 70 | 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/tire/model/persistence/finders.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | module Persistence 5 | 6 | # Provides infrastructure for an _ActiveRecord_-like interface for finding records. 7 | # 8 | module Finders 9 | 10 | module ClassMethods 11 | 12 | def find *args 13 | # TODO: Options like `sort` 14 | options = args.pop if args.last.is_a?(Hash) 15 | args.flatten! 16 | if args.size > 1 17 | Tire::Search::Search.new(index.name, :wrapper => self) do |search| 18 | search.query do |query| 19 | query.ids(args, document_type) 20 | end 21 | search.size args.size 22 | end.results 23 | else 24 | case args = args.pop 25 | when Fixnum, String 26 | index.retrieve document_type, args, :wrapper => self 27 | when :all, :first 28 | send(args) 29 | else 30 | raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument" 31 | end 32 | end 33 | end 34 | 35 | def all 36 | # TODO: Options like `sort`; Possibly `filters` 37 | s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all } 38 | s.version(true).results 39 | end 40 | 41 | def first 42 | # TODO: Options like `sort`; Possibly `filters` 43 | s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all }.size(1) 44 | s.version(true).results.first 45 | end 46 | 47 | end 48 | 49 | end 50 | 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/tire/model/persistence/storage.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | 4 | module Persistence 5 | 6 | # Provides infrastructure for storing records in _Elasticsearch_. 7 | # 8 | module Storage 9 | def self.included(base) 10 | base.class_eval do 11 | extend ClassMethods 12 | include InstanceMethods 13 | end 14 | end 15 | 16 | module ClassMethods 17 | def create(args={}) 18 | document = new(args) 19 | return false unless document.valid? 20 | if result = document.save 21 | document 22 | else 23 | result 24 | end 25 | end 26 | 27 | def delete(&block) 28 | DeleteByQuery.new(index_name, {:type => document_type}, &block).perform 29 | end 30 | end 31 | 32 | module InstanceMethods 33 | def update_attribute(name, value) 34 | __update_attributes name => value 35 | save 36 | end 37 | 38 | def update_attributes(attributes={}) 39 | __update_attributes attributes 40 | save 41 | end 42 | 43 | def update_index 44 | run_callbacks :update_elasticsearch_index do 45 | if destroyed? 46 | response = index.remove self 47 | else 48 | if response = index.store( self, {:percolate => percolator} ) 49 | self.id ||= response['_id'] 50 | self._index = response['_index'] 51 | self._type = response['_type'] 52 | self._version = response['_version'] 53 | self.matches = response['matches'] 54 | end 55 | end 56 | response 57 | end 58 | end 59 | 60 | def save 61 | return false unless valid? 62 | run_callbacks :save do 63 | response = update_index 64 | !! response['ok'] 65 | end 66 | end 67 | 68 | def destroy 69 | run_callbacks :destroy do 70 | @destroyed = true 71 | response = update_index 72 | ! response.nil? 73 | end 74 | end 75 | 76 | def destroyed? ; !!@destroyed; end 77 | def persisted? ; !!id && !!_version; end 78 | def new_record? ; !persisted?; end 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/tire/model/suggest.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Model 3 | module Suggest 4 | module ClassMethods 5 | def suggest(*args, &block) 6 | default_options = {:type => document_type, :index => index.name} 7 | 8 | if block_given? 9 | options = args.shift || {} 10 | else 11 | query, options = args 12 | options ||= {} 13 | end 14 | 15 | options = default_options.update(options) 16 | 17 | s = Tire::Suggest::Suggest.new(options.delete(:index), options) 18 | 19 | if block_given? 20 | block.arity < 1 ? s.instance_eval(&block) : block.call(s) 21 | else 22 | s.suggestion 'default_suggestion' do 23 | text query 24 | completion 'suggest' 25 | end 26 | end 27 | 28 | s.results 29 | 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tire/results/item.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Results 3 | 4 | class Item 5 | extend ActiveModel::Naming 6 | include ActiveModel::Conversion 7 | 8 | # Create new instance, recursively converting all Hashes to Item 9 | # and leaving everything else alone. 10 | # 11 | def initialize(args={}) 12 | raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair) 13 | @attributes = {} 14 | args.each_pair do |key, value| 15 | if value.is_a?(Array) 16 | @attributes[key.to_sym] = value.map { |item| @attributes[key.to_sym] = item.is_a?(Hash) ? Item.new(item.to_hash) : item } 17 | else 18 | @attributes[key.to_sym] = value.is_a?(Hash) ? Item.new(value.to_hash) : value 19 | end 20 | end 21 | end 22 | 23 | # Delegate method to a key in underlying hash, if present, otherwise return +nil+. 24 | # 25 | def method_missing(method_name, *arguments) 26 | @attributes[method_name.to_sym] 27 | end 28 | 29 | def respond_to?(method_name, include_private = false) 30 | (@attributes || {}).has_key?(method_name.to_sym) || super 31 | end 32 | 33 | def [](key) 34 | @attributes[key.to_sym] 35 | end 36 | 37 | alias :read_attribute_for_serialization :[] 38 | 39 | 40 | def id 41 | @attributes[:_id] || @attributes[:id] 42 | end 43 | 44 | def type 45 | @attributes[:_type] || @attributes[:type] 46 | end 47 | 48 | def persisted? 49 | !!id 50 | end 51 | 52 | def errors 53 | ActiveModel::Errors.new(self) 54 | end 55 | 56 | def valid? 57 | true 58 | end 59 | 60 | def to_key 61 | persisted? ? [id] : nil 62 | end 63 | 64 | def to_hash 65 | @attributes.reduce({}) do |sum, item| 66 | if item.last.is_a?(Array) 67 | sum[ item.first ] = item.last.map { |item| item.respond_to?(:to_hash) ? item.to_hash : item } 68 | else 69 | sum[ item.first ] = item.last.respond_to?(:to_hash) ? item.last.to_hash : item.last 70 | end 71 | sum 72 | end 73 | end 74 | 75 | def as_json(options=nil) 76 | hash = to_hash 77 | hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access.as_json(options) : hash.as_json(options) 78 | end 79 | 80 | def to_json(options=nil) 81 | as_json.to_json(options) 82 | end 83 | alias_method :to_indexed_json, :to_json 84 | 85 | # Let's pretend we're someone else in Rails 86 | # 87 | def class 88 | defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super 89 | rescue NameError 90 | super 91 | end 92 | 93 | def inspect 94 | s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" } 95 | %Q|| 96 | end 97 | 98 | end 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/tire/results/pagination.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Results 3 | 4 | # Adds support for WillPaginate and Kaminari 5 | # 6 | module Pagination 7 | 8 | def default_per_page 9 | 10 10 | end 11 | module_function :default_per_page 12 | 13 | def total_entries 14 | @total 15 | end 16 | 17 | def per_page 18 | (@options[:per_page] || @options[:size] || default_per_page ).to_i 19 | end 20 | 21 | def total_pages 22 | ( @total.to_f / per_page ).ceil 23 | end 24 | 25 | def current_page 26 | if @options[:page] 27 | @options[:page].to_i 28 | else 29 | (per_page + @options[:from].to_i) / per_page 30 | end 31 | end 32 | 33 | def previous_page 34 | current_page > 1 ? (current_page - 1) : nil 35 | end 36 | 37 | def next_page 38 | current_page < total_pages ? (current_page + 1) : nil 39 | end 40 | 41 | def offset 42 | per_page * (current_page - 1) 43 | end 44 | 45 | def out_of_bounds? 46 | current_page > total_pages 47 | end 48 | 49 | # Kaminari support 50 | # 51 | alias :limit_value :per_page 52 | alias :total_count :total_entries 53 | alias :num_pages :total_pages 54 | alias :offset_value :offset 55 | alias :out_of_range? :out_of_bounds? 56 | 57 | def first_page? 58 | current_page == 1 59 | end 60 | 61 | def last_page? 62 | current_page == total_pages 63 | end 64 | 65 | 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/tire/results/suggestions.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Results 3 | 4 | class Suggestions 5 | include Enumerable 6 | 7 | def initialize(response, options={}) 8 | @response = response 9 | @options = options 10 | @shards_info ||= @response.delete '_shards' 11 | @keys ||= @response.keys 12 | end 13 | 14 | def results 15 | return [] if failure? 16 | @results ||= @response 17 | end 18 | 19 | def keys 20 | @keys 21 | end 22 | 23 | def each(&block) 24 | results.each(&block) 25 | end 26 | 27 | def size 28 | results.size 29 | end 30 | 31 | def options(suggestion=:all) 32 | if suggestion == :all 33 | results.map {|k,v| v.map{|s| s['options']}}.flatten 34 | else 35 | results[suggestion.to_s].map{|s| s['options']}.flatten 36 | end 37 | end 38 | 39 | def texts(suggestion=:all) 40 | if suggestion == :all 41 | results.map {|k,v| v.map{|s| s['options'].map {|o| o['text']}}}.flatten 42 | else 43 | results[suggestion.to_s].map{|s| s['options'].map {|o| o['text']}}.flatten 44 | end 45 | end 46 | 47 | def payloads(suggestion=:all) 48 | if suggestion == :all 49 | results.map {|k,v| v.map{|s| s['options'].map {|o| o['payload']}}}.flatten 50 | else 51 | results[suggestion.to_s].map{|s| s['options'].map {|o| o['payload']}}.flatten 52 | end 53 | end 54 | 55 | def error 56 | @response['error'] 57 | end 58 | 59 | def success? 60 | error.to_s.empty? 61 | end 62 | 63 | def failure? 64 | ! success? 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/tire/rubyext/hash.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | 3 | def to_json(options=nil) 4 | MultiJson.encode(self) 5 | end unless respond_to?(:to_json) 6 | 7 | alias_method :to_indexed_json, :to_json 8 | end 9 | -------------------------------------------------------------------------------- /lib/tire/rubyext/ruby_1_8.rb: -------------------------------------------------------------------------------- 1 | require 'tire/rubyext/uri_escape' unless defined?(URI.encode_www_form_component) && defined?(URI.decode_www_form_component) 2 | -------------------------------------------------------------------------------- /lib/tire/rubyext/symbol.rb: -------------------------------------------------------------------------------- 1 | # ActiveModel::Serialization Ruby < 1.9.x compatibility 2 | 3 | class Symbol 4 | def <=> other 5 | self.to_s <=> other.to_s 6 | end unless method_defined?(:'<=>') 7 | 8 | def capitalize 9 | to_s.capitalize 10 | end unless method_defined?(:capitalize) 11 | end 12 | -------------------------------------------------------------------------------- /lib/tire/rubyext/uri_escape.rb: -------------------------------------------------------------------------------- 1 | # Steal the URI escape/unescape compatibility layer from Rack 2 | # 3 | # See 4 | 5 | # :stopdoc: 6 | 7 | # Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x 8 | # 9 | # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb 10 | # 11 | # 12 | 13 | module URI 14 | TBLENCWWWCOMP_ = {} # :nodoc: 15 | TBLDECWWWCOMP_ = {} # :nodoc: 16 | 17 | # Encode given +s+ to URL-encoded form data. 18 | # 19 | # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP 20 | # (ASCII space) to + and converts others to %XX. 21 | # 22 | # This is an implementation of 23 | # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data 24 | # 25 | # See URI.decode_www_form_component, URI.encode_www_form 26 | def self.encode_www_form_component(s) 27 | str = s.to_s 28 | if RUBY_VERSION < "1.9" && $KCODE =~ /u/i 29 | str.gsub(/([^ a-zA-Z0-9_.-]+)/) do 30 | '%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase 31 | end.tr(' ', '+') 32 | else 33 | if TBLENCWWWCOMP_.empty? 34 | tbl = {} 35 | 256.times do |i| 36 | tbl[i.chr] = '%%%02X' % i 37 | end 38 | tbl[' '] = '+' 39 | begin 40 | TBLENCWWWCOMP_.replace(tbl) 41 | TBLENCWWWCOMP_.freeze 42 | rescue 43 | end 44 | end 45 | str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]} 46 | end 47 | end 48 | 49 | # Decode given +str+ of URL-encoded form data. 50 | # 51 | # This decods + to SP. 52 | # 53 | # See URI.encode_www_form_component, URI.decode_www_form 54 | def self.decode_www_form_component(str, enc=nil) 55 | if TBLDECWWWCOMP_.empty? 56 | tbl = {} 57 | 256.times do |i| 58 | h, l = i>>4, i&15 59 | tbl['%%%X%X' % [h, l]] = i.chr 60 | tbl['%%%x%X' % [h, l]] = i.chr 61 | tbl['%%%X%x' % [h, l]] = i.chr 62 | tbl['%%%x%x' % [h, l]] = i.chr 63 | end 64 | tbl['+'] = ' ' 65 | begin 66 | TBLDECWWWCOMP_.replace(tbl) 67 | TBLDECWWWCOMP_.freeze 68 | rescue 69 | end 70 | end 71 | raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str 72 | str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]} 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/tire/search/facet.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | class Facet 5 | 6 | def initialize(name, options={}, &block) 7 | @name = name 8 | @options = options 9 | @value = {} 10 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 11 | end 12 | 13 | def terms(field, options={}) 14 | size = options.delete(:size) || 10 15 | all_terms = options.delete(:all_terms) || false 16 | @value[:terms] = if field.is_a?(Enumerable) and not field.is_a?(String) 17 | { :fields => field }.update({ :size => size, :all_terms => all_terms }).update(options) 18 | else 19 | { :field => field }.update({ :size => size, :all_terms => all_terms }).update(options) 20 | end 21 | self 22 | end 23 | 24 | def date(field, options={}) 25 | interval = { :interval => options.delete(:interval) || 'day' } 26 | fields = options[:value_field] || options[:value_script] ? { :key_field => field } : { :field => field } 27 | @value[:date_histogram] = {}.update(fields).update(interval).update(options) 28 | self 29 | end 30 | 31 | def range(field, ranges=[], options={}) 32 | @value[:range] = { :field => field, :ranges => ranges }.update(options) 33 | self 34 | end 35 | 36 | def histogram(field, options={}) 37 | @value[:histogram] = (options.delete(:histogram) || {:field => field}.update(options)) 38 | self 39 | end 40 | 41 | def statistical(field, options={}) 42 | @value[:statistical] = (options.delete(:statistical) || {:field => field}.update(options)) 43 | self 44 | end 45 | 46 | def geo_distance(field, point, ranges=[], options={}) 47 | @value[:geo_distance] = { field => point, :ranges => ranges }.update(options) 48 | self 49 | end 50 | 51 | def terms_stats(key_field, value_field, options={}) 52 | @value[:terms_stats] = {:key_field => key_field, :value_field => value_field}.update(options) 53 | self 54 | end 55 | 56 | def query(&block) 57 | @value[:query] = Query.new(&block).to_hash 58 | end 59 | 60 | def filter(type, options={}) 61 | @value[:filter] = Filter.new(type, options) 62 | self 63 | end 64 | 65 | def facet_filter(type, *options) 66 | @value[:facet_filter] = Filter.new(type, *options).to_hash 67 | self 68 | end 69 | 70 | def to_json(options={}) 71 | to_hash.to_json 72 | end 73 | 74 | def to_hash 75 | @value.update @options 76 | { @name => @value } 77 | end 78 | end 79 | 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/tire/search/filter.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | # http://www.elasticsearch.org/guide/reference/api/search/filter.html 5 | # http://www.elasticsearch.org/guide/reference/query-dsl/ 6 | # 7 | class Filter 8 | 9 | def initialize(type, *options) 10 | value = if options.size < 2 11 | options.first || {} 12 | else 13 | options # An +or+ filter encodes multiple filters as an array 14 | end 15 | @hash = { type => value } 16 | end 17 | 18 | def to_json(options={}) 19 | to_hash.to_json 20 | end 21 | 22 | def to_hash 23 | @hash 24 | end 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/tire/search/highlight.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | # http://www.elasticsearch.org/guide/reference/api/search/highlighting.html 5 | # 6 | class Highlight 7 | 8 | def initialize(*args) 9 | @options = (args.last.is_a?(Hash) && args.last.delete(:options)) || {} 10 | extract_highlight_tags 11 | @fields = args.inject({}) do |result, field| 12 | field.is_a?(Hash) ? result.update(field) : result[field.to_sym] = {}; result 13 | end 14 | end 15 | 16 | def to_json(options={}) 17 | to_hash.to_json 18 | end 19 | 20 | def to_hash 21 | { :fields => @fields }.update @options 22 | end 23 | 24 | private 25 | 26 | def extract_highlight_tags 27 | if tag = @options.delete(:tag) 28 | @options.update \ 29 | :pre_tags => [tag], 30 | :post_tags => [tag.to_s.gsub(/^<([a-z]+).*/, '')] 31 | end 32 | end 33 | 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tire/search/queries/custom_filters_score.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | # Custom Filters Score 5 | # ============== 6 | # 7 | # Author: Jerry Luk 8 | # 9 | # 10 | # Adds support for "custom_filters_score" queries in Tire DSL. 11 | # 12 | # It hooks into the Query class and inserts the custom_filters_score query types. 13 | # 14 | # 15 | # Usage: 16 | # ------ 17 | # 18 | # Require the component: 19 | # 20 | # require 'tire/queries/custom_filters_score' 21 | # 22 | # Example: 23 | # ------- 24 | # 25 | # Tire.search 'articles' do 26 | # query do 27 | # custom_filters_score do 28 | # query { term :title, 'Harry Potter' } 29 | # filter do 30 | # filter :match_all 31 | # boost 1.1 32 | # end 33 | # filter do 34 | # filter :term, :author => 'Rowling', 35 | # script '2.0' 36 | # end 37 | # score_mode 'total' 38 | # end 39 | # end 40 | # end 41 | # 42 | # For available options for these queries see: 43 | # 44 | # * 45 | # 46 | # 47 | class Query 48 | 49 | def custom_filters_score(&block) 50 | @custom_filters_score = CustomFiltersScoreQuery.new 51 | block.arity < 1 ? @custom_filters_score.instance_eval(&block) : block.call(@custom_filters_score) if 52 | block_given? 53 | @value[:custom_filters_score] = @custom_filters_score.to_hash 54 | @value 55 | end 56 | 57 | class CustomFiltersScoreQuery 58 | class CustomFilter 59 | def initialize(&block) 60 | @value = {} 61 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 62 | end 63 | 64 | def filter(type, *options) 65 | @value[:filter] = Filter.new(type, *options).to_hash 66 | @value 67 | end 68 | 69 | def boost(value) 70 | @value[:boost] = value 71 | @value 72 | end 73 | 74 | def script(value) 75 | @value[:script] = value 76 | @value 77 | end 78 | 79 | def to_hash 80 | @value 81 | end 82 | 83 | def to_json 84 | to_hash.to_json 85 | end 86 | end 87 | 88 | def initialize(&block) 89 | @value = {} 90 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 91 | end 92 | 93 | def query(options={}, &block) 94 | @value[:query] = Query.new(&block).to_hash 95 | @value 96 | end 97 | 98 | def filter(&block) 99 | custom_filter = CustomFilter.new 100 | block.arity < 1 ? custom_filter.instance_eval(&block) : block.call(custom_filter) if block_given? 101 | @value[:filters] ||= [] 102 | @value[:filters] << custom_filter.to_hash 103 | @value 104 | end 105 | 106 | def score_mode(value) 107 | @value[:score_mode] = value 108 | @value 109 | end 110 | 111 | def params(value) 112 | @value[:params] = value 113 | @value 114 | end 115 | 116 | def to_hash 117 | @value[:filters] ? 118 | @value : 119 | @value.merge(:filters => [CustomFilter.new{ filter(:match_all); boost(1) }.to_hash]) # Needs at least one filter 120 | end 121 | 122 | def to_json 123 | to_hash.to_json 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/tire/search/queries/match.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | class Query 4 | 5 | def match(field, value, options={}) 6 | if @value.empty? 7 | @value = MatchQuery.new(field, value, options).to_hash 8 | else 9 | MatchQuery.add(self, field, value, options) 10 | end 11 | @value 12 | end 13 | end 14 | 15 | class MatchQuery 16 | def initialize(field, value, options={}) 17 | query_options = { :query => value }.merge(options) 18 | 19 | if field.is_a?(Array) 20 | @value = { :multi_match => query_options.merge( :fields => field ) } 21 | else 22 | @value = { :match => { field => query_options } } 23 | end 24 | end 25 | 26 | def self.add(query, field, value, options={}) 27 | unless query.value[:bool] 28 | original_value = query.value.dup 29 | query.value = { :bool => {} } 30 | (query.value[:bool][:must] ||= []) << original_value 31 | end 32 | query.value[:bool][:must] << MatchQuery.new(field, value, options).to_hash 33 | end 34 | 35 | def to_hash 36 | @value 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/tire/search/scan.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | 5 | # Performs a "scan/scroll" search request, which obtains a `scroll_id` 6 | # and keeps returning documents matching the passed query (or all documents) in batches. 7 | # 8 | # You may want to iterate over the batches being returned: 9 | # 10 | # search = Tire::Search::Scan.new('articles') 11 | # search.each do |results| 12 | # puts results.map(&:title) 13 | # end 14 | # 15 | # The scan object has a fully Enumerable-compatible interface, so you may 16 | # call methods like `map` or `each_with_index` on it. 17 | # 18 | # To iterate over individual documents, use the `each_document` method: 19 | # 20 | # search.each_document do |document| 21 | # puts document.title 22 | # end 23 | # 24 | # You may limit the result set being returned by a regular Tire DSL query 25 | # (or a hash, if you prefer), passed as a second argument: 26 | # 27 | # search = Tire::Search::Scan.new('articles') do 28 | # query { term 'author.exact', 'John Smith' } 29 | # end 30 | # 31 | # The feature is also exposed in the Tire top-level DSL: 32 | # 33 | # search = Tire.scan 'articles' do 34 | # query { term 'author.exact', 'John Smith' } 35 | # end 36 | # 37 | # See Elasticsearch documentation for further reference: 38 | # 39 | # * http://www.elasticsearch.org/guide/reference/api/search/search-type.html 40 | # * http://www.elasticsearch.org/guide/reference/api/search/scroll.html 41 | # 42 | class Scan 43 | include Enumerable 44 | 45 | attr_reader :indices, :options, :search 46 | 47 | def initialize(indices=nil, options={}, &block) 48 | @indices = Array(indices) 49 | @options = options.update(:search_type => 'scan', :scroll => '10m') 50 | @seen = 0 51 | @search = Search.new(@indices, @options, &block) 52 | end 53 | 54 | def url; Configuration.url + "/_search/scroll"; end 55 | def params; @options.empty? ? '' : '?' + @options.to_param; end 56 | def results; @results || (__perform; @results); end 57 | def response; @response || (__perform; @response); end 58 | def json; @json || (__perform; @json); end 59 | def total; @total || (__perform; @total); end 60 | def seen; @seen || (__perform; @seen); end 61 | 62 | def scroll_id 63 | @scroll_id ||= @search.perform.json['_scroll_id'] 64 | end 65 | 66 | def each 67 | until results.empty? 68 | yield results.results 69 | __perform 70 | end 71 | end 72 | 73 | def each_document 74 | until results.empty? 75 | results.each { |item| yield item } 76 | __perform 77 | end 78 | end 79 | 80 | def size 81 | results.size 82 | end 83 | 84 | def __perform 85 | @response = Configuration.client.get [url, params].join, scroll_id 86 | @json = MultiJson.decode @response.body 87 | @results = Results::Collection.new @json, @options 88 | @total = @json['hits']['total'].to_i 89 | @seen += @results.size 90 | @scroll_id = @json['_scroll_id'] 91 | return self 92 | ensure 93 | __logged 94 | end 95 | 96 | def to_a; results; end; alias :to_ary :to_a 97 | def to_curl; %Q|curl -X GET '#{url}?pretty' -d '#{@scroll_id}'|; end 98 | 99 | def __logged(error=nil) 100 | if Configuration.logger 101 | Configuration.logger.log_request 'scroll', nil, to_curl 102 | 103 | took = @json['took'] rescue nil 104 | code = @response.code rescue nil 105 | body = "#{@seen}/#{@total} (#{@seen/@total.to_f*100}%)" rescue nil 106 | 107 | Configuration.logger.log_response code || 'N/A', took || 'N/A', body 108 | end 109 | end 110 | 111 | end 112 | 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/tire/search/script_field.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | # http://www.elasticsearch.org/guide/reference/api/search/script-fields.html 5 | # http://www.elasticsearch.org/guide/reference/modules/scripting.html 6 | 7 | class ScriptField 8 | 9 | def initialize(name, options) 10 | @hash = { name => options } 11 | end 12 | 13 | def to_json(options={}) 14 | to_hash.to_json 15 | end 16 | 17 | def to_hash 18 | @hash 19 | end 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/tire/search/sort.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Search 3 | 4 | class Sort 5 | def initialize(&block) 6 | @value = [] 7 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 8 | end 9 | 10 | def by(name, direction=nil) 11 | @value << ( direction ? { name => direction } : name ) 12 | self 13 | end 14 | 15 | def to_ary 16 | @value 17 | end 18 | 19 | def to_json(options={}) 20 | @value.to_json 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/tire/suggest.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Suggest 3 | class SuggestRequestFailed < StandardError; end 4 | 5 | class Suggest 6 | 7 | attr_reader :indices, :suggestion, :options 8 | 9 | def initialize(indices=nil, options={}, &block) 10 | if indices.is_a?(Hash) 11 | @indices = indices.keys 12 | else 13 | @indices = Array(indices) 14 | end 15 | 16 | #TODO no options for now 17 | @options = options 18 | 19 | @path = ['/', @indices.join(','), '_suggest'].compact.join('/').squeeze('/') 20 | 21 | block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given? 22 | end 23 | 24 | def suggestion(name, &block) 25 | @suggestion = Suggestion.new(name, &block) 26 | self 27 | end 28 | 29 | def multi(&block) 30 | @suggestion = MultiSuggestion.new(&block) 31 | self 32 | end 33 | 34 | def results 35 | @results || (perform; @results) 36 | end 37 | 38 | def response 39 | @response || (perform; @response) 40 | end 41 | 42 | def json 43 | @json || (perform; @json) 44 | end 45 | 46 | def url 47 | Configuration.url + @path 48 | end 49 | 50 | def params 51 | options = @options.except(:wrapper, :payload, :load) 52 | options.empty? ? '' : '?' + options.to_param 53 | end 54 | 55 | def perform 56 | @response = Configuration.client.get(self.url + self.params, self.to_json) 57 | if @response.failure? 58 | STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n" 59 | raise Tire::Search::SearchRequestFailed, @response.to_s 60 | end 61 | @json = MultiJson.decode(@response.body) 62 | @results = Results::Suggestions.new(@json, @options) 63 | return self 64 | ensure 65 | logged 66 | end 67 | 68 | def to_curl 69 | to_json_escaped = to_json.gsub("'",'\u0027') 70 | %Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '#{to_json_escaped}'| 71 | end 72 | 73 | def to_hash 74 | request = {} 75 | request.update( @suggestion.to_hash ) 76 | request 77 | end 78 | 79 | def to_json(options={}) 80 | payload = to_hash 81 | MultiJson.encode(payload, :pretty => Configuration.pretty) 82 | end 83 | 84 | def logged(endpoint='_search') 85 | if Configuration.logger 86 | 87 | Configuration.logger.log_request endpoint, indices, to_curl 88 | 89 | took = @json['took'] rescue nil 90 | code = @response.code rescue nil 91 | 92 | if Configuration.logger.level.to_s == 'debug' 93 | body = if @json 94 | MultiJson.encode( @json, :pretty => Configuration.pretty) 95 | else 96 | MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue '' 97 | end 98 | else 99 | body = '' 100 | end 101 | 102 | Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A' 103 | end 104 | end 105 | end 106 | end 107 | end 108 | 109 | -------------------------------------------------------------------------------- /lib/tire/suggest/suggestion.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | module Suggest 3 | 4 | class Suggestion 5 | attr_accessor :value, :name 6 | 7 | def initialize(name, &block) 8 | @name = name 9 | @value = {} 10 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 11 | end 12 | 13 | def text(value) 14 | @value[:text] = value 15 | self 16 | end 17 | 18 | def completion(value, options={}) 19 | @value[:completion] = {:field => value}.update(options) 20 | self 21 | end 22 | 23 | def term(value, options={}) 24 | @value[:term] = { :field => value }.update(options) 25 | self 26 | end 27 | 28 | def phrase(field, options={}, &block) 29 | @value[:phrase] = PhraseSuggester.new(field, options, &block).to_hash 30 | self 31 | end 32 | 33 | def to_hash 34 | {@name.to_sym => @value} 35 | end 36 | 37 | def to_json(options={}) 38 | to_hash.to_json 39 | end 40 | 41 | end 42 | 43 | # Used to generate phrase suggestions 44 | class PhraseSuggester 45 | 46 | def initialize(field, options={}, &block) 47 | @options = options 48 | @value = { :field => field } 49 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 50 | end 51 | 52 | def generator(field, options={}) 53 | @generators ||= [] 54 | @generators << { :field => field }.update(options).to_hash 55 | self 56 | end 57 | 58 | def smoothing(type, options={}) 59 | @value[:smoothing] = { type => options } 60 | end 61 | 62 | def to_json(options={}) 63 | to_hash.to_json 64 | end 65 | 66 | def to_hash 67 | @value.update(@options) 68 | @value.update( { :direct_generator => @generators } ) if @generators && @generators.size > 0 69 | 70 | @value 71 | end 72 | 73 | end 74 | 75 | class MultiSuggestion 76 | attr_accessor :suggestions 77 | 78 | def initialize(&block) 79 | @value = {} 80 | block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given? 81 | end 82 | 83 | def text(value) 84 | @global_text = value 85 | self 86 | end 87 | 88 | def suggestion(name, &block) 89 | @suggestions ||= {} 90 | @suggestions.update Suggestion.new(name, &block).to_hash 91 | self 92 | end 93 | 94 | def to_hash 95 | @value.update @suggestions 96 | @value[:text] = @global_text if @global_text 97 | @value 98 | end 99 | 100 | def to_json(options={}) 101 | to_hash.to_json 102 | end 103 | end 104 | end 105 | end -------------------------------------------------------------------------------- /lib/tire/utils.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Tire 4 | module Utils 5 | 6 | def escape(s) 7 | URI.encode_www_form_component(s.to_s) 8 | end 9 | 10 | def unescape(s) 11 | s = s.to_s.respond_to?(:force_encoding) ? s.to_s.force_encoding(Encoding::UTF_8) : s.to_s 12 | URI.decode_www_form_component(s) 13 | end 14 | 15 | extend self 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tire/version.rb: -------------------------------------------------------------------------------- 1 | module Tire 2 | VERSION = "0.6.2" 3 | 4 | CHANGELOG =<<-END 5 | IMPORTANT CHANGES LATELY: 6 | 7 | 19e524c [ACTIVEMODEL] Exposed the response from `MyModel#.update_index` as the `response` method on return value 8 | bfcde21 [#916] Added support for the Suggest API (@marc-villanueva) 9 | 710451d [#857] Added support for the Suggest API (@fbatista) 10 | END 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/articles/1.json: -------------------------------------------------------------------------------- 1 | {"title" : "One", "tags" : ["ruby"], "published_on" : "2011-01-01", "words" : 125, "draft" : true} 2 | -------------------------------------------------------------------------------- /test/fixtures/articles/2.json: -------------------------------------------------------------------------------- 1 | {"title" : "Two", "tags" : ["ruby", "python"], "published_on" : "2011-01-02", "words" : 250} 2 | -------------------------------------------------------------------------------- /test/fixtures/articles/3.json: -------------------------------------------------------------------------------- 1 | {"title" : "Three", "tags" : ["java"], "published_on" : "2011-01-02", "words" : 375} 2 | -------------------------------------------------------------------------------- /test/fixtures/articles/4.json: -------------------------------------------------------------------------------- 1 | {"title" : "Four", "tags" : ["erlang"], "published_on" : "2011-01-03", "words" : 250} 2 | -------------------------------------------------------------------------------- /test/fixtures/articles/5.json: -------------------------------------------------------------------------------- 1 | {"title" : "Five", "tags" : ["javascript", "java"], "published_on" : "2011-01-04", "words" : 125} 2 | -------------------------------------------------------------------------------- /test/integration/active_model_indexing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require File.expand_path('../../models/supermodel_article', __FILE__) 3 | 4 | module Tire 5 | 6 | class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase 7 | include Test::Integration 8 | 9 | class ::ActiveModelArticleWithCustomAsSerialization < ActiveModelArticleWithCallbacks 10 | mapping do 11 | indexes :title 12 | indexes :content 13 | indexes :characters, :as => 'content.length' 14 | indexes :readability, :as => proc { 15 | content.split(/\W/).reject { |t| t.blank? }.size / 16 | content.split(/\./).size 17 | } 18 | end 19 | end 20 | 21 | def setup 22 | super 23 | ActiveModelArticleWithCustomAsSerialization.index.delete 24 | end 25 | 26 | def teardown 27 | super 28 | ActiveModelArticleWithCustomAsSerialization.index.delete 29 | end 30 | 31 | context "ActiveModel serialization" do 32 | 33 | setup do 34 | @model = ActiveModelArticleWithCustomAsSerialization.new \ 35 | :id => 1, 36 | :title => 'Test article', 37 | :content => 'Lorem Ipsum. Dolor Sit Amet.' 38 | @model.update_index 39 | @model.index.refresh 40 | end 41 | 42 | should "serialize the content length" do 43 | m = ActiveModelArticleWithCustomAsSerialization.search('*').first 44 | assert_equal 28, m.characters 45 | assert_equal 2, m.readability 46 | end 47 | 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/integration/active_model_searchable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require File.expand_path('../../models/supermodel_article', __FILE__) 3 | 4 | module Tire 5 | 6 | class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase 7 | include Test::Integration 8 | 9 | def setup 10 | super 11 | Redis::Persistence.config.redis = Redis.new db: ENV['REDIS_PERSISTENCE_TEST_DATABASE'] || 14 12 | Redis::Persistence.config.redis.flushdb 13 | @model = SupermodelArticle.new :title => 'Test' 14 | end 15 | 16 | def teardown 17 | super 18 | SupermodelArticle.all.each { |a| a.destroy } 19 | end 20 | 21 | context "ActiveModel integration" do 22 | 23 | setup do 24 | Tire.index('supermodel_articles').delete 25 | load File.expand_path('../../models/supermodel_article.rb', __FILE__) 26 | end 27 | teardown { Tire.index('supermodel_articles').delete } 28 | 29 | should "configure mapping" do 30 | assert_equal 'czech', SupermodelArticle.mapping[:title][:analyzer] 31 | assert_equal 15, SupermodelArticle.mapping[:title][:boost] 32 | 33 | assert_equal 'czech', SupermodelArticle.index.mapping['supermodel_article']['properties']['title']['analyzer'] 34 | end 35 | 36 | should "save document into index on save and find it with score" do 37 | a = SupermodelArticle.new :title => 'Test' 38 | a.save 39 | id = a.id 40 | 41 | # Store document of another type in the index 42 | Index.new 'supermodel_articles' do 43 | store :type => 'other-thing', :title => 'Title for other thing' 44 | end 45 | 46 | a.index.refresh 47 | 48 | # The index should contain 2 documents 49 | assert_equal 2, Tire.search('supermodel_articles') { query { all } }.results.size 50 | 51 | results = SupermodelArticle.search 'test' 52 | 53 | # The model should find only 1 document 54 | assert_equal 1, results.count 55 | 56 | assert_instance_of Results::Item, results.first 57 | assert_equal 'Test', results.first.title 58 | assert_not_nil results.first._score 59 | assert_equal id.to_s, results.first.id.to_s 60 | end 61 | 62 | should "remove document from index on destroy" do 63 | a = SupermodelArticle.new :title => 'Test' 64 | a.save 65 | assert_equal 1, SupermodelArticle.all.size 66 | 67 | a.destroy 68 | assert_equal 0, SupermodelArticle.all.size 69 | 70 | a.index.refresh 71 | results = SupermodelArticle.search 'test' 72 | 73 | assert_equal 0, results.count 74 | end 75 | 76 | should "retrieve sorted documents by IDs returned from search" do 77 | SupermodelArticle.create :title => 'foo' 78 | SupermodelArticle.create :id => 'abc123', :title => 'bar' 79 | 80 | SupermodelArticle.index.refresh 81 | results = SupermodelArticle.search 'foo OR bar^100' 82 | 83 | assert_equal 2, results.count 84 | 85 | assert_equal 'bar', results.first.title 86 | assert_equal 'abc123', results.first.id 87 | end 88 | 89 | should "return facets" do 90 | a = SupermodelArticle.new :title => 'Test' 91 | a.save 92 | a.index.refresh 93 | 94 | s = SupermodelArticle.search do 95 | query { match :title, 'test' } 96 | facet 'title' do 97 | terms :title 98 | end 99 | end 100 | 101 | assert_equal 1, s.facets['title']['terms'][0]['count'] 102 | end 103 | 104 | context "within Rails" do 105 | 106 | setup do 107 | module ::Rails; end 108 | @article = SupermodelArticle.new :title => 'Test' 109 | @article.save 110 | @article.index.refresh 111 | end 112 | 113 | should "fake the underlying model with _source" do 114 | results = SupermodelArticle.search 'test' 115 | 116 | assert_instance_of Results::Item, results.first 117 | assert_instance_of SupermodelArticle, results.first.load 118 | assert_equal 'Test', results.first.load.title 119 | end 120 | 121 | should "load the record from database" do 122 | results = SupermodelArticle.search 'test', load: true 123 | 124 | assert_instance_of SupermodelArticle, results.first 125 | assert_equal 'Test', results.first.title 126 | end 127 | 128 | end 129 | 130 | end 131 | 132 | end 133 | 134 | end 135 | -------------------------------------------------------------------------------- /test/integration/boolean_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class BooleanQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Boolean queries" do 9 | 10 | should "allow to set multiple queries per condition" do 11 | s = Tire.search('articles-test') do 12 | query do 13 | boolean do 14 | must { term :tags, 'ruby' } 15 | must { term :tags, 'python' } 16 | end 17 | end 18 | end 19 | 20 | assert_equal 1, s.results.size 21 | assert_equal 'Two', s.results.first.title 22 | end 23 | 24 | should "allow to set multiple queries for multiple conditions" do 25 | s = Tire.search('articles-test') do 26 | query do 27 | boolean do 28 | must { term :tags, 'ruby' } 29 | should { term :tags, 'python' } 30 | end 31 | end 32 | end 33 | 34 | assert_equal 2, s.results.size 35 | assert_equal 'Two', s.results[0].title 36 | assert_equal 'One', s.results[1].title 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/integration/boosting_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class BoostingQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Boosting queries" do 9 | 10 | should "allow to set multiple queries per condition" do 11 | s = Tire.search('articles-test', search_type: 'dfs_query_then_fetch') do 12 | query do 13 | boosting negative_boost: 0.2 do 14 | positive { string "title:Two title:One tags:ruby tags:python" } 15 | negative { term :tags, 'python' } 16 | end 17 | end 18 | end 19 | 20 | assert_equal 'One', s.results[0].title 21 | assert_equal 'Two', s.results[1].title # Matches 'python', so is demoted 22 | end 23 | 24 | context "in the featured results scenario" do 25 | setup do 26 | # Tire.configure { logger STDERR } 27 | @index = Tire.index('featured-results-test') do 28 | delete 29 | create 30 | store title: 'Kitchen special tool', featured: true 31 | store title: 'Kitchen tool tool tool', featured: false 32 | store title: 'Garage tool', featured: false 33 | refresh 34 | end 35 | end 36 | 37 | teardown do 38 | @index.delete 39 | end 40 | 41 | should "return featured results first" do 42 | s = Tire.search('featured-results-test', search_type: 'dfs_query_then_fetch') do 43 | query do 44 | boosting negative_boost: 0.1 do 45 | positive do 46 | match :title, 'tool' 47 | end 48 | # The `negative` query runs _within_ the results of the `positive` query, 49 | # and "rescores" the documents which match it, lowering their score. 50 | negative do 51 | filtered do 52 | query { match :title, 'kitchen' } 53 | filter :term, featured: false 54 | end 55 | end 56 | end 57 | end 58 | end 59 | 60 | assert_equal 'Garage tool', s.results[0].title # Non-kitchen first 61 | assert_equal 'Kitchen special tool', s.results[1].title # Featured first 62 | assert_equal 'Kitchen tool tool tool', s.results[2].title # Rest 63 | end 64 | end 65 | 66 | end 67 | 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /test/integration/constant_score_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ConstantScoreQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | context "Constant score queries" do 8 | 9 | should "return the same score for all results" do 10 | s = Tire.search('articles-test') do 11 | query do 12 | constant_score do 13 | query do 14 | terms :tags, ['ruby', 'python'] 15 | end 16 | end 17 | end 18 | end 19 | 20 | assert_equal 2, s.results.size 21 | assert s.results[0]._score == s.results[1]._score 22 | end 23 | 24 | context "in the featured results scenario" do 25 | # Adapted from: http://www.fullscale.co/blog/2013/01/24/Implementing_Featured_Results_With_ElasticSearch.html 26 | setup do 27 | @index = Tire.index('featured-results-test') do 28 | delete 29 | create 30 | store title: 'Kitchen special tool', featured: true 31 | store title: 'Kitchen tool tool tool', featured: false 32 | store title: 'Garage tool', featured: false 33 | refresh 34 | end 35 | end 36 | 37 | teardown do 38 | @index.delete 39 | end 40 | 41 | 42 | should "return featured results first" do 43 | s = Tire.search('featured-results-test', search_type: 'dfs_query_then_fetch') do 44 | query do 45 | boolean do 46 | should do 47 | constant_score do 48 | query { match :title, 'tool' } 49 | filter :term, featured: true 50 | boost 100 51 | end 52 | end 53 | should do 54 | match :title, 'tool' 55 | end 56 | end 57 | end 58 | end 59 | 60 | assert_equal 'Kitchen special tool', s.results[0].title 61 | assert_equal 'Kitchen tool tool tool', s.results[1].title 62 | end 63 | end 64 | 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/integration/count_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class CountIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Count with search type" do 9 | 10 | should "return total number of hits for the query, but no hits" do 11 | s = Tire.search 'articles-test', :search_type => 'count' do 12 | query { term :tags, 'ruby' } 13 | end 14 | 15 | assert_equal 2, s.results.total 16 | assert_equal 0, s.results.count 17 | assert s.results.empty? 18 | end 19 | 20 | should "return facets in results" do 21 | s = Tire.search 'articles-test', :search_type => 'count' do 22 | query { term :tags, 'ruby' } 23 | facet('tags') { terms :tags } 24 | end 25 | 26 | assert ! s.results.facets['tags'].empty? 27 | assert_equal 2, s.results.facets['tags']['terms'].select { |t| t['term'] == 'ruby' }. first['count'] 28 | assert_equal 1, s.results.facets['tags']['terms'].select { |t| t['term'] == 'python' }.first['count'] 29 | end 30 | 31 | end 32 | 33 | context "Count with the count method" do 34 | setup { Tire.index('articles-test-count') { delete; create and store(title: 'Test') and refresh } } 35 | teardown { Tire.index('articles-test-count') { delete } } 36 | 37 | should "return number of documents in the index" do 38 | assert_equal 5, Tire.count('articles-test') 39 | end 40 | 41 | should "return number of documents in the index for specific query" do 42 | # Tire.configure { logger STDERR, level: 'debug' } 43 | count = Tire.count('articles-test') do 44 | term :tags, 'ruby' 45 | end 46 | assert_equal 2, count 47 | end 48 | 49 | should "return number of documents in multiple indices" do 50 | assert_equal 6, Tire.count(['articles-test', 'articles-test-count']) 51 | end 52 | 53 | should "allow access to the JSON and response" do 54 | c = Tire::Search::Count.new('articles-test') 55 | c.perform 56 | assert_equal 5, c.value 57 | assert_equal 0, c.json['_shards']['failed'] 58 | assert c.response.success?, "Response should be successful: #{c.response.inspect}" 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/integration/custom_filters_score_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class CustomFiltersScoreQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Custom filters score queries" do 9 | 10 | should "score the document based on a matching filter" do 11 | s = Tire.search('articles-test') do 12 | query do 13 | custom_filters_score do 14 | query { all } 15 | 16 | # Give documents over 300 words a score of 3 17 | filter do 18 | filter :range, words: { gt: 300 } 19 | boost 3 20 | end 21 | end 22 | end 23 | end 24 | 25 | assert_equal 3, s.results[0]._score 26 | assert_equal 1, s.results[1]._score 27 | end 28 | 29 | should "allow to use a script based boost factor" do 30 | s = Tire.search('articles-test') do 31 | query do 32 | custom_filters_score do 33 | query { all } 34 | 35 | # Give documents over 300 words a score of 3 36 | filter do 37 | filter :range, words: { gt: 300 } 38 | script 'doc.words.value * 2' 39 | end 40 | end 41 | end 42 | end 43 | 44 | # p s.results.to_a.map { |r| [r.title, r.words, r._score] } 45 | 46 | assert_equal 750, s.results[0]._score 47 | assert_equal 1, s.results[1]._score 48 | end 49 | 50 | should "allow to define multiple score factors" do 51 | s = Tire.search('articles-test') do 52 | query do 53 | custom_filters_score do 54 | query { all } 55 | 56 | # The more words a document contains, the more its score is boosted 57 | 58 | filter do 59 | filter :range, words: { to: 10 } 60 | boost 1 61 | end 62 | 63 | filter do 64 | filter :range, words: { to: 100 } 65 | boost 2 66 | end 67 | 68 | filter do 69 | filter :range, words: { to: 150 } 70 | boost 3 71 | end 72 | 73 | filter do 74 | filter :range, words: { to: 250 } 75 | boost 5 76 | end 77 | 78 | filter do 79 | filter :range, words: { to: 350 } 80 | boost 7 81 | end 82 | 83 | filter do 84 | filter :range, words: { from: 350 } 85 | boost 10 86 | end 87 | end 88 | end 89 | end 90 | 91 | # p s.results.to_a.map { |r| [r.title, r.words, r._score] } 92 | 93 | assert_equal 'Three', s.results[0].title 94 | assert_equal 375, s.results[0].words 95 | assert_equal 10, s.results[0]._score 96 | 97 | assert_equal 5, s.results[1]._score 98 | assert_equal 5, s.results[2]._score 99 | assert_equal 3, s.results[3]._score 100 | assert_equal 3, s.results[4]._score 101 | end 102 | end 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /test/integration/custom_score_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class CustomScoreQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Custom score queries" do 9 | 10 | should "allow to define custom score queries (base score on field value)" do 11 | s = Tire.search('articles-test') do 12 | query do 13 | # Give longer documents higher score 14 | # 15 | custom_score :script => "1.0 / doc['words'].value" do 16 | string "title:T*" 17 | end 18 | end 19 | end 20 | 21 | assert_equal 2, s.results.size 22 | assert_equal ['Two', 'Three'], s.results.map(&:title) 23 | 24 | assert s.results[0]._score > 0 25 | assert s.results[1]._score > 0 26 | assert s.results[0]._score > s.results[1]._score 27 | end 28 | 29 | should "allow to manipulate the default score (boost recent)" do 30 | s = Tire.search('articles-test') do 31 | query do 32 | # Boost recent documents 33 | # 34 | custom_score :script => "_score + ( doc['published_on'].date.getMillis() / time() )" do 35 | string 'title:F*' 36 | end 37 | end 38 | end 39 | 40 | assert_equal 2, s.results.size 41 | assert_equal ['Five', 'Four'], s.results.map(&:title) 42 | 43 | assert s.results[0]._score > 1 44 | assert s.results[1]._score > 1 45 | end 46 | 47 | should "allow to define arbitrary custom scoring" do 48 | s = Tire.search('articles-test') do 49 | query do 50 | # Replace documents score with the count of characters in their title 51 | # 52 | custom_score :script => "doc['title'].value.length()" do 53 | string "title:T*" 54 | end 55 | end 56 | end 57 | 58 | assert_equal 2, s.results.size 59 | assert_equal ['Three', 'Two'], s.results.map(&:title) 60 | 61 | assert_equal 5.0, s.results.max_score 62 | assert_equal 5.0, s.results[0]._score 63 | assert_equal 3.0, s.results[1]._score 64 | end 65 | 66 | should "allow to pass parameters to the script" do 67 | s = Tire.search('articles-test') do 68 | query do 69 | # Replace documents score with parameterized computation 70 | # 71 | custom_score :script => "doc['words'].value.doubleValue() / max(a, b)", 72 | :params => { :a => 1, :b => 2 } do 73 | string "title:T*" 74 | end 75 | end 76 | end 77 | 78 | assert_equal 2, s.results.size 79 | assert_equal ['Three', 'Two'], s.results.map(&:title) 80 | 81 | assert_equal 187.5, s.results[0]._score 82 | assert_equal 125.0, s.results[1]._score 83 | end 84 | 85 | end 86 | 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /test/integration/delete_by_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | class DeleteByQueryIntegrationTest < Test::Unit::TestCase 5 | include Test::Integration 6 | 7 | should "delete documents matching a query" do 8 | assert_python_size(1) 9 | delete_by_query 10 | assert_python_size(0) 11 | end 12 | 13 | should "leave documents not matching a query" do 14 | assert_python_size(1) 15 | delete_by_query('article', 'go') 16 | assert_python_size(1) 17 | end 18 | 19 | should "not delete documents with different types" do 20 | assert_python_size(1) 21 | delete_by_query('different_type') 22 | assert_python_size(1) 23 | end 24 | 25 | context "DSL" do 26 | should "delete documents matching a query" do 27 | assert_python_size(1) 28 | Tire.delete('articles-test') { term :tags, 'python' } 29 | assert_python_size(0) 30 | end 31 | end 32 | 33 | private 34 | 35 | def delete_by_query(type='article', token='python') 36 | Tire::DeleteByQuery.new('articles-test', :type => type) do 37 | term :tags, token 38 | end.perform 39 | end 40 | 41 | def assert_python_size(size) 42 | Tire.index('articles-test').refresh 43 | search = Tire.search('articles-test') { query { term :tags, 'python' } } 44 | assert_equal size, search.results.size 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/integration/dis_max_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class DisMaxQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Dis Max queries" do 9 | setup do 10 | Tire.index 'dis_max_test' do 11 | delete 12 | create 13 | 14 | store title: "It's an Albino, Albino, Albino thing!", text: "Albino, albino, albino! Wanna know about albino? ..." 15 | store title: "Albino Vampire Monkey Attacks!", text: "The night was just setting in when ..." 16 | store title: "Pinky Elephant", text: "An albino walks into a ZOO and ..." 17 | refresh 18 | end 19 | end 20 | 21 | teardown do 22 | Tire.index('dis_max_test').delete 23 | end 24 | 25 | should_eventually "boost matches in both fields" do 26 | dis_max = Tire.search 'dis_max_test' do 27 | query do 28 | dis_max do 29 | query { string "albino elephant", fields: ['title', 'text'] } 30 | end 31 | end 32 | end 33 | # p "DisMax:", dis_max.results.map(&:title) 34 | 35 | assert_equal 'Pinky Elephant', dis_max.results.first.title 36 | 37 | # NOTE: This gives exactly the same result as a boolean query: 38 | # boolean = Tire.search 'dis_max_test' do 39 | # query do 40 | # boolean do 41 | # should { string "albino", fields: ['title', 'text'] } 42 | # should { string "elephant", fields: ['title', 'text'] } 43 | # end 44 | # end 45 | # end 46 | # p "Boolean:", boolean.results.map(&:title) 47 | end 48 | 49 | should "allow to set multiple queries" do 50 | s = Tire.search('articles-test') do 51 | query do 52 | dis_max do 53 | query { term :tags, 'ruby' } 54 | query { term :tags, 'python' } 55 | end 56 | end 57 | end 58 | 59 | assert_equal 2, s.results.size 60 | assert_equal 'Two', s.results[0].title 61 | assert_equal 'One', s.results[1].title 62 | end 63 | 64 | end 65 | 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /test/integration/dsl_search_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class DSLSearchIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "DSL" do 9 | 10 | should "allow passing search payload as a Hash" do 11 | s = Tire.search 'articles-test', :query => { :query_string => { :query => 'ruby' } }, 12 | :facets => { 'tags' => { :filter => { :term => {:tags => 'ruby' } } } } 13 | 14 | assert_equal 2, s.results.count 15 | assert_equal 2, s.results.facets['tags']['count'] 16 | assert_match %r|articles-test/_search\?pretty' -d '{|, s.to_curl, 'Make sure to ignore payload in URL params' 17 | end 18 | 19 | should "allow passing URL parameters" do 20 | s = Tire.search 'articles-test', search_type: 'count', query: { match: { tags: 'ruby' } } 21 | 22 | assert_equal 0, s.results.count 23 | assert_equal 2, s.results.total 24 | assert_match %r|articles-test/_search.*search_type=count.*' -d '{|, s.to_curl 25 | end 26 | 27 | should "allow to pass document type in index name" do 28 | s = Tire.search 'articles-test/article', query: { match: { tags: 'ruby' } } 29 | 30 | assert_equal 2, s.results.total 31 | assert_match %r|articles-test/article/_search|, s.to_curl 32 | end 33 | 34 | should "allow building search query iteratively" do 35 | s = Tire.search 'articles-test' 36 | s.query { string 'T*' } 37 | s.filter :terms, :tags => ['java'] 38 | 39 | assert_equal 1, s.results.count 40 | end 41 | 42 | context "when passing the wrapper option" do 43 | class ::MyCustomWrapper < Tire::Results::Item 44 | def title_size 45 | self.title.size 46 | end 47 | end 48 | 49 | should "be allowed when passing a block" do 50 | s = Tire.search 'articles-test', wrapper: ::MyCustomWrapper do 51 | query { match :title, 'one' } 52 | end 53 | 54 | assert_equal ::MyCustomWrapper, s.options[:wrapper] 55 | 56 | assert_instance_of ::MyCustomWrapper, s.results.first 57 | assert_equal 3, s.results.first.title_size 58 | end 59 | 60 | should "be allowed when not passing a block" do 61 | s = Tire.search( 62 | 'articles-test', 63 | payload: { query: { match: { title: 'one' } } }, 64 | wrapper: ::MyCustomWrapper 65 | ) 66 | 67 | assert_equal ::MyCustomWrapper, s.options[:wrapper] 68 | 69 | assert_instance_of ::MyCustomWrapper, s.results.first 70 | assert_equal 3, s.results.first.title_size 71 | end 72 | end 73 | 74 | end 75 | 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /test/integration/explanation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ExplanationIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Explanation" do 9 | teardown { Tire.index('explanation-test').delete } 10 | 11 | setup do 12 | content = "A Fox one day fell into a deep well and could find no means of escape." 13 | 14 | Tire.index 'explanation-test' do 15 | delete 16 | create 17 | store :id => 1, :content => content 18 | refresh 19 | end 20 | end 21 | 22 | should "add '_explanation' field to the result item" do 23 | s = Tire.search 'explanation-test', :explain => true do 24 | query do 25 | boolean do 26 | should { string 'content:Fox' } 27 | end 28 | end 29 | end 30 | 31 | doc = s.results.first 32 | d = doc._explanation.details.first 33 | 34 | assert d.description.include?("product of:") 35 | assert_not_nil d.details 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/integration/filtered_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class FilteredQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Filtered queries" do 9 | 10 | should "restrict the results with a filter" do 11 | # 2.json > Begins with "T" and is tagged "ruby" 12 | 13 | s = Tire.search('articles-test') do 14 | query do 15 | filtered do 16 | query { string 'title:T*' } 17 | filter :terms, :tags => ['ruby'] 18 | end 19 | end 20 | end 21 | 22 | assert_equal 1, s.results.count 23 | assert_equal 'Two', s.results.first.title 24 | end 25 | 26 | should "restrict the results with multiple filters, chained with AND by default" do 27 | # 2.json > Is tagged "ruby" and has 250 words 28 | 29 | s = Tire.search('articles-test') do 30 | query do 31 | filtered do 32 | query { all } 33 | filter :terms, :tags => ['ruby', 'python'] 34 | filter :range, :words => { :from => '250', :to => '250' } 35 | end 36 | end 37 | end 38 | 39 | assert_equal 1, s.results.count 40 | assert_equal 'Two', s.results.first.title 41 | end 42 | 43 | should "restrict the results with multiple OR filters" do 44 | # 1.json > Is tagged "ruby" 45 | # 1.json > Is tagged "ruby" and has 250 words 46 | # 4.json > Has 250 words 47 | 48 | s = Tire.search('articles-test') do 49 | query do 50 | filtered do 51 | query { all } 52 | filter :or, { :terms => { :tags => ['ruby', 'python'] } }, 53 | { :range => { :words => { :from => '250', :to => '250' } } } 54 | end 55 | end 56 | end 57 | 58 | assert_equal 3, s.results.count 59 | assert_equal %w(Four One Two), s.results.map(&:title).sort 60 | end 61 | 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /test/integration/filters_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class FiltersIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Filters" do 9 | 10 | should "filter the results" do 11 | # 2.json > Begins with "T" and is tagged "ruby" 12 | 13 | s = Tire.search('articles-test') do 14 | query { string 'title:T*' } 15 | filter :terms, :tags => ['ruby'] 16 | end 17 | 18 | assert_equal 1, s.results.count 19 | assert_equal 'Two', s.results.first.title 20 | end 21 | 22 | should "filter the results with multiple terms" do 23 | # 2.json > Is tagged *both* "ruby" and "python" 24 | 25 | s = Tire.search('articles-test') do 26 | query { all } 27 | filter :terms, :tags => ['ruby', 'python'], :execution => 'and' 28 | end 29 | 30 | assert_equal 1, s.results.count 31 | assert_equal 'Two', s.results.first.title 32 | end 33 | 34 | should "filter the results with multiple 'or' filters" do 35 | # 4.json > Begins with "F" and is tagged "erlang" 36 | 37 | s = Tire.search('articles-test') do 38 | query { string 'title:F*' } 39 | filter :or, {:terms => {:tags => ['ruby']}}, 40 | {:terms => {:tags => ['erlang']}} 41 | end 42 | 43 | assert_equal 1, s.results.count 44 | assert_equal 'Four', s.results.first.title 45 | end 46 | 47 | should "filter the results with multiple 'and' filters" do 48 | # 5.json > Is tagged ["java", "javascript"] and is published on 2011-01-04 49 | 50 | s = Tire.search('articles-test') do 51 | filter :terms, :tags => ["java"] 52 | filter :term, :published_on => "2011-01-04" 53 | end 54 | 55 | assert_equal 1, s.results.count 56 | assert_equal 'Five', s.results.first.title 57 | end 58 | 59 | should "not influence facets" do 60 | s = Tire.search('articles-test') do 61 | query { string 'title:T*' } 62 | filter :terms, :tags => ['ruby'] 63 | 64 | facet('tags') { terms :tags } 65 | end 66 | 67 | assert_equal 1, s.results.count 68 | assert_equal 3, s.results.facets['tags']['terms'].size 69 | end 70 | 71 | end 72 | 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /test/integration/fuzzy_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class FuzzyQueryIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Fuzzy query" do 9 | should "fuzzily find article by tag" do 10 | results = Tire.search('articles-test') { query { fuzzy :tags, 'irlang' } }.results 11 | 12 | assert_equal 1, results.count 13 | assert_equal ["erlang"], results.first[:tags] 14 | end 15 | 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /test/integration/highlight_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class HighlightIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Highlight" do 9 | teardown { Tire.index('highlight-test').delete } 10 | 11 | should "add 'highlight' field to the result item" do 12 | # Tire::Configuration.logger STDERR, :level => 'debug' 13 | s = Tire.search('articles-test') do 14 | query { string 'Two' } 15 | highlight :title 16 | end 17 | 18 | doc = s.results.first 19 | 20 | assert_equal 1, doc.highlight.title.size 21 | assert doc.highlight.title.to_s.include?(''), "Highlight does not include default highlight tag" 22 | end 23 | 24 | should "highlight multiple fields with custom highlight tag" do 25 | s = Tire.search('articles-test') do 26 | query { string 'Two OR ruby' } 27 | highlight :tags, :title, :options => { :tag => '' } 28 | end 29 | 30 | doc = s.results.first 31 | 32 | assert_equal 1, doc.highlight.title.size 33 | assert_equal "Two", doc.highlight.title.first, "Highlight does not include highlight tag" 34 | assert_equal "ruby", doc.highlight.tags.first, "Highlight does not include highlight tag" 35 | end 36 | 37 | should "return entire content with highlighted fragments" do 38 | # Tire::Configuration.logger STDERR, :level => 'debug' 39 | 40 | content = "A Fox one day fell into a deep well and could find no means of escape. A Goat, overcome with thirst, came to the same well, and seeing the Fox, inquired if the water was good. Concealing his sad plight under a merry guise, the Fox indulged in a lavish praise of the water, saying it was excellent beyond measure, and encouraging him to descend. The Goat, mindful only of his thirst, thoughtlessly jumped down, but just as he drank, the Fox informed him of the difficulty they were both in and suggested a scheme for their common escape. \"If,\" said he, \"you will place your forefeet upon the wall and bend your head, I will run up your back and escape, and will help you out afterwards.\" The Goat readily assented and the Fox leaped upon his back. Steadying himself with the Goat horns, he safely reached the mouth of the well and made off as fast as he could. When the Goat upbraided him for breaking his promise, he turned around and cried out, \"You foolish old fellow! If you had as many brains in your head as you have hairs in your beard, you would never have gone down before you had inspected the way up, nor have exposed yourself to dangers from which you had no means of escape.\" Look before you leap." 41 | 42 | Tire.index 'highlight-test' do 43 | delete 44 | create 45 | store :id => 1, :content => content 46 | refresh 47 | end 48 | 49 | s = Tire.search('highlight-test') do 50 | query { string 'fox' } 51 | highlight :content => { :number_of_fragments => 0 } 52 | end 53 | 54 | doc = s.results.first 55 | assert_not_nil doc.highlight.content 56 | 57 | highlight = doc.highlight.content 58 | assert highlight.to_s.include?(''), "Highlight does not include default highlight tag" 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/integration/index_aliases_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'active_support/core_ext/numeric' 4 | require 'active_support/core_ext/date/calculations' 5 | 6 | module Tire 7 | 8 | class IndexAliasesIntegrationTest < Test::Unit::TestCase 9 | include Test::Integration 10 | 11 | context "With a filtered alias" do 12 | setup do 13 | 14 | @index = Tire.index 'index-original' do 15 | delete 16 | create 17 | end 18 | 19 | end 20 | 21 | teardown { Tire.index('index-original').delete } 22 | 23 | should "create the alias" do 24 | @index.add_alias 'index-aliased' 25 | assert_equal 1, @index.aliases.size 26 | end 27 | 28 | should "find only portion of documents in the filtered alias" do 29 | Tire.index 'index-original' do 30 | add_alias 'index-aliased', :filter => { :term => { :user => 'anne' } } 31 | store :title => 'Document 1', :user => 'anne' 32 | store :title => 'Document 2', :user => 'mary' 33 | 34 | refresh 35 | end 36 | 37 | assert_equal 2, Tire.search('index-original') { query { all } }.results.size 38 | assert_equal 1, Tire.search('index-aliased') { query { all } }.results.size 39 | end 40 | 41 | should "remove the alias" do 42 | @index.add_alias 'index-aliased' 43 | assert_equal 1, @index.aliases.size 44 | 45 | @index.remove_alias 'index-aliased' 46 | assert_equal 0, @index.aliases.size 47 | 48 | assert_raise Tire::Search::SearchRequestFailed do 49 | Tire.search('index-aliased') { query { all } }.results 50 | end 51 | end 52 | 53 | should "retrieve a list of aliases for an index" do 54 | @index.add_alias 'index-aliased' 55 | 56 | assert_equal ['index-aliased'], @index.aliases.map(&:name) 57 | end 58 | 59 | should "retrieve the properties of an alias" do 60 | @index.add_alias 'index-aliased', :routing => '1' 61 | 62 | assert_equal '1', @index.aliases('index-aliased').search_routing 63 | end 64 | end 65 | 66 | context "In the 'sliding window' scenario" do 67 | 68 | setup do 69 | WINDOW_SIZE_IN_WEEKS = 4 70 | 71 | @indices = WINDOW_SIZE_IN_WEEKS.times.map { |number| "articles_#{number.weeks.ago.strftime('%Y-%m-%d')}" } 72 | 73 | @indices.each_with_index do |name,i| 74 | Tire.index(name) do 75 | delete 76 | create 77 | store :title => "Document #{i}" 78 | refresh 79 | end 80 | Alias.new(:name => "articles_current") { |a| a.indices(name) and a.save } 81 | end 82 | end 83 | 84 | teardown do 85 | @indices.each { |index| Tire.index(index).delete } 86 | end 87 | 88 | should "add a new index to alias" do 89 | @indices << "articles_#{(WINDOW_SIZE_IN_WEEKS+1).weeks.ago.strftime('%Y-%m-%d')}" 90 | Tire.index(@indices.last).create 91 | Alias.new(:name => "articles_current") { |a| a.index @indices.last and a.save } 92 | 93 | a = Alias.find("articles_current") 94 | assert_equal 5, a.indices.size 95 | end 96 | 97 | should "remove the stale index from the alias" do 98 | Alias.find("articles_current") do |a| 99 | # Remove all indices older then 2 weeks from the alias 100 | a.indices.delete_if do |i| 101 | Time.parse( i.gsub(/articles_/, '') ) < 2.weeks.ago rescue false 102 | end 103 | a.save 104 | end 105 | 106 | assert_equal 2, Alias.find("articles_current").indices.size 107 | end 108 | 109 | should "search within the alias" do 110 | Alias.find("articles_current") do |a| 111 | a.indices.clear and a.indices @indices[0..1] and a.save 112 | end 113 | 114 | assert_equal 4, Tire.search(@indices) { query {all} }.results.size 115 | assert_equal 2, Tire.search("articles_current") { query {all} }.results.size 116 | end 117 | 118 | end 119 | 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /test/integration/index_mapping_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class IndexMappingIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Default mapping" do 9 | teardown { Tire.index('mapped-index').delete; sleep 0.1 } 10 | 11 | should "create and return the default mapping as a Hash" do 12 | 13 | index = Tire.index 'mapped-index' do 14 | create 15 | store :type => :article, :title => 'One' 16 | refresh 17 | sleep 1 18 | end 19 | 20 | assert_equal 'string', index.mapping['article']['properties']['title']['type'], index.mapping.inspect 21 | assert_nil index.mapping['article']['properties']['title']['boost'], index.mapping.inspect 22 | end 23 | end 24 | 25 | context "Creating index with mapping" do 26 | teardown { Tire.index('mapped-index').delete; sleep 0.1 } 27 | 28 | should "create the specified mapping" do 29 | index = Tire.index 'mapped-index' do 30 | create mappings: { 31 | article: { 32 | _all: { enabled: false }, 33 | properties: { 34 | title: { type: 'string', boost: 2.0, store: 'yes' } 35 | } 36 | } 37 | } 38 | end 39 | 40 | # p index.mapping 41 | assert_equal false, index.mapping['article']['_all']['enabled'], index.mapping.inspect 42 | assert_equal 2.0, index.mapping['article']['properties']['title']['boost'], index.mapping.inspect 43 | end 44 | end 45 | 46 | context "Update mapping" do 47 | setup { Tire.index("mapped-index").create; sleep 1 } 48 | teardown { Tire.index("mapped-index").delete; sleep 0.1 } 49 | 50 | should "update the mapping for type" do 51 | index = Tire.index("mapped-index") 52 | 53 | index.mapping "article", :properties => { :body => { :type => "string" } } 54 | assert_equal({ "type" => "string" }, index.mapping["article"]["properties"]["body"]) 55 | 56 | assert index.mapping("article", :properties => { :title => { :type => "string" } }) 57 | 58 | mapping = index.mapping 59 | 60 | # Verify return value 61 | assert mapping, index.response.inspect 62 | 63 | # Verify response 64 | assert_equal( { "type" => "string" }, mapping["article"]["properties"]["body"] ) 65 | assert_equal( { "type" => "string" }, mapping["article"]["properties"]["title"] ) 66 | end 67 | 68 | should "fail to update the mapping in an incompatible way" do 69 | index = Tire.index("mapped-index") 70 | 71 | # 1. Update initial index mapping 72 | assert index.mapping "article", properties: { body: { type: "string" } } 73 | assert_equal( { "type" => "string" }, index.mapping["article"]["properties"]["body"] ) 74 | 75 | # 2. Attempt to update the mapping in incompatible way (change property type) 76 | mapping = index.mapping "article", :properties => { :body => { :type => "integer" } } 77 | 78 | # Verify return value 79 | assert !mapping, index.response.inspect 80 | # 81 | # Verify response 82 | assert_match /MergeMappingException/, index.response.body 83 | end 84 | 85 | should "honor the `ignore_conflicts` option" do 86 | index = Tire.index("mapped-index") 87 | 88 | # 1. Update initial index mapping 89 | assert index.mapping "article", properties: { body: { type: "string" } } 90 | assert_equal( { "type" => "string" }, index.mapping["article"]["properties"]["body"] ) 91 | 92 | # 2. Attempt to update the mapping in incompatible way and ignore conflicts 93 | mapping = index.mapping "article", ignore_conflicts: true, properties: { body: { type: "integer" } } 94 | 95 | # Verify return value (true since we ignore conflicts) 96 | assert mapping, index.response.inspect 97 | end 98 | 99 | end 100 | 101 | context "Delete mapping" do 102 | setup { Tire.index("mapped-index").create; sleep 1 } 103 | teardown { Tire.index("mapped-index").delete; sleep 0.1 } 104 | 105 | should "delete the mapping for type" do 106 | index = Tire.index("mapped-index") 107 | 108 | # 1. Update initial index mapping 109 | assert index.mapping 'article', properties: { body: { type: "string" } } 110 | 111 | assert index.delete_mapping 'article' 112 | assert index.mapping.empty?, index.response.inspect 113 | end 114 | end 115 | 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /test/integration/index_store_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class IndexStoreIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Storing the documents in index" do 9 | 10 | setup do 11 | Tire.index 'articles-test-ids' do 12 | delete 13 | create 14 | 15 | store :id => 1, :title => 'One' 16 | store :id => 2, :title => 'Two' 17 | store :id => 3, :title => 'Three' 18 | store :id => 4, :title => 'Four' 19 | store :id => 4, :title => 'Four' 20 | 21 | refresh 22 | end 23 | end 24 | 25 | teardown do 26 | Tire.index('articles-test-ids').delete 27 | Tire.index('articles-test-types').delete 28 | end 29 | 30 | should "happen in existing index" do 31 | assert Tire.index("articles-test-ids").exists? 32 | assert ! Tire.index("four-oh-four-index").exists? 33 | end 34 | 35 | should "store hashes under their IDs" do 36 | s = Tire.search('articles-test-ids') { query { string '*' } } 37 | 38 | assert_equal 4, s.results.count 39 | 40 | document = Tire.index('articles-test-ids').retrieve :document, 4 41 | assert_equal 'Four', document.title 42 | assert_equal 2, document._version.to_i 43 | 44 | end 45 | 46 | should "store documents as proper types" do 47 | Tire.index 'articles-test-types' do 48 | delete 49 | create 50 | store :type => 'my_type', :title => 'One' 51 | refresh 52 | end 53 | 54 | s = Tire.search('articles-test-types/my_type') { query { all } } 55 | assert_equal 1, s.results.count 56 | assert_equal 'my_type', s.results.first.type 57 | end 58 | 59 | end 60 | 61 | context "Removing documents from the index" do 62 | 63 | teardown { Tire.index('articles-test-remove').delete } 64 | 65 | setup do 66 | Tire.index 'articles-test-remove' do 67 | delete 68 | create 69 | store :id => 1, :title => 'One' 70 | store :id => 2, :title => 'Two' 71 | refresh 72 | end 73 | end 74 | 75 | should "remove document from the index" do 76 | assert_equal 2, Tire.search('articles-test-remove') { query { string '*' } }.results.count 77 | 78 | assert_nothing_raised do 79 | assert Tire.index('articles-test-remove').remove 1 80 | assert ! Tire.index('articles-test-remove').remove(1) 81 | end 82 | end 83 | 84 | end 85 | 86 | context "Retrieving documents from the index" do 87 | 88 | teardown { Tire.index('articles-test-retrieve').delete } 89 | 90 | setup do 91 | Tire.index 'articles-test-retrieve' do 92 | delete 93 | create 94 | store :id => 1, :title => 'One' 95 | store :id => 2, :title => 'Two' 96 | refresh 97 | end 98 | end 99 | 100 | should "retrieve document from the index" do 101 | assert_instance_of Tire::Results::Item, Tire.index('articles-test-retrieve').retrieve(:document, 1) 102 | end 103 | 104 | should "return nil when retrieving missing document" do 105 | assert_nil Tire.index('articles-test-retrieve').retrieve :document, 4 106 | end 107 | 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /test/integration/match_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class MatchQueryIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Match query" do 9 | setup do 10 | Tire.index 'match-query-test' do 11 | delete 12 | create settings: { index: { number_of_shards: 1, number_of_replicas: 0 } }, 13 | mappings: { 14 | document: { properties: { 15 | last_name: { type: 'string', analyzer: 'english' }, 16 | age: { type: 'integer' } 17 | } } 18 | } 19 | 20 | store first_name: 'John', last_name: 'Smith', age: 30, gender: 'male' 21 | store first_name: 'John', last_name: 'Smithson', age: 25, gender: 'male' 22 | store first_name: 'Adam', last_name: 'Smith', age: 75, gender: 'male' 23 | store first_name: 'Mary', last_name: 'John', age: 30, gender: 'female' 24 | refresh 25 | end 26 | end 27 | 28 | teardown do 29 | Tire.index('match-query-test').delete 30 | end 31 | 32 | should "find documents by single field" do 33 | s = Tire.search 'match-query-test' do 34 | query do 35 | match :last_name, 'Smith' 36 | end 37 | end 38 | 39 | assert_equal 2, s.results.count 40 | end 41 | 42 | should "find document by multiple fields with multi_match" do 43 | s = Tire.search 'match-query-test' do 44 | query do 45 | match [:first_name, :last_name], 'John' 46 | end 47 | end 48 | 49 | assert_equal 3, s.results.count 50 | end 51 | 52 | should "find documents by prefix" do 53 | s = Tire.search 'match-query-test' do 54 | query do 55 | match :last_name, 'Smi', type: 'phrase_prefix' 56 | end 57 | end 58 | 59 | assert_equal 3, s.results.count 60 | end 61 | 62 | should "automatically create a boolean query when called repeatedly" do 63 | s = Tire.search 'match-query-test' do 64 | query do 65 | match [:first_name, :last_name], 'John' 66 | match :age, 30 67 | match :gender, 'male' 68 | end 69 | # puts to_curl 70 | end 71 | 72 | assert_equal 1, s.results.count 73 | end 74 | 75 | end 76 | 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /test/integration/multi_search_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class MultiSearchIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Multi search" do 9 | # Tire.configure { logger STDERR } 10 | setup do 11 | Tire.index 'multi-search-test-1' do 12 | delete 13 | create 14 | store first_name: 'John', last_name: 'Smith', age: 30, gender: 'male' 15 | store first_name: 'John', last_name: 'Smithson', age: 25, gender: 'male' 16 | store first_name: 'Mary', last_name: 'Smith', age: 20, gender: 'female' 17 | refresh 18 | end 19 | Tire.index 'multi-search-test-2' do 20 | delete 21 | create 22 | store first_name: 'John', last_name: 'Milton', age: 35, gender: 'male' 23 | store first_name: 'Mary', last_name: 'Milson', age: 44, gender: 'female' 24 | store first_name: 'Mary', last_name: 'Reilly', age: 55, gender: 'female' 25 | refresh 26 | end 27 | end 28 | 29 | teardown do 30 | Tire.index('multi-search-test-1').delete 31 | Tire.index('multi-search-test-2').delete 32 | end 33 | 34 | should "return multiple results" do 35 | s = Tire.multi_search 'multi-search-test-1' do 36 | search :johns do 37 | query { match :_all, 'john' } 38 | end 39 | search :males do 40 | query { match :gender, 'male' } 41 | end 42 | search :facets, search_type: 'count' do 43 | facet('age') { statistical :age } 44 | end 45 | end 46 | 47 | assert_equal 3, s.results.size 48 | 49 | assert_equal 2, s.results[:johns].size 50 | assert_equal 2, s.results[:males].size 51 | 52 | assert s.results[:facets].results.empty?, "Results not empty? #{s.results[:facets].results}" 53 | assert_equal 75.0, s.results[:facets].facets['age']['total'] 54 | end 55 | 56 | should "mix named and numbered searches" do 57 | s = Tire.multi_search 'multi-search-test-1' do 58 | search(:johns) { query { match :_all, 'john' } } 59 | search { query { match :_all, 'mary' } } 60 | end 61 | 62 | assert_equal 2, s.results.size 63 | 64 | assert_equal 2, s.results[:johns].size 65 | assert_equal 1, s.results[1].size 66 | end 67 | 68 | should "iterate over mixed searches" do 69 | s = Tire.multi_search 'multi-search-test-1' do 70 | search(:johns) { query { match :_all, 'john' } } 71 | search { query { match :_all, 'mary' } } 72 | end 73 | 74 | assert_equal [:johns, 1], s.searches.names 75 | assert_equal [:johns, 1], s.results.to_hash.keys 76 | 77 | s.results.each_with_index do |results, i| 78 | assert_equal 2, results.size if i == 0 79 | assert_equal 1, results.size if i == 1 80 | end 81 | 82 | s.results.each_pair do |name, results| 83 | assert_equal 2, results.size if name == :johns 84 | assert_equal 1, results.size if name == 1 85 | end 86 | end 87 | 88 | should "return results from different indices" do 89 | s = Tire.multi_search do 90 | search( index: 'multi-search-test-1' ) { query { match :_all, 'john' } } 91 | search( index: 'multi-search-test-2' ) { query { match :_all, 'john' } } 92 | end 93 | 94 | assert_equal 2, s.results[0].size 95 | assert_equal 1, s.results[1].size 96 | end 97 | 98 | should "return error for failed searches" do 99 | s = Tire.multi_search 'multi-search-test-1' do 100 | search() { query { match :_all, 'john' } } 101 | search() { query { string '[x' } } 102 | end 103 | 104 | assert_equal 2, s.results[0].size 105 | assert s.results[0].success? 106 | 107 | assert_equal 0, s.results[1].size 108 | assert s.results[1].failure? 109 | end 110 | end 111 | 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /test/integration/nested_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class NestedQueryTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context 'Nested queries' do 9 | 10 | setup do 11 | @index = Tire.index('products-test') do 12 | delete 13 | 14 | create mappings: { 15 | product: { 16 | properties: { 17 | name: { type: 'string' }, 18 | variants: { type: 'nested', size: 'string', color: 'string' } 19 | } 20 | } 21 | } 22 | 23 | store type: 'product', 24 | name: 'Duck Shirt', 25 | variants: [{ size: 'M', color: 'yellow'}, { size: 'L', color: 'silver'}] 26 | store type: 'product', 27 | name: 'Western Shirt', 28 | variants: [{ size: 'S', color: 'yellow'}, { size: 'M', color: 'silver'}] 29 | 30 | refresh 31 | end 32 | end 33 | 34 | # teardown { @index.delete } 35 | 36 | should "not return a results when properties match for different objects" do 37 | s = Tire.search @index.name do 38 | query do 39 | nested path: 'variants' do 40 | query do 41 | boolean do 42 | # No product matches size "S" and color "silver" 43 | must { match 'variants.size', 'S' } 44 | must { match 'variants.color', 'silver'} 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | assert_equal 0, s.results.size 52 | end 53 | 54 | should "return all matching documents when nested documents meet criteria" do 55 | s = Tire.search @index.name do 56 | query do 57 | nested path: 'variants' do 58 | query do 59 | match 'variants.size', 'M' 60 | end 61 | end 62 | end 63 | end 64 | 65 | assert_equal 2, s.results.size 66 | end 67 | 68 | should "return matching document when a nested document meets all criteria" do 69 | s = Tire.search @index.name do 70 | query do 71 | nested path: 'variants' do 72 | query do 73 | boolean do 74 | must { match 'variants.size', 'M' } 75 | must { match 'variants.color', 'silver'} 76 | end 77 | end 78 | end 79 | end 80 | end 81 | 82 | assert_equal 1, s.results.size 83 | assert_equal 'Western Shirt', s.results.first.name 84 | end 85 | 86 | should "return matching document when both the query and nested document meet all criteria" do 87 | s = Tire.search @index.name do 88 | query do 89 | boolean do 90 | must do 91 | match 'name', 'Western' 92 | end 93 | must do 94 | nested path: 'variants' do 95 | query do 96 | match 'variants.size', 'M' 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | 104 | assert_equal 1, s.results.size 105 | assert_equal 'Western Shirt', s.results.first.name 106 | end 107 | 108 | should "not return results when the query and the nested document contradict" do 109 | s = Tire.search @index.name do 110 | query do 111 | boolean do 112 | must do 113 | match 'name', 'Duck' 114 | end 115 | must do 116 | nested path: 'variants' do 117 | query do 118 | boolean do 119 | must { match 'variants.size', 'M' } 120 | must { match 'variants.color', 'silver'} 121 | end 122 | end 123 | end 124 | end 125 | end 126 | end 127 | end 128 | 129 | assert_equal 0, s.results.size 130 | end 131 | 132 | end 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /test/integration/prefix_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class PrefixQueryTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Prefix queries" do 9 | 10 | should "search by a prefix" do 11 | s = Tire.search('articles-test') do 12 | query do 13 | # "on" => "One" 14 | prefix :title, "on" 15 | end 16 | end 17 | 18 | assert_equal 1, s.results.size 19 | assert_equal ['One'], s.results.map(&:title) 20 | end 21 | 22 | should "allow to specify boost" do 23 | s = Tire.search('articles-test') do 24 | query do 25 | boolean do 26 | # "on" => "One", boost it 27 | should { prefix :title, "on", :boost => 2.0 } 28 | should { all } 29 | end 30 | end 31 | sort { by :_score } 32 | end 33 | 34 | assert_equal 5, s.results.size 35 | assert_equal 'One', s.results.first.title 36 | 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/integration/query_return_version_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class DslVersionIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "DSL Version" do 9 | 10 | setup do 11 | Tire.index 'articles-test-ids' do 12 | delete 13 | create 14 | 15 | store :id => 1, :title => 'One' 16 | store :id => 2, :title => 'Two' 17 | 18 | refresh 19 | end 20 | end 21 | 22 | teardown { Tire.index('articles-test-ids').delete } 23 | 24 | should "returns actual version (non-nil) value for records when 'version' is true" do 25 | s = Tire.search('articles-test-ids') do 26 | version true 27 | query { string 'One' } 28 | end 29 | 30 | assert_equal 1, s.results.count 31 | 32 | document = s.results.first 33 | assert_equal 'One', document.title 34 | assert_equal 1, document._version.to_i 35 | 36 | end 37 | 38 | should "returns a nil version field when 'version' is false" do 39 | s = Tire.search('articles-test-ids') do 40 | version false 41 | query { string 'One' } 42 | end 43 | 44 | assert_equal 1, s.results.count 45 | 46 | document = s.results.first 47 | assert_equal 'One', document.title 48 | assert_nil document._version 49 | 50 | end 51 | 52 | should "returns a nil version field when 'version' is not included" do 53 | s = Tire.search('articles-test-ids') do 54 | query { string 'One' } 55 | end 56 | 57 | assert_equal 1, s.results.count 58 | 59 | document = s.results.first 60 | assert_equal 'One', document.title 61 | assert_nil document._version 62 | 63 | end 64 | 65 | end 66 | 67 | end 68 | 69 | end 70 | 71 | -------------------------------------------------------------------------------- /test/integration/query_string_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class QueryStringIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Searching for query string" do 9 | 10 | should "find article by title" do 11 | q = 'title:one' 12 | assert_equal 1, string_query(q).results.count 13 | assert_equal 'One', string_query(q).results.first[:title] 14 | end 15 | 16 | should "find articles by title with boosting" do 17 | q = 'title:one^100 OR title:two' 18 | assert_equal 2, string_query(q).results.count 19 | assert_equal 'One', string_query(q).results.first[:title] 20 | end 21 | 22 | should "find articles by tags" do 23 | q = 'tags:ruby AND tags:python' 24 | assert_equal 1, string_query(q).results.count 25 | assert_equal 'Two', string_query(q).results.first[:title] 26 | end 27 | 28 | should "find any article with tags" do 29 | q = 'tags:ruby OR tags:python OR tags:java' 30 | assert_equal 4, string_query(q).results.count 31 | end 32 | 33 | should "pass options to query definition" do 34 | s = Tire.search 'articles-test' do 35 | query do 36 | string 'ruby python', :default_operator => 'AND' 37 | end 38 | end 39 | assert_equal 1, s.results.count 40 | end 41 | 42 | end 43 | 44 | private 45 | 46 | def string_query(q) 47 | Tire.search('articles-test') { query { string q } } 48 | end 49 | 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /test/integration/range_queries_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class RangeQueriesIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Range queries" do 9 | 10 | should "allow simple range queries" do 11 | s = Tire.search('articles-test') do 12 | query do 13 | range :words, { :gte => 250 } 14 | end 15 | end 16 | 17 | assert_equal 3, s.results.size 18 | assert_equal ['Two', 'Three', 'Four'].sort, s.results.map(&:title).sort 19 | end 20 | 21 | should "allow combined range queries" do 22 | s = Tire.search('articles-test') do 23 | query do 24 | range :words, { :gte => 250, :lt => 375 } 25 | end 26 | end 27 | 28 | assert_equal 2, s.results.size 29 | assert_equal ['Two', 'Four'].sort, s.results.map(&:title).sort 30 | end 31 | 32 | end 33 | 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /test/integration/reindex_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ReindexIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Reindex" do 9 | setup do 10 | Tire.index('reindex-test-new').delete 11 | 12 | documents = (1..100).map { |i| { id: i, type: 'test', title: "Document #{i}" } } 13 | 14 | Tire.index 'reindex-test' do 15 | delete 16 | create :settings => { :number_of_shards => 1, :number_of_replicas => 0 } 17 | import documents 18 | refresh 19 | end 20 | end 21 | 22 | teardown do 23 | Index.new('reindex-test').delete 24 | Index.new('reindex-test-new').delete 25 | end 26 | 27 | should "reindex the index into a new index with different settings" do 28 | Tire.index('reindex-test').reindex 'reindex-test-new', settings: { number_of_shards: 3 } 29 | 30 | Tire.index('reindex-test-new').refresh 31 | assert_equal 100, Tire.search('reindex-test-new').results.total 32 | assert_equal '3', Tire.index('reindex-test-new').settings['index.number_of_shards'] 33 | end 34 | 35 | should "reindex a portion of an index into a new index" do 36 | Tire.index('reindex-test').reindex('reindex-test-new') { query { string '10*' } } 37 | 38 | Tire.index('reindex-test-new').refresh 39 | assert_equal 2, Tire.search('reindex-test-new').results.total 40 | end 41 | 42 | should "transform documents with a passed lambda" do 43 | Tire.index('reindex-test').reindex 'reindex-test-new', transform: lambda { |document| 44 | document[:title] << 'UPDATED' 45 | document 46 | } 47 | 48 | Tire.index('reindex-test-new').refresh 49 | assert_match /UPDATED/, Tire.search('reindex-test-new').results.first.title 50 | end 51 | 52 | end 53 | 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /test/integration/results_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ResultsIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Query results" do 9 | 10 | should "allow easy access to returned documents" do 11 | q = 'title:one' 12 | s = Tire.search('articles-test') { query { string q } } 13 | 14 | assert_equal 'One', s.results.first.title 15 | assert_equal 'ruby', s.results.first.tags[0] 16 | end 17 | 18 | should "allow easy access to returned documents with limited fields" do 19 | q = 'title:one' 20 | s = Tire.search('articles-test') do 21 | query { string q } 22 | fields :title 23 | end 24 | 25 | assert_equal 'One', s.results.first.title 26 | assert_nil s.results.first.tags 27 | end 28 | 29 | should "allow to retrieve multiple fields" do 30 | q = 'title:one' 31 | s = Tire.search('articles-test') do 32 | query { string q } 33 | fields 'title', 'tags' 34 | end 35 | 36 | assert_equal 'One', s.results.first.title 37 | assert_equal 'ruby', s.results.first.tags[0] 38 | assert_nil s.results.first.published_on 39 | end 40 | 41 | should "return script fields" do 42 | s = Tire.search('articles-test') do 43 | query { string 'title:one' } 44 | fields :title 45 | script_field :words_double, :script => "doc.words.value * 2" 46 | end 47 | 48 | assert_equal 'One', s.results.first.title 49 | assert_equal 250, s.results.first.words_double 50 | end 51 | 52 | should "return specific fields, script fields and _source fields" do 53 | # Tire.configure { logger STDERR, level: 'debug' } 54 | 55 | s = Tire.search('articles-test') do 56 | query { string 'title:one' } 57 | fields :title, :_source 58 | script_field :words_double, :script => "doc.words.value * 2" 59 | end 60 | 61 | assert_equal 'One', s.results.first.title 62 | assert_equal 250, s.results.first.words_double 63 | end 64 | 65 | should "iterate results with hits" do 66 | s = Tire.search('articles-test') { query { string 'title:one' } } 67 | 68 | s.results.each_with_hit do |result, hit| 69 | assert_instance_of Tire::Results::Item, result 70 | assert_instance_of Hash, hit 71 | 72 | assert_equal 'One', result.title 73 | assert_equal 'One', hit['_source']['title'] 74 | assert_not_nil hit['_score'] 75 | end 76 | end 77 | 78 | should "be serialized to JSON" do 79 | s = Tire.search('articles-test') { query { string 'title:one' } } 80 | 81 | assert_not_nil s.results.as_json(only: 'title').first['title'] 82 | assert_nil s.results.as_json(only: 'title').first['published_on'] 83 | end 84 | 85 | end 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/integration/scan_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ScanIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Scan" do 9 | setup do 10 | documents = (1..100).map { |i| { id: i, type: 'test', title: "Document #{i}" } } 11 | 12 | Tire.index 'scantest' do 13 | delete 14 | create :settings => { :number_of_shards => 1, :number_of_replicas => 0 } 15 | import documents 16 | refresh 17 | end 18 | end 19 | 20 | teardown { Index.new('scantest').delete } 21 | 22 | should "iterate over batches of documents" do 23 | count = 0 24 | 25 | s = Tire.scan 'scantest' 26 | s.each { |results| count += 1 } 27 | 28 | assert_equal 10, count 29 | end 30 | 31 | should "iterate over individual documents" do 32 | count = 0 33 | 34 | s = Tire.scan 'scantest' 35 | s.each_document { |results| count += 1 } 36 | 37 | assert_equal 100, count 38 | end 39 | 40 | should "limit the returned results by query" do 41 | count = 0 42 | 43 | s = Tire.scan('scantest') { query { string '10*' } } 44 | s.each do |results| 45 | count += 1 46 | assert_equal ['Document 10', 'Document 100'], results.map(&:title) 47 | end 48 | 49 | assert_equal 1, count 50 | end 51 | 52 | end 53 | 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /test/integration/script_fields_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ScriptFieldsIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "ScriptFields" do 9 | 10 | should "add multiple fields to the results" do 11 | # 1.json > title: "One", words: 125 12 | 13 | s = Tire.search('articles-test') do 14 | query { string "One" } 15 | script_field :double_words, :script => "doc['words'].value * 2" 16 | script_field :triple_words, :script => "doc['words'].value * 3" 17 | end 18 | 19 | assert_equal 250, s.results.first.double_words 20 | assert_equal 375, s.results.first.triple_words 21 | end 22 | 23 | should "allow passing parameters to the script" do 24 | # 1.json > title: "One", words: 125 25 | 26 | s = Tire.search('articles-test') do 27 | query { string "One" } 28 | script_field :double_words, :script => "doc['words'].value * factor", :params => { :factor => 2 } 29 | end 30 | 31 | assert_equal 250, s.results.first.double_words 32 | end 33 | 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/integration/search_response_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | #require File.expand_path('../../models/supermodel_article', __FILE__) 3 | 4 | module Tire 5 | 6 | class SearchResponseIntegrationTest < Test::Unit::TestCase 7 | include Test::Integration 8 | 9 | class ::ActiveModelArticleWithTitle < ActiveModelArticleWithCallbacks 10 | mapping do 11 | indexes :title, type: :string 12 | end 13 | end 14 | 15 | class ::ActiveModelArticleWithMalformedTitle < ActiveModelArticleWithCallbacks 16 | mapping do 17 | indexes :title, type: :string 18 | end 19 | 20 | def to_indexed_json 21 | json = JSON.parse(super) 22 | json["title"] = { key: "value" } 23 | json.to_json 24 | end 25 | end 26 | 27 | def setup 28 | super 29 | ActiveModelArticleWithTitle.index.delete 30 | ActiveModelArticleWithMalformedTitle.index.delete 31 | end 32 | 33 | def teardown 34 | super 35 | ActiveModelArticleWithTitle.index.delete 36 | ActiveModelArticleWithMalformedTitle.index.delete 37 | end 38 | 39 | context "Successful index update" do 40 | 41 | setup do 42 | @model = ActiveModelArticleWithTitle.new \ 43 | :id => 1, 44 | :title => 'Test article', 45 | :content => 'Lorem Ipsum. Dolor Sit Amet.' 46 | @response = @model.update_index 47 | end 48 | 49 | should "expose the index response on successful update" do 50 | assert_not_nil @response.response['_version'] 51 | end 52 | 53 | end 54 | 55 | context "Unsuccessful index update" do 56 | setup do 57 | ActiveModelArticleWithMalformedTitle.create_elasticsearch_index 58 | @model = ActiveModelArticleWithMalformedTitle.new \ 59 | :id => 1, 60 | :title => 'Test article', 61 | :content => 'Lorem Ipsum. Dolor Sit Amet.' 62 | @response = @model.update_index 63 | end 64 | 65 | should "expose the index response on update error" do 66 | assert_equal @response.response["status"], 400 67 | end 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /test/integration/sort_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class SortIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context "Sort" do 9 | 10 | should "sort by title" do 11 | q = '*' 12 | s = Tire.search('articles-test') do 13 | query { string q } 14 | sort { by :title } 15 | end 16 | 17 | assert_equal 5, s.results.count 18 | assert_equal 'Five', s.results.first[:title] 19 | end 20 | 21 | should "sort by title, descending" do 22 | q = '*' 23 | s = Tire.search('articles-test') do 24 | query { string q } 25 | sort { by :title, :desc } 26 | end 27 | 28 | assert_equal 5, s.results.count 29 | assert_equal 'Two', s.results.first[:title] 30 | end 31 | 32 | should "sort by multiple fields" do 33 | q = '*' 34 | s = Tire.search('articles-test') do 35 | query { string q } 36 | sort do 37 | by :words, :desc 38 | by :title, :asc 39 | end 40 | end 41 | 42 | # p s.results.to_a.map { |i| [i.title, i.sort] } 43 | assert_equal 'Three', s.results[0][:title] 44 | assert_equal 'Four', s.results[1][:title] 45 | assert_equal 'Two', s.results[2][:title] 46 | end 47 | 48 | end 49 | 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /test/integration/suggest_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class SuggestIntegrationTest < Test::Unit::TestCase 6 | include Test::Integration 7 | 8 | context 'Search Suggest' do 9 | 10 | should 'add suggestions field to the results using the term suggester' do 11 | # Tire::Configuration.logger STDERR, :level => 'debug' 12 | s = Tire.search('articles-test') do 13 | suggest :term_suggest1 do 14 | text 'thrree' 15 | term :title 16 | end 17 | end 18 | 19 | assert_equal 1, s.results.suggestions.size 20 | assert_equal 'three', s.results.suggestions.texts.first 21 | end 22 | 23 | should 'add suggestions field to the results using the phrase suggester' do 24 | # Tire::Configuration.logger STDERR, :level => 'debug' 25 | s = Tire.search('articles-test') do 26 | suggest :phrase_suggest1 do 27 | text 'thrree' 28 | phrase :title 29 | end 30 | end 31 | 32 | assert_equal 1, s.results.suggestions.size 33 | assert_equal 'three', s.results.suggestions.texts.first 34 | end 35 | 36 | end 37 | 38 | context 'Standalone term and phrase suggest' do 39 | 40 | should 'return term suggestions when used with standalone api' do 41 | # Tire::Configuration.logger STDERR, :level => 'debug' 42 | s = Tire.suggest('articles-test') do 43 | suggestion :term_suggest do 44 | text 'thrree' 45 | term :title 46 | end 47 | end 48 | 49 | assert_equal 1, s.results.texts.size 50 | assert_equal 'three', s.results.texts.first 51 | end 52 | 53 | should 'return phrase suggestions when used with standalone api' do 54 | # Tire::Configuration.logger STDERR, :level => 'debug' 55 | s = Tire.suggest('articles-test') do 56 | suggestion :prhase_suggest do 57 | text 'thrree' 58 | phrase :title 59 | end 60 | end 61 | 62 | assert_equal 1, s.results.texts.size 63 | assert_equal 'three', s.results.texts.first 64 | end 65 | 66 | end 67 | 68 | context 'Standalone suggest' do 69 | setup do 70 | Tire.index('suggest-test') do 71 | delete 72 | create :mappings => { 73 | :article => { 74 | :properties => { 75 | :title => {:type => 'string', :analyzer => 'simple'}, 76 | :title_suggest => {:type => 'completion', :analyzer => 'simple'}, 77 | } 78 | } 79 | } 80 | import([ 81 | {:id => '1', :type => 'article', :title => 'one', :title_suggest => 'one'}, 82 | # this document has multiple inputs for completion field and a specified output 83 | {:id => '2', :type => 'article', :title => 'two', :title_suggest => {:input => %w(two dos due), :output => 'Two[2]'}}, 84 | {:id => '3', :type => 'article', :title => 'three', :title_suggest => 'three'} 85 | ]) 86 | refresh 87 | end 88 | end 89 | 90 | teardown do 91 | Tire.index('suggest-test') { delete } 92 | end 93 | 94 | should 'return completion suggestions when used with standalone api' do 95 | # Tire::Configuration.logger STDERR, :level => 'debug' 96 | s = Tire.suggest('suggest-test') do 97 | suggestion 'complete' do 98 | text 't' 99 | completion 'title_suggest' 100 | end 101 | end 102 | 103 | assert_equal 2, s.results.texts.size 104 | assert_equal %w(Two[2] three), s.results.texts 105 | end 106 | 107 | should 'allow multiple completion requests in the same request' do 108 | # Tire::Configuration.logger STDERR, :level => 'debug' 109 | s = Tire.suggest('suggest-test') do 110 | multi do 111 | suggestion 'foo' do 112 | text 'o' 113 | completion 'title_suggest' 114 | end 115 | suggestion 'bar' do 116 | text 'd' 117 | completion 'title_suggest' 118 | end 119 | end 120 | end 121 | 122 | assert_equal 2, s.results.size 123 | assert_equal %w(one), s.results.texts(:foo) 124 | assert_equal %w(Two[2]), s.results.texts(:bar) 125 | end 126 | 127 | end 128 | end 129 | end -------------------------------------------------------------------------------- /test/models/active_model_article.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel class 2 | 3 | require 'rubygems' 4 | require 'active_model' 5 | 6 | class ActiveModelArticle 7 | 8 | extend ActiveModel::Naming 9 | include ActiveModel::AttributeMethods 10 | include ActiveModel::Serialization 11 | include ActiveModel::Serializers::JSON 12 | 13 | include Tire::Model::Search 14 | 15 | attr_reader :attributes 16 | 17 | def initialize(attributes = {}) 18 | @attributes = attributes 19 | end 20 | 21 | def id; attributes['id'] || attributes['_id']; end 22 | def id=(value); attributes['id'] = value; end 23 | 24 | def method_missing(name, *args, &block) 25 | attributes[name.to_sym] || attributes[name.to_s] || super 26 | end 27 | 28 | def persisted?; true; end 29 | def save; true; end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/models/active_model_article_with_callbacks.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel class with callbacks 2 | 3 | require 'rubygems' 4 | require 'active_model' 5 | 6 | class ActiveModelArticleWithCallbacks 7 | 8 | include ActiveModel::AttributeMethods 9 | include ActiveModel::Validations 10 | include ActiveModel::Serialization 11 | include ActiveModel::Serializers::JSON 12 | include ActiveModel::Naming 13 | 14 | extend ActiveModel::Callbacks 15 | define_model_callbacks :save, :destroy 16 | 17 | include Tire::Model::Search 18 | include Tire::Model::Callbacks 19 | 20 | attr_reader :attributes 21 | 22 | def initialize(attributes = {}) 23 | @attributes = attributes 24 | end 25 | 26 | def method_missing(id, *args, &block) 27 | attributes[id.to_sym] || attributes[id.to_s] || super 28 | end 29 | 30 | def persisted? 31 | true 32 | end 33 | 34 | def save 35 | _run_save_callbacks do 36 | STDERR.puts "[Saving ...]" 37 | end 38 | end 39 | 40 | def destroy 41 | _run_destroy_callbacks do 42 | STDERR.puts "[Destroying ...]" 43 | @destroyed = true 44 | end 45 | end 46 | 47 | def destroyed?; !!@destroyed; end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/models/active_model_article_with_custom_document_type.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel class with custom document type 2 | 3 | require File.expand_path('../active_model_article', __FILE__) 4 | 5 | class ActiveModelArticleWithCustomDocumentType < ActiveModelArticle 6 | document_type 'my_custom_type' 7 | end 8 | -------------------------------------------------------------------------------- /test/models/active_model_article_with_custom_index_name.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel class with custom index name 2 | 3 | require File.expand_path('../active_model_article', __FILE__) 4 | 5 | class ActiveModelArticleWithCustomIndexName < ActiveModelArticle 6 | index_name 'custom-index-name' 7 | end 8 | -------------------------------------------------------------------------------- /test/models/active_record_models.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | class ActiveRecordArticle < ActiveRecord::Base 4 | has_many :comments, :class_name => "ActiveRecordComment", :foreign_key => "article_id" 5 | has_many :stats, :class_name => "ActiveRecordStat", :foreign_key => "article_id" 6 | 7 | include Tire::Model::Search 8 | include Tire::Model::Callbacks 9 | 10 | tire do 11 | mapping do 12 | indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball' 13 | indexes :created_at, :type => 'date' 14 | indexes :suggest, :type => :completion, 15 | :index_analyzer => :simple, 16 | :search_analyzer => :simple, 17 | :payloads => true 18 | indexes :comments do 19 | indexes :author 20 | indexes :body 21 | end 22 | end 23 | end 24 | 25 | def to_indexed_json 26 | { 27 | :title => title, 28 | :length => length, 29 | :suggest => suggest, 30 | 31 | :comments => comments.map { |c| { :_type => 'active_record_comment', 32 | :_id => c.id, 33 | :author => c.author, 34 | :body => c.body } }, 35 | :stats => stats.map { |s| { :pageviews => s.pageviews } } 36 | }.to_json 37 | end 38 | 39 | def suggest 40 | { 41 | input: self.title.split(/\W/).reject(&:empty?), 42 | output: self.title, 43 | payload: { length: length, comment_authors: comment_authors} 44 | } 45 | end 46 | 47 | def length 48 | title.length 49 | end 50 | 51 | def comment_authors 52 | comments.map(&:author).to_sentence 53 | end 54 | end 55 | 56 | class ActiveRecordComment < ActiveRecord::Base 57 | belongs_to :article, :class_name => "ActiveRecordArticle", :foreign_key => "article_id" 58 | end 59 | 60 | class ActiveRecordStat < ActiveRecord::Base 61 | belongs_to :article, :class_name => "ActiveRecordArticle", :foreign_key => "article_id" 62 | end 63 | 64 | class ActiveRecordClassWithTireMethods < ActiveRecord::Base 65 | 66 | def self.mapping 67 | "THIS IS MY MAPPING!" 68 | end 69 | 70 | def index 71 | "THIS IS MY INDEX!" 72 | end 73 | 74 | include Tire::Model::Search 75 | include Tire::Model::Callbacks 76 | 77 | tire do 78 | mapping do 79 | indexes :title, :type => 'string', :analyzer => 'snowball' 80 | end 81 | end 82 | end 83 | 84 | class ActiveRecordClassWithDynamicIndexName < ActiveRecord::Base 85 | include Tire::Model::Search 86 | include Tire::Model::Callbacks 87 | 88 | index_name do 89 | "dynamic" + '_' + "index" 90 | end 91 | end 92 | 93 | # Used in test for multiple class instances in one index, 94 | # and single table inheritance (STI) support. 95 | 96 | class ActiveRecordModelOne < ActiveRecord::Base 97 | include Tire::Model::Search 98 | include Tire::Model::Callbacks 99 | self.table_name = 'active_record_model_one' 100 | index_name 'active_record_model_one' 101 | end 102 | 103 | class ActiveRecordModelTwo < ActiveRecord::Base 104 | include Tire::Model::Search 105 | include Tire::Model::Callbacks 106 | self.table_name = 'active_record_model_two' 107 | index_name 'active_record_model_two' 108 | end 109 | 110 | class ActiveRecordAsset < ActiveRecord::Base 111 | include Tire::Model::Search 112 | include Tire::Model::Callbacks 113 | end 114 | 115 | class ActiveRecordVideo < ActiveRecordAsset 116 | index_name 'active_record_assets' 117 | end 118 | 119 | class ActiveRecordPhoto < ActiveRecordAsset 120 | index_name 'active_record_assets' 121 | end 122 | 123 | # Namespaced ActiveRecord models 124 | 125 | module ActiveRecordNamespace 126 | def self.table_name_prefix 127 | 'active_record_namespace_' 128 | end 129 | end 130 | 131 | class ActiveRecordNamespace::MyModel < ActiveRecord::Base 132 | include Tire::Model::Search 133 | include Tire::Model::Callbacks 134 | end 135 | 136 | # Model with percolation 137 | 138 | class ActiveRecordModelWithPercolation < ActiveRecord::Base 139 | include Tire::Model::Search 140 | include Tire::Model::Callbacks 141 | 142 | percolate! 143 | end 144 | -------------------------------------------------------------------------------- /test/models/article.rb: -------------------------------------------------------------------------------- 1 | # Example non-ActiveModel custom wrapper for result item 2 | 3 | class Article 4 | attr_reader :id, :title, :body 5 | 6 | def initialize(attributes={}) 7 | attributes.each { |k,v| instance_variable_set(:"@#{k}", v) } 8 | end 9 | 10 | def to_json(options={}) 11 | { :id => @id, :title => @title, :body => @body }.to_json 12 | end 13 | 14 | alias :to_indexed_json :to_json 15 | end 16 | -------------------------------------------------------------------------------- /test/models/mongoid_models.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid' 2 | 3 | class MongoidArticle 4 | 5 | include Mongoid::Document 6 | 7 | 8 | has_many :comments, :class_name => "MongoidComment", :foreign_key => "article_id" 9 | has_many :stats, :class_name => "MongoidStat", :foreign_key => "article_id" 10 | 11 | include Tire::Model::Search 12 | include Tire::Model::Callbacks 13 | 14 | tire do 15 | mapping do 16 | indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball' 17 | indexes :created_at, :type => 'date' 18 | 19 | indexes :comments do 20 | indexes :author 21 | indexes :body 22 | end 23 | end 24 | end 25 | 26 | def to_indexed_json 27 | { 28 | :title => title, 29 | :length => length, 30 | 31 | :comments => comments.map { |c| { :_type => 'mongoid_comment', 32 | :_id => c.id, 33 | :author => c.author, 34 | :body => c.body } }, 35 | :stats => stats.map { |s| { :pageviews => s.pageviews } } 36 | }.to_json 37 | end 38 | 39 | def length 40 | title.length 41 | end 42 | 43 | def comment_authors 44 | comments.map(&:author).to_sentence 45 | end 46 | end 47 | 48 | class MongoidComment 49 | 50 | include Mongoid::Document 51 | 52 | 53 | belongs_to :article, :class_name => "MongoidArticle", :foreign_key => "article_id" 54 | end 55 | 56 | class MongoidStat 57 | 58 | include Mongoid::Document 59 | 60 | 61 | belongs_to :article, :class_name => "MongoidArticle", :foreign_key => "article_id" 62 | end 63 | 64 | class MongoidClassWithTireMethods 65 | 66 | include Mongoid::Document 67 | 68 | 69 | def self.mapping 70 | "THIS IS MY MAPPING!" 71 | end 72 | 73 | def index 74 | "THIS IS MY INDEX!" 75 | end 76 | 77 | include Tire::Model::Search 78 | include Tire::Model::Callbacks 79 | 80 | tire do 81 | mapping do 82 | indexes :title, :type => 'string', :analyzer => 'snowball' 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/models/persistent_article.rb: -------------------------------------------------------------------------------- 1 | # Example class with Elasticsearch persistence 2 | 3 | class PersistentArticle 4 | 5 | include Tire::Model::Persistence 6 | 7 | property :title 8 | property :published_on 9 | property :tags 10 | 11 | end 12 | -------------------------------------------------------------------------------- /test/models/persistent_article_in_index.rb: -------------------------------------------------------------------------------- 1 | # Example class with Elasticsearch persistence in index `persistent_articles` 2 | # 3 | # The `index` is `persistent_articles` 4 | # 5 | 6 | class PersistentArticleInIndex 7 | 8 | include Tire::Model::Persistence 9 | 10 | property :title 11 | property :published_on 12 | property :tags 13 | 14 | index_name "persistent_articles" 15 | 16 | end 17 | -------------------------------------------------------------------------------- /test/models/persistent_article_in_namespace.rb: -------------------------------------------------------------------------------- 1 | # Example namespaced class with Elasticsearch persistence 2 | # 3 | # The `document_type` is `my_namespace/persistent_article_in_namespace` 4 | # 5 | 6 | module MyNamespace 7 | class PersistentArticleInNamespace 8 | include Tire::Model::Persistence 9 | 10 | property :title 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/models/persistent_article_with_casting.rb: -------------------------------------------------------------------------------- 1 | class Author 2 | attr_accessor :first_name, :last_name 3 | def initialize(attributes) 4 | @first_name = HashWithIndifferentAccess.new(attributes)[:first_name] 5 | @last_name = HashWithIndifferentAccess.new(attributes)[:last_name] 6 | end 7 | end 8 | 9 | class Comment 10 | def initialize(params); @attributes = HashWithIndifferentAccess.new(params); end 11 | def method_missing(method_name, *arguments); @attributes[method_name]; end 12 | def as_json(*); @attributes; end 13 | end 14 | 15 | class PersistentArticleWithCastedItem 16 | include Tire::Model::Persistence 17 | 18 | property :title 19 | property :author, :class => Author 20 | property :stats 21 | end 22 | 23 | class PersistentArticleWithCastedCollection 24 | include Tire::Model::Persistence 25 | 26 | property :title 27 | property :comments, :class => [Comment] 28 | end 29 | -------------------------------------------------------------------------------- /test/models/persistent_article_with_defaults.rb: -------------------------------------------------------------------------------- 1 | class PersistentArticleWithDefaults 2 | 3 | include Tire::Model::Persistence 4 | 5 | property :title 6 | property :published_on 7 | property :tags, :default => [] 8 | property :hidden, :default => false 9 | property :options, :type => 'object', :default => {:switches => []} 10 | property :created_at, :default => lambda { Time.now } 11 | 12 | end 13 | -------------------------------------------------------------------------------- /test/models/persistent_article_with_percolation.rb: -------------------------------------------------------------------------------- 1 | class PersistentArticleWithPercolation 2 | include Tire::Model::Persistence 3 | property :title 4 | percolate! 5 | end 6 | -------------------------------------------------------------------------------- /test/models/persistent_article_with_strict_mapping.rb: -------------------------------------------------------------------------------- 1 | # Example class with Elasticsearch persistence and strict mapping 2 | 3 | class PersistentArticleWithStrictMapping 4 | 5 | include Tire::Model::Persistence 6 | 7 | mapping :dynamic => 'strict' do 8 | property :title, :type => 'string' 9 | property :created, :type => 'date' 10 | end 11 | 12 | def myproperty 13 | @myproperty 14 | end 15 | 16 | def myproperty= value 17 | self.class.properties << 'myproperty' 18 | @myproperty = value 19 | end 20 | 21 | def to_indexed_json 22 | json = { :title => self.title, :created => self.created } 23 | json[:myproperty] = 'NOTVALID' if self.myproperty 24 | json.to_json 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/models/persistent_articles_with_custom_index_name.rb: -------------------------------------------------------------------------------- 1 | # Example class with Elasticsearch persistence and custom index name 2 | 3 | class PersistentArticleWithCustomIndexName 4 | 5 | include Tire::Model::Persistence 6 | 7 | property :title 8 | 9 | index_name 'custom-index-name' 10 | end 11 | -------------------------------------------------------------------------------- /test/models/supermodel_article.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel class for testing :searchable mode 2 | 3 | require 'rubygems' 4 | require 'redis/persistence' 5 | 6 | class SupermodelArticle 7 | include Redis::Persistence 8 | 9 | include Tire::Model::Search 10 | include Tire::Model::Callbacks 11 | 12 | property :title 13 | 14 | mapping do 15 | indexes :title, :type => 'string', :boost => 15, :analyzer => 'czech' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/models/validated_model.rb: -------------------------------------------------------------------------------- 1 | # Example ActiveModel with validations 2 | 3 | class ValidatedModel 4 | 5 | include Tire::Model::Persistence 6 | 7 | property :name 8 | 9 | validates_presence_of :name 10 | 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['DEBUG'] = 'true' 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | require 'pathname' 7 | require 'test/unit' 8 | 9 | JRUBY = defined?(JRUBY_VERSION) 10 | 11 | if ENV['JSON_LIBRARY'] 12 | puts "Using '#{ENV['JSON_LIBRARY']}' JSON library" 13 | require ENV['JSON_LIBRARY'] 14 | elsif JRUBY 15 | require 'json' 16 | else 17 | require 'yajl/json_gem' 18 | end 19 | 20 | if JRUBY 21 | require 'jdbc/sqlite3' 22 | require 'active_record' 23 | require 'active_record/connection_adapters/jdbcsqlite3_adapter' 24 | else 25 | require 'sqlite3' 26 | end 27 | 28 | require 'shoulda-context' 29 | require 'mocha/setup' 30 | require 'turn/autorun' unless ENV["TM_FILEPATH"] || JRUBY 31 | 32 | require 'active_support/core_ext/hash/indifferent_access' 33 | 34 | require 'tire' 35 | if ENV['CURB'] 36 | puts "Using 'curb' as the HTTP library" 37 | require 'tire/http/clients/curb' 38 | Tire.configure { client Tire::HTTP::Client::Curb } 39 | end 40 | 41 | # Require basic model files 42 | # 43 | require File.dirname(__FILE__) + '/models/active_model_article' 44 | require File.dirname(__FILE__) + '/models/active_model_article_with_callbacks' 45 | require File.dirname(__FILE__) + '/models/active_model_article_with_custom_document_type' 46 | require File.dirname(__FILE__) + '/models/active_model_article_with_custom_index_name' 47 | require File.dirname(__FILE__) + '/models/active_record_models' 48 | require File.dirname(__FILE__) + '/models/article' 49 | require File.dirname(__FILE__) + '/models/persistent_article' 50 | require File.dirname(__FILE__) + '/models/persistent_article_in_index' 51 | require File.dirname(__FILE__) + '/models/persistent_article_in_namespace' 52 | require File.dirname(__FILE__) + '/models/persistent_article_with_casting' 53 | require File.dirname(__FILE__) + '/models/persistent_article_with_defaults' 54 | require File.dirname(__FILE__) + '/models/persistent_article_with_strict_mapping' 55 | require File.dirname(__FILE__) + '/models/persistent_articles_with_custom_index_name' 56 | require File.dirname(__FILE__) + '/models/validated_model' 57 | 58 | class Test::Unit::TestCase 59 | 60 | def assert_block(message=nil) 61 | raise Test::Unit::AssertionFailedError.new(message.to_s) if (! yield) 62 | return true 63 | end if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' 64 | 65 | def mock_response(body, code=200, headers={}) 66 | Tire::HTTP::Response.new(body, code, headers) 67 | end 68 | 69 | def fixtures_path 70 | Pathname( File.expand_path( 'fixtures', File.dirname(__FILE__) ) ) 71 | end 72 | 73 | def fixture_file(path) 74 | File.read File.expand_path( path, fixtures_path ) 75 | end 76 | 77 | end 78 | 79 | module Test::Integration 80 | URL = "http://localhost:9200" 81 | 82 | def setup 83 | begin; Object.send(:remove_const, :Rails); rescue; end 84 | 85 | begin 86 | ::RestClient.get URL 87 | rescue Errno::ECONNREFUSED 88 | abort "\n\n#{'-'*87}\n[ABORTED] You have to run Elasticsearch on #{URL} for integration tests\n#{'-'*87}\n\n" 89 | end 90 | 91 | ::RestClient.delete "#{URL}/articles-test" rescue nil 92 | ::RestClient.post "#{URL}/articles-test", '' 93 | fixtures_path.join('articles').entries.each do |f| 94 | filename = f.to_s 95 | next if filename =~ /^\./ 96 | ::RestClient.put "#{URL}/articles-test/article/#{File.basename(filename, '.*')}", 97 | fixtures_path.join('articles').join(f).read 98 | end 99 | ::RestClient.post "#{URL}/articles-test/_refresh", '' 100 | 101 | Dir[File.dirname(__FILE__) + '/models/**/*.rb'].each { |m| load m } 102 | end 103 | 104 | def teardown 105 | %w[ 106 | articles-test 107 | active_record_articles 108 | active_model_article_with_custom_as_serializations 109 | active_record_class_with_tire_methods 110 | mongoid_articles 111 | mongoid_class_with_tire_methods 112 | supermodel_articles 113 | dynamic_index 114 | model_with_nested_documents 115 | model_with_incorrect_mappings ].each do |index| 116 | ::RestClient.delete "#{URL}/#{index}" rescue nil 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/unit/active_model_lint_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | module Model 5 | 6 | class ActiveModelLintTest < Test::Unit::TestCase 7 | 8 | include ActiveModel::Lint::Tests 9 | 10 | def setup 11 | @model = PersistentArticle.new :title => 'Test' 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/unit/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class ConfigurationTest < Test::Unit::TestCase 6 | 7 | def teardown 8 | Tire::Configuration.reset 9 | ENV['ELASTICSEARCH_URL'] = nil 10 | end 11 | 12 | context "Configuration" do 13 | setup do 14 | Configuration.instance_variable_set(:@url, nil) 15 | Configuration.instance_variable_set(:@client, nil) 16 | end 17 | 18 | teardown do 19 | Configuration.reset 20 | end 21 | 22 | should "return default URL" do 23 | assert_equal 'http://localhost:9200', Configuration.url 24 | end 25 | 26 | should "use environment variable, if present" do 27 | ENV['ELASTICSEARCH_URL'] = 'http://es.example.com' 28 | assert_equal 'http://es.example.com', Configuration.url 29 | end 30 | 31 | should "allow setting and retrieving the URL" do 32 | assert_nothing_raised { Configuration.url 'http://example.com' } 33 | assert_equal 'http://example.com', Configuration.url 34 | end 35 | 36 | should "strip trailing slash from the URL" do 37 | assert_nothing_raised { Configuration.url 'http://slash.com:9200/' } 38 | assert_equal 'http://slash.com:9200', Configuration.url 39 | end 40 | 41 | should "return default client" do 42 | assert_equal HTTP::Client::RestClient, Configuration.client 43 | end 44 | 45 | should "return nil as logger by default" do 46 | assert_nil Configuration.logger 47 | end 48 | 49 | should "return set and return logger" do 50 | Configuration.logger STDERR 51 | assert_not_nil Configuration.logger 52 | assert_instance_of Tire::Logger, Configuration.logger 53 | end 54 | 55 | should "set pretty option to true by default" do 56 | assert_not_nil Configuration.pretty 57 | assert Configuration.pretty, "Should be true, but is: #{Configuration.pretty.inspect}" 58 | end 59 | 60 | should "set the pretty option to false" do 61 | Configuration.pretty(false) 62 | assert ! Configuration.pretty, "Should be falsy, but is: #{Configuration.pretty.inspect}" 63 | end 64 | 65 | should "allow to reset the configuration for specific property" do 66 | Configuration.url 'http://example.com' 67 | assert_equal 'http://example.com', Configuration.url 68 | Configuration.reset :url 69 | assert_equal 'http://localhost:9200', Configuration.url 70 | end 71 | 72 | should "allow to reset the configuration for all properties" do 73 | Configuration.url 'http://example.com' 74 | Configuration.wrapper Hash 75 | assert_equal 'http://example.com', Configuration.url 76 | Configuration.reset 77 | assert_equal 'http://localhost:9200', Configuration.url 78 | assert_equal HTTP::Client::RestClient, Configuration.client 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/unit/count_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | class CountTest < Test::Unit::TestCase 5 | 6 | context "Count" do 7 | setup { Configuration.reset } 8 | 9 | should "be initialized with single index" do 10 | c = Search::Count.new('index') 11 | assert_equal ['index'], c.indices 12 | assert_match %r|/index/_count|, c.url 13 | end 14 | 15 | should "count all documents by the leaving index empty" do 16 | c = Search::Count.new 17 | assert c.indices.empty?, "#{c.indices.inspect} should be empty" 18 | assert_match %r|localhost:9200/_count|, c.url 19 | end 20 | 21 | should "limit count with document type" do 22 | c = Search::Count.new('index', :type => 'bar') 23 | assert_equal ['bar'], c.types 24 | assert_match %r|index/bar/_count|, c.url 25 | end 26 | 27 | should "pass URL parameters" do 28 | c = Search::Count.new('index', :routing => 123) 29 | assert ! c.params.empty? 30 | assert_match %r|routing=123|, c.params 31 | end 32 | 33 | should "evaluate the query" do 34 | Search::Query.any_instance.expects(:instance_eval) 35 | 36 | c = Search::Count.new('index') { string 'foo' } 37 | assert_not_nil c.query 38 | end 39 | 40 | should "allow access to the JSON and the response" do 41 | Configuration.client.expects(:get).returns(mock_response( '{"count":1}', 200 )) 42 | c = Search::Count.new('index') 43 | c.perform 44 | assert_equal 1, c.json['count'] 45 | assert_equal 200, c.response.code 46 | end 47 | 48 | should "return curl snippet for debugging" do 49 | c = Search::Count.new('index') { term :title, 'foo' } 50 | assert_match %r|curl \-X GET 'http://localhost:9200/index/_count\?pretty' -d |, c.to_curl 51 | assert_match %r|"term"\s*:\s*"foo"|, c.to_curl 52 | end 53 | 54 | should "log the request when logger is set" do 55 | Configuration.logger STDERR 56 | 57 | Configuration.client.expects(:get).returns(mock_response( '{"count":1}', 200 )) 58 | Configuration.logger.expects(:log_request).returns(true) 59 | Configuration.logger.expects(:log_response).returns(true) 60 | 61 | Search::Count.new('index').value 62 | end 63 | 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/unit/http_client_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | module HTTP 5 | 6 | class ClientTest < Test::Unit::TestCase 7 | 8 | context "RestClient" do 9 | 10 | should "be default" do 11 | assert_equal Client::RestClient, Configuration.client 12 | end 13 | 14 | should "respond to HTTP methods" do 15 | assert_respond_to Client::RestClient, :get 16 | assert_respond_to Client::RestClient, :post 17 | assert_respond_to Client::RestClient, :put 18 | assert_respond_to Client::RestClient, :delete 19 | assert_respond_to Client::RestClient, :head 20 | end 21 | 22 | should "not rescue generic exceptions" do 23 | Client::RestClient.expects(:get).raises(RuntimeError, "Something bad happened in YOUR code") 24 | 25 | assert_raise(RuntimeError) do 26 | Client::RestClient.get 'http://example.com' 27 | end 28 | end 29 | 30 | should "not rescue ServerBrokeConnection errors" do 31 | Client::RestClient.expects(:get).raises(RestClient::ServerBrokeConnection) 32 | 33 | assert_raise(RestClient::ServerBrokeConnection) do 34 | Client::RestClient.get 'http://example.com' 35 | end 36 | end 37 | 38 | should "not rescue RequestTimeout errors" do 39 | Client::RestClient.expects(:get).raises(RestClient::RequestTimeout) 40 | 41 | assert_raise(RestClient::RequestTimeout) do 42 | Client::RestClient.get 'http://example.com' 43 | end 44 | end 45 | 46 | should "have __host_unreachable_exceptions" do 47 | assert_respond_to Client::RestClient, :__host_unreachable_exceptions 48 | end 49 | 50 | end 51 | 52 | if defined?(Curl) 53 | require 'tire/http/clients/curb' 54 | 55 | context "Curb" do 56 | setup do 57 | Configuration.client Client::Curb 58 | end 59 | 60 | teardown do 61 | Configuration.client Client::RestClient 62 | end 63 | 64 | should "use POST method if request body passed" do 65 | ::Curl::Easy.any_instance.expects(:http_post) 66 | 67 | response = Configuration.client.get "http://localhost:3000", '{ "query_string" : { "query" : "apple" }}' 68 | end 69 | 70 | should "use GET method if request body is nil" do 71 | ::Curl::Easy.any_instance.expects(:http_get) 72 | 73 | response = Configuration.client.get "http://localhost:9200/articles/article/1" 74 | end 75 | 76 | should "be threadsafe" do 77 | threads = [] 78 | 79 | %w| foo bar |.each do |q| 80 | threads << Thread.new do 81 | Tire.search { query { match :_all, q } }.results.to_a 82 | end 83 | end 84 | 85 | threads.each { |t| t.join() } 86 | end 87 | 88 | should "have __host_unreachable_exceptions" do 89 | assert_respond_to Client::RestClient, :__host_unreachable_exceptions 90 | end 91 | 92 | end 93 | 94 | end 95 | 96 | end 97 | end 98 | 99 | end 100 | -------------------------------------------------------------------------------- /test/unit/http_response_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | module HTTP 5 | 6 | class ResponseTest < Test::Unit::TestCase 7 | 8 | context "Response" do 9 | 10 | should "take response body, code and headers on initialization" do 11 | response = Response.new "http response body", 12 | 200, 13 | :content_length => 20, 14 | :content_encoding => 'gzip' 15 | 16 | assert_equal "http response body", response.body 17 | assert_equal 200, response.code 18 | end 19 | 20 | should "not require headers" do 21 | assert_nothing_raised do 22 | Response.new "Forbidden", 403 23 | end 24 | end 25 | 26 | should "return success" do 27 | responses = [] 28 | responses << Response.new('OK', 200) 29 | responses << Response.new('Redirect', 302) 30 | 31 | responses.each { |response| assert response.success? } 32 | 33 | assert ! Response.new('NotFound', 404).success? 34 | end 35 | 36 | should "return failure" do 37 | assert Response.new('NotFound', 404).failure? 38 | end 39 | 40 | should "return string representation" do 41 | assert_equal "200 : Hello", Response.new('Hello', 200).to_s 42 | end 43 | 44 | end 45 | 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/unit/logger_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'time' 3 | 4 | module Tire 5 | 6 | class LoggerTest < Test::Unit::TestCase 7 | include Tire 8 | 9 | context "Logger" do 10 | 11 | context "initialized with an IO object" do 12 | 13 | should "take STDOUT" do 14 | assert_nothing_raised do 15 | logger = Logger.new STDOUT 16 | end 17 | end 18 | 19 | should "write to STDERR" do 20 | STDERR.expects(:write).with('BOOM!') 21 | logger = Logger.new STDERR 22 | logger.write('BOOM!') 23 | end 24 | 25 | end 26 | 27 | context "initialized with file" do 28 | teardown { File.delete('myfile.log') } 29 | 30 | should "create the file" do 31 | assert_nothing_raised do 32 | logger = Logger.new 'myfile.log' 33 | assert File.exists?('myfile.log') 34 | end 35 | end 36 | 37 | should "write to file" do 38 | File.any_instance.expects(:write).with('BOOM!') 39 | logger = Logger.new 'myfile.log' 40 | logger.write('BOOM!') 41 | end 42 | 43 | end 44 | 45 | end 46 | 47 | context "levels" do 48 | 49 | should "have the default level" do 50 | logger = Logger.new STDERR 51 | assert_equal 'info', logger.level 52 | end 53 | 54 | should "set the level" do 55 | logger = Logger.new STDERR, :level => 'debug' 56 | assert_equal 'debug', logger.level 57 | end 58 | 59 | end 60 | 61 | context "tracing requests" do 62 | setup do 63 | Time.stubs(:now).returns(Time.parse('2011-03-19 11:00')) 64 | @logger = Logger.new STDERR 65 | end 66 | 67 | should "log request in correct format" do 68 | log = (<<-"log;").gsub(/^ +/, '') 69 | # 2011-03-19 11:00:00:000 [_search] (["articles", "users"]) 70 | # 71 | curl -X GET http://... 72 | 73 | log; 74 | @logger.expects(:write).with do |payload| 75 | payload =~ Regexp.new( Regexp.escape('2011-03-19 11:00:00') ) 76 | payload =~ Regexp.new( Regexp.escape('_search') ) 77 | payload =~ Regexp.new( Regexp.escape('(["articles", "users"])') ) 78 | end 79 | @logger.log_request('_search', ["articles", "users"], 'curl -X GET http://...') 80 | end 81 | 82 | should "log response in correct format" do 83 | json = (<<-"json;").gsub(/^\s*/, '') 84 | { 85 | "took" : 4, 86 | "hits" : { 87 | "total" : 20, 88 | "max_score" : 1.0, 89 | "hits" : [ { 90 | "_index" : "articles", 91 | "_type" : "article", 92 | "_id" : "Hmg0B0VSRKm2VAlsasdnqg", 93 | "_score" : 1.0, "_source" : { "title" : "Article 1", "published_on" : "2011-01-01" } 94 | }, { 95 | "_index" : "articles", 96 | "_type" : "article", 97 | "_id" : "booSWC8eRly2I06GTUilNA", 98 | "_score" : 1.0, "_source" : { "title" : "Article 2", "published_on" : "2011-01-12" } 99 | } 100 | ] 101 | } 102 | } 103 | json; 104 | log = (<<-"log;").gsub(/^\s*/, '') 105 | # 2011-03-19 11:00:00:000 [200 OK] (4 msec) 106 | # 107 | log; 108 | # log << json.split.map { |line| "# #{line}" }.join("\n") 109 | json.each_line { |line| log << "# #{line}" } 110 | log << "\n\n" 111 | @logger.expects(:write).with do |payload| 112 | payload =~ Regexp.new( Regexp.escape('2011-03-19 11:00:00') ) 113 | payload =~ Regexp.new( Regexp.escape('[200 OK]') ) 114 | payload =~ Regexp.new( Regexp.escape('(4 msec)') ) 115 | payload =~ Regexp.new( Regexp.escape('took') ) 116 | payload =~ Regexp.new( Regexp.escape('hits') ) 117 | payload =~ Regexp.new( Regexp.escape('_score') ) 118 | end 119 | @logger.log_response('200 OK', 4, json) 120 | end 121 | 122 | end 123 | 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/unit/model_callbacks_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModelOne 4 | extend ActiveModel::Naming 5 | include Tire::Model::Search 6 | include Tire::Model::Callbacks 7 | 8 | def save; false; end 9 | def destroy; false; end 10 | end 11 | 12 | class ModelTwo 13 | extend ActiveModel::Naming 14 | extend ActiveModel::Callbacks 15 | define_model_callbacks :save, :destroy 16 | 17 | include Tire::Model::Search 18 | include Tire::Model::Callbacks 19 | 20 | def save; _run_save_callbacks {}; end 21 | def destroy; _run_destroy_callbacks { @destroyed = true }; end 22 | 23 | def destroyed?; !!@destroyed; end 24 | end 25 | 26 | class ModelThree 27 | extend ActiveModel::Naming 28 | extend ActiveModel::Callbacks 29 | define_model_callbacks :save, :destroy 30 | 31 | include Tire::Model::Search 32 | include Tire::Model::Callbacks 33 | 34 | def save; _run_save_callbacks {}; end 35 | def destroy; _run_destroy_callbacks {}; end 36 | end 37 | 38 | class ModelWithoutTireAutoCallbacks 39 | extend ActiveModel::Naming 40 | extend ActiveModel::Callbacks 41 | define_model_callbacks :save, :destroy 42 | 43 | include Tire::Model::Search 44 | # DO NOT include Callbacks 45 | 46 | def save; _run_save_callbacks {}; end 47 | def destroy; _run_destroy_callbacks {}; end 48 | end 49 | 50 | module Tire 51 | module Model 52 | 53 | class ModelCallbacksTest < Test::Unit::TestCase 54 | 55 | context "Model without ActiveModel callbacks" do 56 | 57 | should "not execute any callbacks" do 58 | m = ModelOne.new 59 | m.tire.expects(:update_index).never 60 | 61 | m.save 62 | m.destroy 63 | end 64 | 65 | end 66 | 67 | context "Model with ActiveModel callbacks and implemented destroyed? method" do 68 | 69 | should "execute the callbacks" do 70 | m = ModelTwo.new 71 | m.tire.expects(:update_index).twice 72 | 73 | m.save 74 | m.destroy 75 | end 76 | 77 | end 78 | 79 | context "Model with ActiveModel callbacks without destroyed? method implemented" do 80 | 81 | should "have the destroyed? method added" do 82 | assert_respond_to ModelThree.new, :destroyed? 83 | end 84 | 85 | should "execute the callbacks" do 86 | m = ModelThree.new 87 | m.tire.expects(:update_index).twice 88 | 89 | m.save 90 | m.destroy 91 | end 92 | 93 | end 94 | 95 | context "Model without Tire::Callbacks included" do 96 | 97 | should "respond to Tire update_index callbacks" do 98 | assert_respond_to ModelWithoutTireAutoCallbacks, :after_update_elasticsearch_index 99 | assert_respond_to ModelWithoutTireAutoCallbacks, :before_update_elasticsearch_index 100 | end 101 | 102 | should "not execute the update_index hooks" do 103 | m = ModelWithoutTireAutoCallbacks.new 104 | m.tire.expects(:update_index).never 105 | 106 | m.save 107 | m.destroy 108 | end 109 | end 110 | 111 | # --------------------------------------------------------------------------- 112 | 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/unit/model_import_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ImportModel 4 | extend ActiveModel::Naming 5 | include Tire::Model::Search 6 | include Tire::Model::Callbacks 7 | 8 | DATA = (1..4).to_a 9 | 10 | def self.paginate(options={}) 11 | options = {:page => 1, :per_page => 1000}.update options 12 | DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] ) 13 | end 14 | 15 | def self.all(options={}) 16 | DATA 17 | end 18 | 19 | def self.count 20 | DATA.size 21 | end 22 | end 23 | 24 | module Tire 25 | module Model 26 | 27 | class ImportTest < Test::Unit::TestCase 28 | 29 | context "Model::Import" do 30 | 31 | should "have the import method" do 32 | assert_respond_to ImportModel, :import 33 | end 34 | 35 | should "paginate the results by default when importing" do 36 | Tire::Index.any_instance.expects(:bulk_store).returns(true).times(2) 37 | 38 | ImportModel.import :per_page => 2 39 | end 40 | 41 | should "call the passed block on every batch, and NOT manipulate the documents array" do 42 | Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [1, 2] } 43 | Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [3, 4] } 44 | 45 | runs = 0 46 | ImportModel.import :per_page => 2 do |documents| 47 | runs += 1 48 | # Don't forget to return the documents at the end of the block 49 | documents 50 | end 51 | 52 | assert_equal 2, runs 53 | end 54 | 55 | should "manipulate the documents in passed block" do 56 | Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] } 57 | Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] } 58 | 59 | ImportModel.import :per_page => 2 do |documents| 60 | # Add 1 to every "document" and return them 61 | documents.map { |d| d + 1 } 62 | end 63 | end 64 | 65 | should "store the documents in a different index" do 66 | Tire::Index.expects(:new).with('new_index').returns( mock('index') { expects(:import) } ) 67 | ImportModel.import :index => 'new_index' 68 | end 69 | 70 | context 'Strategy' do 71 | class ::CustomImportStrategy 72 | include Tire::Model::Import::Strategy::Base 73 | end 74 | 75 | should 'return explicitly specified strategy from predefined strategies' do 76 | strategy = Tire::Model::Import::Strategy.from_class(ImportModel, :strategy => 'WillPaginate') 77 | assert_equal strategy.class.name, 'Tire::Model::Import::Strategy::WillPaginate' 78 | end 79 | 80 | should 'return custom strategy class' do 81 | strategy = Tire::Model::Import::Strategy.from_class(ImportModel, :strategy => 'CustomImportStrategy') 82 | assert_equal strategy.class.name, 'CustomImportStrategy' 83 | end 84 | 85 | end 86 | 87 | end 88 | 89 | end 90 | 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/unit/model_initialization_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModelWithIncorrectMapping 4 | extend ActiveModel::Naming 5 | include Tire::Model::Search 6 | include Tire::Model::Callbacks 7 | 8 | tire do 9 | mapping do 10 | indexes :title, :type => 'boo' 11 | end 12 | end 13 | end 14 | 15 | class MyModelForIndexCreate 16 | extend ActiveModel::Naming 17 | include Tire::Model::Search 18 | end 19 | 20 | module Tire 21 | module Model 22 | 23 | class ModelInitializationTest < Test::Unit::TestCase 24 | 25 | context "Model initialization" do 26 | 27 | should "display a warning and not raise exception when creating the index fails" do 28 | assert_nothing_raised do 29 | STDERR.expects(:puts) 30 | result = ModelWithIncorrectMapping.create_elasticsearch_index 31 | assert ! result, result.inspect 32 | end 33 | end 34 | 35 | should "re-raise non-connection related exceptions" do 36 | Tire::Index.any_instance.expects(:exists?).raises(ZeroDivisionError) 37 | 38 | assert_raise(ZeroDivisionError) do 39 | result = MyModelForIndexCreate.create_elasticsearch_index 40 | assert ! result, result.inspect 41 | end 42 | end 43 | 44 | unless defined?(Curl) 45 | 46 | should "display a warning and not raise exception when cannot connect to Elasticsearch (default client)" do 47 | Tire::Index.any_instance.expects(:exists?).raises(Errno::ECONNREFUSED) 48 | assert_nothing_raised do 49 | STDERR.expects(:puts) 50 | result = MyModelForIndexCreate.create_elasticsearch_index 51 | assert ! result, result.inspect 52 | end 53 | end 54 | 55 | else 56 | should "display a warning and not raise exception when cannot connect to Elasticsearch (Curb client)" do 57 | Tire::Index.any_instance.expects(:exists?).raises(::Curl::Err::HostResolutionError) 58 | assert_nothing_raised do 59 | STDERR.expects(:puts) 60 | result = MyModelForIndexCreate.create_elasticsearch_index 61 | assert ! result, result.inspect 62 | end 63 | end 64 | 65 | end 66 | 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/unit/rubyext_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class RubyCoreExtensionsTest < Test::Unit::TestCase 6 | 7 | context "Hash" do 8 | 9 | context "with no to_json method provided" do 10 | 11 | setup do 12 | @hash = { :one => 1} 13 | # Undefine the `to_json` method... 14 | ::Hash.class_eval { remove_method(:to_json) rescue nil } 15 | # ... and reload the extension, so it's added 16 | load 'tire/rubyext/hash.rb' 17 | end 18 | 19 | should "have its own to_json method" do 20 | assert_respond_to( @hash, :to_json ) 21 | assert_equal '{"one":1}', @hash.to_json 22 | end 23 | 24 | should "allow to pass options to to_json for compatibility" do 25 | assert_nothing_raised do 26 | assert_equal '{"one":1}', @hash.to_json({}) 27 | end 28 | end 29 | 30 | end 31 | 32 | should "have a to_json method from a JSON serialization library" do 33 | assert_respond_to( {}, :to_json ) 34 | assert_equal '{"one":1}', { :one => 1}.to_json 35 | end 36 | 37 | should "have to_indexed_json method doing the same as to_json" do 38 | [{}, { :foo => 2 }, { :foo => 4, :bar => 6 }, { :foo => [7,8,9] }].each do |h| 39 | assert_equal MultiJson.decode(h.to_json), MultiJson.decode(h.to_indexed_json) 40 | end 41 | end 42 | 43 | should "properly serialize Time into JSON" do 44 | json = { :time => Time.mktime(2011, 01, 01, 11, 00).to_json }.to_json 45 | assert_match /"2011-01-01T11:00:00.*"/, MultiJson.decode(json)['time'] 46 | end 47 | 48 | end 49 | 50 | context "Array" do 51 | 52 | should "encode itself to JSON" do 53 | assert_equal '["one","two"]', ['one','two'].to_json 54 | end 55 | 56 | end 57 | 58 | context "Ruby Test::Unit" do 59 | should "actually return true from assert..." do 60 | assert_equal true, assert(true) 61 | end 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /test/unit/search_filter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire::Search 4 | 5 | class FilterTest < Test::Unit::TestCase 6 | 7 | context "Filter" do 8 | 9 | should "be serialized to JSON" do 10 | assert_respond_to Filter.new(:terms, {}), :to_json 11 | end 12 | 13 | should "encode simple filter declarations as JSON" do 14 | assert_equal( { :terms => {} }.to_json, 15 | Filter.new('terms').to_json ) 16 | 17 | assert_equal( { :terms => { :tags => ['foo'] } }.to_json, 18 | Filter.new('terms', :tags => ['foo']).to_json ) 19 | 20 | assert_equal( { :range => { :age => { :from => 10, :to => 20 } } }.to_json, 21 | Filter.new('range', { :age => { :from => 10, :to => 20 } }).to_json ) 22 | 23 | assert_equal( { :geo_distance => { :distance => '12km', :location => [40, -70] } }.to_json, 24 | Filter.new('geo_distance', { :distance => '12km', :location => [40, -70] }).to_json ) 25 | end 26 | 27 | should "encode 'or' filter with multiple other filters" do 28 | # See http://www.elasticsearch.org/guide/reference/query-dsl/or-filter.html 29 | assert_equal( { :or => [ {:terms => {:tags => ['foo']}}, {:terms => {:tags => ['bar']}} ] }.to_json, 30 | Filter.new('or', {:terms => {:tags => ['foo']}}, {:terms => {:tags => ['bar']}}).to_json ) 31 | end 32 | 33 | should "encode 'bool' filter with multiple filters" do 34 | # http://www.elasticsearch.org/guide/reference/query-dsl/bool-filter.html 35 | assert_equal( { :bool => [ {:must => {:terms => {:tags => ['foo']}}}, {:should => {:terms => {:tags => ['bar']}}} ] }.to_json, 36 | Filter.new('bool', {:must => {:terms => {:tags => ['foo']}}}, { :should => {:terms => {:tags => ['bar']}}}).to_json ) 37 | end 38 | 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/unit/search_highlight_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire::Search 4 | 5 | class HighlightTest < Test::Unit::TestCase 6 | 7 | context "Highlight" do 8 | 9 | should "be serialized to JSON" do 10 | assert_respond_to Highlight.new(:body), :to_json 11 | end 12 | 13 | should "specify highlight for single field" do 14 | assert_equal( {:fields => { :body => {} }}.to_json, 15 | Highlight.new(:body).to_json ) 16 | end 17 | 18 | should "specify highlight for more fields" do 19 | assert_equal( {:fields => { :title => {}, :body => {} }}.to_json, 20 | Highlight.new(:title, :body).to_json ) 21 | end 22 | 23 | should "specify highlight for more fields with options" do 24 | assert_equal( {:fields => { :title => {}, :body => { :a => 1, :b => 2 } }}.to_json, 25 | Highlight.new(:title, :body => { :a => 1, :b => 2 }).to_json ) 26 | end 27 | 28 | should "specify highlight for more fields with highlight options" do 29 | # p Highlight.new(:title, :body => {}, :options => { :tag => '' }).to_hash 30 | assert_equal( {:fields => { :title => {}, :body => {} }, :pre_tags => [''], :post_tags => [''] }.to_json, 31 | Highlight.new(:title, :body => {}, :options => { :tag => '' }).to_json ) 32 | end 33 | 34 | context "with custom tags" do 35 | 36 | should "properly parse tags with class" do 37 | assert_equal( {:fields => { :title => {} }, :pre_tags => [''], :post_tags => [''] }.to_json, 38 | Highlight.new(:title, :options => { :tag => '' }).to_json ) 39 | end 40 | 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/unit/search_scan_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | module Search 5 | class ScanTest < Test::Unit::TestCase 6 | 7 | context "Scan" do 8 | setup do 9 | Configuration.reset 10 | @results = { 11 | "_scroll_id" => "abc123", 12 | "took" => 3, 13 | "hits" => { 14 | "total" => 10, 15 | "hits" => [ 16 | { "_id" => "1", "_source" => { "title" => "Test" } } 17 | ] 18 | } 19 | } 20 | @empty_results = @results.merge('hits' => {'hits' => []}) 21 | @default_response = mock_response @results.to_json, 200 22 | end 23 | 24 | should "initialize the search object with the indices" do 25 | s = Scan.new(['index1', 'index2']) 26 | assert_instance_of Tire::Search::Search, s.search 27 | end 28 | 29 | should "fetch the initial scroll ID" do 30 | s = Scan.new('index1') 31 | s.search.expects(:perform). 32 | returns(stub :json => { '_scroll_id' => 'abc123' }) 33 | 34 | assert_equal 'abc123', s.scroll_id 35 | end 36 | 37 | should "perform the request lazily" do 38 | s = Scan.new('dummy') 39 | 40 | s.expects(:scroll_id). 41 | returns('abc123'). 42 | at_least_once 43 | 44 | Configuration.client.expects(:get). 45 | with { |url,id| url =~ %r|_search/scroll.*search_type=scan| && id == 'abc123' }. 46 | returns(@default_response). 47 | once 48 | 49 | assert_not_nil s.results 50 | assert_not_nil s.response 51 | assert_not_nil s.json 52 | end 53 | 54 | should "set the total and seen variables" do 55 | s = Scan.new('dummy') 56 | s.expects(:scroll_id).returns('abc123').at_least_once 57 | Configuration.client.expects(:get).returns(@default_response).at_least_once 58 | 59 | assert_equal 10, s.total 60 | assert_equal 1, s.seen 61 | end 62 | 63 | should "log the request and response" do 64 | Tire.configure { logger STDERR } 65 | 66 | s = Scan.new('dummy') 67 | s.expects(:scroll_id).returns('abc123').at_least_once 68 | Configuration.client.expects(:get).returns(@default_response).at_least_once 69 | 70 | Configuration.logger.expects(:log_request). 71 | with { |(endpoint, params, curl)| endpoint == 'scroll' } 72 | 73 | Configuration.logger.expects(:log_response). 74 | with { |code, took, body| code == 200 && took == 3 && body == '1/10 (10.0%)' } 75 | 76 | s.__perform 77 | end 78 | 79 | context "results" do 80 | setup do 81 | @search = Scan.new('dummy') 82 | @search.expects(:results). 83 | returns(Results::Collection.new @results). 84 | then. 85 | returns(Results::Collection.new @empty_results). 86 | at_least_once 87 | @search.results 88 | end 89 | 90 | should "be iterable" do 91 | assert_respond_to @search, :each 92 | assert_respond_to @search, :size 93 | 94 | assert_nothing_raised do 95 | @search.each { |batch| p batch; assert_equal 'Test', batch.first.title } 96 | end 97 | end 98 | 99 | should "be iterable by individual documents" do 100 | assert_respond_to @search, :each_document 101 | 102 | assert_nothing_raised do 103 | @search.each_document { |item| assert_equal 'Test', item.title } 104 | end 105 | end 106 | 107 | end 108 | 109 | end 110 | 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/unit/search_script_field_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire::Search 4 | 5 | class ScriptFieldTest < Test::Unit::TestCase 6 | 7 | context "ScriptField" do 8 | 9 | should "be serialized to JSON" do 10 | assert_respond_to ScriptField.new(:test1, {}), :to_json 11 | end 12 | 13 | should "encode simple declarations as JSON" do 14 | assert_equal( { :test1 => { :script => "doc['my_field_name'].value * factor", 15 | :params => { :factor => 2.2 }, :lang => :js } }.to_json, 16 | 17 | ScriptField.new( :test1, 18 | { :script => "doc['my_field_name'].value * factor", 19 | :params => { :factor => 2.2 }, :lang => :js } ).to_json 20 | ) 21 | end 22 | 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/unit/search_sort_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire::Search 4 | 5 | class SortTest < Test::Unit::TestCase 6 | 7 | context "Sort" do 8 | 9 | should "be serialized to JSON" do 10 | assert_respond_to Sort.new, :to_json 11 | end 12 | 13 | should "encode simple strings" do 14 | assert_equal [:foo].to_json, Sort.new.by(:foo).to_json 15 | end 16 | 17 | should "encode method arguments" do 18 | assert_equal [:foo => 'desc'].to_json, Sort.new.by(:foo, 'desc').to_json 19 | end 20 | 21 | should "encode hash" do 22 | assert_equal [ :foo => { :reverse => true } ].to_json, Sort.new.by(:foo, :reverse => true).to_json 23 | end 24 | 25 | should "encode multiple sort fields in chain" do 26 | assert_equal [:foo, :bar].to_json, Sort.new.by(:foo).by(:bar).to_json 27 | end 28 | 29 | should "encode fields when passed as a block to constructor" do 30 | s = Sort.new do 31 | by :foo 32 | by :bar, 'desc' 33 | by :_score 34 | end 35 | assert_equal [ :foo, {:bar => 'desc'}, :_score ].to_json, s.to_json 36 | end 37 | 38 | should "encode fields deeper in json" do 39 | s = Sort.new { by 'author.name' } 40 | assert_equal [ 'author.name' ].to_json, s.to_json 41 | 42 | s = Sort.new { by 'author.name', 'desc' } 43 | assert_equal [ {'author.name' => 'desc'} ].to_json, s.to_json 44 | end 45 | 46 | end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /test/unit/suggest_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Tire 4 | 5 | class SuggestTest < Test::Unit::TestCase 6 | 7 | context "Suggest" do 8 | setup { Configuration.reset } 9 | 10 | should "be initialized with single index" do 11 | s = Suggest::Suggest.new('index') do 12 | suggestion 'default-suggestion' do 13 | text 'foo' 14 | completion 'bar' 15 | end 16 | end 17 | assert_match %r|/index/_suggest|, s.url 18 | end 19 | 20 | should "allow to suggest all indices by leaving index empty" do 21 | s = Suggest::Suggest.new do 22 | suggestion 'default-suggestion' do 23 | text 'foo' 24 | completion 'bar' 25 | end 26 | end 27 | assert_match %r|localhost:9200/_suggest|, s.url 28 | end 29 | 30 | should "return curl snippet for debugging" do 31 | s = Suggest::Suggest.new('index') do 32 | suggestion 'default-suggestion' do 33 | text 'foo' 34 | completion 'bar' 35 | end 36 | end 37 | assert_match %r|curl \-X GET 'http://localhost:9200/index/_suggest\?pretty' -d |, s.to_curl 38 | assert_match %r|\s*{\s*"default-suggestion"\s*:\s*{\s*"text"\s*:\s*"foo"\s*,\s*"completion"\s*:\s*{\s*"field"\s*:\s*"bar"\s*}\s*}\s*}\s*|, s.to_curl 39 | end 40 | 41 | should "return itself as a Hash" do 42 | s = Suggest::Suggest.new('index') do 43 | suggestion 'default_suggestion' do 44 | text 'foo' 45 | completion 'bar' 46 | end 47 | end 48 | assert_nothing_raised do 49 | assert_instance_of Hash, s.to_hash 50 | assert_equal "foo", s.to_hash[:default_suggestion][:text] 51 | end 52 | end 53 | 54 | should "allow to pass options for completion queries" do 55 | s = Suggest::Suggest.new do 56 | suggestion 'default_suggestion' do 57 | text 'foo' 58 | completion 'bar', :fuzzy => true 59 | end 60 | end 61 | assert_equal true, s.to_hash[:default_suggestion][:completion][:fuzzy] 62 | end 63 | 64 | should "perform the suggest lazily" do 65 | response = mock_response '{"_shards": {"total": 5, "successful": 5, "failed": 0}, "default-suggestion": [{"text": "ssd", "offset": 0, "length": 10, "options": [] } ] }', 200 66 | Configuration.client.expects(:get).returns(response) 67 | Results::Suggestions.expects(:new).returns([]) 68 | 69 | s = Suggest::Suggest.new('index') do 70 | suggestion 'default-suggestion' do 71 | text 'foo' 72 | completion 'bar' 73 | end 74 | end 75 | assert_not_nil s.results 76 | assert_not_nil s.response 77 | assert_not_nil s.json 78 | end 79 | 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /tire.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "tire/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "tire" 7 | s.version = Tire::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.summary = "Ruby client for Elasticsearch" 10 | s.homepage = "http://github.com/karmi/tire" 11 | s.authors = [ 'Karel Minarik' ] 12 | s.email = 'karmi@karmi.cz' 13 | 14 | s.rubyforge_project = "tire" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | 20 | s.require_paths = ["lib"] 21 | 22 | s.extra_rdoc_files = [ "README.markdown", "MIT-LICENSE" ] 23 | s.rdoc_options = [ "--charset=UTF-8" ] 24 | 25 | s.required_rubygems_version = ">= 1.3.6" 26 | 27 | # = Library dependencies 28 | # 29 | s.add_dependency "rake" 30 | s.add_dependency "rest-client", "~> 1.6" 31 | s.add_dependency "multi_json", "~> 1.3" 32 | s.add_dependency "activemodel", ">= 3.0" 33 | s.add_dependency "hashr", "~> 0.0.19" 34 | s.add_dependency "activesupport" 35 | s.add_dependency "ansi" 36 | 37 | # = Development dependencies 38 | # 39 | s.add_development_dependency "bundler", "~> 1.0" 40 | s.add_development_dependency "shoulda-context" 41 | s.add_development_dependency "mocha", "~> 0.13" 42 | s.add_development_dependency "minitest", "~> 2.12" 43 | s.add_development_dependency "activerecord", ">= 3.0" 44 | s.add_development_dependency "active_model_serializers" 45 | s.add_development_dependency "mongoid", "~> 2.2" 46 | s.add_development_dependency "redis-persistence" 47 | s.add_development_dependency "faraday" 48 | 49 | unless defined?(JRUBY_VERSION) 50 | s.add_development_dependency "yajl-ruby", "~> 1.0" 51 | s.add_development_dependency "sqlite3" 52 | s.add_development_dependency "bson_ext" 53 | s.add_development_dependency "curb" 54 | s.add_development_dependency "oj" 55 | s.add_development_dependency "turn", "~> 0.9" 56 | end 57 | 58 | s.description = <<-DESC 59 | Tire is a Ruby client for the Elasticsearch search engine/database. 60 | 61 | It provides Ruby-like API for fluent communication with the Elasticsearch server 62 | and blends with ActiveModel class for convenient usage in Rails applications. 63 | 64 | It allows to delete and create indices, define mapping for them, supports 65 | the bulk API, and presents an easy-to-use DSL for constructing your queries. 66 | 67 | It has full ActiveRecord/ActiveModel compatibility, allowing you to index 68 | your models (incrementally upon saving, or in bulk), searching and 69 | paginating the results. 70 | 71 | Please check the documentation at . 72 | DESC 73 | 74 | s.post_install_message =<<-CHANGELOG.gsub(/^ /, '') 75 | ================================================================================ 76 | 77 | Please check the documentation at . 78 | 79 | -------------------------------------------------------------------------------- 80 | 81 | #{Tire::CHANGELOG} 82 | See the full changelog at . 83 | 84 | -------------------------------------------------------------------------------- 85 | CHANGELOG 86 | end 87 | --------------------------------------------------------------------------------