lycra 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/lycra/engine.rb +15 -0
- data/lib/lycra/errors.rb +53 -0
- data/lib/lycra/model.rb +62 -0
- data/lib/lycra/search/aggregations.rb +15 -0
- data/lib/lycra/search/enumerable.rb +200 -0
- data/lib/lycra/search/filters.rb +43 -0
- data/lib/lycra/search/pagination.rb +32 -0
- data/lib/lycra/search/query.rb +63 -0
- data/lib/lycra/search/scoping.rb +136 -0
- data/lib/lycra/search/sort.rb +8 -0
- data/lib/lycra/search.rb +170 -0
- data/lib/lycra/version.rb +1 -1
- data/lib/lycra.rb +31 -5
- data/spec/lycra_spec.rb +37 -0
- data/spec/spec_helper.rb +9 -11
- metadata +114 -3
- data/lib/lycra/railtie.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ea417f626bf68c1e6f119212965332911aa6500
|
4
|
+
data.tar.gz: 66f08754c51707c08120be29fd4d9560d63683e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd1f851885ead6d4fa92fded263ed0d15a8efb7ab1006a52b5b66396cba5accf9770e2bc70124bb834a539e55126dc5fa3c56f177c287a45f5a0be6960f9067a
|
7
|
+
data.tar.gz: d0decf59b1e7a67c5b18aab8dab21ac586a5aed6bd9e2dabd874b80723889c44d76af0db32fafb1ed4724a1d7c8a14efa43f479fd06633fd1e60d978c22daf3d
|
data/lib/lycra/engine.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Lycra
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Lycra
|
4
|
+
|
5
|
+
initializer "lycra.configure_rails_logger" do
|
6
|
+
Lycra.configure do |config|
|
7
|
+
config.logger = Rails.logger
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer "lycra.elasticsearch.client" do |app|
|
12
|
+
Elasticsearch::Model.client = Lycra.client
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/lycra/errors.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lycra
|
2
|
+
class DocumentNotFoundError < StandardError
|
3
|
+
attr_reader :model
|
4
|
+
|
5
|
+
def initialize(model=nil)
|
6
|
+
@model = model
|
7
|
+
|
8
|
+
if model.nil? || model.name.nil?
|
9
|
+
msg = <<MSG
|
10
|
+
You must define a corresponding document class for all models utilizing Lycra::Model. For example, if your model is called BlogPost:
|
11
|
+
|
12
|
+
# /app/documents/blog_post_document.rb
|
13
|
+
class BlogPostDocument < Lycra::Document
|
14
|
+
index_name 'blog-posts'
|
15
|
+
end
|
16
|
+
MSG
|
17
|
+
else
|
18
|
+
msg = <<MSG
|
19
|
+
You must define a corresponding document class for your #{model.name} model. For example:
|
20
|
+
|
21
|
+
# /app/documents/#{model.name.split('::').map { |n| n.underscore }.join('/')}_document.rb
|
22
|
+
class #{model.name}Document < Lycra::Document
|
23
|
+
index_name '#{model.name.parameterize.pluralize}'
|
24
|
+
end
|
25
|
+
MSG
|
26
|
+
end
|
27
|
+
|
28
|
+
super(msg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class UndefinedIndexError < StandardError
|
33
|
+
attr_reader :document
|
34
|
+
|
35
|
+
def initialize(document=nil)
|
36
|
+
@document = document
|
37
|
+
|
38
|
+
if document.nil?
|
39
|
+
super("You must define an index_name for your document class. Try: `index_name 'my-searchable-things'`")
|
40
|
+
else
|
41
|
+
super("You must define an index_name for your #{document.class.name}. Try: `index_name '#{subject_name.underscore.pluralize}'`")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def subject_name
|
46
|
+
if document.subject.is_a?(Class)
|
47
|
+
document.subject.name
|
48
|
+
else
|
49
|
+
document.subject.class.name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/lycra/model.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Model
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, ClassMethods
|
5
|
+
base.send :include, Elasticsearch::Model
|
6
|
+
base.send :include, Elasticsearch::Model::Callbacks
|
7
|
+
|
8
|
+
base.send :lycra_document
|
9
|
+
|
10
|
+
base.send :delegate, :as_indexed_json, to: :lycra_document
|
11
|
+
end
|
12
|
+
|
13
|
+
def lycra_document
|
14
|
+
self.class.lycra_document.new(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def index_name(idx=nil)
|
19
|
+
lycra_document.index_name idx
|
20
|
+
end
|
21
|
+
|
22
|
+
def document_type(doctype=nil)
|
23
|
+
lycra_document.document_type doctype
|
24
|
+
end
|
25
|
+
|
26
|
+
def mapping(options={}, &block)
|
27
|
+
lycra_document.mapping options, &block
|
28
|
+
end
|
29
|
+
alias_method :mappings, :mapping
|
30
|
+
|
31
|
+
def lycra_document(klass=nil)
|
32
|
+
if klass.present?
|
33
|
+
if klass.respond_to?(:constantize)
|
34
|
+
@lycra_document = klass.constantize.new(self)
|
35
|
+
else
|
36
|
+
@lycra_document = klass.new(self)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@lycra_document ||= lycra_document_klass.new(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def lycra_document_klass
|
44
|
+
begin
|
45
|
+
return "#{self.name}Document".constantize
|
46
|
+
rescue NameError => e
|
47
|
+
# noop, we just continue
|
48
|
+
end
|
49
|
+
|
50
|
+
if respond_to?(:base_class) && self.base_class.name != self.name
|
51
|
+
begin
|
52
|
+
return "#{self.base_class.name}Document".constantize
|
53
|
+
rescue NameError => e
|
54
|
+
# noop, we just continue
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
raise Lycra::DocumentNotFoundError.new(self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
class Aggregations < Array
|
4
|
+
def to_query
|
5
|
+
return {} if empty?
|
6
|
+
|
7
|
+
# can probably inject here or be a bit more elegant?
|
8
|
+
aggregations = {}
|
9
|
+
each { |agg| aggregations.merge!(agg) }
|
10
|
+
aggregations
|
11
|
+
end
|
12
|
+
alias_method :to_q, :to_query
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
module Enumerable
|
4
|
+
def self.included(base)
|
5
|
+
base.send :delegate, :count, :size, :results, :records, :total_count, to: :response
|
6
|
+
base.send :delegate, :first, to: :to_a
|
7
|
+
|
8
|
+
base.send :alias_method, :total, :total_count
|
9
|
+
end
|
10
|
+
|
11
|
+
def result_type
|
12
|
+
@result_type ||= :results
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_results!
|
16
|
+
@result_type = :results
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_records!
|
21
|
+
@result_type = :records
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_results?
|
26
|
+
@result_type != :records
|
27
|
+
end
|
28
|
+
|
29
|
+
def as_records?
|
30
|
+
@result_type == :records
|
31
|
+
end
|
32
|
+
|
33
|
+
def decorate!
|
34
|
+
@decorated = true
|
35
|
+
self
|
36
|
+
end
|
37
|
+
alias_method :decorated!, :decorate!
|
38
|
+
|
39
|
+
def undecorate!
|
40
|
+
@decorated = false
|
41
|
+
self
|
42
|
+
end
|
43
|
+
alias_method :undecorated!, :undecorate!
|
44
|
+
|
45
|
+
def decorate?
|
46
|
+
@decorated == true
|
47
|
+
end
|
48
|
+
|
49
|
+
def undecorate?
|
50
|
+
@decorated != true
|
51
|
+
end
|
52
|
+
|
53
|
+
#### Enumeration ####
|
54
|
+
|
55
|
+
def enumerable_method
|
56
|
+
"#{'decorated_' if decorate?}#{result_type}".to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_ary
|
60
|
+
if decorate?
|
61
|
+
send(enumerable_method)
|
62
|
+
else
|
63
|
+
send(enumerable_method).each do |result|
|
64
|
+
apply_transformers(result)
|
65
|
+
end.to_a
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias_method :to_a, :to_ary
|
69
|
+
|
70
|
+
def each(&block)
|
71
|
+
if decorate?
|
72
|
+
send("each_#{enumerable_method}", &block)
|
73
|
+
else
|
74
|
+
send(enumerable_method).each do |result|
|
75
|
+
apply_transformers(result)
|
76
|
+
yield(result) if block_given?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def map(&block)
|
82
|
+
if decorate?
|
83
|
+
send("map_#{enumerable_method}", &block)
|
84
|
+
else
|
85
|
+
send(enumerable_method).map do |result|
|
86
|
+
apply_transformers(result)
|
87
|
+
yield(result) if block_given?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
#### Decoration & Transformations ####
|
93
|
+
|
94
|
+
def transform(&block)
|
95
|
+
(@transformers ||= []) << block
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def transformers
|
100
|
+
@transformers ||= []
|
101
|
+
end
|
102
|
+
|
103
|
+
def apply_transformers(result)
|
104
|
+
transformers.each do |transformer|
|
105
|
+
transformer.call(result)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def each_decorated_results(decorator=nil, &block)
|
110
|
+
map_decorated_results(decorator, &block)
|
111
|
+
@decorated_results
|
112
|
+
end
|
113
|
+
alias_method :decorated_results, :each_decorated_results
|
114
|
+
alias_method :each_decorated_result, :each_decorated_results
|
115
|
+
|
116
|
+
def map_decorated_results(decorator=nil, &block)
|
117
|
+
mapped = []
|
118
|
+
if @decorated_results
|
119
|
+
mapped = @decorated_results.map(&block)
|
120
|
+
else
|
121
|
+
@decorated_results ||= results.map do |result|
|
122
|
+
decorated = decorate_result(result, decorator)
|
123
|
+
|
124
|
+
apply_transformers(decorated)
|
125
|
+
mapped << (block_given? ? yield(decorated) : decorated)
|
126
|
+
|
127
|
+
decorated
|
128
|
+
end
|
129
|
+
end
|
130
|
+
mapped
|
131
|
+
end
|
132
|
+
|
133
|
+
def decorate_result(result, decorator=nil)
|
134
|
+
if decorator
|
135
|
+
decorator.decorate(result)
|
136
|
+
else
|
137
|
+
SearchDecorator.decorate(result)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def each_decorated_records(decorator=nil, &block)
|
142
|
+
map_decorated_records(decorator, &block)
|
143
|
+
@decorated_records
|
144
|
+
end
|
145
|
+
alias_method :decorated_records, :each_decorated_records
|
146
|
+
alias_method :each_decorated_record, :each_decorated_records
|
147
|
+
|
148
|
+
def map_decorated_records(decorator=nil, &block)
|
149
|
+
mapped = []
|
150
|
+
if @decorated_records
|
151
|
+
mapped = @decorated_records.map(&block)
|
152
|
+
else
|
153
|
+
@decorated_records ||= records.map do |record|
|
154
|
+
decorated = decorate_record(record, decorator)
|
155
|
+
|
156
|
+
apply_transformers(decorated)
|
157
|
+
mapped << (block_given? ? yield(decorated) : decorated)
|
158
|
+
|
159
|
+
decorated
|
160
|
+
end
|
161
|
+
end
|
162
|
+
mapped
|
163
|
+
end
|
164
|
+
|
165
|
+
def decorate_record(record, decorator=nil)
|
166
|
+
if decorator
|
167
|
+
decorator.decorate(record)
|
168
|
+
else
|
169
|
+
record.decorate
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def records_with_hit(&block)
|
174
|
+
@records_with_hit ||= records.map_with_hit do |record,hit|
|
175
|
+
mash = Hashie::Mash.new(record: record, hit: hit)
|
176
|
+
apply_transformers(mash)
|
177
|
+
yield(mash) if block_given?
|
178
|
+
mash
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def decorated_with_hit(decorator=nil, &block)
|
183
|
+
@decorated_with_hit ||= records.map_with_hit do |record,hit|
|
184
|
+
decorated = begin
|
185
|
+
if decorator
|
186
|
+
Hashie::Mash.new(record: decorator.decorate(record), hit: hit)
|
187
|
+
else
|
188
|
+
Hashie::Mash.new(record: record.decorate, hit: hit)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
apply_transformers(decorated)
|
193
|
+
yield(decorated) if block_given?
|
194
|
+
|
195
|
+
decorated
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
class Filters < Array
|
4
|
+
def to_query
|
5
|
+
return {} if empty?
|
6
|
+
|
7
|
+
filters = {}
|
8
|
+
filters.merge!(must_filters) unless must_filters.empty?
|
9
|
+
filters.merge!(must_not_filters) unless must_not_filters.empty?
|
10
|
+
{bool: filters}
|
11
|
+
end
|
12
|
+
alias_method :to_q, :to_query
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def must_filters
|
17
|
+
queries = select { |q| q.key?(:must) || !q.key?(:must_not) }
|
18
|
+
return {} if queries.empty?
|
19
|
+
|
20
|
+
matchers = {must: []}
|
21
|
+
queries.each do |query|
|
22
|
+
if query.key?(:must)
|
23
|
+
matchers[:must].concat(query[:must])
|
24
|
+
else
|
25
|
+
matchers[:must] << query
|
26
|
+
end
|
27
|
+
end
|
28
|
+
matchers
|
29
|
+
end
|
30
|
+
|
31
|
+
def must_not_filters
|
32
|
+
queries = select { |q| q.key?(:must_not) }
|
33
|
+
return {} if queries.empty?
|
34
|
+
|
35
|
+
matchers = {must_not: []}
|
36
|
+
queries.each do |query|
|
37
|
+
matchers[:must_not].concat(query[:must_not])
|
38
|
+
end
|
39
|
+
matchers
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
module Pagination
|
4
|
+
def self.included(base)
|
5
|
+
base.send :delegate, :total_pages, :current_page, :limit_value, :offset_value, :last_page?, to: :response
|
6
|
+
|
7
|
+
base.send :alias_method, :pages, :total_pages
|
8
|
+
end
|
9
|
+
|
10
|
+
def page(pg=nil)
|
11
|
+
@response = response.page(pg || 1)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def per(pr=nil)
|
16
|
+
@response = response.per(pr || Lycra.configuration.per_page)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def offset(ofst=nil)
|
21
|
+
@response = response.offset(ofst || 0)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def limit(lmt=nil)
|
26
|
+
@response = response.limit(lmt || Lycra.configuration.per_page)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
class Query < Array
|
4
|
+
def to_query
|
5
|
+
return {} if empty? && filters.empty?
|
6
|
+
|
7
|
+
query_matcher = {}
|
8
|
+
query_matcher.merge!(must_matchers) unless must_matchers.empty?
|
9
|
+
query_matcher.merge!(should_matchers) unless should_matchers.empty?
|
10
|
+
query_filters = {filter: filters.to_query}
|
11
|
+
{
|
12
|
+
bool: query_matcher.merge(query_filters)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
alias_method :to_q, :to_query
|
16
|
+
|
17
|
+
def filters
|
18
|
+
@filters ||= Lycra::Search::Filters.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter(fltr=nil)
|
22
|
+
if fltr.present?
|
23
|
+
filters << fltr
|
24
|
+
end
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def refilter(fltr)
|
30
|
+
@filters = Lycra::Search::Filters.new
|
31
|
+
filter fltr
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def must_matchers
|
37
|
+
queries = select { |q| q.key?(:must) || !q.key?(:should) }
|
38
|
+
return {} if queries.empty?
|
39
|
+
|
40
|
+
matchers = {must: []}
|
41
|
+
queries.each do |query|
|
42
|
+
if query.key?(:must)
|
43
|
+
matchers[:must].concat(query[:must])
|
44
|
+
else
|
45
|
+
matchers[:must] << query
|
46
|
+
end
|
47
|
+
end
|
48
|
+
matchers
|
49
|
+
end
|
50
|
+
|
51
|
+
def should_matchers
|
52
|
+
queries = select { |q| q.key?(:should) }
|
53
|
+
return {} if queries.empty?
|
54
|
+
|
55
|
+
matchers = {should: []}
|
56
|
+
queries.each do |query|
|
57
|
+
matchers[:should].concat(query[:should])
|
58
|
+
end
|
59
|
+
matchers
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Lycra
|
2
|
+
module Search
|
3
|
+
module Scoping
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def aggregations
|
9
|
+
@aggregations ||= Lycra::Search::Aggregations.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def aggregate(agg=nil)
|
13
|
+
aggregations << agg unless agg.nil?
|
14
|
+
self
|
15
|
+
end
|
16
|
+
alias_method :aggregation, :aggregate
|
17
|
+
|
18
|
+
def reaggregate(agg)
|
19
|
+
@aggregations = Lycra::Search::Aggregations.new
|
20
|
+
aggregate agg
|
21
|
+
end
|
22
|
+
|
23
|
+
def filters
|
24
|
+
@filters ||= Lycra::Search::Filters.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def filter(fltr=nil)
|
28
|
+
filters << fltr unless fltr.nil?
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def refilter(fltr)
|
33
|
+
@filters = Lycra::Search::Filters.new
|
34
|
+
filter fltr
|
35
|
+
end
|
36
|
+
|
37
|
+
def query_filters
|
38
|
+
query.filters
|
39
|
+
end
|
40
|
+
|
41
|
+
def query_filter(fltr=nil)
|
42
|
+
query.filter fltr
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def requery_filter(fltr)
|
47
|
+
query.refilter fltr
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def post_filters
|
52
|
+
@post_filters ||= Lycra::Search::Filters.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def post_filter(fltr=nil)
|
56
|
+
post_filters << fltr unless fltr.nil?
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def repost_filter(fltr)
|
61
|
+
@post_filters = Lycra::Search::Filters.new
|
62
|
+
post_filter fltr
|
63
|
+
end
|
64
|
+
|
65
|
+
def sorter
|
66
|
+
@sorter ||= Lycra::Search::Sort.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def sort(srt=nil)
|
70
|
+
sorter << srt unless srt.nil?
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def resort(srt=nil)
|
75
|
+
@sorter = Lycra::Search::Sort.new
|
76
|
+
sort srt
|
77
|
+
end
|
78
|
+
|
79
|
+
def filter_by(attr, vals)
|
80
|
+
return self if vals.nil? || vals.empty?
|
81
|
+
|
82
|
+
attr_filter = {
|
83
|
+
bool: {
|
84
|
+
must: [{
|
85
|
+
or: vals.map { |val| {term: {attr.to_sym => val}} }
|
86
|
+
}]
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
filter(filter << attr_filter)
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def where(*args)
|
96
|
+
args.extract_options!.each do |attr, vals|
|
97
|
+
vals = [vals] unless vals.is_a?(Array)
|
98
|
+
filter_by(attr, vals)
|
99
|
+
end
|
100
|
+
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def find_by(*args)
|
105
|
+
unfiltered.offset(0).limit(1).where(*args).first
|
106
|
+
end
|
107
|
+
|
108
|
+
def find(id)
|
109
|
+
find_by(id: id).first
|
110
|
+
end
|
111
|
+
|
112
|
+
module ClassMethods
|
113
|
+
def self.extended(base)
|
114
|
+
# Generic Shared Scopes
|
115
|
+
base.send :scope, :all, -> { offset(0).limit(10_000) }
|
116
|
+
base.send :scope, :unfiltered, -> { refilter }
|
117
|
+
base.send :scope, :unsorted, -> { resort }
|
118
|
+
base.send :scope, :sort_by, -> (field, order=:asc) { sort({field => {order: order}}) }
|
119
|
+
base.send :scope, :by_id, -> (*objs) {
|
120
|
+
filter_by(:id, [objs].flatten.map { |obj| obj.respond_to?(:id) ? obj.id : obj })
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def scope(name, block)
|
125
|
+
instance_eval do
|
126
|
+
define_method name.to_sym do |*args|
|
127
|
+
@response = nil
|
128
|
+
instance_exec *args, &block
|
129
|
+
self
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/lycra/search.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'lycra/search/aggregations'
|
2
|
+
require 'lycra/search/filters'
|
3
|
+
require 'lycra/search/query'
|
4
|
+
require 'lycra/search/sort'
|
5
|
+
require 'lycra/search/enumerable'
|
6
|
+
require 'lycra/search/pagination'
|
7
|
+
require 'lycra/search/scoping'
|
8
|
+
|
9
|
+
module Lycra
|
10
|
+
module Search
|
11
|
+
def self.included(base)
|
12
|
+
base.send :include, Lycra::Search::Enumerable
|
13
|
+
base.send :include, Lycra::Search::Pagination
|
14
|
+
base.send :include, Lycra::Search::Scoping
|
15
|
+
base.send :extend, ClassMethods
|
16
|
+
base.send :attr_reader, :term
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(term=nil, query: nil, filter: nil, post_filter: nil, sort: nil, models: nil, fields: nil, aggregations: nil, &block)
|
20
|
+
@term = term
|
21
|
+
@models = models
|
22
|
+
@fields = fields
|
23
|
+
self.query(block_given? ? instance_eval(&block) : query)
|
24
|
+
self.filter(filter)
|
25
|
+
self.post_filter(post_filter)
|
26
|
+
self.sort(sort)
|
27
|
+
self.aggregate(aggregations)
|
28
|
+
end
|
29
|
+
|
30
|
+
def response
|
31
|
+
@response ||= search
|
32
|
+
end
|
33
|
+
|
34
|
+
def response!
|
35
|
+
@response = search
|
36
|
+
end
|
37
|
+
|
38
|
+
def document_types
|
39
|
+
document_types ||= response.search.definition[:type]
|
40
|
+
end
|
41
|
+
|
42
|
+
def entry_name
|
43
|
+
if document_types.count == 1
|
44
|
+
document_types.first
|
45
|
+
elsif document_types.count > 1
|
46
|
+
return document_types.map(&:pluralize).to_sentence
|
47
|
+
else
|
48
|
+
'result'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def search(qry=nil, &block)
|
53
|
+
if block_given?
|
54
|
+
Elasticsearch::Model.search(instance_eval(&block), models)
|
55
|
+
else
|
56
|
+
Elasticsearch::Model.search((qry || to_query), models)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def query(qry=nil)
|
61
|
+
if !qry.nil?
|
62
|
+
@query = Lycra::Search::Query[qry]
|
63
|
+
self
|
64
|
+
else
|
65
|
+
@query ||= Lycra::Search::Query[send("#{query_method}_query")]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_query_hash
|
70
|
+
{
|
71
|
+
query: query.to_query,
|
72
|
+
filter: filters.to_query,
|
73
|
+
sort: sorter.to_query,
|
74
|
+
post_filter: post_filters.to_query,
|
75
|
+
aggregations: aggregations.to_query
|
76
|
+
}
|
77
|
+
end
|
78
|
+
alias_method :to_query, :to_query_hash
|
79
|
+
alias_method :to_q, :to_query_hash
|
80
|
+
|
81
|
+
def match_all_query
|
82
|
+
{match_all: {}}
|
83
|
+
end
|
84
|
+
|
85
|
+
def multi_match_query
|
86
|
+
return if term.nil?
|
87
|
+
|
88
|
+
{
|
89
|
+
multi_match: {
|
90
|
+
query: term,
|
91
|
+
type: :best_fields,
|
92
|
+
fields: fields,
|
93
|
+
tie_breaker: 0.5,
|
94
|
+
operator: 'and'
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def match_phrase_prefix_query
|
100
|
+
return if term.nil?
|
101
|
+
|
102
|
+
field_queries = fields.map do |field|
|
103
|
+
{match_phrase_prefix: {field.to_s.gsub(/\^\d\Z/, '').to_sym => term}}
|
104
|
+
end
|
105
|
+
|
106
|
+
{or: field_queries}
|
107
|
+
end
|
108
|
+
|
109
|
+
def query_method(meth=false)
|
110
|
+
if meth == false
|
111
|
+
@query_method ||= :match_all
|
112
|
+
else
|
113
|
+
@query = nil
|
114
|
+
@query_method = meth
|
115
|
+
self
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def models(mdls=false)
|
120
|
+
if mdls != false
|
121
|
+
@models = mdls.is_a?(Array) ? mdls : [mdls]
|
122
|
+
else
|
123
|
+
@models ||= self.class.models
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def fields(flds=false)
|
128
|
+
if flds != false
|
129
|
+
@fields = flds.is_a?(Array) ? flds : [flds]
|
130
|
+
else
|
131
|
+
@fields ||= self.class.fields
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
module ClassMethods
|
136
|
+
def inherited(base)
|
137
|
+
# Make sure we inherit the parent's class-level instance variables whenever we inherit from the class.
|
138
|
+
base.send :instance_variable_set, :@models, models.try(:dup)
|
139
|
+
base.send :instance_variable_set, :@fields, fields.try(:dup)
|
140
|
+
end
|
141
|
+
|
142
|
+
def search(term=nil, query: nil, filter: nil, post_filter: nil, sort: nil, models: nil, fields: nil, aggregations: nil, &block)
|
143
|
+
new(term, query: query, filter: filter, post_filter: post_filter, sort: sort, models: models, fields: fields, aggregations: aggregations, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
def singleton
|
147
|
+
@singleton ||= search
|
148
|
+
end
|
149
|
+
|
150
|
+
def method_missing(meth, *args, &block)
|
151
|
+
return search.send(meth, *args, &block) if singleton.respond_to?(meth)
|
152
|
+
super
|
153
|
+
end
|
154
|
+
|
155
|
+
def respond_to_missing?(meth, include_private=false)
|
156
|
+
singleton.respond_to?(meth, include_private) || super
|
157
|
+
end
|
158
|
+
|
159
|
+
def fields(*fields)
|
160
|
+
@fields = fields unless fields.empty?
|
161
|
+
@fields ||= []
|
162
|
+
end
|
163
|
+
|
164
|
+
def models(*models)
|
165
|
+
@models = models unless models.empty?
|
166
|
+
@models ||= []
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/lib/lycra/version.rb
CHANGED
data/lib/lycra.rb
CHANGED
@@ -1,15 +1,41 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'canfig'
|
2
|
-
require '
|
3
|
+
require 'elasticsearch/model'
|
4
|
+
require 'lycra/model'
|
5
|
+
require 'lycra/search'
|
6
|
+
require 'lycra/engine' if defined?(Rails)
|
3
7
|
|
4
8
|
module Lycra
|
5
9
|
include Canfig::Module
|
6
10
|
|
7
11
|
configure do |config|
|
8
|
-
config.
|
9
|
-
config.
|
12
|
+
config.elasticsearch_host = ENV['ELASTICSEARCH_HOST'] || 'localhost' # elasticsearch host to use when connecting, defaults to ENV var if set or falls back to localhost
|
13
|
+
config.elasticsearch_port = ENV['ELASTICSEARCH_PORT'] || 9200 # elasticsearch port to use when connecting, defaults to ENV var if set or falls back to 9200
|
14
|
+
config.elasticsearch_url = ENV['ELASTICSEARCH_URL'] # elasticsearch URL to use when connecting (i.e. 'https://localhost:9200'), this will override host/port if set
|
15
|
+
config.page_size = 50 # default number of results to return per-page when searching an index
|
16
|
+
config.index_prefix = nil # a prefix to use for index names (i.e. 'my-app' prefix and 'people' index becomes 'my-app-people')
|
17
|
+
config.log = false # whether or not the elasticsearch client should perform standard logging
|
18
|
+
config.logger = nil # logger use for standard elasticsearch logging, defaults to STDOUT but will use Rails.logger in a rails environment (via Lycra::Engine)
|
19
|
+
config.trace = false # whether or not the elasticsearch client should log request/response traces
|
20
|
+
config.tracer = nil # logger used when tracing request/response data
|
21
|
+
|
22
|
+
def elasticsearch_url
|
23
|
+
@state[:elasticsearch_url] || "#{self.elasticsearch_host}:#{self.elasticsearch_port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger ||= (@state[:logger] || Logger.new(STDOUT))
|
28
|
+
end
|
10
29
|
end
|
11
30
|
|
12
|
-
def self.
|
13
|
-
|
31
|
+
def self.client
|
32
|
+
@client ||= Elasticsearch::Client.new(
|
33
|
+
host: configuration.elasticsearch_url,
|
34
|
+
log: configuration.log,
|
35
|
+
logger: configuration.logger,
|
36
|
+
trace: configuration.trace,
|
37
|
+
tracer: configuration.tracer
|
38
|
+
)
|
14
39
|
end
|
40
|
+
|
15
41
|
end
|
data/spec/lycra_spec.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lycra do
|
4
|
+
describe '.logger' do
|
5
|
+
context 'when configured with a custom logger' do
|
6
|
+
let(:logger) do
|
7
|
+
Dir.mktmpdir do |dir|
|
8
|
+
Logger.new(File.open(File.join(dir, 'lycra.log'), 'w'))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
Lycra.configuration.configure(logger: logger)
|
14
|
+
Lycra.configuration.instance_variable_set(:@logger, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'uses the custom logger' do
|
18
|
+
expect(Lycra.configuration.logger).to eq(logger)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with default configuration' do
|
23
|
+
before do
|
24
|
+
Lycra.configuration.configure(logger: nil)
|
25
|
+
Lycra.configuration.instance_variable_set(:@logger, nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'is an instance of Logger' do
|
29
|
+
expect(Lycra.configuration.logger).to be_an_instance_of(::Logger)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'uses STDOUT' do
|
33
|
+
expect(Lycra.configuration.logger.instance_variable_get(:@logdev).dev).to eq(STDOUT)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,23 +1,21 @@
|
|
1
|
+
require 'faker'
|
2
|
+
require 'factory_girl'
|
1
3
|
require 'lycra'
|
2
4
|
require 'rspec'
|
3
|
-
#require 'vcr'
|
4
5
|
require 'coveralls'
|
5
6
|
Coveralls.wear!
|
6
7
|
|
7
8
|
#Dir[File.join(File.dirname(__FILE__), '..', "spec/support/**/*.rb")].each { |f| require f }
|
9
|
+
Dir[File.join(File.dirname(__FILE__), '..', "spec/factories/**/*.rb")].each { |f| require f }
|
8
10
|
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
#end
|
11
|
+
#LYCRA_ES1_CLIENT = Elasticsearch::Client.new host: 'localhost', port: 9201
|
12
|
+
LYCRA_ES2_CLIENT = Elasticsearch::Client.new host: 'localhost', port: 4500
|
13
|
+
Elasticsearch::Model.client = LYCRA_ES2_CLIENT
|
13
14
|
|
14
15
|
RSpec.configure do |config|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
# load 'support/models.rb'
|
19
|
-
#end
|
20
|
-
|
16
|
+
# configure factory_girl syntax methods
|
17
|
+
config.include FactoryGirl::Syntax::Methods
|
18
|
+
|
21
19
|
# rspec-expectations config goes here. You can use an alternate
|
22
20
|
# assertion/expectation library such as wrong or the stdlib/minitest
|
23
21
|
# assertions if you prefer.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lycra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Rebec
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: canfig
|
@@ -24,6 +24,62 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: elasticsearch
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.18
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.18
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: elasticsearch-persistence
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: elasticsearch-model
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: elasticsearch-rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
27
83
|
- !ruby/object:Gem::Dependency
|
28
84
|
name: rake
|
29
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +94,20 @@ dependencies:
|
|
38
94
|
- - ">="
|
39
95
|
- !ruby/object:Gem::Version
|
40
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
41
111
|
- !ruby/object:Gem::Dependency
|
42
112
|
name: rspec
|
43
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +122,34 @@ dependencies:
|
|
52
122
|
- - ">="
|
53
123
|
- !ruby/object:Gem::Version
|
54
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: factory_girl
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: faker
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.6.6
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.6.6
|
55
153
|
description: Open source business intelligence based on elasticsearch queries, inspired
|
56
154
|
by https://github.com/ankane/blazer
|
57
155
|
email:
|
@@ -61,8 +159,19 @@ extensions: []
|
|
61
159
|
extra_rdoc_files: []
|
62
160
|
files:
|
63
161
|
- lib/lycra.rb
|
64
|
-
- lib/lycra/
|
162
|
+
- lib/lycra/engine.rb
|
163
|
+
- lib/lycra/errors.rb
|
164
|
+
- lib/lycra/model.rb
|
165
|
+
- lib/lycra/search.rb
|
166
|
+
- lib/lycra/search/aggregations.rb
|
167
|
+
- lib/lycra/search/enumerable.rb
|
168
|
+
- lib/lycra/search/filters.rb
|
169
|
+
- lib/lycra/search/pagination.rb
|
170
|
+
- lib/lycra/search/query.rb
|
171
|
+
- lib/lycra/search/scoping.rb
|
172
|
+
- lib/lycra/search/sort.rb
|
65
173
|
- lib/lycra/version.rb
|
174
|
+
- spec/lycra_spec.rb
|
66
175
|
- spec/spec_helper.rb
|
67
176
|
homepage: http://github.com/markrebec/lycra
|
68
177
|
licenses: []
|
@@ -88,4 +197,6 @@ signing_key:
|
|
88
197
|
specification_version: 4
|
89
198
|
summary: Business intelligence based on elasticsearch queries
|
90
199
|
test_files:
|
200
|
+
- spec/lycra_spec.rb
|
91
201
|
- spec/spec_helper.rb
|
202
|
+
has_rdoc:
|