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
data/lib/redi_search.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "redis"
4
4
  require "active_support"
5
+ require "active_model"
5
6
  require "active_support/core_ext/object"
6
7
  require "active_support/core_ext/module/delegation"
7
8
 
@@ -28,8 +29,10 @@ module RediSearch
28
29
  yield(configuration)
29
30
  end
30
31
 
31
- def client
32
- configuration.client
32
+ delegate :client, to: :configuration
33
+
34
+ def env
35
+ @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
33
36
  end
34
37
  end
35
38
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Add
5
+ include ActiveModel::Validations
6
+
7
+ validates :score, numericality: {
8
+ greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0
9
+ }
10
+
11
+ def initialize(index, document, score: 1.0, replace: {}, language: nil,
12
+ no_save: false)
13
+ @index = index
14
+ @document = document
15
+ @score = score || 1.0
16
+ @replace = replace
17
+ @language = language
18
+ @no_save = no_save
19
+ end
20
+
21
+ def call!
22
+ validate!
23
+
24
+ RediSearch.client.call!(*command).ok?
25
+ end
26
+
27
+ def call
28
+ call!
29
+ rescue Redis::CommandError
30
+ false
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :index, :document, :score, :replace, :language, :no_save
36
+
37
+ def command
38
+ [
39
+ "ADD",
40
+ index.name,
41
+ document.document_id,
42
+ score,
43
+ *extract_options,
44
+ "FIELDS",
45
+ document.redis_attributes
46
+ ].compact
47
+ end
48
+
49
+ def extract_options
50
+ opts = []
51
+ opts << ["LANGUAGE", language] if language
52
+ opts << "NOSAVE" if no_save
53
+ opts << replace_options if replace?
54
+ opts
55
+ end
56
+
57
+ def replace?
58
+ replace.present?
59
+ end
60
+
61
+ def replace_options
62
+ ["REPLACE"].tap do |replace_option|
63
+ if replace.is_a?(Hash)
64
+ replace_option << "PARTIAL" if replace[:partial]
65
+ # replace_option << "NOCREATE" if replace[:no_create]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Alter
5
+ def initialize(index, field_name, schema)
6
+ @index = index
7
+ @field_name = field_name
8
+ @raw_schema = schema
9
+ end
10
+
11
+ def call!
12
+ index.schema.alter(field_name, raw_schema)
13
+ RediSearch.client.call!(
14
+ "ALTER",
15
+ index.name,
16
+ "SCHEMA",
17
+ "ADD",
18
+ *field_schema
19
+ ).ok?
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :index, :field_name, :raw_schema
25
+
26
+ def field_schema
27
+ @field_schema ||= Schema.make_field(field_name, raw_schema)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Create
5
+ OPTION_MAPPER = {
6
+ max_text_fields: "MAXTEXTFIELDS",
7
+ no_offsets: "NOOFFSETS",
8
+ no_highlight: "NOHL",
9
+ no_fields: "NOFIELDS",
10
+ no_frequencies: "NOFREQS"
11
+ }.freeze
12
+
13
+ def initialize(index, schema, options)
14
+ @index = index
15
+ @schema = schema
16
+ @options = options
17
+ end
18
+
19
+ def call!
20
+ RediSearch.client.call!(
21
+ "CREATE",
22
+ index.name,
23
+ *extract_options.compact,
24
+ "SCHEMA",
25
+ schema.to_a
26
+ ).ok?
27
+ end
28
+
29
+ def call
30
+ call!
31
+ rescue Redis::CommandError
32
+ false
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :index, :schema, :options
38
+
39
+ def extract_options
40
+ options.map do |clause, switch|
41
+ next unless OPTION_MAPPER.key?(clause.to_sym) && switch
42
+
43
+ OPTION_MAPPER[clause.to_sym]
44
+ end << temporary_option
45
+ end
46
+
47
+ def temporary_option
48
+ return [] unless options[:temporary]
49
+
50
+ ["TEMPORARY", options[:temporary]]
51
+ end
52
+ end
53
+ end
@@ -3,8 +3,22 @@
3
3
  module RediSearch
4
4
  class Document
5
5
  class << self
6
+ def for_object(index, record, serializer: nil, only: [])
7
+ object_to_serialize = serializer&.new(record) || record
8
+
9
+ field_values = index.schema.fields.map do |field|
10
+ next if only.present? && !only.include?(field.to_sym)
11
+
12
+ [field.to_s, object_to_serialize.public_send(field)]
13
+ end.compact.to_h
14
+
15
+ new(index, object_to_serialize.id, field_values)
16
+ end
17
+
6
18
  def get(index, document_id)
7
- response = RediSearch.client.call!("GET", index.name, document_id)
19
+ response = RediSearch.client.call!(
20
+ "GET", index.name, prepend_document_id(index, document_id)
21
+ )
8
22
 
9
23
  return if response.blank?
10
24
 
@@ -12,41 +26,62 @@ module RediSearch
12
26
  end
13
27
 
14
28
  def mget(index, *document_ids)
29
+ unique_document_ids = document_ids.map do |id|
30
+ prepend_document_id(index, id)
31
+ end
15
32
  document_ids.zip(
16
- RediSearch.client.call!("MGET", index.name, *document_ids)
33
+ RediSearch.client.call!("MGET", index.name, *unique_document_ids)
17
34
  ).map do |document|
18
35
  next if document[1].blank?
19
36
 
20
37
  new(index, document[0], Hash[*document[1]])
21
38
  end.compact
22
39
  end
40
+
41
+ def prepend_document_id(index, document_id)
42
+ if document_id.to_s.starts_with? index.name
43
+ document_id
44
+ else
45
+ "#{index.name}#{document_id}"
46
+ end
47
+ end
23
48
  end
24
49
 
25
- attr_reader :document_id
50
+ attr_reader :attributes, :score
26
51
 
27
- def initialize(index, document_id, fields)
52
+ def initialize(index, document_id, fields, score = nil)
28
53
  @index = index
29
54
  @document_id = document_id
30
- @to_a = []
55
+ @attributes = fields
56
+ @score = score
31
57
 
32
- schema_fields.each do |field|
33
- @to_a.push([field, fields[field]])
34
- instance_variable_set(:"@#{field}", fields[field])
35
- define_singleton_method field do
36
- fields[field]
37
- end
58
+ attributes.each do |field, value|
59
+ next unless schema_fields.include? field
60
+
61
+ instance_variable_set(:"@#{field}", value)
62
+ define_singleton_method(field) { value }
38
63
  end
39
64
  end
40
65
 
41
- def del
42
- client.call!("DEL", index.name, document_id).ok?
66
+ def del(delete_document: false)
67
+ client.call!(
68
+ "DEL", index.name, document_id, ("DD" if delete_document)
69
+ ) == 1
43
70
  end
44
71
 
45
72
  #:nocov:
73
+ def inspect
74
+ inspection = pretty_print_attributes.map do |field_name|
75
+ "#{field_name}: #{public_send(field_name)}"
76
+ end.compact.join(", ")
77
+
78
+ "#<#{self.class} #{inspection}>"
79
+ end
80
+
46
81
  def pretty_print(printer) # rubocop:disable Metrics/MethodLength
47
82
  printer.object_address_group(self) do
48
83
  printer.seplist(
49
- schema_fields.append("document_id"), proc { printer.text "," }
84
+ pretty_print_attributes , proc { printer.text "," }
50
85
  ) do |field_name|
51
86
  printer.breakable " "
52
87
  printer.group(1) do
@@ -58,14 +93,34 @@ module RediSearch
58
93
  end
59
94
  end
60
95
  end
96
+
97
+ def pretty_print_attributes
98
+ pp_attrs = attributes.keys.dup
99
+ pp_attrs.push("document_id")
100
+ pp_attrs.push("score") if score.present?
101
+
102
+ pp_attrs.compact
103
+ end
61
104
  #:nocov:
62
105
 
63
106
  def schema_fields
64
107
  @schema_fields ||= index.schema.fields.map(&:to_s)
65
108
  end
66
109
 
67
- def to_a
68
- @to_a.flatten
110
+ def redis_attributes
111
+ attributes.to_a.flatten
112
+ end
113
+
114
+ def document_id
115
+ self.class.prepend_document_id(index, @document_id)
116
+ end
117
+
118
+ def document_id_without_index
119
+ if @document_id.to_s.starts_with? index.name
120
+ @document_id.gsub(index.name, "")
121
+ else
122
+ @document_id
123
+ end
69
124
  end
70
125
 
71
126
  private
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "redi_search/add"
4
+ require "redi_search/create"
3
5
  require "redi_search/schema"
4
6
  require "redi_search/search"
5
7
  require "redi_search/spellcheck"
8
+ require "redi_search/alter"
6
9
 
7
10
  module RediSearch
8
11
  class Index
@@ -15,21 +18,19 @@ module RediSearch
15
18
  end
16
19
 
17
20
  def search(term = nil, **term_options)
18
- Search.new(self, term, model, **term_options)
21
+ Search.new(self, term, **term_options)
19
22
  end
20
23
 
21
24
  def spellcheck(query, distance: 1)
22
25
  Spellcheck.new(self, query, distance: distance)
23
26
  end
24
27
 
25
- def create
26
- create!
27
- rescue Redis::CommandError
28
- false
28
+ def create(**options)
29
+ Create.new(self, schema, options).call
29
30
  end
30
31
 
31
- def create!
32
- client.call!("CREATE", name, "SCHEMA", schema.to_a).ok?
32
+ def create!(**options)
33
+ Create.new(self, schema, options).call!
33
34
  end
34
35
 
35
36
  def drop
@@ -42,29 +43,24 @@ module RediSearch
42
43
  client.call!("DROP", name).ok?
43
44
  end
44
45
 
45
- def add(record, score = 1.0)
46
- add!(record, score)
47
- rescue Redis::CommandError
48
- false
46
+ def add(document, **options)
47
+ Add.new(self, document, **options).call
49
48
  end
50
49
 
51
- def add!(record, score = 1.0)
52
- client.call!(
53
- "ADD", name, record.id, score, "REPLACE", "FIELDS",
54
- Document::Converter.new(self, record).document.to_a
55
- )
50
+ def add!(document, **options)
51
+ Add.new(self, document, **options).call!
56
52
  end
57
53
 
58
- def add_multiple!(records)
54
+ def add_multiple!(documents, **options)
59
55
  client.pipelined do
60
- records.each do |record|
61
- add!(record)
56
+ documents.each do |document|
57
+ add!(document, **options)
62
58
  end
63
59
  end.ok?
64
60
  end
65
61
 
66
- def del(record, delete_document: false)
67
- client.call!("DEL", name, record.id, ("DD" if delete_document))
62
+ def del(document, delete_document: false)
63
+ document.del(delete_document: delete_document)
68
64
  end
69
65
 
70
66
  def exist?
@@ -82,13 +78,22 @@ module RediSearch
82
78
  end
83
79
 
84
80
  def fields
85
- @fields ||= schema.fields.map(&:to_s)
81
+ schema.fields.map(&:to_s)
82
+ end
83
+
84
+ def reindex(documents, recreate: false, **options)
85
+ drop if recreate
86
+ create unless exist?
87
+
88
+ add_multiple! documents, **options
89
+ end
90
+
91
+ def document_count
92
+ info["num_docs"].to_i
86
93
  end
87
94
 
88
- def reindex(docs)
89
- drop if exist?
90
- create
91
- add_multiple! docs
95
+ def alter(field_name, schema)
96
+ Alter.new(self, field_name, schema).call!
92
97
  end
93
98
 
94
99
  private
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ module LazilyLoad
5
+ extend ActiveSupport::Concern
6
+
7
+ include Enumerable
8
+
9
+ included do
10
+ delegate :size, :each, to: :to_a
11
+ end
12
+
13
+ def loaded?
14
+ @loaded = false unless defined? @loaded
15
+
16
+ @loaded
17
+ end
18
+
19
+ def to_a
20
+ execute unless loaded?
21
+
22
+ @documents
23
+ end
24
+
25
+ alias load to_a
26
+
27
+ #:nocov:
28
+ def inspect
29
+ execute unless loaded?
30
+
31
+ to_a
32
+ end
33
+
34
+ def pretty_print(printer)
35
+ execute unless loaded?
36
+
37
+ printer.pp(documents)
38
+ rescue Redis::CommandError => e
39
+ printer.pp(e.message)
40
+ end
41
+ #:nocov:
42
+
43
+ def count
44
+ to_a.size
45
+ end
46
+
47
+ private
48
+
49
+ def command
50
+ raise NotImplementedError, "included class did not define #{__method__}"
51
+ end
52
+
53
+ def execute
54
+ @loaded = true
55
+
56
+ RediSearch.client.call!(*command).yield_self do |response|
57
+ parse_response(response)
58
+ end
59
+ end
60
+
61
+ def parse_response(_response)
62
+ raise NotImplementedError, "included class did not define #{__method__}"
63
+ end
64
+ end
65
+ end