search_flip 1.0.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.
- 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
|
+
|