tire 0.1.15 → 0.1.16

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.
data/.gitignore CHANGED
@@ -3,6 +3,8 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
  rdoc/
6
+ doc/
7
+ .yardoc/
6
8
  coverage/
7
9
  scratch/
8
10
  examples/*.html
data/README.markdown CHANGED
@@ -439,19 +439,25 @@ Any other parameters you provide to the `import` method are passed down to the `
439
439
  Are we saying you have to fiddle with this thing in a `rails console` or silly Ruby scripts? No.
440
440
  Just call the included _Rake_ task on the commandline:
441
441
 
442
+ ```bash
442
443
  $ rake environment tire:import CLASS='Article'
444
+ ```
443
445
 
444
446
  You can also force-import the data by deleting the index first (and creating it with mapping
445
447
  provided by the `mapping` block in your model):
446
448
 
449
+ ```bash
447
450
  $ rake environment tire:import CLASS='Article' FORCE=true
451
+ ```
448
452
 
449
453
  When you'll spend more time with _ElasticSearch_, you'll notice how
450
454
  [index aliases](http://www.elasticsearch.org/guide/reference/api/admin-indices-aliases.html)
451
455
  are the best idea since the invention of inverted index.
452
456
  You can index your data into a fresh index (and possibly update an alias if everything's fine):
453
457
 
458
+ ```bash
454
459
  $ rake environment tire:import CLASS='Article' INDEX='articles-2011-05'
460
+ ```
455
461
 
456
462
  OK. All this time we have been talking about `ActiveRecord` models, since
457
463
  it is a reasonable Rails' default for the storage layer.
data/Rakefile CHANGED
@@ -30,7 +30,7 @@ require 'rdoc/task'
30
30
  Rake::RDocTask.new do |rdoc|
31
31
  rdoc.rdoc_dir = 'rdoc'
32
32
  rdoc.title = "Tire"
33
- rdoc.rdoc_files.include('README.rdoc')
33
+ rdoc.rdoc_files.include('README.markdown')
34
34
  rdoc.rdoc_files.include('lib/**/*.rb')
35
35
  end
36
36
 
@@ -213,7 +213,7 @@ gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'New Article', new
213
213
  CODE
214
214
 
215
215
  gsub_file 'config/routes.rb', %r{resources :articles}, <<-CODE
216
- resources :articles do
216
+ resources :articles do
217
217
  collection { get :search }
218
218
  end
219
219
  CODE
data/examples/tire-dsl.rb CHANGED
@@ -580,6 +580,7 @@ end
580
580
  #
581
581
  # * [terms](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html)
582
582
  # * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
583
+ # * [range](http://www.elasticsearch.org/guide/reference/api/search/facets/range-facet.html)
583
584
 
584
585
  # We have seen that _ElasticSearch_ facets enable us to fetch complex aggregations from our data.
585
586
  #
data/lib/tire/dsl.rb CHANGED
@@ -5,7 +5,7 @@ module Tire
5
5
  Configuration.class_eval(&block)
6
6
  end
7
7
 
8
- def search(indices, options={}, &block)
8
+ def search(indices=nil, options={}, &block)
9
9
  if block_given?
10
10
  Search::Search.new(indices, options, &block).perform
11
11
  else
@@ -16,9 +16,23 @@ module Tire
16
16
  end
17
17
  end
18
18
 
19
- def indexes(name, options = {})
20
- # p "#{self}, SEARCH PROPERTY, #{name}"
21
- mapping[name] = options
19
+ def indexes(name, options = {}, &block)
20
+ options[:type] ||= 'string'
21
+
22
+ if block_given?
23
+ mapping[name] ||= { :type => 'object', :properties => {} }
24
+ @_nested_mapping = name
25
+ nested = yield
26
+ @_nested_mapping = nil
27
+ self
28
+ else
29
+ if @_nested_mapping
30
+ mapping[@_nested_mapping][:properties][name] = options
31
+ else
32
+ mapping[name] = options
33
+ end
34
+ self
35
+ end
22
36
  end
23
37
 
24
38
  def store_mapping?
@@ -35,7 +35,10 @@ module Tire
35
35
  def search(query=nil, options={}, &block)
36
36
  old_wrapper = Tire::Configuration.wrapper
37
37
  Tire::Configuration.wrapper self
38
- sort = Array( options[:order] || options[:sort] )
38
+
39
+ sort = Array( options[:order] || options[:sort] )
40
+ options = {:type => document_type}.update(options)
41
+
39
42
  unless block_given?
40
43
  s = Tire::Search::Search.new(elasticsearch_index.name, options)
41
44
  s.query { string query }
@@ -24,7 +24,7 @@ module Tire
24
24
  document = {}
25
25
 
26
26
  # Update the document with content and ID
27
- document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( h['fields'] || {} )
27
+ document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
28
28
  document.update( {'id' => h['_id']} )
29
29
 
30
30
  # Update the document with meta information
@@ -61,6 +61,28 @@ module Tire
61
61
  self
62
62
  end
63
63
 
64
+ # Handles _source prefixed fields properly: strips the prefix and converts fields to nested Hashes
65
+ #
66
+ def __parse_fields__(fields={})
67
+ ( fields ||= {} ).clone.each_pair do |key,value|
68
+ next unless key.to_s =~ /_source/ # Skip regular JSON immediately
69
+
70
+ keys = key.to_s.split('.').reject { |n| n == '_source' }
71
+ fields.delete(key)
72
+
73
+ result = {}
74
+ path = []
75
+
76
+ keys.each do |name|
77
+ path << name
78
+ eval "result[:#{path.join('][:')}] ||= {}"
79
+ eval "result[:#{path.join('][:')}] = #{value.inspect}" if keys.last == name
80
+ end
81
+ fields.update result
82
+ end
83
+ fields
84
+ end
85
+
64
86
  end
65
87
 
66
88
  end
data/lib/tire/search.rb CHANGED
@@ -5,11 +5,17 @@ module Tire
5
5
 
6
6
  attr_reader :indices, :url, :results, :response, :json, :query, :facets, :filters, :options
7
7
 
8
- def initialize(*indices, &block)
9
- @options = indices.last.is_a?(Hash) ? indices.pop : {}
10
- @indices = indices
11
- raise ArgumentError, 'Please pass index or indices to search' if @indices.empty?
8
+ def initialize(indices=nil, options = {}, &block)
9
+ Tire.warn "Passing indices as multiple arguments to the `Search.new` method " +
10
+ "has been deprecated, please pass them as an Array: " +
11
+ "Search.new([#{indices}, #{options}])" if options.is_a?(String)
12
+ @indices = Array(indices)
13
+ @options = options
14
+ @type = @options[:type]
12
15
 
16
+ @url = Configuration.url+['/', @indices.join(','), @type, '_search'].compact.join('/').squeeze('/')
17
+
18
+ # TODO: Do not allow changing the wrapper here or set it back after yield
13
19
  Configuration.wrapper @options[:wrapper] if @options[:wrapper]
14
20
  block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
15
21
  end
@@ -64,7 +70,6 @@ module Tire
64
70
  end
65
71
 
66
72
  def perform
67
- @url = "#{Configuration.url}/#{indices.join(',')}/_search"
68
73
  @response = Configuration.client.get(@url, self.to_json)
69
74
  @json = MultiJson.decode(@response.body)
70
75
  @results = Results::Collection.new(@json, @options)
@@ -77,7 +82,7 @@ module Tire
77
82
  end
78
83
 
79
84
  def to_curl
80
- %Q|curl -X GET "#{Configuration.url}/#{indices.join(',')}/_search?pretty=true" -d '#{self.to_json}'|
85
+ %Q|curl -X GET "#{@url}?pretty=true" -d '#{self.to_json}'|
81
86
  end
82
87
 
83
88
  def to_hash
data/lib/tire/version.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  module Tire
2
- VERSION = "0.1.15"
2
+ VERSION = "0.1.16"
3
3
 
4
4
  CHANGELOG =<<-END
5
5
  IMPORTANT CHANGES LATELY:
6
6
 
7
- # Cleanup of code for getting document type, id, JSON serialization
8
- # Bunch of deprecations: sorting, passing document type to store/remove
9
- # Displaying a warning when no ID is passed when storing in bulk
10
- # Correct handling of import for Mongoid/Kaminari combos
7
+ # Defined mapping for nested fields [#56]
8
+ # Mapping type is optional and defaults to "string"
9
+ # Fixed handling of fields returned prefixed by _source from ES [#31]
10
+ # Allow passing the type to search and added that model passes `document_type` to search [@jonkarna, #38]
11
+ # Allow leaving index name empty for searching the whole server
11
12
  END
12
13
  end
@@ -36,12 +36,22 @@ module Tire
36
36
  a.save
37
37
  id = a.id
38
38
 
39
+ # Store document of another type in the index
40
+ Index.new 'supermodel_articles' do
41
+ store :type => 'other-thing', :title => 'Title for other thing'
42
+ end
43
+
39
44
  a.index.refresh
40
45
  sleep(1.5)
41
46
 
47
+ # The index should contain 2 documents
48
+ assert_equal 2, Tire.search('supermodel_articles') { query { all } }.results.size
49
+
42
50
  results = SupermodelArticle.search 'test'
43
51
 
52
+ # The model should find only 1 document
44
53
  assert_equal 1, results.count
54
+
45
55
  assert_instance_of SupermodelArticle, results.first
46
56
  assert_equal 'Test', results.first.title
47
57
  assert_not_nil results.first._score
@@ -63,14 +73,15 @@ module Tire
63
73
 
64
74
  should "retrieve sorted documents by IDs returned from search" do
65
75
  SupermodelArticle.create! :title => 'foo'
66
- SupermodelArticle.create! :title => 'bar'
76
+ SupermodelArticle.create! :id => 'abc123', :title => 'bar'
67
77
 
68
78
  SupermodelArticle.elasticsearch_index.refresh
69
79
  results = SupermodelArticle.search 'foo OR bar^100'
70
80
 
71
81
  assert_equal 2, results.count
72
82
 
73
- assert_equal 'bar', results.first.title
83
+ assert_equal 'bar', results.first.title
84
+ assert_equal 'abc123', results.first.id
74
85
  end
75
86
 
76
87
  end
@@ -20,6 +20,7 @@ module Tire
20
20
 
21
21
  setup do
22
22
  @stub = stub('search') { stubs(:query).returns(self); stubs(:perform).returns(self); stubs(:results).returns([]) }
23
+ Tire::Index.any_instance.stubs(:exists?).returns(false)
23
24
  end
24
25
 
25
26
  teardown do
@@ -51,31 +52,36 @@ module Tire
51
52
  should_eventually "NOT overload existing top-level instance methods" do
52
53
  end
53
54
 
54
- should "search in index named after class name by default" do
55
- i = 'active_model_articles'
56
- Tire::Search::Search.expects(:new).with(i, {}).returns(@stub)
55
+ should "limit searching in index for documents matching the model 'document_type'" do
56
+ Tire::Search::Search.
57
+ expects(:new).
58
+ with(ActiveModelArticle.index_name, { :type => ActiveModelArticle.document_type }).
59
+ returns(@stub).
60
+ twice
57
61
 
58
62
  ActiveModelArticle.search 'foo'
59
- end
60
-
61
- should_eventually "search only in document types for this class by default" do
63
+ ActiveModelArticle.search { query { string 'foo' } }
62
64
  end
63
65
 
64
66
  should "search in custom name" do
65
67
  first = 'custom-index-name'
66
68
  second = 'another-custom-index-name'
69
+ expected_options = { :type => ActiveModelArticleWithCustomIndexName.document_type }
67
70
 
68
- Tire::Search::Search.expects(:new).with(first, {}).returns(@stub)
69
- ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
71
+ Tire::Search::Search.expects(:new).with(first, expected_options).returns(@stub).twice
72
+ ActiveModelArticleWithCustomIndexName.index_name first
70
73
  ActiveModelArticleWithCustomIndexName.search 'foo'
74
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
71
75
 
72
- Tire::Search::Search.expects(:new).with(second, {}).returns(@stub)
73
- ActiveModelArticleWithCustomIndexName.index_name 'another-custom-index-name'
76
+ Tire::Search::Search.expects(:new).with(second, expected_options).returns(@stub).twice
77
+ ActiveModelArticleWithCustomIndexName.index_name second
74
78
  ActiveModelArticleWithCustomIndexName.search 'foo'
79
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
75
80
 
76
- Tire::Search::Search.expects(:new).with(first, {}).returns(@stub)
77
- ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
81
+ Tire::Search::Search.expects(:new).with(first, expected_options).returns(@stub).twice
82
+ ActiveModelArticleWithCustomIndexName.index_name first
78
83
  ActiveModelArticleWithCustomIndexName.search 'foo'
84
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
79
85
  end
80
86
 
81
87
  should "allow to refresh index" do
@@ -256,6 +262,45 @@ module Tire
256
262
  assert_equal 'snowball', ModelWithCustomMapping.mapping[:title][:analyzer]
257
263
  end
258
264
 
265
+ should "define mapping for nested properties with a block" do
266
+ expected_mapping = {
267
+ :mappings => { :model_with_nested_mapping => {
268
+ :properties => {
269
+ :title => { :type => 'string' },
270
+ :author => {
271
+ :type => 'object',
272
+ :properties => {
273
+ :first_name => { :type => 'string' },
274
+ :last_name => { :type => 'string', :boost => 100 }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }}
280
+
281
+ Tire::Index.any_instance.expects(:create).with(expected_mapping)
282
+
283
+ class ::ModelWithNestedMapping
284
+ extend ActiveModel::Naming
285
+ extend ActiveModel::Callbacks
286
+
287
+ include Tire::Model::Search
288
+ include Tire::Model::Callbacks
289
+
290
+ mapping do
291
+ indexes :title, :type => 'string'
292
+ indexes :author do
293
+ indexes :first_name, :type => 'string'
294
+ indexes :last_name, :type => 'string', :boost => 100
295
+ end
296
+ end
297
+
298
+ end
299
+
300
+ assert_not_nil ModelWithNestedMapping.mapping[:author][:properties][:last_name]
301
+ assert_equal 100, ModelWithNestedMapping.mapping[:author][:properties][:last_name][:boost]
302
+ end
303
+
259
304
  end
260
305
 
261
306
  context "with index update callbacks" do
@@ -105,6 +105,44 @@ module Tire
105
105
 
106
106
  end
107
107
 
108
+ context "wrapping results with selected fields" do
109
+ # When limiting fields from _source to return ES returns them prefixed, not as "real" Hashes.
110
+ # Underlying issue: https://github.com/karmi/tire/pull/31#issuecomment-1340967
111
+ #
112
+ setup do
113
+ Configuration.reset :wrapper
114
+ @default_response = { 'hits' => { 'hits' =>
115
+ [ { '_id' => 1, '_score' => 0.5, '_index' => 'testing', '_type' => 'article',
116
+ 'fields' => {
117
+ 'title' => 'Knee Deep in JSON',
118
+ 'crazy.field' => 'CRAAAAZY!',
119
+ '_source.artist' => {
120
+ 'name' => 'Elastiq',
121
+ 'meta' => {
122
+ 'favorited' => 1000,
123
+ 'url' => 'http://first.fm/abc123/xyz567'
124
+ }
125
+ },
126
+ '_source.track.info.duration' => {
127
+ 'minutes' => 3
128
+ }
129
+ } } ] } }
130
+ collection = Results::Collection.new(@default_response)
131
+ @item = collection.first
132
+ end
133
+
134
+ should "return fields from the first level" do
135
+ assert_equal 'Knee Deep in JSON', @item.title
136
+ end
137
+
138
+ should "return fields from the _source prefixed and nested fields" do
139
+ assert_equal 'Elastiq', @item.artist.name
140
+ assert_equal 1000, @item.artist.meta.favorited
141
+ assert_equal 3, @item.track.info.duration.minutes
142
+ end
143
+
144
+ end
145
+
108
146
  context "while paginating results" do
109
147
 
110
148
  setup do
@@ -36,7 +36,7 @@ module Tire
36
36
 
37
37
  should "properly serialize Time into JSON" do
38
38
  json = { :time => Time.mktime(2011, 01, 01, 11, 00).to_json }.to_json
39
- assert_equal '"2011-01-01T11:00:00+01:00"', MultiJson.decode(json)['time']
39
+ assert_match /"2011-01-01T11:00:00.*"/, MultiJson.decode(json)['time']
40
40
  end
41
41
 
42
42
  end
@@ -7,8 +7,32 @@ module Tire
7
7
  context "Search" do
8
8
  setup { Configuration.reset :logger }
9
9
 
10
- should "be initialized with index/indices" do
11
- assert_raise(ArgumentError) { Search::Search.new }
10
+ should "be initialized with single index" do
11
+ s = Search::Search.new('index') { query { string 'foo' } }
12
+ assert_match %r|/index/_search|, s.url
13
+ end
14
+
15
+ should "be initialized with multiple indices" do
16
+ s = Search::Search.new(['index1','index2']) { query { string 'foo' } }
17
+ assert_match %r|/index1,index2/_search|, s.url
18
+ end
19
+
20
+ should "be initialized with multiple indices as string" do
21
+ s = Search::Search.new(['index1,index2,index3']) { query { string 'foo' } }
22
+ assert_match %r|/index1,index2,index3/_search|, s.url
23
+ end
24
+
25
+ should "allow to search all indices by leaving index empty" do
26
+ s = Search::Search.new { query { string 'foo' } }
27
+ assert_match %r|localhost:9200/_search|, s.url
28
+ end
29
+
30
+ should "allow to limit results with document type" do
31
+ s = Search::Search.new('index', :type => 'bar') do
32
+ query { string 'foo' }
33
+ end
34
+
35
+ assert_match %r|index/bar/_search|, s.url
12
36
  end
13
37
 
14
38
  should "allow to pass block to query" do
@@ -35,7 +59,7 @@ module Tire
35
59
  s = Search::Search.new('index1') do;end
36
60
  assert_equal ['index1'], s.indices
37
61
 
38
- s = Search::Search.new('index1', 'index2') do;end
62
+ s = Search::Search.new(['index1', 'index2']) do;end
39
63
  assert_equal ['index1', 'index2'], s.indices
40
64
  end
41
65
 
@@ -49,14 +73,14 @@ module Tire
49
73
  end
50
74
 
51
75
  should "return curl snippet with multiple indices for debugging" do
52
- s = Search::Search.new('index_1', 'index_2') do
76
+ s = Search::Search.new(['index_1', 'index_2']) do
53
77
  query { string 'title:foo' }
54
78
  end
55
79
  assert_match /index_1,index_2/, s.to_curl
56
80
  end
57
81
 
58
82
  should "return itself as a Hash" do
59
- s = Search::Search.new('index_1', 'index_2') do
83
+ s = Search::Search.new('index') do
60
84
  query { string 'title:foo' }
61
85
  end
62
86
  assert_nothing_raised do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tire
3
3
  version: !ruby/object:Gem::Version
4
- hash: 5
4
+ hash: 59
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 15
10
- version: 0.1.15
9
+ - 16
10
+ version: 0.1.16
11
11
  platform: ruby
12
12
  authors:
13
13
  - Karel Minarik
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-04 00:00:00 Z
18
+ date: 2011-07-14 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rake
@@ -255,7 +255,6 @@ files:
255
255
  - MIT-LICENSE
256
256
  - README.markdown
257
257
  - Rakefile
258
- - examples/dsl.rb
259
258
  - examples/rails-application-template.rb
260
259
  - examples/tire-dsl.rb
261
260
  - lib/tire.rb
@@ -347,12 +346,13 @@ post_install_message: |
347
346
 
348
347
  IMPORTANT CHANGES LATELY:
349
348
 
350
- # Cleanup of code for getting document type, id, JSON serialization
351
- # Bunch of deprecations: sorting, passing document type to store/remove
352
- # Displaying a warning when no ID is passed when storing in bulk
353
- # Correct handling of import for Mongoid/Kaminari combos
349
+ # Defined mapping for nested fields [#56]
350
+ # Mapping type is optional and defaults to "string"
351
+ # Fixed handling of fields returned prefixed by _source from ES [#31]
352
+ # Allow passing the type to search and added that model passes `document_type` to search [@jonkarna, #38]
353
+ # Allow leaving index name empty for searching the whole server
354
354
 
355
- See the full changelog at <http://github.com/karmi/tire/commits/v0.1.15>.
355
+ See the full changelog at <http://github.com/karmi/tire/commits/v0.1.16>.
356
356
 
357
357
  --------------------------------------------------------------------------------
358
358
 
data/examples/dsl.rb DELETED
@@ -1,73 +0,0 @@
1
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
-
3
- require 'rubygems'
4
- require 'tire'
5
-
6
- extend Tire::DSL
7
-
8
- configure do
9
- url "http://localhost:9200"
10
- end
11
-
12
- index 'articles' do
13
- delete
14
- create
15
-
16
- puts "Documents:", "-"*80
17
- [
18
- { :title => 'One', :tags => ['ruby'] },
19
- { :title => 'Two', :tags => ['ruby', 'python'] },
20
- { :title => 'Three', :tags => ['java'] },
21
- { :title => 'Four', :tags => ['ruby', 'php'] }
22
- ].each do |article|
23
- puts "Indexing article: #{article.to_json}"
24
- store article
25
- end
26
-
27
- refresh
28
- end
29
-
30
- s = search 'articles' do
31
- query do
32
- string 'T*'
33
- end
34
-
35
- filter :terms, :tags => ['ruby']
36
-
37
- sort do
38
- title 'desc'
39
- end
40
-
41
- facet 'global-tags' do
42
- terms :tags, :global => true
43
- end
44
-
45
- facet 'current-tags' do
46
- terms :tags
47
- end
48
- end
49
-
50
- puts "", "Query:", "-"*80
51
- puts s.to_json
52
-
53
- puts "", "Raw JSON result:", "-"*80
54
- puts JSON.pretty_generate(s.response)
55
-
56
- puts "", "Try the query in Curl:", "-"*80
57
- puts s.to_curl
58
-
59
- puts "", "Results:", "-"*80
60
- s.results.each_with_index do |document, i|
61
- puts "#{i+1}. #{ document.title.ljust(10) } [id] #{document._id}"
62
- end
63
-
64
- puts "", "Facets: tags distribution across the whole database:", "-"*80
65
- s.results.facets['global-tags']['terms'].each do |f|
66
- puts "#{f['term'].ljust(13)} #{f['count']}"
67
- end
68
-
69
- puts "", "Facets: tags distribution for the current query ",
70
- "(Notice that 'java' is included, because of the filter)", "-"*80
71
- s.results.facets['current-tags']['terms'].each do |f|
72
- puts "#{f['term'].ljust(13)} #{f['count']}"
73
- end