redi_search 1.0.4 → 3.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/.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