solrb 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
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