redi_search 1.0.4 → 3.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/.github/workflows/lint.yml +18 -0
  3. data/.github/workflows/tests.yml +45 -0
  4. data/.rubocop.yml +136 -193
  5. data/Appraisals +6 -6
  6. data/Gemfile +4 -4
  7. data/README.md +79 -92
  8. data/Rakefile +15 -3
  9. data/bin/console +29 -0
  10. data/gemfiles/{rails_51.gemfile → activerecord_51.gemfile} +3 -4
  11. data/gemfiles/{rails_52.gemfile → activerecord_52.gemfile} +3 -4
  12. data/gemfiles/{rails_6.gemfile → activerecord_61.gemfile} +3 -4
  13. data/lib/redi_search.rb +8 -7
  14. data/lib/redi_search/{alter.rb → add_field.rb} +13 -5
  15. data/lib/redi_search/client.rb +23 -11
  16. data/lib/redi_search/client/response.rb +5 -1
  17. data/lib/redi_search/configuration.rb +1 -11
  18. data/lib/redi_search/create.rb +7 -4
  19. data/lib/redi_search/document.rb +5 -13
  20. data/lib/redi_search/document/display.rb +9 -9
  21. data/lib/redi_search/document/finder.rb +12 -42
  22. data/lib/redi_search/hset.rb +28 -0
  23. data/lib/redi_search/index.rb +24 -22
  24. data/lib/redi_search/lazily_load.rb +6 -11
  25. data/lib/redi_search/log_subscriber.rb +25 -53
  26. data/lib/redi_search/model.rb +37 -39
  27. data/lib/redi_search/schema.rb +3 -3
  28. data/lib/redi_search/schema/field.rb +2 -3
  29. data/lib/redi_search/schema/tag_field.rb +1 -1
  30. data/lib/redi_search/schema/text_field.rb +1 -1
  31. data/lib/redi_search/search.rb +15 -25
  32. data/lib/redi_search/search/clauses.rb +6 -7
  33. data/lib/redi_search/search/clauses/application_clause.rb +20 -5
  34. data/lib/redi_search/search/clauses/boolean.rb +9 -9
  35. data/lib/redi_search/search/clauses/highlight.rb +18 -2
  36. data/lib/redi_search/search/clauses/limit.rb +7 -5
  37. data/lib/redi_search/search/clauses/or.rb +1 -1
  38. data/lib/redi_search/search/clauses/return.rb +1 -1
  39. data/lib/redi_search/search/clauses/slop.rb +1 -1
  40. data/lib/redi_search/search/clauses/sort_by.rb +1 -1
  41. data/lib/redi_search/search/clauses/where.rb +10 -2
  42. data/lib/redi_search/search/result.rb +10 -10
  43. data/lib/redi_search/search/term.rb +7 -6
  44. data/lib/redi_search/spellcheck.rb +3 -4
  45. data/lib/redi_search/spellcheck/result.rb +1 -1
  46. data/lib/redi_search/validatable.rb +49 -0
  47. data/lib/redi_search/validations/inclusion.rb +26 -0
  48. data/lib/redi_search/validations/numericality.rb +45 -0
  49. data/lib/redi_search/validations/presence.rb +29 -0
  50. data/lib/redi_search/version.rb +1 -1
  51. data/redi_search.gemspec +1 -3
  52. metadata +20 -50
  53. data/.travis.yml +0 -31
  54. data/bin/test +0 -7
  55. data/lib/redi_search/add.rb +0 -63
@@ -2,16 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
+ gem "appraisal", "~> 2.2"
5
6
  gem "faker"
6
- gem "minitest", "~> 5.0"
7
7
  gem "mocha"
8
8
  gem "pry"
9
- gem "pry-rails"
10
9
  gem "rubocop"
10
+ gem "rubocop-minitest"
11
11
  gem "rubocop-performance"
12
- gem "rubocop-rails"
13
12
  gem "simplecov"
14
13
  gem "sqlite3"
15
- gem "rails", "6.0.0.rc1"
14
+ gem "activerecord", "6.1.0.rc1"
16
15
 
17
16
  gemspec path: "../"
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+ require "forwardable"
3
5
  require "redis"
4
- require "active_support"
5
- require "active_model"
6
- require "active_support/core_ext/object/blank"
6
+ require "active_support/lazy_load_hooks"
7
7
 
8
8
  require "redi_search/configuration"
9
+ require "redi_search/client"
9
10
 
10
11
  require "redi_search/model"
11
12
  require "redi_search/index"
@@ -14,8 +15,6 @@ require "redi_search/document"
14
15
 
15
16
  module RediSearch
16
17
  class << self
17
- extend Forwardable
18
-
19
18
  attr_writer :configuration
20
19
 
21
20
  def configuration
@@ -30,10 +29,12 @@ module RediSearch
30
29
  yield(configuration)
31
30
  end
32
31
 
33
- def_delegator :configuration, :client
32
+ def client
33
+ @client ||= Client.new(Redis.new(configuration.redis_config.to_h))
34
+ end
34
35
 
35
36
  def env
36
- @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
37
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
37
38
  end
38
39
  end
39
40
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RediSearch
4
- class Alter
4
+ class AddField
5
5
  def initialize(index, field_name, schema)
6
6
  @index = index
7
7
  @field_name = field_name
@@ -9,17 +9,25 @@ module RediSearch
9
9
  end
10
10
 
11
11
  def call!
12
- index.schema.alter(field_name, raw_schema)
12
+ index.schema.add_field(field_name, raw_schema)
13
13
 
14
- RediSearch.client.call!(
15
- "ALTER", index.name, "SCHEMA", "ADD", *field_schema
16
- ).ok?
14
+ RediSearch.client.call!(*command).ok?
15
+ end
16
+
17
+ def call
18
+ call!
19
+ rescue Redis::CommandError
20
+ false
17
21
  end
18
22
 
19
23
  private
20
24
 
21
25
  attr_reader :index, :field_name, :raw_schema
22
26
 
27
+ def command
28
+ ["ALTER", index.name, "SCHEMA", "ADD", *field_schema]
29
+ end
30
+
23
31
  def field_schema
24
32
  @field_schema ||= Schema.make_field(field_name, raw_schema)
25
33
  end
@@ -1,41 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "redis"
4
+ require "active_support/notifications"
5
+
4
6
  require "redi_search/client/response"
5
7
 
6
8
  module RediSearch
7
9
  class Client
8
- def initialize(redis_config)
9
- @redis = Redis.new(redis_config)
10
+ def initialize(redis = Redis.new)
11
+ @redis = redis
12
+ @pipeline = false
10
13
  end
11
14
 
12
- def call!(command, *params)
15
+ def call!(command, *params, skip_ft: false)
13
16
  instrument(command.downcase, query: [command, params]) do
17
+ command = "FT.#{command}" unless skip_ft
14
18
  send_command(command, *params)
15
19
  end
16
20
  end
17
21
 
18
- def pipelined
19
- Response.new(redis.pipelined do
22
+ def multi
23
+ Response.new(redis.multi do
20
24
  instrument("pipeline", query: ["begin pipeline"])
21
- yield
25
+ capture_pipeline { yield }
22
26
  instrument("pipeline", query: ["finish pipeline"])
23
27
  end)
24
28
  end
25
29
 
26
30
  private
27
31
 
28
- attr_reader :redis
32
+ attr_reader :redis
33
+ attr_accessor :pipeline
34
+
35
+ def capture_pipeline
36
+ self.pipeline = true
37
+ yield
38
+ self.pipeline = false
39
+ end
29
40
 
30
41
  def send_command(command, *params)
31
- Response.new(redis.call("FT.#{command}", *params))
42
+ Response.new(redis.call(command, *params))
32
43
  end
33
44
 
34
45
  def instrument(action, payload, &block)
35
46
  ActiveSupport::Notifications.instrument(
36
- "#{action}.redi_search",
37
- { name: "RediSearch" }.merge(payload),
38
- &Proc.new(&(block || proc {}))
47
+ "action.redi_search",
48
+ { name: "RediSearch", action: action, inside_pipeline: pipeline }.
49
+ merge(payload),
50
+ &Proc.new(&(block || proc {})) # rubocop:disable Lint/EmptyBlock
39
51
  )
40
52
  end
41
53
  end
@@ -6,12 +6,16 @@ module RediSearch
6
6
  def ok?
7
7
  case response
8
8
  when String then response == "OK"
9
- when Integer then response == 1
9
+ when Integer then response >= 1
10
10
  when Array then array_ok?
11
11
  else response
12
12
  end
13
13
  end
14
14
 
15
+ def nil?
16
+ response.nil?
17
+ end
18
+
15
19
  private
16
20
 
17
21
  def array_ok?
@@ -1,17 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/client"
4
-
5
3
  module RediSearch
6
4
  class Configuration
7
- attr_writer :redis_config
8
-
9
- def client
10
- @client ||= Client.new(redis_config)
11
- end
12
-
13
- def redis_config
14
- @redis_config ||= { host: "127.0.0.1", port: "6379" }
15
- end
5
+ attr_accessor :redis_config
16
6
  end
17
7
  end
@@ -17,9 +17,7 @@ module RediSearch
17
17
  end
18
18
 
19
19
  def call!
20
- RediSearch.client.call!(
21
- "CREATE", index.name, *extract_options.compact, "SCHEMA", schema.to_a
22
- ).ok?
20
+ RediSearch.client.call!(*command).ok?
23
21
  end
24
22
 
25
23
  def call
@@ -32,12 +30,17 @@ module RediSearch
32
30
 
33
31
  attr_reader :index, :schema, :options
34
32
 
33
+ def command
34
+ ["CREATE", index.name, "ON", "HASH", "PREFIX", 1, index.name,
35
+ *extract_options.compact, "SCHEMA", schema.to_a]
36
+ end
37
+
35
38
  def extract_options
36
39
  options.map do |clause, switch|
37
40
  next unless OPTION_MAPPER.key?(clause.to_sym) && switch
38
41
 
39
42
  OPTION_MAPPER[clause.to_sym]
40
- end << temporary_option
43
+ end + temporary_option
41
44
  end
42
45
 
43
46
  def temporary_option
@@ -12,7 +12,7 @@ module RediSearch
12
12
  object_to_serialize = serializer&.new(record) || record
13
13
 
14
14
  field_values = index.schema.fields.map do |field|
15
- next if only.present? && !only.include?(field.to_sym)
15
+ next unless only.empty? || only.include?(field.to_sym)
16
16
 
17
17
  [field.to_s, object_to_serialize.public_send(field)]
18
18
  end.compact.to_h
@@ -23,10 +23,6 @@ module RediSearch
23
23
  def get(index, document_id)
24
24
  Finder.new(index, document_id).find
25
25
  end
26
-
27
- def mget(index, *document_ids)
28
- Finder.new(index, *document_ids).find
29
- end
30
26
  end
31
27
 
32
28
  attr_reader :attributes, :score
@@ -40,8 +36,8 @@ module RediSearch
40
36
  load_attributes
41
37
  end
42
38
 
43
- def del(delete_document: false)
44
- call!("DEL", index.name, document_id, ("DD" if delete_document)).ok?
39
+ def del
40
+ RediSearch.client.call!("DEL", document_id, skip_ft: true).ok?
45
41
  end
46
42
 
47
43
  def schema_fields
@@ -61,7 +57,7 @@ module RediSearch
61
57
  end
62
58
 
63
59
  def document_id_without_index
64
- if @document_id.to_s.starts_with? index.name
60
+ if @document_id.to_s.start_with? index.name
65
61
  @document_id.gsub(index.name, "")
66
62
  else
67
63
  @document_id
@@ -72,13 +68,9 @@ module RediSearch
72
68
 
73
69
  attr_reader :index
74
70
 
75
- def call!(*command)
76
- RediSearch.client.call!(*command)
77
- end
78
-
79
71
  def load_attributes
80
72
  attributes.each do |field, value|
81
- next unless schema_fields.include? field
73
+ next unless schema_fields.include? field.to_s
82
74
 
83
75
  instance_variable_set(:"@#{field}", value)
84
76
  define_singleton_method(field) { value }
@@ -3,7 +3,6 @@
3
3
  module RediSearch
4
4
  class Document
5
5
  module Display
6
- #:nocov:
7
6
  def inspect
8
7
  inspection = pretty_print_attributes.map do |field_name|
9
8
  "#{field_name}: #{public_send(field_name)}"
@@ -12,6 +11,15 @@ module RediSearch
12
11
  "#<#{self.class} #{inspection}>"
13
12
  end
14
13
 
14
+ def pretty_print_attributes
15
+ pp_attrs = attributes.keys.dup
16
+ pp_attrs.push("document_id")
17
+ pp_attrs.push("score") if score
18
+
19
+ pp_attrs.compact
20
+ end
21
+
22
+ #:nocov:
15
23
  def pretty_print(printer) # rubocop:disable Metrics/MethodLength
16
24
  printer.object_address_group(self) do
17
25
  printer.seplist(
@@ -27,14 +35,6 @@ module RediSearch
27
35
  end
28
36
  end
29
37
  end
30
-
31
- def pretty_print_attributes
32
- pp_attrs = attributes.keys.dup
33
- pp_attrs.push("document_id")
34
- pp_attrs.push("score") if score.present?
35
-
36
- pp_attrs.compact
37
- end
38
38
  #:nocov:
39
39
  end
40
40
  end
@@ -3,67 +3,37 @@
3
3
  module RediSearch
4
4
  class Document
5
5
  class Finder
6
- def initialize(index, *document_ids)
6
+ def initialize(index, document_id)
7
7
  @index = index
8
- @document_ids = Array.wrap(document_ids)
8
+ @document_id = document_id
9
9
  end
10
10
 
11
11
  def find
12
- if multi?
13
- parse_multi_documents
14
- else
15
- parse_document(document_ids.first, response)
16
- end
12
+ Document.new(index, document_id, Hash[*response]) if response?
17
13
  end
18
14
 
19
15
  private
20
16
 
21
- attr_reader :index, :document_ids
17
+ attr_reader :index, :document_id
22
18
 
23
19
  def response
24
- @response ||= call!(get_command, index.name, *prepended_document_ids)
20
+ @response ||= call!("HGETALL", prepended_document_id)
25
21
  end
26
22
 
27
23
  def call!(*command)
28
- RediSearch.client.call!(*command)
29
- end
30
-
31
- def get_command
32
- if multi?
33
- "MGET"
34
- else
35
- "GET"
36
- end
37
- end
38
-
39
- def multi?
40
- document_ids.size > 1
41
- end
42
-
43
- def prepended_document_ids
44
- document_ids.map do |document_id|
45
- prepend_document_id(document_id)
46
- end
24
+ RediSearch.client.call!(*command, skip_ft: true)
47
25
  end
48
26
 
49
- def prepend_document_id(id)
50
- if id.to_s.start_with? index.name
51
- id
27
+ def prepended_document_id
28
+ if document_id.to_s.start_with? index.name
29
+ document_id
52
30
  else
53
- "#{index.name}#{id}"
31
+ "#{index.name}#{document_id}"
54
32
  end
55
33
  end
56
34
 
57
- def parse_multi_documents
58
- document_ids.map.with_index do |document_id, index|
59
- parse_document(document_id, response[index])
60
- end.compact
61
- end
62
-
63
- def parse_document(document_id, document_response)
64
- return if document_response.blank?
65
-
66
- Document.new(index, document_id, Hash[*document_response])
35
+ def response?
36
+ !response.to_a.empty?
67
37
  end
68
38
  end
69
39
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Hset
5
+ def initialize(index, document)
6
+ @index = index
7
+ @document = document
8
+ end
9
+
10
+ def call!
11
+ RediSearch.client.call!(*command, skip_ft: true)
12
+ end
13
+
14
+ def call
15
+ call!
16
+ rescue Redis::CommandError
17
+ false
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :index, :document
23
+
24
+ def command
25
+ ["HSET", document.document_id, document.redis_attributes].compact
26
+ end
27
+ end
28
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redi_search/add"
3
+ require "redi_search/hset"
4
4
  require "redi_search/create"
5
5
  require "redi_search/schema"
6
6
  require "redi_search/search"
7
7
  require "redi_search/spellcheck"
8
- require "redi_search/alter"
8
+ require "redi_search/add_field"
9
9
 
10
10
  module RediSearch
11
11
  class Index
12
12
  attr_reader :name, :schema, :model
13
13
 
14
14
  def initialize(name, schema, model = nil)
15
- @name = name
15
+ @name = name.to_s
16
16
  @schema = Schema.new(schema)
17
17
  @model = model
18
18
  end
@@ -33,34 +33,36 @@ module RediSearch
33
33
  Create.new(self, schema, options).call!
34
34
  end
35
35
 
36
- def drop
37
- drop!
36
+ def drop(keep_docs: false)
37
+ drop!(keep_docs: keep_docs)
38
38
  rescue Redis::CommandError
39
39
  false
40
40
  end
41
41
 
42
- def drop!
43
- client.call!("DROP", name).ok?
42
+ def drop!(keep_docs: false)
43
+ command = ["DROPINDEX", name]
44
+ command << "DD" unless keep_docs
45
+ client.call!(*command.compact).ok?
44
46
  end
45
47
 
46
- def add(document, **options)
47
- Add.new(self, document, **options).call
48
+ def add(document)
49
+ Hset.new(self, document).call
48
50
  end
49
51
 
50
- def add!(document, **options)
51
- Add.new(self, document, **options).call!
52
+ def add!(document)
53
+ Hset.new(self, document).call!
52
54
  end
53
55
 
54
- def add_multiple!(documents, **options)
55
- client.pipelined do
56
+ def add_multiple(documents)
57
+ client.multi do
56
58
  documents.each do |document|
57
- add!(document, **options)
59
+ add(document)
58
60
  end
59
- end.ok?
61
+ end.all? { |response| response >= 0 }
60
62
  end
61
63
 
62
- def del(document, delete_document: false)
63
- document.del(delete_document: delete_document)
64
+ def del(document)
65
+ document.del
64
66
  end
65
67
 
66
68
  def exist?
@@ -81,19 +83,19 @@ module RediSearch
81
83
  schema.fields.map(&:to_s)
82
84
  end
83
85
 
84
- def reindex(documents, recreate: false, **options)
86
+ def reindex(documents, recreate: false)
85
87
  drop if recreate
86
88
  create unless exist?
87
89
 
88
- add_multiple! documents, **options
90
+ add_multiple documents
89
91
  end
90
92
 
91
93
  def document_count
92
- info["num_docs"].to_i
94
+ info.num_docs.to_i
93
95
  end
94
96
 
95
- def alter(field_name, schema)
96
- Alter.new(self, field_name, schema).call!
97
+ def add_field(field_name, schema)
98
+ AddField.new(self, field_name, schema).call!
97
99
  end
98
100
 
99
101
  private