tire 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -820,3 +820,5 @@ You can send feedback via [e-mail](mailto:karmi@karmi.cz) or via [Github Issues]
820
820
  -----
821
821
 
822
822
  [Karel Minarik](http://karmi.cz) and [contributors](http://github.com/karmi/tire/contributors)
823
+
824
+ ![](https://ga-beacon.appspot.com/UA-46901128-1/karmi/retire?pixel)
@@ -816,6 +816,156 @@ s = Tire.search 'articles' do
816
816
  highlight :title, :body, :options => { :tag => '<strong class="highlight">' }
817
817
  end
818
818
 
819
+ #### Suggest
820
+
821
+ #
822
+ # _Elasticsearch_
823
+ # [suggest](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html)
824
+ # feature suggests similar terms based on user input.
825
+ # You can specify either the `term` or `phrase` suggester in the Tire DSL, or
826
+ # use the `completion` suggester to get fast completions of user inputs, suitable
827
+ # for auto-complete and instant search features.
828
+
829
+ # Suggestion API is available either as standalone method or part of the search request.
830
+
831
+ # To get search suggestions while doing a search, call the suggest API
832
+ #
833
+ s = Tire.search 'articles' do
834
+
835
+ # To define a suggest using the term suggester, first provide a custom name for the suggest.
836
+ #
837
+ suggest :suggest_title do
838
+ # Specify the input text.
839
+ #
840
+ text 'thrree blind mice'
841
+ # Then, define the field you want to use for suggestions and any options.
842
+ #
843
+ term :title, size: 3, sort: 'frequency'
844
+ end
845
+
846
+ # To define a suggest using the `phrase` suggest, use a different name.
847
+ suggest :phrase_suggest_title do
848
+ # Specify the text input text.
849
+ #
850
+ text 'thrree blind mice'
851
+ # Again, define the field you want to use for suggestions and any options.
852
+ #
853
+ phrase :title, size: 3 do
854
+ # Optinally, configure the `smoothing` option...
855
+ #
856
+ smoothing :stupid_backoff, discount: 0.5
857
+
858
+ # ...or the `generator` option.
859
+ generator :title, min_word_len: 1
860
+ end
861
+ end
862
+ end
863
+
864
+ # The results will be available in the `suggestions` property (which is iterable)
865
+ #
866
+ s.results.suggestions.each do |name, options|
867
+ puts "Suggestion returned for #{name}:\n"
868
+ options.each do |option|
869
+ puts "* Raw result: #{option}"
870
+ end
871
+ end
872
+
873
+ # You can also use helper methods available in suggestions results to get only
874
+ # the suggested terms or phrases.
875
+ #
876
+ puts "Available corrections for suggest_title: #{s.results.suggestions.texts(:suggest_title).join(', ')}"
877
+
878
+ # You can use the standalone API to achieve the same result:
879
+ #
880
+ s = Tire.suggest('articles') do
881
+
882
+ # Notice that for standalone API, the block method is `suggestion` rather than `suggest`:
883
+ #
884
+ suggestion :term_suggest do
885
+ text 'thrree'
886
+ term :title, size: 3, sort: 'frequency'
887
+ end
888
+
889
+ end
890
+
891
+ # You'll get the same object as above but as top level object
892
+ #
893
+ puts "Available corrections: #{s.results.texts.join(', ')}"
894
+
895
+ #### Completion
896
+
897
+ # In order to use _Elasticsearch_ completion you'll need to update your mappings to provide a field
898
+ # with completion type. The example is adapted from this
899
+ # [blog post](http://www.elasticsearch.org/blog/you-complete-me/).
900
+ #
901
+ index = Tire.index('hotels') do
902
+ delete
903
+
904
+ # Notice the type completion for the field _name_suggest_:
905
+ #
906
+ create :mappings => {
907
+ :hotel => {
908
+ :properties => {
909
+ :name => {:type => 'string'},
910
+ :city => {:type => 'string'},
911
+ :name_suggest => {:type => 'completion'}
912
+ }
913
+ }
914
+ }
915
+
916
+ # Let's add some documents into this index:
917
+ #
918
+ import([
919
+ {:id => '1', :type => 'hotel', :name => 'Mercure Hotel Munich', :city => 'Munich', :name_suggest => 'Mercure Hotel Munich'},
920
+ {:id => '2', :type => 'hotel', :name => 'Hotel Monaco', :city => 'Munich', :name_suggest => 'Hotel Monaco'},
921
+ ])
922
+ refresh
923
+
924
+ end
925
+
926
+ # We can ask for all hotels starting with a given prefix (such as "m") with this query:
927
+ #
928
+ s = Tire.suggest('hotels') do
929
+ suggestion 'complete' do
930
+ text 'm'
931
+ completion 'name_suggest'
932
+ end
933
+ end
934
+
935
+ # And retrieve results as above with the same object:
936
+ #
937
+ puts "There are #{s.results.texts.size} hotels starting with m:"
938
+ s.results.texts.each do |hotel|
939
+ puts "* #{hotel}"
940
+ end
941
+
942
+ # You can use some advanced features of completion such as multiple inputs and unified output for
943
+ # the same document.
944
+
945
+ # If you add a document which has inputs and output values for the suggest field:
946
+ #
947
+ index.store({:id => '1', :type => 'hotel', :name => 'Mercure Hotel Munich', :city => 'Munich',
948
+ :name_suggest => {:input => ['Mercure Hotel Munich', 'Mercure Munich'], :output => 'Hotel Mercure'}})
949
+ index.store({:id => '2', :type => 'hotel', :name => 'Hotel Monaco', :city => 'Munich',
950
+ :name_suggest => {:input => ['Monaco Munich', 'Hotel Monaco'], :output => 'Hotel Monaco'}})
951
+ index.refresh
952
+
953
+ # ... a completion request with the same input as above ...
954
+ #
955
+ s = Tire.suggest('hotels') do
956
+ suggestion 'complete' do
957
+ text 'm'
958
+ completion 'name_suggest'
959
+ end
960
+ end
961
+
962
+ # ... will match multiple inputs for the same document and return unified output in results:
963
+ #
964
+ puts "There are #{s.results.texts.size} hotels starting with m:"
965
+ s.results.texts.each do |hotel|
966
+ puts "* #{hotel}"
967
+ end
968
+
819
969
  #### Percolation
820
970
 
821
971
  # _Elasticsearch_ comes with one very interesting, and rather unique feature:
@@ -30,12 +30,15 @@ require 'tire/search/filter'
30
30
  require 'tire/search/highlight'
31
31
  require 'tire/search/scan'
32
32
  require 'tire/search/script_field'
33
+ require 'tire/suggest'
34
+ require 'tire/suggest/suggestion'
33
35
  require 'tire/delete_by_query'
34
36
  require 'tire/multi_search'
35
37
  require 'tire/count'
36
38
  require 'tire/results/pagination'
37
39
  require 'tire/results/collection'
38
40
  require 'tire/results/item'
41
+ require 'tire/results/suggestions'
39
42
  require 'tire/index'
40
43
  require 'tire/alias'
41
44
  require 'tire/dsl'
@@ -44,6 +47,7 @@ require 'tire/model/callbacks'
44
47
  require 'tire/model/percolate'
45
48
  require 'tire/model/indexing'
46
49
  require 'tire/model/import'
50
+ require 'tire/model/suggest'
47
51
  require 'tire/model/search'
48
52
  require 'tire/model/persistence/finders'
49
53
  require 'tire/model/persistence/attributes'
@@ -117,6 +117,10 @@ module Tire
117
117
  Search::Scan.new(names, options, &block)
118
118
  end
119
119
 
120
+ def suggest(indices=nil, options={}, &block)
121
+ Suggest::Suggest.new(indices, options, &block)
122
+ end
123
+
120
124
  def aliases
121
125
  Alias.all
122
126
  end
@@ -145,8 +145,8 @@ module Tire
145
145
  if instance.destroyed?
146
146
  index.remove instance
147
147
  else
148
- response = index.store( instance, {:percolate => percolator} )
149
- instance.tire.matches = response['matches'] if instance.tire.respond_to?(:matches=)
148
+ @response = index.store( instance, {:percolate => percolator} )
149
+ instance.tire.matches = @response['matches'] if instance.tire.respond_to?(:matches=)
150
150
  self
151
151
  end
152
152
  end
@@ -225,6 +225,7 @@ module Tire
225
225
  include Tire::Model::Import::ClassMethods
226
226
  include Tire::Model::Indexing::ClassMethods
227
227
  include Tire::Model::Percolate::ClassMethods
228
+ include Tire::Model::Suggest::ClassMethods
228
229
  include ClassMethods
229
230
 
230
231
  INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
@@ -245,9 +246,10 @@ module Tire
245
246
 
246
247
  INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
247
248
 
248
- attr_reader :instance
249
+ attr_reader :instance, :response
249
250
  def initialize(instance)
250
251
  @instance = instance
252
+ @response = {}
251
253
  end
252
254
  end
253
255
 
@@ -0,0 +1,34 @@
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
@@ -5,16 +5,17 @@ module Tire
5
5
  include Enumerable
6
6
  include Pagination
7
7
 
8
- attr_reader :time, :total, :options, :facets, :max_score
8
+ attr_reader :time, :total, :options, :facets, :max_score, :suggestions
9
9
 
10
10
  def initialize(response, options={})
11
- @response = response
12
- @options = options
13
- @time = response['took'].to_i
14
- @total = response['hits']['total'].to_i rescue nil
15
- @facets = response['facets']
16
- @max_score = response['hits']['max_score'].to_f rescue nil
17
- @wrapper = options[:wrapper] || Configuration.wrapper
11
+ @response = response
12
+ @options = options
13
+ @time = response['took'].to_i
14
+ @total = response['hits']['total'].to_i rescue nil
15
+ @facets = response['facets']
16
+ @suggestions = Suggestions.new(response['suggest']) if response['suggest']
17
+ @max_score = response['hits']['max_score'].to_f rescue nil
18
+ @wrapper = options[:wrapper] || Configuration.wrapper
18
19
  end
19
20
 
20
21
  def results
@@ -0,0 +1,68 @@
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
@@ -81,6 +81,12 @@ module Tire
81
81
  self
82
82
  end
83
83
 
84
+ def suggest(name, &block)
85
+ @suggest ||= {}
86
+ @suggest.update Tire::Suggest::Suggestion.new(name, &block).to_hash
87
+ self
88
+ end
89
+
84
90
  def highlight(*args)
85
91
  unless args.empty?
86
92
  @highlight = Highlight.new(*args)
@@ -160,6 +166,7 @@ module Tire
160
166
  request.update( { :filter => @filters.first.to_hash } ) if @filters && @filters.size == 1
161
167
  request.update( { :filter => { :and => @filters.map {|filter| filter.to_hash} } } ) if @filters && @filters.size > 1
162
168
  request.update( { :highlight => @highlight.to_hash } ) if @highlight
169
+ request.update( { :suggest => @suggest.to_hash } ) if @suggest
163
170
  request.update( { :size => @size } ) if @size
164
171
  request.update( { :from => @from } ) if @from
165
172
  request.update( { :fields => @fields } ) if @fields
@@ -0,0 +1,108 @@
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
+
@@ -0,0 +1,105 @@
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
@@ -1,15 +1,11 @@
1
1
  module Tire
2
- VERSION = "0.6.1"
2
+ VERSION = "0.6.2"
3
3
 
4
4
  CHANGELOG =<<-END
5
5
  IMPORTANT CHANGES LATELY:
6
6
 
7
- * Added support for bulk update
8
- * Improved Kaminari support
9
- * Improved the behaviour of `default` properties in Tire::Persistence
10
- * Added the information about the gem "retirement" and other documentation improvements
11
- * Fixed errors due to NewRelic's patching of Curl
12
- * [ACTIVEMODEL] Use Object#id#to_s in `get_id_from_document`
13
- * Added support for "Delete By Query" API
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)
14
10
  END
15
11
  end
@@ -61,7 +61,7 @@ module Tire
61
61
  end
62
62
  @index.bulk_update documents, refresh: true
63
63
 
64
- documents = Tire.search('bulk-test') { query {all} }.results.to_a
64
+ documents = Tire.search('bulk-test') { query {all} }.results.to_a.sort { |a,b| a.id <=> b.id }
65
65
  assert_equal 'one-updated', documents[0][:title]
66
66
  assert_equal 'two-updated', documents[1][:title]
67
67
  assert_equal 'three-updated', documents[2][:title]
@@ -0,0 +1,70 @@
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_equal @response.response["ok"], true
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
@@ -0,0 +1,129 @@
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
@@ -11,7 +11,10 @@ class ActiveRecordArticle < ActiveRecord::Base
11
11
  mapping do
12
12
  indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball'
13
13
  indexes :created_at, :type => 'date'
14
-
14
+ indexes :suggest, :type => :completion,
15
+ :index_analyzer => :simple,
16
+ :search_analyzer => :simple,
17
+ :payloads => true
15
18
  indexes :comments do
16
19
  indexes :author
17
20
  indexes :body
@@ -23,6 +26,7 @@ class ActiveRecordArticle < ActiveRecord::Base
23
26
  {
24
27
  :title => title,
25
28
  :length => length,
29
+ :suggest => suggest,
26
30
 
27
31
  :comments => comments.map { |c| { :_type => 'active_record_comment',
28
32
  :_id => c.id,
@@ -32,6 +36,14 @@ class ActiveRecordArticle < ActiveRecord::Base
32
36
  }.to_json
33
37
  end
34
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
+
35
47
  def length
36
48
  title.length
37
49
  end
@@ -339,6 +339,33 @@ module Tire
339
339
 
340
340
  end
341
341
 
342
+ context "suggest" do
343
+ should "allow to specify term suggest" do
344
+ s = Search::Search.new('index') do
345
+ suggest :suggest_name do
346
+ text 'text'
347
+ term :candidate_field
348
+ end
349
+ end
350
+
351
+ hash = MultiJson.decode( s.to_json )
352
+ assert_not_nil hash['suggest']
353
+ end
354
+
355
+ should "allow to specify phrase suggest" do
356
+ s = Search::Search.new('index') do
357
+ suggest :suggest_name do
358
+ text 'text'
359
+ phrase :candidate_field
360
+ end
361
+ end
362
+
363
+ hash = MultiJson.decode( s.to_json )
364
+ assert_not_nil hash['suggest']
365
+ end
366
+
367
+ end
368
+
342
369
  context "with version" do
343
370
 
344
371
  should "set the version" do
@@ -0,0 +1,82 @@
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tire
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-16 00:00:00.000000000 Z
12
+ date: 2014-01-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -409,10 +409,12 @@ files:
409
409
  - lib/tire/model/persistence/finders.rb
410
410
  - lib/tire/model/persistence/storage.rb
411
411
  - lib/tire/model/search.rb
412
+ - lib/tire/model/suggest.rb
412
413
  - lib/tire/multi_search.rb
413
414
  - lib/tire/results/collection.rb
414
415
  - lib/tire/results/item.rb
415
416
  - lib/tire/results/pagination.rb
417
+ - lib/tire/results/suggestions.rb
416
418
  - lib/tire/rubyext/hash.rb
417
419
  - lib/tire/rubyext/ruby_1_8.rb
418
420
  - lib/tire/rubyext/symbol.rb
@@ -427,6 +429,8 @@ files:
427
429
  - lib/tire/search/scan.rb
428
430
  - lib/tire/search/script_field.rb
429
431
  - lib/tire/search/sort.rb
432
+ - lib/tire/suggest.rb
433
+ - lib/tire/suggest/suggestion.rb
430
434
  - lib/tire/tasks.rb
431
435
  - lib/tire/utils.rb
432
436
  - lib/tire/version.rb
@@ -472,7 +476,9 @@ files:
472
476
  - test/integration/results_test.rb
473
477
  - test/integration/scan_test.rb
474
478
  - test/integration/script_fields_test.rb
479
+ - test/integration/search_response_test.rb
475
480
  - test/integration/sort_test.rb
481
+ - test/integration/suggest_test.rb
476
482
  - test/models/active_model_article.rb
477
483
  - test/models/active_model_article_with_callbacks.rb
478
484
  - test/models/active_model_article_with_custom_document_type.rb
@@ -516,18 +522,17 @@ files:
516
522
  - test/unit/search_script_field_test.rb
517
523
  - test/unit/search_sort_test.rb
518
524
  - test/unit/search_test.rb
525
+ - test/unit/suggest_test.rb
519
526
  - test/unit/tire_test.rb
520
527
  - tire.gemspec
521
528
  homepage: http://github.com/karmi/tire
522
529
  licenses: []
523
530
  post_install_message: ! "================================================================================\n\n
524
531
  \ Please check the documentation at <http://karmi.github.com/retire/>.\n\n--------------------------------------------------------------------------------\n\n
525
- \ IMPORTANT CHANGES LATELY:\n\n * Added support for bulk update\n * Improved Kaminari
526
- support\n * Improved the behaviour of `default` properties in Tire::Persistence\n
527
- \ * Added the information about the gem \"retirement\" and other documentation improvements\n
528
- \ * Fixed errors due to NewRelic's patching of Curl\n * [ACTIVEMODEL] Use Object#id#to_s
529
- in `get_id_from_document`\n * Added support for \"Delete By Query\" API\n\n See
530
- the full changelog at <http://github.com/karmi/tire/commits/v0.6.1>.\n\n--------------------------------------------------------------------------------\n"
532
+ \ IMPORTANT CHANGES LATELY:\n\n 19e524c [ACTIVEMODEL] Exposed the response from
533
+ `MyModel#.update_index` as the `response` method on return value\n bfcde21 [#916]
534
+ Added support for the Suggest API (@marc-villanueva)\n 710451d [#857] Added support
535
+ for the Suggest API (@fbatista)\n\n See the full changelog at <http://github.com/karmi/tire/commits/v0.6.2>.\n\n--------------------------------------------------------------------------------\n"
531
536
  rdoc_options:
532
537
  - --charset=UTF-8
533
538
  require_paths:
@@ -593,7 +598,9 @@ test_files:
593
598
  - test/integration/results_test.rb
594
599
  - test/integration/scan_test.rb
595
600
  - test/integration/script_fields_test.rb
601
+ - test/integration/search_response_test.rb
596
602
  - test/integration/sort_test.rb
603
+ - test/integration/suggest_test.rb
597
604
  - test/models/active_model_article.rb
598
605
  - test/models/active_model_article_with_callbacks.rb
599
606
  - test/models/active_model_article_with_custom_document_type.rb
@@ -637,5 +644,6 @@ test_files:
637
644
  - test/unit/search_script_field_test.rb
638
645
  - test/unit/search_sort_test.rb
639
646
  - test/unit/search_test.rb
647
+ - test/unit/suggest_test.rb
640
648
  - test/unit/tire_test.rb
641
649
  has_rdoc: