solrb 0.2.3 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/reviewdog.yml +20 -0
  3. data/.github/workflows/tests.yaml +37 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -6
  6. data/README.md +47 -0
  7. data/lib/solr.rb +21 -1
  8. data/lib/solr/cloud/configuration.rb +2 -1
  9. data/lib/solr/cloud/helper_methods.rb +1 -1
  10. data/lib/solr/commands.rb +3 -2
  11. data/lib/solr/commit/request.rb +15 -5
  12. data/lib/solr/configuration.rb +27 -5
  13. data/lib/solr/connection.rb +6 -4
  14. data/lib/solr/data_import/request.rb +7 -1
  15. data/lib/solr/delete/request.rb +7 -1
  16. data/lib/solr/errors/solr_connection_failed_error.rb +3 -2
  17. data/lib/solr/helper_methods.rb +11 -0
  18. data/lib/solr/indexing/request.rb +7 -1
  19. data/lib/solr/master_slave/configuration.rb +38 -0
  20. data/lib/solr/master_slave/helper_methods.rb +25 -0
  21. data/lib/solr/master_slave/nodes_gray_list/disabled.rb +21 -0
  22. data/lib/solr/master_slave/nodes_gray_list/in_memory.rb +47 -0
  23. data/lib/solr/query/handler.rb +8 -7
  24. data/lib/solr/query/http_request_builder.rb +6 -12
  25. data/lib/solr/query/request.rb +19 -2
  26. data/lib/solr/query/request/boosting/dictionary_boost_function.rb +4 -3
  27. data/lib/solr/query/request/facet.rb +5 -2
  28. data/lib/solr/request/cloud/first_shard_leader_node_selection_strategy.rb +27 -0
  29. data/lib/solr/request/cloud/leader_node_selection_strategy.rb +21 -0
  30. data/lib/solr/request/default_node_selection_strategy.rb +2 -1
  31. data/lib/solr/request/master_slave/master_node_selection_strategy.rb +12 -0
  32. data/lib/solr/request/node_selection_strategy.rb +6 -0
  33. data/lib/solr/request/runner.rb +18 -11
  34. data/lib/solr/response/parser.rb +1 -1
  35. data/lib/solr/support/url_helper.rb +8 -3
  36. data/lib/solr/version.rb +1 -1
  37. data/solrb.gemspec +2 -3
  38. metadata +25 -35
  39. data/.circleci/config.yml +0 -69
  40. data/.circleci/run-with-local-config.sh +0 -7
  41. data/Gemfile.lock +0 -83
  42. data/lib/solr/request/first_shard_leader_node_selection_strategy.rb +0 -24
  43. data/lib/solr/request/leader_node_selection_strategy.rb +0 -18
@@ -18,7 +18,13 @@ module Solr
18
18
  private
19
19
 
20
20
  def default_runner_options
21
- { node_selection_strategy: Solr::Request::LeaderNodeSelectionStrategy }
21
+ if Solr.cloud_enabled?
22
+ { node_selection_strategy: Solr::Request::Cloud::LeaderNodeSelectionStrategy }
23
+ elsif Solr.master_slave_enabled?
24
+ { node_selection_strategy: Solr::Request::MasterSlave::MasterNodeSelectionStrategy }
25
+ else
26
+ {}
27
+ end
22
28
  end
23
29
 
24
30
  def build_http_request(commit)
@@ -0,0 +1,38 @@
1
+ require 'solr/master_slave/nodes_gray_list/disabled'
2
+ require 'solr/master_slave/nodes_gray_list/in_memory'
3
+
4
+ module Solr
5
+ module MasterSlave
6
+ class Configuration
7
+ attr_accessor :master_url, :slave_url, :disable_read_from_master
8
+
9
+ attr_reader :master_slave_enabled
10
+ attr_writer :nodes_gray_list
11
+
12
+ def enable_master_slave!(_)
13
+ @master_slave_enabled = true
14
+ end
15
+
16
+ def master_slave_enabled?
17
+ @master_slave_enabled
18
+ end
19
+
20
+ def active_nodes_for(collection:)
21
+ urls = []
22
+ urls.push(master_url) unless disable_read_from_master
23
+ urls.push(*slave_url) if slave_url
24
+ nodes_gray_list.select_active(urls, collection_name: collection)
25
+ end
26
+
27
+ def nodes_gray_list
28
+ @nodes_gray_list || gray_list_disabled_instance
29
+ end
30
+
31
+ private
32
+
33
+ def gray_list_disabled_instance
34
+ @gray_list_disabled_instance ||= Solr::MasterSlave::NodesGrayList::Disabled.new
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ require 'solr/master_slave/configuration'
2
+
3
+ module Solr
4
+ module MasterSlave
5
+ module HelperMethods
6
+ def master_slave_active_nodes_for(collection:)
7
+ master_slave_configuration.active_nodes_for(collection: collection)
8
+ end
9
+
10
+ def master_slave_enabled?
11
+ master_slave_configuration.master_slave_enabled?
12
+ end
13
+
14
+ def enable_master_slave!
15
+ master_slave_configuration.enable_master_slave!(configuration.cores.keys)
16
+ end
17
+
18
+ private
19
+
20
+ def master_slave_configuration
21
+ configuration.master_slave_configuration
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Solr
2
+ module MasterSlave
3
+ module NodesGrayList
4
+ class Disabled
5
+ def add(_)
6
+ end
7
+
8
+ def remove(_)
9
+ end
10
+
11
+ def added?(_)
12
+ true
13
+ end
14
+
15
+ def select_active(urls, collection_name:)
16
+ urls
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ module Solr
2
+ module MasterSlave
3
+ module NodesGrayList
4
+ class InMemory
5
+ attr_reader :gray_list, :removal_period
6
+
7
+ DEFAULT_REMOVAL_PERIOD = 5 * 60 # 5 minutes in seconds
8
+
9
+ def initialize(removal_period: DEFAULT_REMOVAL_PERIOD)
10
+ @gray_list = {}
11
+ @removal_period = removal_period
12
+ end
13
+
14
+ def add(url)
15
+ return if gray_list.has_key?(url)
16
+ ::Solr.configuration.logger.info("#{url} added to a gray list")
17
+ gray_list[url] = Time.now.utc
18
+ end
19
+
20
+ def remove(url)
21
+ gray_list.delete(url)
22
+ end
23
+
24
+ def added?(url)
25
+ added_at = gray_list[url]
26
+ return false unless added_at
27
+
28
+ if added_at + removal_period > Time.now.utc
29
+ true
30
+ else
31
+ remove(url)
32
+ false
33
+ end
34
+ end
35
+
36
+ def select_active(urls, collection_name:)
37
+ urls = Array(urls)
38
+ active_urls = urls.reject do |url|
39
+ collection_url = File.join(url, collection_name.to_s)
40
+ added?(collection_url)
41
+ end
42
+ active_urls.any? ? active_urls : urls
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -5,21 +5,22 @@ require 'solr/query/http_request_builder'
5
5
  module Solr
6
6
  module Query
7
7
  class Handler
8
- attr_reader :query, :page, :page_size
8
+ attr_reader :query, :rows, :start, :runner_options
9
9
 
10
10
  def self.call(opts)
11
- new(opts).call
11
+ new(**opts).call
12
12
  end
13
13
 
14
- def initialize(query:, page:, page_size:)
14
+ def initialize(query:, rows:, start:, runner_options: {})
15
15
  @query = query
16
- @page = page
17
- @page_size = page_size
16
+ @rows = rows
17
+ @start = start
18
+ @runner_options = runner_options || {}
18
19
  end
19
20
 
20
21
  def call
21
- http_request = Solr::Query::HttpRequestBuilder.call(query: query, page: page, page_size: page_size)
22
- solr_response = Solr::Request::Runner.call(request: http_request)
22
+ http_request = Solr::Query::HttpRequestBuilder.call(query: query, start: start, rows: rows)
23
+ solr_response = Solr::Request::Runner.call(request: http_request, **runner_options)
23
24
  Solr::Query::Response::Parser.new(request: query, solr_response: solr_response.body).to_response
24
25
  end
25
26
  end
@@ -5,16 +5,16 @@ module Solr
5
5
  class HttpRequestBuilder
6
6
  PATH = '/select'.freeze
7
7
 
8
- attr_reader :query, :page, :page_size
8
+ attr_reader :query, :start, :rows
9
9
 
10
10
  def self.call(opts)
11
- new(opts).call
11
+ new(**opts).call
12
12
  end
13
13
 
14
- def initialize(query:, page:, page_size:)
14
+ def initialize(query:, start:, rows:)
15
15
  @query = query
16
- @page = page
17
- @page_size = page_size
16
+ @rows = rows
17
+ @start = start
18
18
  end
19
19
 
20
20
  def call
@@ -27,13 +27,7 @@ module Solr
27
27
 
28
28
  # 🏋️
29
29
  def build_body
30
- @request_params ||= { params: solr_params.merge(wt: :json, rows: page_size.to_i, start: start) }
31
- end
32
-
33
- def start
34
- start_page = page.to_i - 1
35
- start_page = start_page < 1 ? 0 : start_page
36
- start_page * page_size
30
+ @request_params ||= { params: solr_params.merge(wt: :json, rows: rows, start: start) }
37
31
  end
38
32
 
39
33
  def solr_params
@@ -30,8 +30,11 @@ module Solr
30
30
  @filters = filters
31
31
  end
32
32
 
33
- def run(page: 1, page_size: 10)
34
- Solr::Query::Handler.call(query: self, page: page, page_size: page_size)
33
+ def run(page: nil, start: nil, rows: nil, page_size: nil, runner_options: {})
34
+ rows ||= page_size
35
+ return run_paged(page: page, page_size: rows, runner_options: runner_options) if page && rows
36
+ return run_start(start: start, rows: rows, runner_options: runner_options) if start && rows
37
+ raise ArgumentError, 'You must specify either page/rows or start/rows arguments'
35
38
  end
36
39
 
37
40
  def grouping
@@ -45,6 +48,20 @@ module Solr
45
48
  def to_h
46
49
  Solr::Query::Request::EdismaxAdapter.new(self).to_h
47
50
  end
51
+
52
+ private
53
+
54
+ def run_paged(page: 1, page_size: 10, runner_options:)
55
+ start_page = page.to_i - 1
56
+ start_page = start_page < 1 ? 0 : start_page
57
+ start = start_page * page_size
58
+
59
+ Solr::Query::Handler.call(query: self, start: start, rows: page_size, runner_options: runner_options)
60
+ end
61
+
62
+ def run_start(rows: 10, start: 0, runner_options:)
63
+ Solr::Query::Handler.call(query: self, start: start, rows: rows, runner_options: runner_options)
64
+ end
48
65
  end
49
66
  end
50
67
  end
@@ -5,12 +5,13 @@ module Solr
5
5
  class DictionaryBoostFunction
6
6
  include Solr::Support::SchemaHelper
7
7
  using Solr::Support::StringExtensions
8
- attr_reader :field, :dictionary
8
+ attr_reader :field, :dictionary, :default_boost
9
9
 
10
- def initialize(field:, dictionary:)
10
+ def initialize(field:, dictionary:, default_boost: 1)
11
11
  raise 'dictionary must be a non-empty Hash' if Hash(dictionary).empty?
12
12
  @field = field
13
13
  @dictionary = dictionary
14
+ @default_boost = default_boost
14
15
  end
15
16
 
16
17
  # example: given a hash (dictionary)
@@ -21,7 +22,7 @@ module Solr
21
22
  # note that I added spaces for readability, real Solr query functions must always be w/out spaces
22
23
  def to_solr_s
23
24
  sf = solarize_field(field)
24
- dictionary.to_a.reverse.reduce(1) do |acc, (field_value, boost)|
25
+ dictionary.to_a.reverse.reduce(default_boost) do |acc, (field_value, boost)|
25
26
  if field_value.is_a?(String) || field_value.is_a?(Symbol)
26
27
  "if(termfreq(#{sf},\"#{field_value.to_s.solr_escape}\"),#{boost},#{acc})"
27
28
  else
@@ -17,7 +17,8 @@ module Solr
17
17
  :subfacets,
18
18
  :gap,
19
19
  :lower_bound,
20
- :upper_bound
20
+ :upper_bound,
21
+ :domain
21
22
 
22
23
  def initialize(field:, type:, name: nil, value: nil, filters: [], subfacets: [], options: {})
23
24
  if options[:limit].nil? && type == TERMS_TYPE
@@ -38,6 +39,7 @@ module Solr
38
39
  @gap = options[:gap]
39
40
  @lower_bound = options[:lower_bound]
40
41
  @upper_bound = options[:upper_bound]
42
+ @domain = options[:domain]
41
43
  end
42
44
 
43
45
  def to_solr_h
@@ -58,7 +60,8 @@ module Solr
58
60
  facet: subfacets.map(&:to_solr_h).reduce(&:merge),
59
61
  gap: gap,
60
62
  start: lower_bound,
61
- end: upper_bound
63
+ end: upper_bound,
64
+ domain: domain
62
65
  }.compact
63
66
  }
64
67
  end
@@ -0,0 +1,27 @@
1
+ module Solr
2
+ module Request
3
+ module Cloud
4
+ class FirstShardLeaderNodeSelectionStrategy < NodeSelectionStrategy
5
+ def call
6
+ leader = first_shard_leader_replica_node_for(collection: collection_name)
7
+ replicas = solr_cloud_active_nodes_urls.shuffle
8
+ urls = ([leader] + replicas).flatten.uniq
9
+ map_urls_to_collections(urls)
10
+ end
11
+
12
+ private
13
+
14
+ def first_shard_leader_replica_node_for(collection:)
15
+ shards = Solr.shards_for(collection: collection)
16
+ return unless shards
17
+ first_shard_name = shards.sort.first
18
+ Solr.leader_replica_node_for(collection: collection, shard: first_shard_name)
19
+ end
20
+
21
+ def solr_cloud_active_nodes_urls
22
+ Solr.active_nodes_for(collection: collection_name)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module Solr
2
+ module Request
3
+ module Cloud
4
+ class LeaderNodeSelectionStrategy < NodeSelectionStrategy
5
+ def call
6
+ urls = [leader_replica_node_for(collection: collection_name)]
7
+ map_urls_to_collections(urls)
8
+ end
9
+
10
+ private
11
+
12
+ def leader_replica_node_for(collection:)
13
+ shards = Solr.shards_for(collection: collection)
14
+ return unless shards
15
+ first_shard_name = shards.sort.first
16
+ Solr.leader_replica_node_for(collection: collection, shard: first_shard_name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,7 +2,8 @@ module Solr
2
2
  module Request
3
3
  class DefaultNodeSelectionStrategy < NodeSelectionStrategy
4
4
  def call
5
- Solr.active_nodes_for(collection: collection_name).shuffle
5
+ urls = Solr.active_nodes_for(collection: collection_name)
6
+ map_urls_to_collections(urls).shuffle
6
7
  end
7
8
  end
8
9
  end
@@ -0,0 +1,12 @@
1
+ module Solr
2
+ module Request
3
+ module MasterSlave
4
+ class MasterNodeSelectionStrategy < NodeSelectionStrategy
5
+ def call
6
+ urls = [Solr.configuration.master_url]
7
+ map_urls_to_collections(urls)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -14,6 +14,12 @@ module Solr
14
14
  def call
15
15
  raise "Not implemented"
16
16
  end
17
+
18
+ private
19
+
20
+ def map_urls_to_collections(urls)
21
+ urls&.map { |u| File.join(u, collection_name.to_s) }
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -1,7 +1,8 @@
1
1
  require 'solr/request/node_selection_strategy'
2
2
  require 'solr/request/default_node_selection_strategy'
3
- require 'solr/request/first_shard_leader_node_selection_strategy'
4
- require 'solr/request/leader_node_selection_strategy'
3
+ require 'solr/request/cloud/first_shard_leader_node_selection_strategy'
4
+ require 'solr/request/cloud/leader_node_selection_strategy'
5
+ require 'solr/request/master_slave/master_node_selection_strategy'
5
6
  require 'solr/errors/solr_query_error'
6
7
  require 'solr/errors/solr_connection_failed_error'
7
8
  require 'solr/errors/no_active_solr_nodes_error'
@@ -15,7 +16,7 @@ module Solr
15
16
  attr_reader :request, :response_parser, :node_selection_strategy
16
17
 
17
18
  def self.call(opts)
18
- new(opts).call
19
+ new(**opts).call
19
20
  end
20
21
 
21
22
  def initialize(request:,
@@ -28,6 +29,7 @@ module Solr
28
29
  end
29
30
 
30
31
  def call
32
+ solr_url_errors = {}
31
33
  solr_urls.each do |node_url|
32
34
  request_url = build_request_url(url: node_url,
33
35
  path: request.path,
@@ -37,19 +39,29 @@ module Solr
37
39
  solr_response = Solr::Response::Parser.call(raw_response)
38
40
  raise Solr::Errors::SolrQueryError, solr_response.error_message unless solr_response.ok?
39
41
  return solr_response
40
- rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::EADDRNOTAVAIL => e
42
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::EADDRNOTAVAIL, Timeout::Error => e
43
+ if Solr.master_slave_enabled?
44
+ Solr.configuration.nodes_gray_list.add(node_url)
45
+ end
46
+ solr_url_errors[node_url] = "#{e.class.name} - #{e.message}"
41
47
  # Try next node
42
48
  end
43
49
  end
44
50
 
45
- raise Solr::Errors::SolrConnectionFailedError.new(solr_urls)
51
+ raise Solr::Errors::SolrConnectionFailedError.new(solr_url_errors)
46
52
  end
47
53
 
48
54
  private
49
55
 
50
56
  def solr_urls
51
57
  @solr_urls ||= begin
52
- urls = Solr.cloud_enabled? ? solr_cloud_collection_urls : [Solr.current_core_config.url]
58
+ urls = if Solr.node_url_override
59
+ [File.join(Solr.node_url_override, collection_name.to_s)]
60
+ elsif Solr.cloud_enabled? || Solr.master_slave_enabled?
61
+ node_selection_strategy.call(collection_name)
62
+ else
63
+ [Solr.current_core_config.url]
64
+ end
53
65
  unless urls && urls.any?
54
66
  raise Solr::Errors::NoActiveSolrNodesError
55
67
  end
@@ -57,11 +69,6 @@ module Solr
57
69
  end
58
70
  end
59
71
 
60
- def solr_cloud_collection_urls
61
- urls = node_selection_strategy.call(collection_name)
62
- urls&.map { |u| File.join(u, collection_name.to_s) }
63
- end
64
-
65
72
  def collection_name
66
73
  Solr.current_core_config.name
67
74
  end