redi_search 4.1.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
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