redi_search 0.1.0 → 1.0.0

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