tire 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: