solrb 0.1.8 → 0.1.9

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile.lock +3 -3
  4. data/README.md +56 -3
  5. data/lib/solr/cloud/collections_state_manager.rb +48 -0
  6. data/lib/solr/cloud/configuration.rb +27 -0
  7. data/lib/solr/cloud/helper_methods.rb +37 -0
  8. data/lib/solr/cloud/zookeeper_connection.rb +64 -0
  9. data/lib/solr/commands.rb +24 -0
  10. data/lib/solr/commit/request.rb +2 -5
  11. data/lib/solr/configuration.rb +28 -3
  12. data/lib/solr/connection.rb +17 -10
  13. data/lib/solr/core_configuration/core_config_builder.rb +1 -1
  14. data/lib/solr/data_import/request.rb +27 -0
  15. data/lib/solr/delete/request.rb +6 -4
  16. data/lib/solr/errors/ambiguous_core_error.rb +7 -5
  17. data/lib/solr/errors/could_not_infer_implicit_core_name.rb +15 -0
  18. data/lib/solr/errors/no_active_solr_nodes_error.rb +6 -0
  19. data/lib/solr/errors/solr_connection_failed_error.rb +13 -0
  20. data/lib/solr/errors/solr_query_error.rb +4 -2
  21. data/lib/solr/errors/solr_url_not_defined_error.rb +24 -11
  22. data/lib/solr/errors/zookeeper_required.rb +19 -0
  23. data/lib/solr/indexing/request.rb +10 -6
  24. data/lib/solr/query/handler.rb +27 -0
  25. data/lib/solr/query/http_request_builder.rb +44 -0
  26. data/lib/solr/query/request/filter.rb +9 -7
  27. data/lib/solr/query/request.rb +6 -9
  28. data/lib/solr/request/default_node_selection_strategy.rb +19 -0
  29. data/lib/solr/request/first_shard_leader_node_selection_strategy.rb +32 -0
  30. data/lib/solr/request/http_request.rb +14 -0
  31. data/lib/solr/request/runner.rb +70 -0
  32. data/lib/solr/response/parser.rb +5 -1
  33. data/lib/solr/response.rb +0 -4
  34. data/lib/solr/support/url_helper.rb +23 -2
  35. data/lib/solr/testing.rb +5 -5
  36. data/lib/solr/version.rb +1 -1
  37. data/lib/solr.rb +17 -15
  38. metadata +19 -5
  39. data/.ruby-version +0 -1
  40. data/lib/solr/query/request/runner.rb +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f60f6a0571ce8fd909c4fd286f93ff5f57b6874597bc5980207eb97f6d43438f
4
- data.tar.gz: df632f68830e1ffb178fd5dfcc757e8f2a11a397fdc3f49c8da112d0e076543b
3
+ metadata.gz: 9ec22b8f3fb8d4af1362389b30173de437f3a66e0a56fd11189a66aa28c0a4f2
4
+ data.tar.gz: 36ae13f391bc00291bd4661c1ac910b6cdc2242f0c090a64d11c2385bca39274
5
5
  SHA512:
6
- metadata.gz: a363f5c19f5cbb71cdbd24bb1387f93c7c9b7003fb1dac1f1dd0d538052fe2f7f5de333ae4f8208d2533acbc53d92ee38e2d4ad975022ae6456f04c33b9cf01d
7
- data.tar.gz: b3fd3713b0f78420ee9491bbc2ea930b3aed58885af7623cf7475efe703d477f200e903282473088bb78014fa98d6ad897c7a028a7b422016fd264bd582c3694
6
+ metadata.gz: ef11a6e93954a9b5245861b4753f722aa6f936f91fe2523ac4c31b685cf300cb37ae07247b24e70515914053d2e96330501bcb213c07aeef15cc6168c5540c71
7
+ data.tar.gz: 743961e7ae4540f4e67362554470bc3485ddab0ddfc557d4b0d27fd99b51c91b64c333e2a24a9a7839b9d87d7b514142e4302a701ba73f4a51be20a213f12c8c
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ .ruby-version
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- solrb (0.1.3)
4
+ solrb (0.1.9)
5
5
  addressable
6
6
  faraday
7
7
 
@@ -15,7 +15,7 @@ GEM
15
15
  coderay (1.1.2)
16
16
  diff-lcs (1.3)
17
17
  docile (1.3.1)
18
- faraday (0.15.2)
18
+ faraday (0.15.3)
19
19
  multipart-post (>= 1.2, < 3)
20
20
  jaro_winkler (1.5.1)
21
21
  json (2.1.0)
@@ -80,4 +80,4 @@ DEPENDENCIES
80
80
  solrb!
81
81
 
82
82
  BUNDLED WITH
83
- 1.16.4
83
+ 1.16.6
data/README.md CHANGED
@@ -7,15 +7,15 @@ Solrb
7
7
 
8
8
  Object-Oriented approach to Solr in Ruby.
9
9
 
10
- Installation: `gem install solrb`
11
-
12
10
  ## Table of contents
13
11
 
14
-
12
+ * [Installation](#installation)
15
13
  * [Configuration](#configuration)
16
14
  * [Setting Solr URL via environment variable](#setting-solr-url-via-environment-variable)
17
15
  * [Single core configuration](#single-core-configuration)
18
16
  * [Multiple core configuration](#multiple-core-configuration)
17
+ * [Solr Cloud](#solr-cloud)
18
+ * [Basic Authentication](#basic-authentication)
19
19
  * [Indexing](#indexing)
20
20
  * [Querying](#querying)
21
21
  * [Simple Query](#simple-query)
@@ -34,6 +34,20 @@ Installation: `gem install solrb`
34
34
  * [Running specs](#running-specs)
35
35
 
36
36
 
37
+ # Installation
38
+
39
+ Add `solrb` to your Gemfile:
40
+
41
+ ```ruby
42
+ gem solrb
43
+ ```
44
+
45
+ If you are going to use solrb with solr cloud:
46
+
47
+ ```ruby
48
+ gem zk # required for solrb solr-cloud integration
49
+ gem solrb
50
+ ```
37
51
 
38
52
  # Configuration
39
53
 
@@ -122,6 +136,45 @@ end
122
136
  ...
123
137
  ```
124
138
 
139
+ ## Solr Cloud
140
+
141
+ To enable solr cloud mode you must define a zookeeper url on solr config block.
142
+ In solr cloud mode you don't need to provide a solr url (`config.url` or `ENV['SOLR_URL']`).
143
+ Solrb will watch the zookeeper state to receive up-to-date information about active solr nodes including the solr urls.
144
+
145
+
146
+ You can also specify the ACL credentials for Zookeeper. [More Information](https://lucene.apache.org/solr/guide/7_6/zookeeper-access-control.html#ZooKeeperAccessControl-AboutZooKeeperACLs)
147
+
148
+
149
+ ```ruby
150
+ Solr.configure do |config|
151
+ config.zookeeper_urls = ['localhost:2181', 'localhost:2182', 'localhost:2183']
152
+ config.zookeeper_auth_user = 'zk_acl_user'
153
+ config.zookeeper_auth_password = 'zk_acl_password'
154
+ end
155
+ ```
156
+
157
+ If you are using puma web server in clustered mode you must call `enable_solr_cloud!` on `on_worker_boot`
158
+ callback to make each puma worker connect with zookeeper.
159
+
160
+
161
+ ```ruby
162
+ on_worker_boot do
163
+ Solr.enable_solr_cloud!
164
+ end
165
+ ```
166
+
167
+ ## Basic Authentication
168
+
169
+ Basic authentication is supported by solrb. You can enable it by providing `auth_user` and `auth_password`
170
+ on the config block.
171
+
172
+ ```ruby
173
+ Solr.configure do |config|
174
+ config.auth_user = 'user'
175
+ config.auth_password = 'password'
176
+ end
177
+ ```
125
178
 
126
179
  # Indexing
127
180
 
@@ -0,0 +1,48 @@
1
+ module Solr
2
+ module Cloud
3
+ class CollectionsStateManager
4
+ attr_reader :zookeeper, :collections, :collections_state
5
+
6
+ def initialize(zookeeper:, collections:)
7
+ @zookeeper = zookeeper
8
+ @collections = collections
9
+ @collections_state = {}
10
+ watch_solr_collections_state
11
+ end
12
+
13
+ def shards_for(collection:)
14
+ collections_state.dig(collection.to_s, 'shards').keys
15
+ end
16
+
17
+ def active_nodes_for(collection:)
18
+ shards = collections_state.dig(collection.to_s, 'shards')
19
+ return unless shards
20
+ shards.flat_map do |_, shard|
21
+ shard['replicas'].select do |_, replica|
22
+ replica['state'] == 'active'
23
+ end.flat_map do |_, replica|
24
+ replica['base_url']
25
+ end
26
+ end.uniq
27
+ end
28
+
29
+ def leader_replica_node_for(collection:, shard:)
30
+ shards = collections_state.dig(collection.to_s, 'shards')
31
+ return unless shards
32
+ shard_replicas = shards[shard.to_s]
33
+ leader_replica = shard_replicas['replicas'].detect do |_, replica|
34
+ replica['state'] == 'active' && replica['leader'] == 'true'
35
+ end
36
+ leader_replica.last['base_url'] if leader_replica
37
+ end
38
+
39
+ def watch_solr_collections_state
40
+ collections.each do |collection_name|
41
+ zookeeper.watch_collection_state(collection_name) do |state|
42
+ @collections_state[collection_name.to_s] = state
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'solr/cloud/zookeeper_connection'
2
+ require 'solr/cloud/collections_state_manager'
3
+
4
+ module Solr
5
+ module Cloud
6
+ class Configuration
7
+ attr_accessor :zookeeper_url, :zookeeper_auth_user, :zookeeper_auth_password
8
+
9
+ attr_reader :collections_state_manager
10
+
11
+ def enable_solr_cloud!(collections)
12
+ @collections_state_manager = Solr::Cloud::CollectionsStateManager.new(zookeeper: build_zookeeper_connection,
13
+ collections: collections)
14
+ end
15
+
16
+ def cloud_enabled?
17
+ !@collections_state_manager.nil?
18
+ end
19
+
20
+ def build_zookeeper_connection
21
+ Solr::Cloud::ZookeeperConnection.new(zookeeper_url: zookeeper_url.is_a?(Array) ? zookeeper_url.join(',') : zookeeper_url,
22
+ zookeeper_auth_user: zookeeper_auth_user,
23
+ zookeeper_auth_password: zookeeper_auth_password)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ require 'solr/cloud/configuration'
2
+
3
+ module Solr
4
+ module Cloud
5
+ module HelperMethods
6
+ def active_nodes_for(collection:)
7
+ collections_state_manager.active_nodes_for(collection: collection)
8
+ end
9
+
10
+ def leader_replica_node_for(collection:, shard:)
11
+ collections_state_manager.leader_replica_node_for(collection: collection, shard: shard)
12
+ end
13
+
14
+ def shards_for(collection:)
15
+ collections_state_manager.shards_for(collection: collection)
16
+ end
17
+
18
+ def cloud_enabled?
19
+ cloud_configuration.cloud_enabled?
20
+ end
21
+
22
+ def enable_solr_cloud!
23
+ cloud_configuration.enable_solr_cloud!(configuration.cores.keys)
24
+ end
25
+
26
+ private
27
+
28
+ def collections_state_manager
29
+ cloud_configuration.collections_state_manager
30
+ end
31
+
32
+ def cloud_configuration
33
+ configuration.cloud_configuration
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ require 'solr/errors/zookeeper_required'
2
+
3
+ module Solr
4
+ module Cloud
5
+ class ZookeeperConnection
6
+ attr_reader :zookeeper_url, :zookeeper_auth_user, :zookeeper_auth_password
7
+
8
+ def initialize(zookeeper_url:, zookeeper_auth_user: nil, zookeeper_auth_password: nil)
9
+ @zookeeper_url = zookeeper_url
10
+ @zookeeper_auth_user = zookeeper_auth_user
11
+ @zookeeper_auth_password = zookeeper_auth_password
12
+ end
13
+
14
+ def watch_collection_state(collection_name, &block)
15
+ collection_state_znode = collection_state_znode_path(collection_name)
16
+ zookeeper_connection.register(collection_state_znode) do |event|
17
+ state = get_collection_state(collection_name, watch: true)
18
+ block.call(state)
19
+ end
20
+ state = get_collection_state(collection_name, watch: true)
21
+ block.call(state)
22
+ end
23
+
24
+ def get_collection_state(collection_name, watch: true)
25
+ collection_state_znode = collection_state_znode_path(collection_name)
26
+ znode_data = zookeeper_connection.get(collection_state_znode, watch: watch)
27
+ return unless znode_data
28
+ JSON.parse(znode_data.first)[collection_name.to_s]
29
+ end
30
+
31
+ def collection_state_znode_path(collection_name)
32
+ "/collections/#{collection_name}/state.json"
33
+ end
34
+
35
+ private
36
+
37
+ def zookeeper_connection
38
+ @zookeeper_connection ||= build_zookeeper_connection
39
+ end
40
+
41
+ def build_zookeeper_connection
42
+ raise 'You must provide a ZooKeeper URL to enable solr cloud mode' unless zookeeper_url
43
+ raise Solr::Errors::ZookeeperRequired unless require_zk
44
+
45
+ zk = ZK.new(zookeeper_url)
46
+ zk.add_auth(scheme: 'digest', cert: zookeeper_auth) if zookeeper_auth
47
+ zk
48
+ end
49
+
50
+ def zookeeper_auth
51
+ if zookeeper_auth_user && zookeeper_auth_password
52
+ "#{zookeeper_auth_user}:#{zookeeper_auth_password}"
53
+ end
54
+ end
55
+
56
+ def require_zk
57
+ require 'zk'
58
+ true
59
+ rescue LoadError
60
+ false
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ require 'solr/delete/request'
2
+ require 'solr/commit/request'
3
+ require 'solr/query/request'
4
+ require 'solr/data_import/request'
5
+
6
+ module Solr
7
+ module Commands
8
+ def commit
9
+ Solr::Commit::Request.new.run
10
+ end
11
+
12
+ def delete_by_id(id, commit: false)
13
+ Solr::Delete::Request.new(id: id).run(commit: commit)
14
+ end
15
+
16
+ def delete_by_query(query, commit: false)
17
+ Solr::Delete::Request.new(query: query).run(commit: commit)
18
+ end
19
+
20
+ def data_import(params)
21
+ Solr::DataImport::Request.new(params).run
22
+ end
23
+ end
24
+ end
@@ -1,14 +1,11 @@
1
1
  module Solr
2
2
  module Commit
3
3
  class Request
4
- include Solr::Support::ConnectionHelper
5
4
  PATH = '/update'.freeze
6
5
 
7
6
  def run
8
- # the way to do commit message in SOLR is to send an empty
9
- # request with ?commit=true in the URL.
10
- raw_response = connection(PATH, commit: true).post
11
- Solr::Response.from_raw_response(raw_response)
7
+ http_request = Solr::Request::HttpRequest.new(path: PATH, url_params: { commit: true }, method: :post)
8
+ Solr::Request::Runner.call(request: http_request)
12
9
  end
13
10
  end
14
11
  end
@@ -4,13 +4,19 @@ require 'solr/core_configuration/core_config'
4
4
  require 'solr/core_configuration/core_config_builder'
5
5
  require 'solr/errors/solr_url_not_defined_error'
6
6
  require 'solr/errors/ambiguous_core_error'
7
+ require 'solr/errors/could_not_infer_implicit_core_name'
7
8
 
8
9
  module Solr
9
10
  class Configuration
11
+ extend Forwardable
12
+
13
+ delegate [:zookeeper_url, :zookeeper_url=, :zookeeper_auth_user=, :zookeeper_auth_password=] => :@cloud_configuration
14
+
10
15
  SOLRB_USER_AGENT_HEADER = { user_agent: "Solrb v#{Solr::VERSION}" }.freeze
11
16
 
12
- attr_accessor :cores, :test_connection
13
- attr_reader :url, :faraday_options
17
+ attr_accessor :cores, :test_connection, :auth_user, :auth_password
18
+
19
+ attr_reader :url, :faraday_options, :cloud_configuration
14
20
 
15
21
  def initialize
16
22
  @faraday_options = {
@@ -18,6 +24,7 @@ module Solr
18
24
  headers: SOLRB_USER_AGENT_HEADER
19
25
  }
20
26
  @cores = {}
27
+ @cloud_configuration = Solr::Cloud::Configuration.new
21
28
  end
22
29
 
23
30
  def faraday_options=(options)
@@ -41,7 +48,7 @@ module Solr
41
48
  def default_core_config
42
49
  defined_default_core_config = cores.values.detect(&:default?)
43
50
  return defined_default_core_config if defined_default_core_config
44
- raise Errors::AmbiguousCoreError if cores.count > 1
51
+ raise Solr::Errors::AmbiguousCoreError if cores.count > 1
45
52
  cores.values.first || build_env_url_core_config
46
53
  end
47
54
 
@@ -60,7 +67,19 @@ module Solr
60
67
  end
61
68
  end
62
69
 
70
+ def core_name_from_solr_url_env
71
+ full_solr_core_uri = URI.parse(ENV['SOLR_URL'])
72
+ core_name = full_solr_core_uri.path.gsub('/solr', '').delete('/')
73
+
74
+ if !core_name || core_name == ''
75
+ raise Solr::Errors::CouldNotInferImplicitCoreName
76
+ end
77
+
78
+ core_name
79
+ end
80
+
63
81
  def build_env_url_core_config(name: nil)
82
+ name ||= core_name_from_solr_url_env
64
83
  Solr::CoreConfiguration::EnvUrlCoreConfig.new(name: name)
65
84
  end
66
85
 
@@ -70,5 +89,11 @@ module Solr
70
89
  raise ArgumentError, 'Only one default core can be specified'
71
90
  end
72
91
  end
92
+
93
+ def validate!
94
+ if !(url || @cloud_configuration.zookeeper_url || ENV['SOLR_URL'])
95
+ raise Solr::Errors::SolrUrlNotDefinedError
96
+ end
97
+ end
73
98
  end
74
99
  end
@@ -5,23 +5,20 @@ module Solr
5
5
 
6
6
  def initialize(url, faraday_options: Solr.configuration.faraday_options)
7
7
  # Allow mock the connection for testing
8
- @raw_connection = Solr.configuration.test_connection || Faraday.new(url, faraday_options)
8
+ @raw_connection = Solr.configuration.test_connection || build_faraday_connection(url, faraday_options)
9
9
  freeze
10
10
  end
11
11
 
12
- def get
13
- Solr.instrument(name: INSTRUMENT_KEY) { @raw_connection.get }
12
+ def self.call(url:, method:, body:)
13
+ raise "HTTP method not supported: #{method}" unless [:get, :post].include?(method.to_sym)
14
+ new(url).public_send(method, body)
14
15
  end
15
16
 
16
- def post(data = {})
17
- Solr.instrument(name: INSTRUMENT_KEY) do
18
- @raw_connection.post do |req|
19
- req.body = data
20
- end
21
- end
17
+ def get(_)
18
+ Solr.instrument(name: INSTRUMENT_KEY) { @raw_connection.get }
22
19
  end
23
20
 
24
- def post_as_json(data)
21
+ def post(data)
25
22
  Solr.instrument(name: INSTRUMENT_KEY, data: data) do
26
23
  @raw_connection.post do |req|
27
24
  req.headers['Content-Type'] = 'application/json'.freeze
@@ -29,5 +26,15 @@ module Solr
29
26
  end
30
27
  end
31
28
  end
29
+
30
+ private
31
+
32
+ def build_faraday_connection(url, faraday_options)
33
+ connection = Faraday.new(url, faraday_options)
34
+ if Solr.configuration.auth_user && Solr.configuration.auth_password
35
+ connection.basic_auth(Solr.configuration.auth_user, Solr.configuration.auth_password)
36
+ end
37
+ connection
38
+ end
32
39
  end
33
40
  end
@@ -3,7 +3,7 @@ module Solr
3
3
  class CoreConfigBuilder
4
4
  attr_reader :name, :dynamic_fields, :fields_params, :default
5
5
 
6
- def initialize(name: nil, default:)
6
+ def initialize(name:, default:)
7
7
  @name = name
8
8
  @default = default
9
9
  @dynamic_fields = {}
@@ -0,0 +1,27 @@
1
+ require 'solr/request/first_shard_leader_node_selection_strategy'
2
+
3
+ module Solr
4
+ module DataImport
5
+ class Request
6
+ PATH = '/dataimport'.freeze
7
+
8
+ attr_reader :params
9
+
10
+ def initialize(params)
11
+ @params = params
12
+ end
13
+
14
+ # We want to make sure we send every dataimport request to the same node because this same class
15
+ # could be used to start a dataimport and to get dataimport progress data afterwards.
16
+ # To make it consistent we will send dataimport requests only to the first shard leader replica
17
+ def run
18
+ http_request = Solr::Request::HttpRequest.new(path: PATH, url_params: params, method: :get)
19
+ Solr::Request::Runner.call(request: http_request, node_selection_strategy: build_node_selection_strategy)
20
+ end
21
+
22
+ def build_node_selection_strategy
23
+ Solr::Request::FirstShardLeaderNodeSelectionStrategy
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,6 @@
1
1
  module Solr
2
2
  module Delete
3
3
  class Request
4
- include Solr::Support::ConnectionHelper
5
4
  using Solr::Support::HashExtensions
6
5
 
7
6
  PATH = '/update'.freeze
@@ -14,13 +13,16 @@ module Solr
14
13
  end
15
14
 
16
15
  def run(commit: false)
17
- # need to think how to move out commit data from the connection, it doesn't belong there
18
- raw_response = connection(PATH, commit: commit).post_as_json(delete_command)
19
- Solr::Response.from_raw_response(raw_response)
16
+ http_request = build_http_request(commit)
17
+ Solr::Request::Runner.call(request: http_request)
20
18
  end
21
19
 
22
20
  private
23
21
 
22
+ def build_http_request(commit)
23
+ Solr::Request::HttpRequest.new(path: PATH, body: delete_command, url_params: { commit: commit }, method: :post)
24
+ end
25
+
24
26
  def validate_delete_options!(options)
25
27
  options = options.deep_symbolize_keys
26
28
  id, query = options.values_at(:id, :query)
@@ -1,9 +1,11 @@
1
- module Errors
2
- class AmbiguousCoreError < StandardError
3
- ERROR_MESSAGE = 'Multiple cores defined: default core can\'t be found'.freeze
1
+ module Solr
2
+ module Errors
3
+ class AmbiguousCoreError < StandardError
4
+ ERROR_MESSAGE = 'Multiple cores defined: default core can\'t be found'.freeze
4
5
 
5
- def initialize
6
- super(ERROR_MESSAGE)
6
+ def initialize
7
+ super(ERROR_MESSAGE)
8
+ end
7
9
  end
8
10
  end
9
11
  end
@@ -0,0 +1,15 @@
1
+ module Solr
2
+ module Errors
3
+ class CouldNotInferImplicitCoreName < StandardError
4
+ MESSAGE = '
5
+ TODO: Add message
6
+
7
+ '.freeze
8
+
9
+ def initialize
10
+ super(MESSAGE)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,6 @@
1
+ module Solr
2
+ module Errors
3
+ class NoActiveSolrNodesError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module Solr
2
+ module Errors
3
+ class SolrConnectionFailedError < StandardError
4
+ def initialize(solr_urls)
5
+ message = <<~MESSAGE
6
+ Could not connection to any available solr instance:
7
+ #{solr_urls.join(', ')}
8
+ MESSAGE
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,6 @@
1
- module Errors
2
- class SolrQueryError < StandardError
1
+ module Solr
2
+ module Errors
3
+ class SolrQueryError < StandardError
4
+ end
3
5
  end
4
6
  end
@@ -1,16 +1,29 @@
1
- module Errors
2
- class SolrUrlNotDefinedError < StandardError
3
- SOLR_URL_NOT_DEFINED_MESSAGE = '
4
- Solrb gem requires you to set the URL of your Solr instance
5
- either through SOLR_URL environmental variable or explicitly inside the configure block:
1
+ module Solr
2
+ module Errors
3
+ class SolrUrlNotDefinedError < StandardError
4
+ SOLR_URL_NOT_DEFINED_MESSAGE = '
5
+ Solrb gem requires you to set the URL of your Solr instance
6
+ either through SOLR_URL environmental variable or explicitly inside the configure block:
6
7
 
7
- Solr.configure do |config|
8
- config.url = "http://localhost:8983/solr/core"
9
- end
10
- '.freeze
8
+ Solr.configure do |config|
9
+ config.url = "http://localhost:8983/solr/core"
10
+ end
11
+
12
+ If you are using Solr cloud you can specify the zookeeper ensemble urls inside the configure block
13
+ and solrb will automatically get the solr urls from ZK:
14
+
15
+ Solr.configure do |config|
16
+ config.zookeeper_url = ["localhost:2181","localhost:2182","localhost:2183"]
17
+ end
18
+
19
+ For more information please check the solrb README file.
11
20
 
12
- def initialize
13
- super(SOLR_URL_NOT_DEFINED_MESSAGE)
21
+ '.freeze
22
+
23
+ def initialize
24
+ super(SOLR_URL_NOT_DEFINED_MESSAGE)
25
+ end
14
26
  end
15
27
  end
16
28
  end
29
+
@@ -0,0 +1,19 @@
1
+ module Solr
2
+ module Errors
3
+ class ZookeeperRequired < StandardError
4
+ ZOOKEEPER_REQUIRED_MESSAGE = '
5
+
6
+ Solrb gem requires zookeeper for solr-cloud support.
7
+ Please add "zk" gem to your Gemfile and run bundle install:
8
+
9
+ gem "zk"
10
+
11
+ '.freeze
12
+
13
+ def initialize
14
+ super(ZOOKEEPER_REQUIRED_MESSAGE)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -1,9 +1,8 @@
1
+ require 'solr/request/http_request'
2
+
1
3
  module Solr
2
4
  module Indexing
3
5
  class Request
4
- include Solr::Support::ConnectionHelper
5
-
6
- # TODO: potentially make handlers configurable and have them handle the path
7
6
  PATH = '/update'.freeze
8
7
 
9
8
  attr_reader :documents
@@ -13,9 +12,14 @@ module Solr
13
12
  end
14
13
 
15
14
  def run(commit: false)
16
- # need to think how to move out commit data from the connection, it doesn't belong there
17
- raw_response = connection(PATH, commit: commit).post_as_json(documents)
18
- Solr::Response.from_raw_response(raw_response)
15
+ http_request = build_http_request(commit)
16
+ Solr::Request::Runner.call(request: http_request)
17
+ end
18
+
19
+ private
20
+
21
+ def build_http_request(commit)
22
+ Solr::Request::HttpRequest.new(path: PATH, body: documents, url_params: { commit: commit }, method: :post)
19
23
  end
20
24
  end
21
25
  end
@@ -0,0 +1,27 @@
1
+ require 'solr/query/response'
2
+ require 'solr/request/runner'
3
+ require 'solr/query/http_request_builder'
4
+
5
+ module Solr
6
+ module Query
7
+ class Handler
8
+ attr_reader :query, :page, :page_size
9
+
10
+ def self.call(opts)
11
+ new(opts).call
12
+ end
13
+
14
+ def initialize(query:, page:, page_size:)
15
+ @query = query
16
+ @page = page
17
+ @page_size = page_size
18
+ end
19
+
20
+ 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)
23
+ Solr::Query::Response::Parser.new(request: query, solr_response: solr_response.body).to_response
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ require 'solr/request/http_request'
2
+
3
+ module Solr
4
+ module Query
5
+ class HttpRequestBuilder
6
+ PATH = '/select'.freeze
7
+
8
+ attr_reader :query, :page, :page_size
9
+
10
+ def self.call(opts)
11
+ new(opts).call
12
+ end
13
+
14
+ def initialize(query:, page:, page_size:)
15
+ @query = query
16
+ @page = page
17
+ @page_size = page_size
18
+ end
19
+
20
+ def call
21
+ Solr::Request::HttpRequest.new(path: PATH,
22
+ body: build_body,
23
+ method: :post)
24
+ end
25
+
26
+ private
27
+
28
+ # 🏋️
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
37
+ end
38
+
39
+ def solr_params
40
+ query.to_h
41
+ end
42
+ end
43
+ end
44
+ end
@@ -38,21 +38,19 @@ module Solr
38
38
  private
39
39
 
40
40
  def solr_prefix
41
- '-' if NOT_EQUAL_TYPE == @type
41
+ '-' if NOT_EQUAL_TYPE == type
42
42
  end
43
43
 
44
44
  def to_interval_solr_value(range)
45
45
  solr_min = to_primitive_solr_value(range.first)
46
- solr_max = if date_infinity?(range.last) || range.last.to_f.infinite?
47
- '*'
48
- else
49
- to_primitive_solr_value(range.last)
50
- end
46
+ solr_max = to_primitive_solr_value(range.last)
51
47
  "[#{solr_min} TO #{solr_max}]"
52
48
  end
53
49
 
54
50
  def to_primitive_solr_value(value)
55
- if date_or_time?(value)
51
+ if date_infinity?(value) || numeric_infinity?(value)
52
+ '*'
53
+ elsif date_or_time?(value)
56
54
  value.strftime('%Y-%m-%dT%H:%M:%SZ')
57
55
  else
58
56
  %("#{value.to_s.solr_escape}")
@@ -63,6 +61,10 @@ module Solr
63
61
  value.is_a?(DateTime::Infinity)
64
62
  end
65
63
 
64
+ def numeric_infinity?(value)
65
+ value.is_a?(Numeric) && value.infinite?
66
+ end
67
+
66
68
  def date_or_time?(value)
67
69
  return false unless value
68
70
  value.is_a?(::Date) || value.is_a?(::Time)
@@ -12,9 +12,7 @@ require 'solr/query/request/sorting/function'
12
12
  require 'solr/query/request/field_with_boost'
13
13
  require 'solr/query/request/or_filter'
14
14
  require 'solr/query/request/and_filter'
15
- require 'solr/query/request/runner'
16
- require 'solr/query/response'
17
- require 'solr/errors/solr_query_error'
15
+ require 'solr/query/handler'
18
16
 
19
17
  module Solr
20
18
  module Query
@@ -30,13 +28,8 @@ module Solr
30
28
  @filters = filters
31
29
  end
32
30
 
33
- # Runs this Solr::Request against Solr and
34
- # returns [Solr::Response]
35
31
  def run(page: 1, page_size: 10)
36
- solr_params = Solr::Query::Request::EdismaxAdapter.new(self).to_h
37
- solr_response = Solr::Query::Request::Runner.run(page: page, page_size: page_size, solr_params: solr_params)
38
- raise Errors::SolrQueryError, solr_response.error_message unless solr_response.ok?
39
- Solr::Query::Response::Parser.new(request: self, solr_response: solr_response.body).to_response
32
+ Solr::Query::Handler.call(query: self, page: page, page_size: page_size)
40
33
  end
41
34
 
42
35
  def grouping
@@ -46,6 +39,10 @@ module Solr
46
39
  def sorting
47
40
  @sorting ||= Solr::Query::Request::Sorting.none
48
41
  end
42
+
43
+ def to_h
44
+ Solr::Query::Request::EdismaxAdapter.new(self).to_h
45
+ end
49
46
  end
50
47
  end
51
48
  end
@@ -0,0 +1,19 @@
1
+ module Solr
2
+ module Request
3
+ class DefaultNodeSelectionStrategy
4
+ attr_reader :collection_name
5
+
6
+ def self.call(collection_name)
7
+ new(collection_name).call
8
+ end
9
+
10
+ def initialize(collection_name)
11
+ @collection_name = collection_name
12
+ end
13
+
14
+ def call
15
+ Solr.active_nodes_for(collection: collection_name).shuffle
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Solr
2
+ module Request
3
+ class FirstShardLeaderNodeSelectionStrategy
4
+ def self.call(collection_name)
5
+ new(collection_name).call
6
+ end
7
+
8
+ def initialize(collection_name)
9
+ @collection_name = collection_name
10
+ end
11
+
12
+ def call
13
+ return [solr_url] unless Solr.cloud_enabled?
14
+
15
+ ([first_shard_leader_replica_node_for(collection: @collection_name)] + solr_cloud_active_nodes_urls.shuffle).flatten.uniq
16
+ end
17
+
18
+ private
19
+
20
+ def first_shard_leader_replica_node_for(collection:)
21
+ shards = Solr.shards_for(collection: collection)
22
+ return unless shards
23
+ first_shard_name = shards.sort.first
24
+ Solr.leader_replica_node_for(collection: collection, shard: first_shard_name)
25
+ end
26
+
27
+ def solr_cloud_active_nodes_urls
28
+ Solr.active_nodes_for(collection: @collection_name)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ module Solr
2
+ module Request
3
+ class HttpRequest
4
+ attr_reader :path, :body, :url_params, :method
5
+
6
+ def initialize(path: '/', body: {}, url_params: {}, method: :get)
7
+ @path = path
8
+ @body = body
9
+ @url_params = url_params
10
+ @method = method
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ require 'solr/request/default_node_selection_strategy'
2
+ require 'solr/errors/solr_query_error'
3
+ require 'solr/errors/solr_connection_failed_error'
4
+ require 'solr/errors/no_active_solr_nodes_error'
5
+
6
+ module Solr
7
+ module Request
8
+ # TODO: Add documentation about request running
9
+ class Runner
10
+ include Solr::Support::UrlHelper
11
+
12
+ attr_reader :request, :response_parser, :node_selection_strategy
13
+
14
+ def self.call(opts)
15
+ new(opts).call
16
+ end
17
+
18
+ def initialize(request:,
19
+ node_selection_strategy: Solr::Request::DefaultNodeSelectionStrategy,
20
+ solr_connection: Solr::Connection)
21
+ @request = request
22
+ @response_parser = response_parser
23
+ @node_selection_strategy = node_selection_strategy
24
+ @solr_connection = solr_connection
25
+ end
26
+
27
+ def call
28
+ solr_urls.each do |node_url|
29
+ request_url = build_request_url(url: node_url,
30
+ path: request.path,
31
+ url_params: request.url_params)
32
+ begin
33
+ raw_response = @solr_connection.call(url: request_url.to_s, method: request.method, body: request.body)
34
+ solr_response = Solr::Response::Parser.call(raw_response)
35
+ raise Solr::Errors::SolrQueryError, solr_response.error_message unless solr_response.ok?
36
+ return solr_response
37
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::EADDRNOTAVAIL => e
38
+ # Try next node
39
+ end
40
+ end
41
+
42
+ raise Solr::Errors::SolrConnectionFailedError.new(solr_urls)
43
+ end
44
+
45
+ private
46
+
47
+ def solr_urls
48
+ @solr_urls ||= begin
49
+ urls = Solr.cloud_enabled? ? solr_cloud_collection_urls : [Solr.current_core_config.url]
50
+ unless urls && urls.any?
51
+ raise Solr::Errors::NoActiveSolrNodesError
52
+ end
53
+ urls
54
+ end
55
+ end
56
+
57
+ def solr_cloud_collection_urls
58
+ urls = node_selection_strategy.call(collection_name)
59
+ return unless urls
60
+ urls.map do |url|
61
+ File.join(url, collection_name.to_s)
62
+ end
63
+ end
64
+
65
+ def collection_name
66
+ Solr.current_core_config.name
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,11 +1,15 @@
1
1
  module Solr
2
2
  class Response
3
3
  class Parser
4
+ def self.call(raw_response)
5
+ new(raw_response).call
6
+ end
7
+
4
8
  def initialize(raw_response)
5
9
  @raw_response = raw_response
6
10
  end
7
11
 
8
- def parse
12
+ def call
9
13
  # 404 is a special case, it didn't hit Solr (more likely than not)
10
14
  return not_found_response if @raw_response.status == 404
11
15
  parsed_body = JSON.parse(@raw_response.body).freeze
data/lib/solr/response.rb CHANGED
@@ -7,10 +7,6 @@ module Solr
7
7
  class Response
8
8
  OK = 'OK'.freeze
9
9
 
10
- def self.from_raw_response(response)
11
- Solr::Response::Parser.new(response).parse
12
- end
13
-
14
10
  attr_reader :header, :http_status, :solr_error, :body
15
11
 
16
12
  def initialize(header:, http_status: HttpStatus.ok, solr_error: SolrError.none, body: {})
@@ -1,12 +1,33 @@
1
1
  module Solr
2
2
  module Support
3
3
  module UrlHelper
4
- def self.solr_url(path, url_params: {})
5
- full_url = File.join(Solr.current_core_config.uri, path)
4
+ module_function
5
+
6
+ def solr_url(path, url_params: {})
7
+ full_url = File.join(core_url, path)
6
8
  full_uri = Addressable::URI.parse(full_url)
7
9
  full_uri.query_values = url_params if url_params.any?
8
10
  full_uri
9
11
  end
12
+
13
+ def build_request_url(url:,path:, url_params: {})
14
+ action_url = File.join(url, path).chomp('/')
15
+ full_uri = Addressable::URI.parse(action_url)
16
+ full_uri.query_values = url_params if url_params && url_params.any?
17
+ full_uri
18
+ end
19
+
20
+ def core_url
21
+ Solr.cloud_enabled? ? solr_cloud_url : current_core.uri
22
+ end
23
+
24
+ def solr_cloud_url
25
+ File.join(Solr.active_nodes_for(collection: current_core.name.to_s).first, current_core.name.to_s)
26
+ end
27
+
28
+ def current_core
29
+ Solr.current_core_config
30
+ end
10
31
  end
11
32
  end
12
33
  end
data/lib/solr/testing.rb CHANGED
@@ -6,14 +6,14 @@ module Solr
6
6
  end
7
7
  end
8
8
 
9
- module Solr::Query::Request::RunnerExtension
10
- def run
9
+ module Solr::Request::RunnerExtension
10
+ def call
11
11
  response = super
12
- Solr::Testing.last_solr_request_params = request_params[:params]
12
+ Solr::Testing.last_solr_request_params = request.body ? request.body[:params] : nil
13
13
  response
14
14
  end
15
15
  end
16
16
 
17
- class Solr::Query::Request::Runner
18
- prepend Solr::Query::Request::RunnerExtension
17
+ class Solr::Request::Runner
18
+ prepend Solr::Request::RunnerExtension
19
19
  end
data/lib/solr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Solr
2
- VERSION = '0.1.8'.freeze
2
+ VERSION = '0.1.9'.freeze
3
3
  end
data/lib/solr.rb CHANGED
@@ -9,14 +9,19 @@ require 'solr/document'
9
9
  require 'solr/document_collection'
10
10
  require 'solr/grouped_document_collection'
11
11
  require 'solr/response'
12
+ require 'solr/request/runner'
12
13
  require 'solr/query/request'
13
14
  require 'solr/indexing/document'
14
15
  require 'solr/indexing/request'
15
- require 'solr/delete/request'
16
- require 'solr/commit/request'
16
+
17
+ require 'solr/cloud/helper_methods'
18
+ require 'solr/commands'
17
19
 
18
20
  module Solr
19
21
  class << self
22
+ include Solr::Commands
23
+ include Solr::Cloud::HelperMethods
24
+
20
25
  CURRENT_CORE_CONFIG_VARIABLE_NAME = :solrb_current_core_config
21
26
 
22
27
  attr_accessor :configuration
@@ -25,24 +30,15 @@ module Solr
25
30
 
26
31
  def configure
27
32
  yield configuration
33
+ configuration.validate!
34
+ enable_solr_cloud! unless configuration.zookeeper_url.nil?
35
+ configuration
28
36
  end
29
37
 
30
38
  def current_core_config
31
39
  Thread.current[CURRENT_CORE_CONFIG_VARIABLE_NAME] || Solr.configuration.default_core_config
32
40
  end
33
41
 
34
- def commit
35
- Solr::Commit::Request.new.run
36
- end
37
-
38
- def delete_by_id(id, commit: false)
39
- Solr::Delete::Request.new(id: id).run(commit: commit)
40
- end
41
-
42
- def delete_by_query(query, commit: false)
43
- Solr::Delete::Request.new(query: query).run(commit: commit)
44
- end
45
-
46
42
  def with_core(core)
47
43
  core_config = Solr.configuration.core_config_by_name(core)
48
44
  old_core_config = Thread.current[CURRENT_CORE_CONFIG_VARIABLE_NAME]
@@ -52,9 +48,15 @@ module Solr
52
48
  Thread.current[CURRENT_CORE_CONFIG_VARIABLE_NAME] = old_core_config
53
49
  end
54
50
 
51
+ def solr_url(path = '')
52
+ Solr::Support::UrlHelper.solr_url(path)
53
+ end
54
+
55
55
  def instrument(name:, data: {})
56
56
  if defined? ActiveSupport::Notifications
57
- ActiveSupport::Notifications.instrument(name, data) do
57
+ # Create a copy of data to avoid modifications on the original object by rails
58
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/notifications.rb#L66-L70
59
+ ActiveSupport::Notifications.instrument(name, data.dup) do
58
60
  yield if block_given?
59
61
  end
60
62
  else
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adriano Luz
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2018-11-30 00:00:00.000000000 Z
13
+ date: 2019-02-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: addressable
@@ -166,7 +166,6 @@ files:
166
166
  - ".gitignore"
167
167
  - ".rspec"
168
168
  - ".rubocop.yml"
169
- - ".ruby-version"
170
169
  - Gemfile
171
170
  - Gemfile.lock
172
171
  - LICENSE.txt
@@ -175,6 +174,11 @@ files:
175
174
  - bin/console
176
175
  - bin/setup
177
176
  - lib/solr.rb
177
+ - lib/solr/cloud/collections_state_manager.rb
178
+ - lib/solr/cloud/configuration.rb
179
+ - lib/solr/cloud/helper_methods.rb
180
+ - lib/solr/cloud/zookeeper_connection.rb
181
+ - lib/solr/commands.rb
178
182
  - lib/solr/commit/request.rb
179
183
  - lib/solr/configuration.rb
180
184
  - lib/solr/connection.rb
@@ -182,15 +186,22 @@ files:
182
186
  - lib/solr/core_configuration/core_config_builder.rb
183
187
  - lib/solr/core_configuration/dynamic_field.rb
184
188
  - lib/solr/core_configuration/field.rb
189
+ - lib/solr/data_import/request.rb
185
190
  - lib/solr/delete/request.rb
186
191
  - lib/solr/document.rb
187
192
  - lib/solr/document_collection.rb
188
193
  - lib/solr/errors/ambiguous_core_error.rb
194
+ - lib/solr/errors/could_not_infer_implicit_core_name.rb
195
+ - lib/solr/errors/no_active_solr_nodes_error.rb
196
+ - lib/solr/errors/solr_connection_failed_error.rb
189
197
  - lib/solr/errors/solr_query_error.rb
190
198
  - lib/solr/errors/solr_url_not_defined_error.rb
199
+ - lib/solr/errors/zookeeper_required.rb
191
200
  - lib/solr/grouped_document_collection.rb
192
201
  - lib/solr/indexing/document.rb
193
202
  - lib/solr/indexing/request.rb
203
+ - lib/solr/query/handler.rb
204
+ - lib/solr/query/http_request_builder.rb
194
205
  - lib/solr/query/request.rb
195
206
  - lib/solr/query/request/and_filter.rb
196
207
  - lib/solr/query/request/boost_magnitude.rb
@@ -214,7 +225,6 @@ files:
214
225
  - lib/solr/query/request/geo_filter.rb
215
226
  - lib/solr/query/request/grouping.rb
216
227
  - lib/solr/query/request/or_filter.rb
217
- - lib/solr/query/request/runner.rb
218
228
  - lib/solr/query/request/sorting.rb
219
229
  - lib/solr/query/request/sorting/field.rb
220
230
  - lib/solr/query/request/sorting/function.rb
@@ -224,6 +234,10 @@ files:
224
234
  - lib/solr/query/response/field_facets.rb
225
235
  - lib/solr/query/response/parser.rb
226
236
  - lib/solr/query/response/spellcheck.rb
237
+ - lib/solr/request/default_node_selection_strategy.rb
238
+ - lib/solr/request/first_shard_leader_node_selection_strategy.rb
239
+ - lib/solr/request/http_request.rb
240
+ - lib/solr/request/runner.rb
227
241
  - lib/solr/response.rb
228
242
  - lib/solr/response/header.rb
229
243
  - lib/solr/response/http_status.rb
@@ -258,7 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
258
272
  version: '0'
259
273
  requirements: []
260
274
  rubyforge_project:
261
- rubygems_version: 2.7.6
275
+ rubygems_version: 2.7.8
262
276
  signing_key:
263
277
  specification_version: 4
264
278
  summary: Solr Ruby client with a nice object-oriented API
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.5.3
@@ -1,44 +0,0 @@
1
- module Solr
2
- module Query
3
- class Request
4
- class Runner
5
- PATH = '/select'.freeze
6
-
7
- include Solr::Support::ConnectionHelper
8
-
9
- attr_reader :page, :page_size, :solr_params
10
-
11
- class << self
12
- def run(opts)
13
- new(opts).run
14
- end
15
- end
16
-
17
- def initialize(page:, page_size:, solr_params: {})
18
- @page = page
19
- @page_size = page_size
20
- @solr_params = solr_params
21
- end
22
-
23
- def run
24
- raw_response = connection(PATH).post_as_json(request_params)
25
- response = Solr::Response.from_raw_response(raw_response)
26
- response
27
- end
28
-
29
- private
30
-
31
- def start
32
- start_page = @page.to_i - 1
33
- start_page = start_page < 1 ? 0 : start_page
34
- start_page * page_size
35
- end
36
-
37
- def request_params
38
- # https://lucene.apache.org/solr/guide/7_1/json-request-api.html#passing-parameters-via-json
39
- @request_params ||= { params: solr_params.merge(wt: :json, rows: page_size.to_i, start: start) }
40
- end
41
- end
42
- end
43
- end
44
- end