tire 0.1.15 → 0.1.16

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