caoutsearch 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +43 -0
  4. data/lib/caoutsearch/config/client.rb +13 -0
  5. data/lib/caoutsearch/config/mappings.rb +40 -0
  6. data/lib/caoutsearch/config/settings.rb +29 -0
  7. data/lib/caoutsearch/filter/base.rb +101 -0
  8. data/lib/caoutsearch/filter/boolean.rb +19 -0
  9. data/lib/caoutsearch/filter/date.rb +49 -0
  10. data/lib/caoutsearch/filter/default.rb +51 -0
  11. data/lib/caoutsearch/filter/geo_point.rb +11 -0
  12. data/lib/caoutsearch/filter/match.rb +57 -0
  13. data/lib/caoutsearch/filter/none.rb +7 -0
  14. data/lib/caoutsearch/filter/range.rb +28 -0
  15. data/lib/caoutsearch/filter.rb +29 -0
  16. data/lib/caoutsearch/index/base.rb +35 -0
  17. data/lib/caoutsearch/index/document.rb +107 -0
  18. data/lib/caoutsearch/index/indice.rb +55 -0
  19. data/lib/caoutsearch/index/indice_versions.rb +123 -0
  20. data/lib/caoutsearch/index/instrumentation.rb +19 -0
  21. data/lib/caoutsearch/index/internal_dsl.rb +77 -0
  22. data/lib/caoutsearch/index/naming.rb +29 -0
  23. data/lib/caoutsearch/index/reindex.rb +77 -0
  24. data/lib/caoutsearch/index/scoping.rb +54 -0
  25. data/lib/caoutsearch/index/serialization.rb +136 -0
  26. data/lib/caoutsearch/index.rb +7 -0
  27. data/lib/caoutsearch/instrumentation/base.rb +69 -0
  28. data/lib/caoutsearch/instrumentation/index.rb +57 -0
  29. data/lib/caoutsearch/instrumentation/search.rb +41 -0
  30. data/lib/caoutsearch/mappings.rb +79 -0
  31. data/lib/caoutsearch/search/base.rb +27 -0
  32. data/lib/caoutsearch/search/dsl/item.rb +42 -0
  33. data/lib/caoutsearch/search/query/base.rb +16 -0
  34. data/lib/caoutsearch/search/query/boolean.rb +63 -0
  35. data/lib/caoutsearch/search/query/cleaning.rb +29 -0
  36. data/lib/caoutsearch/search/query/getters.rb +35 -0
  37. data/lib/caoutsearch/search/query/merge.rb +27 -0
  38. data/lib/caoutsearch/search/query/nested.rb +23 -0
  39. data/lib/caoutsearch/search/query/setters.rb +68 -0
  40. data/lib/caoutsearch/search/sanitizer.rb +28 -0
  41. data/lib/caoutsearch/search/search/delete_methods.rb +21 -0
  42. data/lib/caoutsearch/search/search/inspect.rb +36 -0
  43. data/lib/caoutsearch/search/search/instrumentation.rb +21 -0
  44. data/lib/caoutsearch/search/search/internal_dsl.rb +77 -0
  45. data/lib/caoutsearch/search/search/naming.rb +47 -0
  46. data/lib/caoutsearch/search/search/query_builder.rb +94 -0
  47. data/lib/caoutsearch/search/search/query_methods.rb +180 -0
  48. data/lib/caoutsearch/search/search/resettable.rb +35 -0
  49. data/lib/caoutsearch/search/search/response.rb +88 -0
  50. data/lib/caoutsearch/search/search/scroll_methods.rb +113 -0
  51. data/lib/caoutsearch/search/search/search_methods.rb +230 -0
  52. data/lib/caoutsearch/search/type_cast.rb +76 -0
  53. data/lib/caoutsearch/search/value.rb +111 -0
  54. data/lib/caoutsearch/search/value_overflow.rb +17 -0
  55. data/lib/caoutsearch/search.rb +6 -0
  56. data/lib/caoutsearch/settings.rb +22 -0
  57. data/lib/caoutsearch/version.rb +5 -0
  58. data/lib/caoutsearch.rb +38 -0
  59. metadata +268 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Instrumentation
5
+ class Index < Base
6
+ def get(event)
7
+ log_request("Get", event, format: log_request_format)
8
+ log_response("Get", event)
9
+ end
10
+
11
+ def index(event)
12
+ log_request("Index", event, format: log_request_format)
13
+ log_response("Index", event)
14
+ end
15
+
16
+ def update(event)
17
+ log_request("Update", event, format: log_request_format)
18
+ log_response("Update", event)
19
+ end
20
+
21
+ def delete(event)
22
+ log_request("Delete", event, format: log_request_format)
23
+ log_response("Delete", event)
24
+ end
25
+
26
+ def bulk(event)
27
+ method = event[:method].to_s.titleize
28
+
29
+ log_request("Bulk #{method}", event, format: log_request_format)
30
+ log_response("Bulk #{method}", event, warn_errors: true)
31
+ end
32
+
33
+ def reindex(event)
34
+ log_request("Reindex", event, format: log_request_format)
35
+ log_response("Reindex", event, warn_errors: true) do |message|
36
+ payload = event.payload
37
+ request = payload[:request]
38
+ request_size = inspect_json_size(request)
39
+
40
+ response = payload[:response]
41
+ response_size = inspect_json_size(response)
42
+
43
+ message += ", request size: #{request_size}"
44
+ message += ", response size: #{response_size}"
45
+ message += ", progress: #{payload[:progress]} / #{payload[:total]}"
46
+ message
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def log_request_format
53
+ Caoutsearch.instrumentation_options[:index]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Instrumentation
5
+ class Search < Base
6
+ def search(event)
7
+ log_request("Search", event, format: log_request_format)
8
+ log_response("Search", event)
9
+ end
10
+
11
+ def scroll_search(event)
12
+ log_request("Search", event, format: log_request_format)
13
+ log_response("Search", event) do |message|
14
+ payload = event.payload
15
+ message += ", progress: #{payload[:progress]} / #{payload[:total]}"
16
+ message
17
+ end
18
+ end
19
+
20
+ def scroll(event)
21
+ log_request("Scroll", event, format: "truncated")
22
+ log_response("Scroll", event) do |message|
23
+ payload = event.payload
24
+ message += ", progress: #{payload[:progress]} / #{payload[:total]}"
25
+ message
26
+ end
27
+ end
28
+
29
+ def delete(event)
30
+ log_request("Delete", event, format: log_request_format)
31
+ log_response("Delete", event)
32
+ end
33
+
34
+ private
35
+
36
+ def log_request_format
37
+ Caoutsearch.instrumentation_options[:search]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ class Mappings
5
+ attr_reader :index_name
6
+
7
+ # Build an index mapping
8
+ #
9
+ # Caoutsearch::Mapping.new({ properties: [...], )
10
+ #
11
+ def initialize(mappings)
12
+ @mappings = mappings.deep_symbolize_keys
13
+ end
14
+
15
+ def to_hash
16
+ @mappings
17
+ end
18
+ alias_method :as_json, :to_hash
19
+
20
+ def to_json(*)
21
+ as_json.to_json
22
+ end
23
+
24
+ def include?(*args)
25
+ find(*args).present?
26
+ end
27
+
28
+ def find(*args)
29
+ each_path(*args).map { |hash, _| hash }.last
30
+ end
31
+
32
+ def find_type(*args)
33
+ definition = find(*args)
34
+ definition[:type]&.to_s if definition.present?
35
+ end
36
+
37
+ def nested?(*args)
38
+ nested_path(*args).present?
39
+ end
40
+
41
+ def include_in_parent?(*args)
42
+ path = nested_path(*args)
43
+ return nil unless path
44
+
45
+ !!find(path)[:include_in_parent]
46
+ end
47
+
48
+ def nested_path(*args)
49
+ return nil unless include?(*args)
50
+
51
+ each_path(*args) do |hash, current_path|
52
+ break nil unless hash
53
+ return current_path if hash[:type] == "nested"
54
+ end
55
+
56
+ nil
57
+ end
58
+
59
+ private
60
+
61
+ def key_path(*args)
62
+ args.flat_map { |path| path.to_s.split(".").map(&:to_sym) }
63
+ end
64
+
65
+ def each_path(*args)
66
+ return to_enum(:each_path, *args) unless block_given?
67
+
68
+ hash = to_hash
69
+ path = []
70
+
71
+ key_path(*args).each do |key|
72
+ path << key
73
+ hash = hash.dig(:properties, key) || hash.dig(:fields, key) if hash
74
+
75
+ yield hash, path.join(".")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ class Base
6
+ include Caoutsearch::Config::Client
7
+ include Caoutsearch::Config::Mappings
8
+ include Caoutsearch::Config::Settings
9
+
10
+ include Caoutsearch::Search::Search::DeleteMethods
11
+ include Caoutsearch::Search::Search::Inspect
12
+ include Caoutsearch::Search::Search::Instrumentation
13
+ include Caoutsearch::Search::Search::InternalDSL
14
+ include Caoutsearch::Search::Search::Naming
15
+ include Caoutsearch::Search::Search::QueryBuilder
16
+ include Caoutsearch::Search::Search::QueryMethods
17
+ include Caoutsearch::Search::Search::Resettable
18
+ include Caoutsearch::Search::Search::Response
19
+ include Caoutsearch::Search::Search::ScrollMethods
20
+ include Caoutsearch::Search::Search::SearchMethods
21
+
22
+ def self.search(*args)
23
+ new.search(*args)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module DSL
6
+ class Item
7
+ attr_reader :name, :options, :block
8
+
9
+ PROPERTIES_TO_INSPECT = %i[name options block].freeze
10
+
11
+ def initialize(name, options = {}, &block)
12
+ @name = name
13
+ @options = options
14
+ @block = block if block
15
+ end
16
+
17
+ def has_block?
18
+ block.present?
19
+ end
20
+
21
+ def indexes
22
+ @indexes ||= Array.wrap(options[:indexes].presence || name)
23
+ end
24
+
25
+ def inspect
26
+ properties = properties_to_inspect.map { |k, v| " #{k}: #{v}" }
27
+
28
+ "#<#{self.class}#{properties.join(",")}>"
29
+ end
30
+
31
+ private
32
+
33
+ def properties_to_inspect
34
+ PROPERTIES_TO_INSPECT.each_with_object({}) do |name, properties|
35
+ value = instance_variable_get("@#{name}")
36
+ properties[name] = value.inspect if value.present?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ class Base < ::Hash
7
+ include Caoutsearch::Search::Query::Boolean
8
+ include Caoutsearch::Search::Query::Cleaning
9
+ include Caoutsearch::Search::Query::Getters
10
+ include Caoutsearch::Search::Query::Merge
11
+ include Caoutsearch::Search::Query::Nested
12
+ include Caoutsearch::Search::Query::Setters
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Boolean
7
+ def should_filter_on(terms)
8
+ terms = flatten_bool_terms(:should, terms)
9
+ terms_without_none = terms.without(Caoutsearch::Filter::NONE)
10
+ return if terms.empty?
11
+
12
+ if terms.size == 1
13
+ filters << terms[0]
14
+ elsif terms_without_none.size == 1
15
+ filters << terms_without_none[0]
16
+ elsif terms_without_none.size > 1
17
+ filters << { bool: { should: terms_without_none } }
18
+ end
19
+ end
20
+
21
+ def must_filter_on(terms)
22
+ terms = flatten_bool_terms(:must, terms)
23
+ return if terms.empty?
24
+
25
+ if terms.include?(Caoutsearch::Filter::NONE)
26
+ filters << Caoutsearch::Filter::NONE
27
+ elsif terms.size == 1
28
+ filters << terms[0]
29
+ elsif terms.size > 1
30
+ filters.push(*terms)
31
+ end
32
+ end
33
+
34
+ def must_not_filter_on(terms)
35
+ terms = flatten_bool_terms(:must_not, terms)
36
+ terms = terms.without(Caoutsearch::Filter::NONE)
37
+ return if terms.empty?
38
+
39
+ filters <<
40
+ if terms.size == 1
41
+ { bool: { must_not: terms[0] } }
42
+ else
43
+ filters << { bool: { must_not: terms } }
44
+ end
45
+ end
46
+
47
+ def flatten_bool_terms(operator, raw_terms)
48
+ terms = []
49
+
50
+ raw_terms.flatten.each do |value|
51
+ if value.is_a?(Hash) && value.keys == [:bool] && value[:bool].keys == [operator]
52
+ terms += Array.wrap(value.dig(:bool, operator))
53
+ else
54
+ terms << value
55
+ end
56
+ end
57
+
58
+ terms.uniq
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Cleaning
7
+ def clean
8
+ nested_queries.each_value(&:clean)
9
+ remove_duplicate_filters
10
+ remove_empty_clauses
11
+ self
12
+ end
13
+
14
+ def remove_duplicate_filters
15
+ filters.replace(filters.uniq) if dig(:query, :bool, :filter)
16
+ self
17
+ end
18
+
19
+ def remove_empty_clauses
20
+ each do |key, value|
21
+ delete(key) if value.blank? && value != false
22
+ end
23
+
24
+ self
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Getters
7
+ def filters
8
+ fetch(:query, :bool, :filter, [])
9
+ end
10
+
11
+ def aggregations
12
+ fetch(:aggregations, {})
13
+ end
14
+
15
+ def suggestions
16
+ fetch(:suggest, {})
17
+ end
18
+
19
+ def sort
20
+ fetch(:sort, [])
21
+ end
22
+
23
+ def fetch(*keys, default_value)
24
+ value = self
25
+
26
+ keys[0..-2].each do |key|
27
+ value = value[key] ||= {}
28
+ end
29
+
30
+ value[keys[-1]] ||= default_value
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Merge
7
+ def merge(other_hash)
8
+ dup.merge!(other_hash)
9
+ end
10
+
11
+ def merge!(other_hash)
12
+ merged_hash = to_h.deep_merge(other_hash) do |key, this_val, other_val|
13
+ if this_val.is_a?(Array) && other_val.is_a?(Array)
14
+ (this_val + other_val).uniq
15
+ elsif block_given?
16
+ yield key, this_val, other_val
17
+ else
18
+ other_val
19
+ end
20
+ end
21
+
22
+ super(merged_hash)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Nested
7
+ def nested_queries
8
+ @nested_queries ||= {}
9
+ end
10
+
11
+ def nested_query(path)
12
+ nested_queries[path.to_s] ||= begin
13
+ query = Caoutsearch::Search::Query::Base.new
14
+ query[:path] = path.to_s
15
+
16
+ filters << { nested: query }
17
+ query
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Query
6
+ module Setters
7
+ def add_none
8
+ filters << Caoutsearch::Filter::NONE
9
+ end
10
+
11
+ def add_filter(...)
12
+ should_filter_on(build_terms(...))
13
+ self
14
+ end
15
+
16
+ # Compose filters
17
+ #
18
+ # Examples :
19
+ #
20
+ # query.build_terms('key', 'value')
21
+ # => [ { term: { "key" => "value" } } ] } } }
22
+ #
23
+ # query.build_terms('key', 'value', type: :integer)
24
+ # => [ { term: { "key" => 0 } } ] } } }
25
+ #
26
+ # query.build_terms('key', 'value', as: :boolean)
27
+ # => [ { term: { "key" => true } } ] } } }
28
+ #
29
+ def build_terms(keys, value, type: :keyword, as: :default, **options)
30
+ filter_class = Caoutsearch::Filter[as]
31
+ raise ArgumentError, "unexpected type of filter: #{as.inspect}" unless filter_class
32
+
33
+ terms = Array.wrap(keys).flat_map { |key| filter_class.new(key, value, type, options).as_json }
34
+ terms.select(&:present?)
35
+ rescue Caoutsearch::Search::ValueOverflow
36
+ [Caoutsearch::Filter::NONE]
37
+ end
38
+
39
+ def add_sort(prop, direction)
40
+ if direction.to_s == "desc"
41
+ sort.push(prop => direction)
42
+ else
43
+ sort.push(prop)
44
+ end
45
+ self
46
+ end
47
+
48
+ def add_aggregation(key, value)
49
+ aggregations[key] = value
50
+ end
51
+
52
+ # TODO,
53
+ # Handle types, ex: prefix, input, text
54
+ # Suggestions should probably use _source: false
55
+ def add_suggestion(key, value, **options)
56
+ suggestions[key] = {
57
+ prefix: value,
58
+ completion: options.reverse_merge(
59
+ field: key,
60
+ skip_duplicates: true,
61
+ fuzzy: true
62
+ )
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Sanitizer
6
+ ESCAPED_CHARACTERS = "\+-&|!(){}[]^~*?:"
7
+ ESCAPED_CHARACTERS_REGEXP = /([+\-&|!(){}\[\]\^~*?:])/
8
+
9
+ class << self
10
+ def sanitize(value, characters = ESCAPED_CHARACTERS)
11
+ case value
12
+ when Array
13
+ value.map { |v| sanitize(v) }
14
+ when Hash
15
+ value.each { |k, v| value[k] = sanitize(v) }
16
+ when String
17
+ regexp = ESCAPED_CHARACTERS_REGEXP if characters == ESCAPED_CHARACTERS
18
+ regexp ||= Regexp.new("([#{Regexp.escape(characters)}])")
19
+
20
+ value.gsub(regexp, '\\\\\1')
21
+ else
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Search
6
+ module DeleteMethods
7
+ def delete_documents
8
+ request_payload = {
9
+ index: index_name,
10
+ body: build.to_h
11
+ }
12
+
13
+ instrument(:delete) do |event_payload|
14
+ event_payload[:request] = request_payload
15
+ event_payload[:response] = client.delete_by_query(request_payload)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Search
6
+ module Inspect
7
+ PROPERTIES_TO_INSPECT = %i[
8
+ search_criteria
9
+ current_context
10
+ current_order
11
+ current_page
12
+ current_limit
13
+ current_offset
14
+ current_aggregations
15
+ current_suggestions
16
+ current_returns
17
+ ].freeze
18
+
19
+ def inspect
20
+ properties = properties_to_inspect.map { |k, v| " #{k}: #{v}" }
21
+
22
+ "#<#{self.class}#{properties.join(",")}>"
23
+ end
24
+
25
+ private
26
+
27
+ def properties_to_inspect
28
+ PROPERTIES_TO_INSPECT.each_with_object({}) do |name, properties|
29
+ value = instance_variable_get("@#{name}")
30
+ properties[name] = value.inspect if value
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Search
6
+ module Instrumentation
7
+ extend ActiveSupport::Concern
8
+
9
+ def instrument(action, **options, &block)
10
+ ActiveSupport::Notifications.instrument("#{action}.caoutsearch_search", **options, klass: self.class.to_s, &block)
11
+ end
12
+
13
+ class_methods do
14
+ def instrument(action, **options, &block)
15
+ ActiveSupport::Notifications.instrument("#{action}.caoutsearch_search", **options, klass: to_s, &block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end