redi_search 0.1.0 → 1.0.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +516 -112
  3. data/lib/redi_search.rb +5 -2
  4. data/lib/redi_search/add.rb +70 -0
  5. data/lib/redi_search/alter.rb +30 -0
  6. data/lib/redi_search/create.rb +53 -0
  7. data/lib/redi_search/document.rb +71 -16
  8. data/lib/redi_search/index.rb +31 -26
  9. data/lib/redi_search/lazily_load.rb +65 -0
  10. data/lib/redi_search/log_subscriber.rb +4 -0
  11. data/lib/redi_search/model.rb +41 -18
  12. data/lib/redi_search/schema.rb +17 -8
  13. data/lib/redi_search/schema/text_field.rb +0 -2
  14. data/lib/redi_search/search.rb +22 -44
  15. data/lib/redi_search/search/clauses.rb +60 -31
  16. data/lib/redi_search/search/clauses/and.rb +17 -0
  17. data/lib/redi_search/search/clauses/application_clause.rb +18 -0
  18. data/lib/redi_search/search/clauses/boolean.rb +72 -0
  19. data/lib/redi_search/search/clauses/highlight.rb +47 -0
  20. data/lib/redi_search/search/clauses/in_order.rb +17 -0
  21. data/lib/redi_search/search/clauses/language.rb +23 -0
  22. data/lib/redi_search/search/clauses/limit.rb +27 -0
  23. data/lib/redi_search/search/clauses/no_content.rb +17 -0
  24. data/lib/redi_search/search/clauses/no_stop_words.rb +17 -0
  25. data/lib/redi_search/search/clauses/or.rb +23 -0
  26. data/lib/redi_search/search/clauses/return.rb +23 -0
  27. data/lib/redi_search/search/clauses/slop.rb +23 -0
  28. data/lib/redi_search/search/clauses/sort_by.rb +25 -0
  29. data/lib/redi_search/search/clauses/verbatim.rb +17 -0
  30. data/lib/redi_search/search/clauses/where.rb +66 -0
  31. data/lib/redi_search/search/clauses/with_scores.rb +17 -0
  32. data/lib/redi_search/search/result.rb +46 -0
  33. data/lib/redi_search/search/term.rb +4 -4
  34. data/lib/redi_search/spellcheck.rb +30 -29
  35. data/lib/redi_search/spellcheck/result.rb +44 -0
  36. data/lib/redi_search/version.rb +1 -1
  37. metadata +101 -31
  38. data/.gitignore +0 -11
  39. data/.rubocop.yml +0 -1757
  40. data/.travis.yml +0 -23
  41. data/Gemfile +0 -17
  42. data/Rakefile +0 -12
  43. data/bin/console +0 -8
  44. data/bin/publish +0 -58
  45. data/bin/setup +0 -8
  46. data/bin/test +0 -7
  47. data/lib/redi_search/document/converter.rb +0 -26
  48. data/lib/redi_search/error.rb +0 -6
  49. data/lib/redi_search/result/collection.rb +0 -22
  50. data/lib/redi_search/search/and_clause.rb +0 -15
  51. data/lib/redi_search/search/boolean_clause.rb +0 -72
  52. data/lib/redi_search/search/highlight_clause.rb +0 -43
  53. data/lib/redi_search/search/or_clause.rb +0 -21
  54. data/lib/redi_search/search/where_clause.rb +0 -66
  55. data/redi_search.gemspec +0 -48
@@ -58,6 +58,10 @@ module RediSearch
58
58
  log_command(event, YELLOW)
59
59
  end
60
60
 
61
+ def explaincli(event)
62
+ log_command(event, BLUE)
63
+ end
64
+
61
65
  private
62
66
 
63
67
  def log_command(event, debug_color)
@@ -1,57 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "redi_search/index"
4
- require "redi_search/document/converter"
5
-
6
4
  require "active_support/concern"
7
5
 
8
6
  module RediSearch
9
7
  module Model
10
8
  extend ActiveSupport::Concern
11
9
 
10
+ # rubocop:disable Metrics/BlockLength
12
11
  class_methods do
13
- attr_reader :redi_search_index
12
+ attr_reader :redi_search_index, :redi_search_serializer
14
13
 
15
- def redi_search(**options) # rubocop:disable Metrics/MethodLength
14
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
15
+ def redi_search(schema:, **options)
16
16
  @redi_search_index = Index.new(
17
- options[:index_name] || "#{name.underscore}_idx",
18
- options[:schema],
17
+ [options[:index_prefix],
18
+ model_name.plural, RediSearch.env].compact.join("_"),
19
+ schema,
19
20
  self
20
21
  )
22
+ @redi_search_serializer = options[:serializer]
23
+ register_redi_search_commit_hooks
21
24
 
22
- if respond_to? :after_commit
23
- after_commit :redi_search_add_document, on: [:create, :update]
24
- end
25
- if respond_to? :after_destroy_commit
26
- after_destroy_commit :redi_search_delete_document
27
- end
25
+ scope :search_import, -> { all }
28
26
 
29
27
  class << self
30
28
  def search(term = nil, **term_options)
31
29
  redi_search_index.search(term, **term_options)
32
30
  end
33
31
 
34
- def reindex
35
- redi_search_index.reindex(all)
32
+ def spellcheck(term, distance: 1)
33
+ redi_search_index.spellcheck(term, distance: distance)
34
+ end
35
+
36
+ def reindex(only: [], **options)
37
+ search_import.find_in_batches.all? do |group|
38
+ redi_search_index.reindex(
39
+ group.map { |record| record.redi_search_document(only: only) },
40
+ **options.deep_merge(replace: { partial: true })
41
+ )
42
+ end
36
43
  end
37
44
  end
38
45
  end
46
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
47
+
48
+ private
49
+
50
+ def register_redi_search_commit_hooks
51
+ after_commit(:redi_search_add_document, on: %i(create update)) if
52
+ respond_to?(:after_commit)
53
+ after_destroy_commit(:redi_search_delete_document) if
54
+ respond_to?(:after_destroy_commit)
55
+ end
39
56
  end
57
+ # rubocop:enable Metrics/BlockLength
40
58
 
41
- def redi_search_document
42
- Document::Converter.new(self.class.redi_search_index, self).document
59
+ def redi_search_document(only: [])
60
+ Document.for_object(
61
+ self.class.redi_search_index, self,
62
+ only: only, serializer: self.class.redi_search_serializer
63
+ )
43
64
  end
44
65
 
45
66
  def redi_search_delete_document
46
67
  return unless self.class.redi_search_index.exist?
47
68
 
48
- self.class.redi_search_index.del(self, delete_document: true)
69
+ self.class.redi_search_index.del(
70
+ redi_search_document, delete_document: true
71
+ )
49
72
  end
50
73
 
51
74
  def redi_search_add_document
52
75
  return unless self.class.redi_search_index.exist?
53
76
 
54
- self.class.redi_search_index.add(self)
77
+ self.class.redi_search_index.add(redi_search_document, replace: true)
55
78
  end
56
79
  end
57
80
  end
@@ -7,19 +7,23 @@ require "redi_search/schema/text_field"
7
7
 
8
8
  module RediSearch
9
9
  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
13
+
14
+ "RediSearch::Schema::#{schema.to_s.capitalize}Field".
15
+ constantize.
16
+ new(field_name, **options.to_h).
17
+ to_a
18
+ end
19
+
10
20
  def initialize(raw)
11
21
  @raw = raw
12
22
  end
13
23
 
14
24
  def to_a
15
25
  raw.map do |field_name, options|
16
- options = [options] if options.is_a? Symbol
17
- schema, options = options.to_a.flatten
18
-
19
- "RediSearch::Schema::#{schema.to_s.capitalize}Field".
20
- constantize.
21
- new(field_name, **options.to_h).
22
- to_a
26
+ self.class.make_field(field_name, options)
23
27
  end.flatten
24
28
  end
25
29
 
@@ -27,8 +31,13 @@ module RediSearch
27
31
  raw.keys
28
32
  end
29
33
 
34
+ def alter(field_name, options)
35
+ raw[field_name] = options
36
+ self
37
+ end
38
+
30
39
  private
31
40
 
32
- attr_reader :raw
41
+ attr_accessor :raw
33
42
  end
34
43
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record/type"
4
-
5
3
  module RediSearch
6
4
  class Schema
7
5
  class TextField < Field
@@ -1,51 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "redi_search/lazily_load"
4
+
3
5
  require "redi_search/search/clauses"
4
6
  require "redi_search/search/term"
5
- require "redi_search/search/highlight_clause"
6
- require "redi_search/result/collection"
7
+ require "redi_search/search/result"
7
8
 
8
9
  module RediSearch
9
10
  class Search
10
11
  include Enumerable
12
+ include LazilyLoad
11
13
  include Clauses
12
14
 
13
- def initialize(index, term = nil, model = nil, **term_options)
15
+ def initialize(index, term = nil, **term_options)
14
16
  @index = index
15
- @model = model
16
- @loaded = false
17
- @no_content = false
18
17
  @clauses = []
18
+ @used_clauses = Set.new
19
19
 
20
20
  @term_clause = term.presence &&
21
- AndClause.new(self, term, nil, **term_options)
22
- end
23
-
24
- #:nocov:
25
- def pretty_print(printer)
26
- execute unless loaded?
27
-
28
- printer.pp(records)
29
- rescue Redis::CommandError => e
30
- printer.pp(e.message)
31
- end
32
- #:nocov:
33
-
34
- def loaded?
35
- @loaded
36
- end
37
-
38
- def to_a
39
- execute unless loaded?
40
-
41
- @records
21
+ And.new(self, term, nil, **term_options)
42
22
  end
43
23
 
44
24
  def results
45
- model.where(id: to_a.map(&:document_id))
25
+ if index.model.present?
26
+ index.model.where(id: to_a.map(&:document_id_without_index))
27
+ else
28
+ to_a
29
+ end
46
30
  end
47
31
 
48
- delegate :count, :each, to: :to_a
32
+ def explain
33
+ RediSearch.client.call!(
34
+ "EXPLAINCLI", index.name, term_clause
35
+ ).join(" ").strip
36
+ end
49
37
 
50
38
  def to_redis
51
39
  command.map do |arg|
@@ -65,25 +53,15 @@ module RediSearch
65
53
 
66
54
  private
67
55
 
68
- attr_reader :records
69
- attr_accessor :index, :model, :clauses
56
+ attr_reader :documents, :used_clauses
57
+ attr_accessor :index, :clauses
70
58
 
71
59
  def command
72
- ["SEARCH", index.name, term_clause, *clauses]
60
+ ["SEARCH", index.name, term_clause, *clauses.uniq]
73
61
  end
74
62
 
75
- def execute
76
- @loaded = true
77
-
78
- RediSearch.client.call!(*command).then do |results|
79
- @records = Result::Collection.new(
80
- index, results[0], results[1..-1].then do |docs|
81
- next docs unless @no_content
82
-
83
- docs.zip([[]] * results[0]).flatten(1)
84
- end
85
- )
86
- end
63
+ def parse_response(response)
64
+ @documents = Result.new(index, used_clauses, response[0], response[1..-1])
87
65
  end
88
66
  end
89
67
  end
@@ -1,60 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "redi_search/search/term"
4
- require "redi_search/search/and_clause"
5
- require "redi_search/search/or_clause"
6
- require "redi_search/search/where_clause"
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"
7
18
 
8
19
  module RediSearch
9
20
  class Search
10
21
  module Clauses
11
- def highlight(**options)
12
- clauses.push(*HighlightClause.new(**options).clause)
13
-
14
- self
22
+ def highlight(fields: [], opening_tag: "<b>", closing_tag: "</b>")
23
+ add_to_clause(Highlight.new(
24
+ fields: fields, opening_tag: opening_tag, closing_tag: closing_tag
25
+ ))
15
26
  end
16
27
 
17
28
  def slop(slop)
18
- clauses.push("SLOP", slop)
19
-
20
- self
29
+ add_to_clause(Slop.new(slop: slop))
21
30
  end
22
31
 
23
32
  def in_order
24
- clauses.push("INORDER")
33
+ add_to_clause(InOrder.new)
34
+ end
25
35
 
26
- self
36
+ def verbatim
37
+ add_to_clause(Verbatim.new)
38
+ end
39
+
40
+ def no_stop_words
41
+ add_to_clause(NoStopWords.new)
42
+ end
43
+
44
+ def with_scores
45
+ add_to_clause(WithScores.new)
27
46
  end
28
47
 
29
48
  def no_content
30
- @no_content = true
31
- clauses.push("NOCONTENT")
49
+ add_to_clause(NoContent.new)
50
+ end
32
51
 
33
- self
52
+ def return(*fields)
53
+ add_to_clause(Return.new(fields: fields))
34
54
  end
35
55
 
36
56
  def language(language)
37
- clauses.push("LANGUAGE", language)
38
-
39
- self
57
+ add_to_clause(Language.new(language: language))
40
58
  end
41
59
 
42
60
  def sort_by(field, order: :asc)
43
- raise ArgumentError unless %i(asc desc).include?(order.to_sym)
44
-
45
- clauses.push("SORTBY", field, order)
46
-
47
- self
61
+ add_to_clause(SortBy.new(field: field, order: order))
48
62
  end
49
63
 
50
- def limit(num, offset = 0)
51
- clauses.push("LIMIT", offset, num)
64
+ def limit(total, offset = 0)
65
+ add_to_clause(Limit.new(total: total, offset: offset))
66
+ end
52
67
 
53
- self
68
+ def count
69
+ if @loaded
70
+ to_a.size
71
+ else
72
+ RediSearch.client.call!(
73
+ "SEARCH", index.name, term_clause, *Limit.new(total: 0).clause
74
+ ).first
75
+ end
54
76
  end
55
77
 
56
78
  def where(**condition)
57
- @term_clause = WhereClause.new(self, condition, @term_clause)
79
+ @term_clause = Where.new(self, condition, @term_clause)
58
80
 
59
81
  if condition.blank?
60
82
  @term_clause
@@ -64,8 +86,7 @@ module RediSearch
64
86
  end
65
87
 
66
88
  def and(new_term = nil, **term_options)
67
- @term_clause =
68
- AndClause.new(self, new_term, @term_clause, **term_options)
89
+ @term_clause = And.new(self, new_term, @term_clause, **term_options)
69
90
 
70
91
  if new_term.blank?
71
92
  @term_clause
@@ -75,8 +96,7 @@ module RediSearch
75
96
  end
76
97
 
77
98
  def or(new_term = nil, **term_options)
78
- @term_clause =
79
- OrClause.new(self, new_term, @term_clause, **term_options)
99
+ @term_clause = Or.new(self, new_term, @term_clause, **term_options)
80
100
 
81
101
  if new_term.blank?
82
102
  @term_clause
@@ -84,6 +104,15 @@ module RediSearch
84
104
  self
85
105
  end
86
106
  end
107
+
108
+ private
109
+
110
+ def add_to_clause(clause)
111
+ used_clauses.add(clause.class.name.demodulize.underscore)
112
+ clauses.push(*clause.clause)
113
+
114
+ self
115
+ end
87
116
  end
88
117
  end
89
118
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redi_search/search/clauses/boolean"
4
+
5
+ module RediSearch
6
+ class Search
7
+ module Clauses
8
+ class And < Boolean
9
+ private
10
+
11
+ def operand
12
+ " "
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Search
5
+ module Clauses
6
+ class ApplicationClause
7
+ include ActiveModel::Validations
8
+
9
+ def self.clause_term(term, *validations)
10
+ attr_reader term
11
+ validations.each do |validation|
12
+ validates term, validation
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end