search_flip 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/irb.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+ require "irb"
3
+ $:.unshift "./lib"
4
+
5
+ require "./test/test_helper.rb"
6
+
7
+ IRB.start
@@ -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
+