search_flip 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +606 -0
- data/Rakefile +9 -0
- data/irb.rb +7 -0
- data/lib/search_flip/aggregatable.rb +69 -0
- data/lib/search_flip/aggregation.rb +57 -0
- data/lib/search_flip/bulk.rb +152 -0
- data/lib/search_flip/config.rb +21 -0
- data/lib/search_flip/criteria.rb +737 -0
- data/lib/search_flip/filterable.rb +240 -0
- data/lib/search_flip/http_client.rb +49 -0
- data/lib/search_flip/index.rb +545 -0
- data/lib/search_flip/json.rb +18 -0
- data/lib/search_flip/model.rb +21 -0
- data/lib/search_flip/post_filterable.rb +252 -0
- data/lib/search_flip/response.rb +319 -0
- data/lib/search_flip/result.rb +12 -0
- data/lib/search_flip/to_json.rb +31 -0
- data/lib/search_flip/version.rb +5 -0
- data/lib/search_flip.rb +82 -0
- data/search_flip.gemspec +35 -0
- data/test/database.yml +4 -0
- data/test/search_flip/aggregation_test.rb +212 -0
- data/test/search_flip/bulk_test.rb +55 -0
- data/test/search_flip/criteria_test.rb +825 -0
- data/test/search_flip/http_client_test.rb +35 -0
- data/test/search_flip/index_test.rb +350 -0
- data/test/search_flip/model_test.rb +39 -0
- data/test/search_flip/response_test.rb +136 -0
- data/test/search_flip/to_json_test.rb +30 -0
- data/test/search_flip_test.rb +26 -0
- data/test/test_helper.rb +243 -0
- metadata +258 -0
data/irb.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# The SearchFlip::Aggregatable mixin provides handy methods for using
|
4
|
+
# the ElasticSearch aggregation framework, which can be chained with
|
5
|
+
# each other, all other criteria methods and even nested.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# ProductIndex.where(available: true).aggregate(:tags, size: 50)
|
9
|
+
# OrderIndex.aggregate(revenue: { sum: { field: "price" }})
|
10
|
+
|
11
|
+
module Aggregatable
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
attr_accessor :aggregation_values
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds an arbitrary aggregation to the request which can be chained as well
|
19
|
+
# as nested. Check out the examples and ElasticSearch docs for further
|
20
|
+
# details.
|
21
|
+
#
|
22
|
+
# @example Basic usage with optons
|
23
|
+
# query = CommentIndex.where(public: true).aggregate(:user_id, size: 100)
|
24
|
+
#
|
25
|
+
# query.aggregations(:user_id)
|
26
|
+
# # => { 4 => #<SearchFlip::Result ...>, 7 => #<SearchFlip::Result ...>, ... }
|
27
|
+
#
|
28
|
+
# @example Simple range aggregation
|
29
|
+
# ranges = [{ to: 50 }, { from: 50, to: 100 }, { from: 100 }]
|
30
|
+
#
|
31
|
+
# ProductIndex.aggregate(price_range: { range: { field: "price", ranges: ranges }})
|
32
|
+
#
|
33
|
+
# @example Basic nested aggregation
|
34
|
+
# # When nesting aggregations, the return value of the aggregate block is
|
35
|
+
# # used.
|
36
|
+
#
|
37
|
+
# OrderIndex.aggregate(:user_id, order: { revenue: "desc" }) do |aggregation|
|
38
|
+
# aggregation.aggregate(revenue: { sum: { field: "price" }})
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @example Nested histogram aggregation
|
42
|
+
# OrderIndex.aggregate(histogram: { date_histogram: { field: "price", interval: "month" }}) do |aggregation|
|
43
|
+
# aggregation.aggregate(:user_id)
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# @example Nested aggregation with filters
|
47
|
+
# OrderIndex.aggregate(average_price: {}) do |aggregation|
|
48
|
+
# aggregation = aggregation.match_all
|
49
|
+
# aggregation = aggregation.where(user_id: current_user.id) if current_user
|
50
|
+
#
|
51
|
+
# aggregation.aggregate(average_price: { avg: { field: "price" }})
|
52
|
+
# end
|
53
|
+
|
54
|
+
def aggregate(field_or_hash, options = {}, &block)
|
55
|
+
fresh.tap do |criteria|
|
56
|
+
hash = field_or_hash.is_a?(Hash) ? field_or_hash : { field_or_hash => { terms: { field: field_or_hash }.merge(options) } }
|
57
|
+
|
58
|
+
if block
|
59
|
+
aggregation = block.call(SearchFlip::Aggregation.new)
|
60
|
+
|
61
|
+
field_or_hash.is_a?(Hash) ? hash[field_or_hash.keys.first].merge!(aggregation.to_hash) : hash[field_or_hash].merge!(aggregation.to_hash)
|
62
|
+
end
|
63
|
+
|
64
|
+
criteria.aggregation_values = (aggregation_values || {}).merge(hash)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# The SearchFlip::Aggregation class puts together everything
|
4
|
+
# required to use the ElasticSearch aggregation framework via mixins and
|
5
|
+
# adds a method to convert it to a hash format to be used in the request.
|
6
|
+
|
7
|
+
class Aggregation
|
8
|
+
include SearchFlip::Filterable
|
9
|
+
include SearchFlip::Aggregatable
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
# Converts the aggregation to a hash format that can be used in the request.
|
14
|
+
#
|
15
|
+
# @return [Hash] A hash version of the aggregation
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
res = {}
|
19
|
+
res[:aggregations] = aggregation_values if aggregation_values
|
20
|
+
|
21
|
+
if must_values || search_values || must_not_values || should_values || filter_values
|
22
|
+
if SearchFlip.version.to_i >= 2
|
23
|
+
res[:filter] = {
|
24
|
+
bool: {}.
|
25
|
+
merge(must_values || search_values ? { must: (must_values || []) + (search_values || []) } : {}).
|
26
|
+
merge(must_not_values ? { must_not: must_not_values } : {}).
|
27
|
+
merge(should_values ? { should: should_values } : {}).
|
28
|
+
merge(filter_values ? { filter: filter_values } : {})
|
29
|
+
}
|
30
|
+
else
|
31
|
+
filters = (filter_values || []) + (must_not_values || []).map { |must_not_value| { not: must_not_value } }
|
32
|
+
|
33
|
+
queries = {}.
|
34
|
+
merge(must_values || search_values ? { must: (must_values || []) + (search_values || []) } : {}).
|
35
|
+
merge(should_values ? { should: should_values } : {})
|
36
|
+
|
37
|
+
filters_and_queries = filters + (queries.size > 0 ? [bool: queries] : [])
|
38
|
+
|
39
|
+
res[:filter] = filters_and_queries.size > 1 ? { and: filters_and_queries } : filters_and_queries.first
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
res
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
#
|
48
|
+
# Simply dups the object for api compatability.
|
49
|
+
#
|
50
|
+
# @return [SearchFlip::Aggregation] The dupped object
|
51
|
+
|
52
|
+
def fresh
|
53
|
+
dup
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# The SearchFlip::Bulk class implements the bulk support, ie it collects
|
4
|
+
# single requests and emits batches of requests.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk" do |bulk|
|
8
|
+
# bulk.create record.id, MyIndex.serialize(record)
|
9
|
+
# bulk.index record.id, MyIndex.serialize(record), version: record.version, version_type: "external"
|
10
|
+
# bulk.delete record.id, routing: record.user_id
|
11
|
+
# bulk.update record.id, doc: MyIndex.serialize(record)
|
12
|
+
# end
|
13
|
+
|
14
|
+
class Bulk
|
15
|
+
class Error < StandardError; end
|
16
|
+
|
17
|
+
attr_accessor :url, :count, :options, :ignore_errors
|
18
|
+
|
19
|
+
# Builds and yields a new Bulk object, ie initiates the buffer, yields,
|
20
|
+
# sends batches of records each time the buffer is full, and sends a final
|
21
|
+
# batch after the yielded code returns and there are still documents
|
22
|
+
# present within the buffer.
|
23
|
+
#
|
24
|
+
# @example Basic use
|
25
|
+
# SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk" do |bulk|
|
26
|
+
# # ...
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @example Ignore certain errors
|
30
|
+
# SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk", 1_000, ignore_errors: [409] do |bulk|
|
31
|
+
# # ...
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @param url [String] The endpoint to send bulk requests to
|
35
|
+
# @param count [Fixnum] The maximum number of documents per bulk request
|
36
|
+
# @param options [Hash] Options for the bulk requests
|
37
|
+
# @option options ignore_errors [Array, Fixnum] Errors that should be
|
38
|
+
# ignored. If you eg want to ignore errors resulting from conflicts,
|
39
|
+
# you can specify to ignore 409 here.
|
40
|
+
# @option options raise [Boolean] If you want the bulk requests to never
|
41
|
+
# raise any exceptions (fire and forget), you can pass false here.
|
42
|
+
# Default is true.
|
43
|
+
|
44
|
+
def initialize(url, count = 1_000, options = {})
|
45
|
+
self.url = url
|
46
|
+
self.count = count
|
47
|
+
self.options = options
|
48
|
+
self.ignore_errors = Array(options[:ignore_errors]).to_set if options[:ignore_errors]
|
49
|
+
|
50
|
+
init
|
51
|
+
|
52
|
+
yield self
|
53
|
+
|
54
|
+
upload if @num > 0
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adds an index request to the bulk batch.
|
58
|
+
#
|
59
|
+
# @param id [Fixnum, String] The document/record id
|
60
|
+
# @param json [String] The json document
|
61
|
+
# @param options [options] Options for the index request, like eg routing
|
62
|
+
# and versioning
|
63
|
+
|
64
|
+
def index(id, object, options = {})
|
65
|
+
perform :index, id, SearchFlip::JSON.generate(object), options
|
66
|
+
end
|
67
|
+
|
68
|
+
# Adds an index request to the bulk batch
|
69
|
+
#
|
70
|
+
# @see #index
|
71
|
+
|
72
|
+
def import(*args)
|
73
|
+
index(*args)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Adds a create request to the bulk batch.
|
77
|
+
#
|
78
|
+
# @param id [Fixnum, String] The document/record id
|
79
|
+
# @param json [String] The json document
|
80
|
+
# @param options [options] Options for the index request, like eg routing
|
81
|
+
# and versioning
|
82
|
+
|
83
|
+
def create(id, object, options = {})
|
84
|
+
perform :create, id, SearchFlip::JSON.generate(object), options
|
85
|
+
end
|
86
|
+
|
87
|
+
# Adds a update request to the bulk batch.
|
88
|
+
#
|
89
|
+
# @param id [Fixnum, String] The document/record id
|
90
|
+
# @param json [String] The json document
|
91
|
+
# @param options [options] Options for the index request, like eg routing
|
92
|
+
# and versioning
|
93
|
+
|
94
|
+
def update(id, object, options = {})
|
95
|
+
perform :update, id, SearchFlip::JSON.generate(object), options
|
96
|
+
end
|
97
|
+
|
98
|
+
# Adds a delete request to the bulk batch.
|
99
|
+
#
|
100
|
+
# @param id [Fixnum, String] The document/record id
|
101
|
+
# @param options [options] Options for the index request, like eg routing
|
102
|
+
# and versioning
|
103
|
+
|
104
|
+
def delete(id, options = {})
|
105
|
+
perform :delete, id, nil, options
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def init
|
111
|
+
@payload = ""
|
112
|
+
@num = 0
|
113
|
+
end
|
114
|
+
|
115
|
+
def upload
|
116
|
+
response = SearchFlip::HTTPClient.headers(accept: "application/json", content_type: "application/x-ndjson").put(url, body: @payload, params: ignore_errors ? {} : { filter_path: "errors" })
|
117
|
+
|
118
|
+
return if options[:raise] == false
|
119
|
+
|
120
|
+
parsed_response = response.parse
|
121
|
+
|
122
|
+
return unless parsed_response["errors"]
|
123
|
+
|
124
|
+
raise(SearchFlip::Bulk::Error, response[0 .. 30]) unless ignore_errors
|
125
|
+
|
126
|
+
parsed_response["items"].each do |item|
|
127
|
+
item.each do |_, _item|
|
128
|
+
status = _item["status"]
|
129
|
+
|
130
|
+
raise(SearchFlip::Bulk::Error, SearchFlip::JSON.generate(_item)) if !status.between?(200, 299) && !ignore_errors.include?(status)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
ensure
|
134
|
+
init
|
135
|
+
end
|
136
|
+
|
137
|
+
def perform(action, id, json = nil, options = {})
|
138
|
+
@payload << SearchFlip::JSON.generate(action => options.merge(_id: id))
|
139
|
+
@payload << "\n"
|
140
|
+
|
141
|
+
if json
|
142
|
+
@payload << json
|
143
|
+
@payload << "\n"
|
144
|
+
end
|
145
|
+
|
146
|
+
@num += 1
|
147
|
+
|
148
|
+
upload if @num >= count
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# Queries and returns the ElasticSearch version used.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# SearchFlip.version # => e.g. 2.4.1
|
7
|
+
#
|
8
|
+
# @return [String] The ElasticSearch version
|
9
|
+
|
10
|
+
def self.version
|
11
|
+
@version ||= SearchFlip::HTTPClient.get("#{Config[:base_url]}/").parse["version"]["number"]
|
12
|
+
end
|
13
|
+
|
14
|
+
Config = {
|
15
|
+
index_prefix: nil,
|
16
|
+
base_url: "http://127.0.0.1:9200",
|
17
|
+
bulk_limit: 1_000,
|
18
|
+
auto_refresh: false
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|