redi_search 4.1.0 → 6.0.1

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +3 -3
  3. data/.github/workflows/tests.yml +9 -6
  4. data/.rubocop.yml +1 -5
  5. data/Appraisals +4 -0
  6. data/Gemfile +2 -1
  7. data/README.md +57 -61
  8. data/bin/console +8 -4
  9. data/bin/publish +2 -2
  10. data/gemfiles/activerecord_60.gemfile +1 -0
  11. data/gemfiles/activerecord_61.gemfile +1 -0
  12. data/gemfiles/activerecord_70.gemfile +16 -0
  13. data/lib/redi_search/add_field.rb +9 -15
  14. data/lib/redi_search/client.rb +10 -8
  15. data/lib/redi_search/document/display.rb +4 -4
  16. data/lib/redi_search/document.rb +7 -12
  17. data/lib/redi_search/index.rb +4 -11
  18. data/lib/redi_search/lazily_load.rb +3 -3
  19. data/lib/redi_search/log_subscriber.rb +6 -2
  20. data/lib/redi_search/model.rb +20 -28
  21. data/lib/redi_search/schema/field.rb +17 -3
  22. data/lib/redi_search/schema/geo_field.rb +5 -6
  23. data/lib/redi_search/schema/numeric_field.rb +13 -6
  24. data/lib/redi_search/schema/tag_field.rb +12 -8
  25. data/lib/redi_search/schema/text_field.rb +7 -6
  26. data/lib/redi_search/schema.rb +33 -25
  27. data/lib/redi_search/search/clauses/and.rb +0 -2
  28. data/lib/redi_search/search/clauses/application_clause.rb +0 -2
  29. data/lib/redi_search/search/clauses/boolean.rb +1 -1
  30. data/lib/redi_search/search/clauses/in_order.rb +0 -2
  31. data/lib/redi_search/search/clauses/language.rb +0 -2
  32. data/lib/redi_search/search/clauses/limit.rb +0 -2
  33. data/lib/redi_search/search/clauses/no_content.rb +0 -2
  34. data/lib/redi_search/search/clauses/no_stop_words.rb +0 -2
  35. data/lib/redi_search/search/clauses/or.rb +0 -2
  36. data/lib/redi_search/search/clauses/return.rb +0 -2
  37. data/lib/redi_search/search/clauses/slop.rb +0 -2
  38. data/lib/redi_search/search/clauses/sort_by.rb +0 -2
  39. data/lib/redi_search/search/clauses/verbatim.rb +0 -2
  40. data/lib/redi_search/search/clauses/where.rb +1 -1
  41. data/lib/redi_search/search/clauses/with_scores.rb +0 -2
  42. data/lib/redi_search/search/clauses.rb +0 -16
  43. data/lib/redi_search/search/result.rb +13 -3
  44. data/lib/redi_search/search/term.rb +16 -13
  45. data/lib/redi_search/search.rb +1 -7
  46. data/lib/redi_search/spellcheck/result.rb +4 -4
  47. data/lib/redi_search/spellcheck.rb +4 -8
  48. data/lib/redi_search/validatable.rb +0 -4
  49. data/lib/redi_search/validations/numericality.rb +0 -2
  50. data/lib/redi_search/version.rb +1 -1
  51. data/lib/redi_search.rb +4 -8
  52. data/redi_search.gemspec +10 -7
  53. metadata +22 -6
@@ -1,23 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/document/display"
4
- require "redi_search/document/finder"
5
-
6
3
  module RediSearch
7
4
  class Document
8
5
  include Display
9
6
 
10
7
  class << self
11
- def for_object(index, record, serializer: nil, only: [])
12
- object_to_serialize = serializer&.new(record) || record
13
-
14
- field_values = index.schema.fields.map(&:name).map do |field|
15
- next unless only.empty? || only.include?(field)
8
+ def for_object(index, record, only: [])
9
+ field_values = index.schema.fields.filter_map do |field|
10
+ next unless only.empty? || only.include?(field.name)
16
11
 
17
- [field.to_s, object_to_serialize.public_send(field)]
18
- end.compact.to_h
12
+ [field.name.to_s, field.serialize(record)]
13
+ end.to_h
19
14
 
20
- new(index, object_to_serialize.id, field_values)
15
+ new(index, record.id, field_values)
21
16
  end
22
17
 
23
18
  def get(index, document_id)
@@ -48,7 +43,7 @@ module RediSearch
48
43
 
49
44
  def redis_attributes
50
45
  attributes.flat_map do |field, value|
51
- [field, index.schema[field.to_sym].serialize(value)]
46
+ [field, index.schema[field.to_sym].coerce(value)]
52
47
  end
53
48
  end
54
49
 
@@ -1,19 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/hset"
4
- require "redi_search/create"
5
- require "redi_search/schema"
6
- require "redi_search/search"
7
- require "redi_search/spellcheck"
8
- require "redi_search/add_field"
9
-
10
3
  module RediSearch
11
4
  class Index
12
5
  attr_reader :name, :schema, :model
13
6
 
14
- def initialize(name, schema, model = nil)
7
+ def initialize(name, model = nil, &schema)
15
8
  @name = name.to_s
16
- @schema = Schema.new(schema)
9
+ @schema = Schema.new(&schema)
17
10
  @model = model
18
11
  end
19
12
 
@@ -94,8 +87,8 @@ module RediSearch
94
87
  info.num_docs.to_i
95
88
  end
96
89
 
97
- def add_field(field_name, schema)
98
- AddField.new(self, field_name, schema).call!
90
+ def add_field(name, type, **options, &block)
91
+ AddField.new(self, name, type, **options, &block).call!
99
92
  end
100
93
 
101
94
  private
@@ -30,7 +30,7 @@ module RediSearch
30
30
  end
31
31
  end
32
32
 
33
- #:nocov:
33
+ # :nocov:
34
34
  def pretty_print(printer)
35
35
  execute_and_rescue_inspection do
36
36
  return super(inspect) unless valid?
@@ -38,7 +38,7 @@ module RediSearch
38
38
  printer.pp(documents)
39
39
  end
40
40
  end
41
- #:nocov:
41
+ # :nocov:
42
42
 
43
43
  private
44
44
 
@@ -53,7 +53,7 @@ module RediSearch
53
53
 
54
54
  @loaded = true
55
55
 
56
- call!.yield_self do |response|
56
+ call!.then do |response|
57
57
  parse_response(response)
58
58
  end
59
59
  end
@@ -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
@@ -12,12 +16,12 @@ module RediSearch
12
16
  Thread.current[:redi_search_runtime] ||= 0
13
17
  end
14
18
 
15
- #:nocov:
19
+ # :nocov:
16
20
  def self.reset_runtime
17
21
  rt, self.runtime = runtime, 0
18
22
  rt
19
23
  end
20
- #:nocov:
24
+ # :nocov:
21
25
 
22
26
  def action(event)
23
27
  self.class.runtime += event.duration
@@ -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
46
  def reindex(recreate: false, only: [])
52
47
  search_import.find_in_batches.all? do |group|
53
- redi_search_index.reindex(
54
- group.map { |record| record.redi_search_document(only: only) },
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,21 +7,35 @@ 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
19
33
 
20
34
  def boolean_options_string
21
- boolean_options.map do |option|
35
+ boolean_options.filter_map do |option|
22
36
  option.to_s.upcase.split("_").join unless
23
37
  FALSES.include?(send(option))
24
- end.compact
38
+ end
25
39
  end
26
40
  end
27
41
  end
@@ -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,16 +21,18 @@ 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
27
29
  end
28
30
 
29
- #:nocov:
31
+ # :nocov:
30
32
  def pretty_print(printer)
31
33
  printer.pp(results)
32
34
  end
33
- #:nocov:
35
+ # :nocov:
34
36
 
35
37
  private
36
38
 
@@ -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