lycra 0.0.1 → 0.0.2
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.
- 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:
|