tire 0.1.0

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