solrb 0.2.3 → 0.2.8

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 (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