tire 0.1.0

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.
Files changed (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,50 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Indexing
5
+
6
+ module ClassMethods
7
+
8
+ def mapping
9
+ if block_given?
10
+ @store_mapping = true
11
+ yield
12
+ @store_mapping = false
13
+ create_index_or_update_mapping
14
+ else
15
+ @mapping ||= {}
16
+ end
17
+ end
18
+
19
+ def indexes(name, options = {})
20
+ # p "#{self}, SEARCH PROPERTY, #{name}"
21
+ mapping[name] = options
22
+ end
23
+
24
+ def store_mapping?
25
+ @store_mapping || false
26
+ end
27
+
28
+ def create_index_or_update_mapping
29
+ # STDERR.puts "Creating index with mapping", mapping_to_hash.inspect
30
+ # STDERR.puts "Index exists?, #{index.exists?}"
31
+ unless index.exists?
32
+ index.create :mappings => mapping_to_hash
33
+ else
34
+ # TODO: Update mapping
35
+ end
36
+ rescue Exception => e
37
+ # TODO: STDERR + logger
38
+ raise
39
+ end
40
+
41
+ def mapping_to_hash
42
+ { document_type.to_sym => { :properties => mapping } }
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Naming
5
+
6
+ module ClassMethods
7
+ def index_name name=nil
8
+ @index_name = name if name
9
+ @index_name || model_name.plural
10
+ end
11
+
12
+ def document_type
13
+ model_name.singular
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def index_name
19
+ self.class.index_name
20
+ end
21
+
22
+ def document_type
23
+ self.class.document_type
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ def self.included(base)
7
+
8
+ base.class_eval do
9
+ include ActiveModel::AttributeMethods
10
+ include ActiveModel::Validations
11
+ include ActiveModel::Serialization
12
+ include ActiveModel::Serializers::JSON
13
+ include ActiveModel::Naming
14
+ include ActiveModel::Conversion
15
+
16
+ extend ActiveModel::Callbacks
17
+ define_model_callbacks :save, :destroy
18
+
19
+ include Tire::Model::Search
20
+ include Tire::Model::Callbacks
21
+
22
+ extend Persistence::Finders::ClassMethods
23
+ extend Persistence::Attributes::ClassMethods
24
+ include Persistence::Attributes::InstanceMethods
25
+
26
+ include Persistence::Storage
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Attributes
7
+
8
+ module ClassMethods
9
+
10
+ def property(name, options = {})
11
+ # p "#{self}, PERSISTENCE PROPERTY, #{name}"
12
+ attr_accessor name.to_sym
13
+ properties << name.to_s unless properties.include?(name.to_s)
14
+ define_query_method name.to_sym
15
+ define_attribute_methods [name.to_sym]
16
+ mapping[name] = options if store_mapping?
17
+ self
18
+ end
19
+
20
+ def properties
21
+ @properties ||= []
22
+ end
23
+
24
+ private
25
+
26
+ def define_query_method name
27
+ define_method("#{name}?") { !! send(name) }
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ attr_accessor :id
35
+
36
+ def initialize(attributes={})
37
+ attributes.each { |name, value| send("#{name}=", value) }
38
+ end
39
+
40
+ def attributes
41
+ self.class.properties.
42
+ inject( self.id ? {'id' => self.id} : {} ) {|attributes, key| attributes[key] = send(key); attributes}
43
+ end
44
+
45
+ def attribute_names
46
+ self.class.properties.sort
47
+ end
48
+
49
+ def has_attribute?(name)
50
+ properties.include?(name.to_s)
51
+ end
52
+ alias :has_property? :has_attribute?
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,61 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Finders
7
+
8
+ module ClassMethods
9
+
10
+ def find *args
11
+ # TODO: Options like `sort`
12
+ old_wrapper = Tire::Configuration.wrapper
13
+ Tire::Configuration.wrapper self
14
+ options = args.pop if args.last.is_a?(Hash)
15
+ args.flatten!
16
+ if args.size > 1
17
+ Tire::Search::Search.new(index.name).query do |query|
18
+ query.ids(args, document_type)
19
+ end.perform.results
20
+ else
21
+ case args = args.pop
22
+ when Fixnum, String
23
+ Index.new(index_name).retrieve document_type, args
24
+ when :all, :first
25
+ send(args)
26
+ else
27
+ raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
28
+ end
29
+ end
30
+ ensure
31
+ Tire::Configuration.wrapper old_wrapper
32
+ end
33
+
34
+ def all
35
+ # TODO: Options like `sort`; Possibly `filters`
36
+ old_wrapper = Tire::Configuration.wrapper
37
+ Tire::Configuration.wrapper self
38
+ s = Tire::Search::Search.new(index_name).query { all }
39
+ s.perform.results
40
+ ensure
41
+ Tire::Configuration.wrapper old_wrapper
42
+ end
43
+
44
+ def first
45
+ # TODO: Options like `sort`; Possibly `filters`
46
+ old_wrapper = Tire::Configuration.wrapper
47
+ Tire::Configuration.wrapper self
48
+ s = Tire::Search::Search.new(index_name).query { all }.size(1)
49
+ s.perform.results.first
50
+ ensure
51
+ Tire::Configuration.wrapper old_wrapper
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,75 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Storage
7
+
8
+ def self.included(base)
9
+
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+ end
14
+
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ def create(args={})
20
+ document = new(args)
21
+ return false unless document.valid?
22
+ document.save
23
+ document
24
+ end
25
+
26
+ def index
27
+ @index = Index.new(index_name)
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ def update_attribute(name, value)
35
+ send("#{name}=", value)
36
+ save
37
+ end
38
+
39
+ def update_attributes(attributes={})
40
+ attributes.each do |name, value|
41
+ send("#{name}=", value)
42
+ end
43
+ save
44
+ end
45
+
46
+ def save
47
+ return false unless valid?
48
+ run_callbacks :save do
49
+ # Document#id is set in the +update_elastic_search_index+ method,
50
+ # where we have access to the JSON response
51
+ end
52
+ self
53
+ end
54
+
55
+ def destroy
56
+ run_callbacks :destroy do
57
+ @destroyed = true
58
+ end
59
+ self.freeze
60
+ end
61
+
62
+ # TODO: Implement `new_record?` and clean up
63
+
64
+ def destroyed?; !!@destroyed; end
65
+
66
+ def persisted?; !!id; end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,97 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Search
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend Tire::Model::Naming::ClassMethods
9
+ include Tire::Model::Naming::InstanceMethods
10
+
11
+ extend Tire::Model::Indexing::ClassMethods
12
+ extend Tire::Model::Import::ClassMethods
13
+
14
+ extend ClassMethods
15
+ include InstanceMethods
16
+
17
+ ['_score', '_type', '_index', '_version', 'sort', 'highlight'].each do |attr|
18
+ # TODO: Find a sane way to add attributes like _score for ActiveRecord -
19
+ # `define_attribute_methods [attr]` does not work in AR.
20
+ define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
21
+ define_method("#{attr}") { @attributes[attr] }
22
+ end
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+
28
+ def search(query=nil, options={}, &block)
29
+ old_wrapper = Tire::Configuration.wrapper
30
+ Tire::Configuration.wrapper self
31
+ sort = options[:order] || options[:sort]
32
+ sort = Array(sort)
33
+ unless block_given?
34
+ s = Tire::Search::Search.new(index.name, options)
35
+ s.query { string query }
36
+ s.sort do
37
+ sort.each do |t|
38
+ field_name, direction = t.split(' ')
39
+ field_name.include?('.') ? field(field_name, direction) : send(field_name, direction)
40
+ end
41
+ end unless sort.empty?
42
+ s.size( options[:per_page].to_i ) if options[:per_page]
43
+ s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
44
+ s.perform.results
45
+ else
46
+ s = Tire::Search::Search.new(index.name, options)
47
+ block.arity < 1 ? s.instance_eval(&block) : block.call(s)
48
+ s.perform.results
49
+ end
50
+ ensure
51
+ Tire::Configuration.wrapper old_wrapper
52
+ end
53
+
54
+ def index
55
+ @index = Index.new(index_name)
56
+ end
57
+
58
+ end
59
+
60
+ module InstanceMethods
61
+
62
+ def score
63
+ attributes['_score']
64
+ end
65
+
66
+ def index
67
+ self.class.index
68
+ end
69
+
70
+ def update_elastic_search_index
71
+ if destroyed?
72
+ self.class.index.remove document_type, self
73
+ else
74
+ response = self.class.index.store document_type, self
75
+ self.id ||= response['_id'] if self.respond_to?(:id=)
76
+ self
77
+ end
78
+ end
79
+
80
+ def to_indexed_json
81
+ if self.class.mapping.empty?
82
+ self.serializable_hash.
83
+ to_json
84
+ else
85
+ self.serializable_hash.
86
+ reject { |key, value| ! self.class.mapping.keys.map(&:to_s).include?(key.to_s) }.
87
+ to_json
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ extend ClassMethods
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,56 @@
1
+ module Tire
2
+ module Results
3
+
4
+ class Collection
5
+ include Enumerable
6
+ include Pagination
7
+
8
+ attr_reader :time, :total, :options, :results, :facets
9
+
10
+ def initialize(response, options={})
11
+ @options = options
12
+ @time = response['took'].to_i
13
+ @total = response['hits']['total'].to_i
14
+ @results = response['hits']['hits'].map do |h|
15
+ if Configuration.wrapper == Hash then h
16
+ else
17
+ document = {}
18
+
19
+ # Update the document with content and ID
20
+ document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( h['fields'] || {} )
21
+ document.update( {'id' => h['_id']} )
22
+
23
+ # Update the document with meta information
24
+ ['_score', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }
25
+
26
+ object = Configuration.wrapper.new(document)
27
+ # TODO: Figure out how to circumvent mass assignment protection for id in ActiveRecord
28
+ object.id = h['_id'] if object.respond_to?(:id=)
29
+ # TODO: Figure out how mark record as "not new record" in ActiveRecord
30
+ object.instance_variable_set(:@new_record, false) if object.respond_to?(:new_record?)
31
+ object
32
+ end
33
+ end
34
+ @facets = response['facets']
35
+ end
36
+
37
+ def each(&block)
38
+ @results.each(&block)
39
+ end
40
+
41
+ def empty?
42
+ @results.empty?
43
+ end
44
+
45
+ def size
46
+ @results.size
47
+ end
48
+
49
+ def to_ary
50
+ self
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+ end