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.
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +435 -0
- data/Rakefile +75 -0
- data/examples/dsl.rb +73 -0
- data/examples/rails-application-template.rb +144 -0
- data/examples/tire-dsl.rb +617 -0
- data/lib/tire.rb +35 -0
- data/lib/tire/client.rb +40 -0
- data/lib/tire/configuration.rb +29 -0
- data/lib/tire/dsl.rb +33 -0
- data/lib/tire/index.rb +209 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +23 -0
- data/lib/tire/model/import.rb +18 -0
- data/lib/tire/model/indexing.rb +50 -0
- data/lib/tire/model/naming.rb +30 -0
- data/lib/tire/model/persistence.rb +34 -0
- data/lib/tire/model/persistence/attributes.rb +60 -0
- data/lib/tire/model/persistence/finders.rb +61 -0
- data/lib/tire/model/persistence/storage.rb +75 -0
- data/lib/tire/model/search.rb +97 -0
- data/lib/tire/results/collection.rb +56 -0
- data/lib/tire/results/item.rb +39 -0
- data/lib/tire/results/pagination.rb +30 -0
- data/lib/tire/rubyext/hash.rb +3 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +117 -0
- data/lib/tire/search/facet.rb +41 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/query.rb +42 -0
- data/lib/tire/search/sort.rb +29 -0
- data/lib/tire/tasks.rb +88 -0
- data/lib/tire/version.rb +3 -0
- data/test/fixtures/articles/1.json +1 -0
- data/test/fixtures/articles/2.json +1 -0
- data/test/fixtures/articles/3.json +1 -0
- data/test/fixtures/articles/4.json +1 -0
- data/test/fixtures/articles/5.json +1 -0
- data/test/integration/active_model_searchable_test.rb +80 -0
- data/test/integration/active_record_searchable_test.rb +193 -0
- data/test/integration/facets_test.rb +65 -0
- data/test/integration/filters_test.rb +46 -0
- data/test/integration/highlight_test.rb +52 -0
- data/test/integration/index_mapping_test.rb +44 -0
- data/test/integration/index_store_test.rb +68 -0
- data/test/integration/persistent_model_test.rb +35 -0
- data/test/integration/query_string_test.rb +43 -0
- data/test/integration/results_test.rb +28 -0
- data/test/integration/sort_test.rb +36 -0
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_index_name.rb +5 -0
- data/test/models/active_record_article.rb +12 -0
- data/test/models/article.rb +15 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +22 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/client_test.rb +43 -0
- data/test/unit/configuration_test.rb +71 -0
- data/test/unit/index_test.rb +390 -0
- data/test/unit/logger_test.rb +114 -0
- data/test/unit/model_callbacks_test.rb +90 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +400 -0
- data/test/unit/model_search_test.rb +289 -0
- data/test/unit/results_collection_test.rb +131 -0
- data/test/unit/results_item_test.rb +59 -0
- data/test/unit/rubyext_hash_test.rb +19 -0
- data/test/unit/search_facet_test.rb +69 -0
- data/test/unit/search_filter_test.rb +36 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +55 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +204 -0
- data/test/unit/tire_test.rb +55 -0
- data/tire.gemspec +54 -0
- 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
|