redi_search 5.0.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +3 -3
  3. data/.github/workflows/tests.yml +8 -5
  4. data/.rubocop.yml +0 -4
  5. data/Gemfile +2 -1
  6. data/README.md +57 -61
  7. data/bin/console +8 -4
  8. data/bin/publish +2 -2
  9. data/gemfiles/activerecord_60.gemfile +1 -0
  10. data/gemfiles/activerecord_61.gemfile +1 -0
  11. data/gemfiles/activerecord_70.gemfile +2 -1
  12. data/lib/redi_search/add_field.rb +9 -15
  13. data/lib/redi_search/client.rb +9 -7
  14. data/lib/redi_search/document.rb +6 -11
  15. data/lib/redi_search/index.rb +4 -11
  16. data/lib/redi_search/lazily_load.rb +1 -1
  17. data/lib/redi_search/log_subscriber.rb +4 -0
  18. data/lib/redi_search/model.rb +22 -30
  19. data/lib/redi_search/schema/field.rb +15 -1
  20. data/lib/redi_search/schema/geo_field.rb +5 -6
  21. data/lib/redi_search/schema/numeric_field.rb +13 -6
  22. data/lib/redi_search/schema/tag_field.rb +12 -8
  23. data/lib/redi_search/schema/text_field.rb +7 -6
  24. data/lib/redi_search/schema.rb +33 -25
  25. data/lib/redi_search/search/clauses/and.rb +0 -2
  26. data/lib/redi_search/search/clauses/application_clause.rb +0 -2
  27. data/lib/redi_search/search/clauses/boolean.rb +1 -1
  28. data/lib/redi_search/search/clauses/in_order.rb +0 -2
  29. data/lib/redi_search/search/clauses/language.rb +0 -2
  30. data/lib/redi_search/search/clauses/limit.rb +0 -2
  31. data/lib/redi_search/search/clauses/no_content.rb +0 -2
  32. data/lib/redi_search/search/clauses/no_stop_words.rb +0 -2
  33. data/lib/redi_search/search/clauses/or.rb +0 -2
  34. data/lib/redi_search/search/clauses/return.rb +0 -2
  35. data/lib/redi_search/search/clauses/slop.rb +0 -2
  36. data/lib/redi_search/search/clauses/sort_by.rb +0 -2
  37. data/lib/redi_search/search/clauses/verbatim.rb +0 -2
  38. data/lib/redi_search/search/clauses/where.rb +1 -1
  39. data/lib/redi_search/search/clauses/with_scores.rb +0 -2
  40. data/lib/redi_search/search/clauses.rb +0 -16
  41. data/lib/redi_search/search/result.rb +11 -1
  42. data/lib/redi_search/search/term.rb +16 -13
  43. data/lib/redi_search/search.rb +0 -6
  44. data/lib/redi_search/spellcheck.rb +3 -7
  45. data/lib/redi_search/validatable.rb +0 -4
  46. data/lib/redi_search/validations/numericality.rb +0 -2
  47. data/lib/redi_search/version.rb +1 -1
  48. data/lib/redi_search.rb +4 -8
  49. data/redi_search.gemspec +8 -5
  50. metadata +18 -3
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/version"
3
4
  require "active_support/log_subscriber"
5
+ if ActiveSupport::VERSION::MAJOR > 6
6
+ require "active_support/isolated_execution_state"
7
+ end
4
8
 
5
9
  module RediSearch
6
10
  class LogSubscriber < ActiveSupport::LogSubscriber
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/index"
4
-
5
3
  module RediSearch
6
4
  module Model
7
5
  def self.included(base)
@@ -9,18 +7,16 @@ module RediSearch
9
7
  end
10
8
 
11
9
  module ClassMethods
12
- attr_reader :redi_search_index, :redi_search_serializer
10
+ attr_reader :search_index
13
11
 
14
12
  # rubocop:disable Metrics/MethodLength
15
- def redi_search(schema:, **options)
16
- @redi_search_index = Index.new(
17
- [options[:index_prefix],
18
- model_name.plural, RediSearch.env].compact.join("_"),
19
- schema,
20
- self
13
+ def redi_search(**options, &schema)
14
+ @search_index = Index.new(
15
+ [options[:index_prefix], model_name.plural, RediSearch.env].
16
+ compact.join("_"),
17
+ self, &schema
21
18
  )
22
- @redi_search_serializer = options[:serializer]
23
- register_redi_search_commit_hooks
19
+ register_search_commit_hooks
24
20
 
25
21
  scope :search_import, -> { all }
26
22
 
@@ -31,27 +27,26 @@ module RediSearch
31
27
 
32
28
  private
33
29
 
34
- def register_redi_search_commit_hooks
35
- after_commit(:redi_search_add_document, on: %i(create update)) if
36
- respond_to?(:after_commit)
37
- after_destroy_commit(:redi_search_delete_document) if
30
+ def register_search_commit_hooks
31
+ after_save_commit(:add_to_index) if respond_to?(:after_save_commit)
32
+ after_destroy_commit(:remove_from_index) if
38
33
  respond_to?(:after_destroy_commit)
39
34
  end
40
35
  end
41
36
 
42
37
  module ModelClassMethods
43
38
  def search(term = nil, **term_options)
44
- redi_search_index.search(term, **term_options)
39
+ search_index.search(term, **term_options)
45
40
  end
46
41
 
47
42
  def spellcheck(term, distance: 1)
48
- redi_search_index.spellcheck(term, distance: distance)
43
+ search_index.spellcheck(term, distance: distance)
49
44
  end
50
45
 
51
- def reindex(recreate: false, only: [])
52
- search_import.find_in_batches.all? do |group|
53
- redi_search_index.reindex(
54
- group.map { |record| record.redi_search_document(only: only) },
46
+ def reindex(recreate: false, only: [], batch_size: 1000)
47
+ search_import.find_in_batches(batch_size:).all? do |group|
48
+ search_index.reindex(
49
+ group.map { |record| record.search_document(only: only) },
55
50
  recreate: recreate
56
51
  )
57
52
  end
@@ -59,19 +54,16 @@ module RediSearch
59
54
  end
60
55
 
61
56
  module InstanceMethods
62
- def redi_search_document(only: [])
63
- Document.for_object(
64
- self.class.redi_search_index, self,
65
- only: only, serializer: self.class.redi_search_serializer
66
- )
57
+ def search_document(only: [])
58
+ Document.for_object(self.class.search_index, self, only: only)
67
59
  end
68
60
 
69
- def redi_search_delete_document
70
- self.class.redi_search_index.del(redi_search_document)
61
+ def remove_from_index
62
+ self.class.search_index.del(search_document)
71
63
  end
72
64
 
73
- def redi_search_add_document
74
- self.class.redi_search_index.add(redi_search_document)
65
+ def add_to_index
66
+ self.class.search_index.add(search_document)
75
67
  end
76
68
  end
77
69
  end
@@ -7,12 +7,26 @@ module RediSearch
7
7
  @name&.to_sym
8
8
  end
9
9
 
10
- def serialize(value)
10
+ def coerce(value)
11
11
  value
12
12
  end
13
13
 
14
+ def cast(value)
15
+ value
16
+ end
17
+
18
+ def serialize(record)
19
+ if value_block
20
+ record.instance_exec(&value_block)
21
+ else
22
+ record.public_send(name)
23
+ end
24
+ end
25
+
14
26
  private
15
27
 
28
+ attr_reader :value_block
29
+
16
30
  FALSES = [
17
31
  nil, "", false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"
18
32
  ].freeze
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/schema/field"
4
-
5
3
  module RediSearch
6
4
  class Schema
7
5
  class GeoField < Field
8
- def initialize(name, sortable: false, no_index: false)
9
- @name = name
10
- @sortable = sortable
11
- @no_index = no_index
6
+ def initialize(name, sortable: false, no_index: false, &block)
7
+ @name = name
8
+ @sortable = sortable
9
+ @no_index = no_index
10
+ @value_block = block
12
11
  end
13
12
 
14
13
  def to_a
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/schema/field"
4
-
5
3
  module RediSearch
6
4
  class Schema
7
5
  class NumericField < Field
8
- def initialize(name, sortable: false, no_index: false)
9
- @name = name
10
- @sortable = sortable
11
- @no_index = no_index
6
+ def initialize(name, sortable: false, no_index: false, &block)
7
+ @name = name
8
+ @sortable = sortable
9
+ @no_index = no_index
10
+ @value_block = block
12
11
  end
13
12
 
14
13
  def to_a
@@ -18,6 +17,14 @@ module RediSearch
18
17
  query
19
18
  end
20
19
 
20
+ def cast(value)
21
+ if value.to_s.include?(".")
22
+ value.to_f
23
+ else
24
+ value.to_i
25
+ end
26
+ end
27
+
21
28
  private
22
29
 
23
30
  attr_reader :sortable, :no_index
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/schema/field"
4
-
5
3
  module RediSearch
6
4
  class Schema
7
5
  class TagField < Field
8
- def initialize(name, separator: ",", sortable: false, no_index: false)
9
- @name = name
10
- @separator = separator
11
- @sortable = sortable
12
- @no_index = no_index
6
+ def initialize(name, separator: ",", sortable: false, no_index: false,
7
+ &block)
8
+ @name = name
9
+ @separator = separator
10
+ @sortable = sortable
11
+ @no_index = no_index
12
+ @value_block = block
13
13
  end
14
14
 
15
15
  def to_a
@@ -20,10 +20,14 @@ module RediSearch
20
20
  query
21
21
  end
22
22
 
23
- def serialize(value)
23
+ def coerce(value)
24
24
  value.join(separator)
25
25
  end
26
26
 
27
+ def cast(value)
28
+ value.split(separator)
29
+ end
30
+
27
31
  private
28
32
 
29
33
  attr_reader :separator, :sortable, :no_index
@@ -4,13 +4,14 @@ module RediSearch
4
4
  class Schema
5
5
  class TextField < Field
6
6
  def initialize(name, weight: 1.0, phonetic: nil, sortable: false,
7
- no_index: false, no_stem: false)
7
+ no_index: false, no_stem: false, &block)
8
8
  @name = name
9
- @weight = weight
10
- @phonetic = phonetic
11
- @sortable = sortable
12
- @no_index = no_index
13
- @no_stem = no_stem
9
+ @value_block = block
10
+
11
+ { weight: weight, phonetic: phonetic, sortable: sortable,
12
+ no_index: no_index, no_stem: no_stem }.each do |attr, value|
13
+ instance_variable_set("@#{attr}", value)
14
+ end
14
15
  end
15
16
 
16
17
  def to_a
@@ -1,46 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/schema/geo_field"
4
- require "redi_search/schema/numeric_field"
5
- require "redi_search/schema/tag_field"
6
- require "redi_search/schema/text_field"
7
-
8
3
  module RediSearch
9
4
  class Schema
10
- def self.make_field(field_name, options)
11
- options = [options] if options.is_a? Symbol
12
- schema, options = options.to_a.flatten
5
+ attr_reader :fields
6
+
7
+ def initialize(&block)
8
+ @fields = []
13
9
 
14
- Object.const_get("RediSearch::Schema::#{schema.to_s.capitalize}Field").
15
- new(field_name, **options.to_h)
10
+ instance_exec(&block)
16
11
  end
17
12
 
18
- def initialize(raw)
19
- @raw = raw
13
+ def text_field(name, **options, &block)
14
+ self[name] || push(Schema::TextField.new(name, **options, &block))
20
15
  end
21
16
 
22
- def to_a
23
- fields.map(&:to_a).flatten
17
+ def numeric_field(name, **options, &block)
18
+ self[name] || push(Schema::NumericField.new(name, **options, &block))
19
+ end
20
+
21
+ def tag_field(name, **options, &block)
22
+ self[name] || push(Schema::TagField.new(name, **options, &block))
24
23
  end
25
24
 
26
- def [](field)
27
- fields.group_by(&:name)[field]&.first
25
+ def geo_field(name, **options, &block)
26
+ self[name] || push(Schema::GeoField.new(name, **options, &block))
28
27
  end
29
28
 
30
- def fields
31
- @fields ||= raw.map do |field_name, options|
32
- self.class.make_field(field_name, options)
33
- end.flatten
29
+ def add_field(name, type, **options, &block)
30
+ case type
31
+ when :text then method(:text_field)
32
+ when :numeric then method(:numeric_field)
33
+ when :tag then method(:tag_field)
34
+ when :geo then method(:geo_field)
35
+ end.call(name, **options, &block)
34
36
  end
35
37
 
36
- def add_field(field_name, options)
37
- raw[field_name] = options
38
- @fields = nil
39
- self
38
+ def to_a
39
+ fields.map(&:to_a).flatten
40
+ end
41
+
42
+ def [](name)
43
+ fields.find { |field| field.name.to_sym == name.to_sym }
40
44
  end
41
45
 
42
46
  private
43
47
 
44
- attr_accessor :raw
48
+ def push(field)
49
+ @fields.push(field)
50
+
51
+ field
52
+ end
45
53
  end
46
54
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/boolean"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/validatable"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -48,7 +48,7 @@ module RediSearch
48
48
  @term = if term.is_a? RediSearch::Search
49
49
  term
50
50
  else
51
- Term.new(term, **term_options)
51
+ Term.new(term, nil, **term_options)
52
52
  end
53
53
  end
54
54
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/boolean"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -62,7 +62,7 @@ module RediSearch
62
62
  if condition[1].is_a? RediSearch::Search
63
63
  condition[1]
64
64
  else
65
- Term.new(condition[1], **options.to_h)
65
+ Term.new(condition[1], search.index.schema[@field], **options.to_h)
66
66
  end
67
67
  end
68
68
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/clauses/application_clause"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  module Clauses
@@ -1,21 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/search/term"
4
- require "redi_search/search/clauses/slop"
5
- require "redi_search/search/clauses/in_order"
6
- require "redi_search/search/clauses/language"
7
- require "redi_search/search/clauses/sort_by"
8
- require "redi_search/search/clauses/limit"
9
- require "redi_search/search/clauses/no_content"
10
- require "redi_search/search/clauses/verbatim"
11
- require "redi_search/search/clauses/no_stop_words"
12
- require "redi_search/search/clauses/return"
13
- require "redi_search/search/clauses/with_scores"
14
- require "redi_search/search/clauses/highlight"
15
- require "redi_search/search/clauses/and"
16
- require "redi_search/search/clauses/or"
17
- require "redi_search/search/clauses/where"
18
-
19
3
  module RediSearch
20
4
  class Search
21
5
  module Clauses
@@ -21,6 +21,8 @@ module RediSearch
21
21
  end
22
22
 
23
23
  def_delegators :results, :each, :empty?, :[], :last
24
+ def_delegator :search, :index
25
+ def_delegator :index, :schema
24
26
 
25
27
  def inspect
26
28
  results
@@ -58,9 +60,17 @@ module RediSearch
58
60
  fields = slice.last unless no_content?
59
61
  score = slice[1].to_f if with_scores?
60
62
 
61
- Document.new(search.index, document_id, Hash[*fields.to_a], score)
63
+ parse_result(document_id, fields, score)
62
64
  end
63
65
  end
66
+
67
+ def parse_result(document_id, fields, score)
68
+ field_values = fields.to_a.each_slice(2).to_h do |name, value|
69
+ [name, schema[name].cast(value)]
70
+ end
71
+
72
+ Document.new(search.index, document_id, field_values, score)
73
+ end
64
74
  end
65
75
  end
66
76
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/validatable"
4
-
5
3
  module RediSearch
6
4
  class Search
7
5
  class Term
@@ -12,16 +10,17 @@ module RediSearch
12
10
  validates_inclusion_of :option, within: %i(fuzziness optional prefix),
13
11
  allow_nil: true
14
12
 
15
- def initialize(term, **options)
16
- @term = term
13
+ def initialize(term, field = nil, **options)
14
+ @term = term
15
+ @field = field
17
16
  @options = options
18
17
 
19
18
  validate!
20
19
  end
21
20
 
22
21
  def to_s
23
- if @term.is_a? Range
24
- stringify_range
22
+ if term.is_a?(Range) then stringify_range
23
+ elsif field.is_a?(Schema::TagField) then stringify_tag
25
24
  else
26
25
  stringify_query
27
26
  end
@@ -29,7 +28,7 @@ module RediSearch
29
28
 
30
29
  private
31
30
 
32
- attr_accessor :term, :options
31
+ attr_accessor :term, :field, :options
33
32
 
34
33
  def fuzziness
35
34
  @fuzziness ||= options[:fuzziness]
@@ -52,16 +51,20 @@ module RediSearch
52
51
  end
53
52
 
54
53
  def stringify_query
55
- @term.to_s.
54
+ term.to_s.
56
55
  tr("`", "\`").
57
- yield_self { |str| "#{fuzzy_operator}#{str}#{fuzzy_operator}" }.
58
- yield_self { |str| "#{optional_operator}#{str}" }.
59
- yield_self { |str| "#{str}#{prefix_operator}" }.
60
- yield_self { |str| "`#{str}`" }
56
+ then { |str| "#{fuzzy_operator}#{str}#{fuzzy_operator}" }.
57
+ then { |str| "#{optional_operator}#{str}" }.
58
+ then { |str| "#{str}#{prefix_operator}" }.
59
+ then { |str| "`#{str}`" }
60
+ end
61
+
62
+ def stringify_tag
63
+ "{ #{Array(term).join(' | ')} }"
61
64
  end
62
65
 
63
66
  def stringify_range
64
- first, last = @term.first, @term.last
67
+ first, last = term.first, term.last
65
68
  first = "-inf" if first == -Float::INFINITY
66
69
  last = "+inf" if last == Float::INFINITY
67
70
 
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/lazily_load"
4
-
5
- require "redi_search/search/clauses"
6
- require "redi_search/search/term"
7
- require "redi_search/search/result"
8
-
9
3
  module RediSearch
10
4
  class Search
11
5
  extend Forwardable
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/lazily_load"
4
- require "redi_search/spellcheck/result"
5
- require "redi_search/validatable"
6
-
7
3
  module RediSearch
8
4
  class Spellcheck
9
5
  include LazilyLoad
@@ -35,15 +31,15 @@ module RediSearch
35
31
 
36
32
  @loaded = true
37
33
 
38
- RediSearch.client.call!(*command).yield_self do |response|
34
+ RediSearch.client.call!(*command).then do |response|
39
35
  parse_response(response)
40
36
  end
41
37
  end
42
38
 
43
39
  def parse_response(response)
44
- suggestions = response.map do |suggestion|
40
+ suggestions = response.to_h do |suggestion|
45
41
  suggestion[1..2]
46
- end.to_h
42
+ end
47
43
 
48
44
  @documents = parsed_terms.map do |term|
49
45
  Result.new(term, suggestions[term] || [])
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/validations/inclusion"
4
- require "redi_search/validations/presence"
5
- require "redi_search/validations/numericality"
6
-
7
3
  module RediSearch
8
4
  class ValidationError < StandardError
9
5
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/validations/inclusion"
4
-
5
3
  module RediSearch
6
4
  module Validations
7
5
  class Numericality
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RediSearch
4
- VERSION = "5.0.0"
4
+ VERSION = "6.1.0"
5
5
  end