solr_cloud-connection 0.5.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/LICENSE.md +27 -0
- data/README.md +46 -15
- data/Rakefile +1 -1
- data/compose.yml +37 -2
- data/env.test.solr10 +3 -0
- data/lib/solr_cloud/alias.rb +7 -7
- data/lib/solr_cloud/collection.rb +21 -14
- data/lib/solr_cloud/configset.rb +0 -1
- data/lib/solr_cloud/connection/alias_admin.rb +10 -7
- data/lib/solr_cloud/connection/collection_admin.rb +5 -6
- data/lib/solr_cloud/connection/configset_admin.rb +105 -19
- data/lib/solr_cloud/connection/version.rb +1 -1
- data/lib/solr_cloud/connection.rb +25 -13
- data/readme.rb +4 -10
- data/solr10_compat_update_plan.md +290 -0
- metadata +54 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19bcfd12d8bc2169aad5f3a01163996e671c0e8b2594ad50197ee7127abf15a9
|
|
4
|
+
data.tar.gz: 9088cfcdf21b11fe5ac92fd35ea2d5ec66ea55471d9a0e3a16edf2304db22bf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61d0a5ce50ec30182bc0e60c915a78b6cc492039291634b53e48efb5fd91124643c97ebafc9ad9043d690bf0f28dbd047c5d1688acfb927c5ca126320d9e00c7
|
|
7
|
+
data.tar.gz: 9771f4d1047ef1210eecd1fc78b376363c7d9d7cd34b4c72023eb9f502aa746c1247844de176d03c9c3db8aec9673fa9d65edd97eb06c7f4d9bb18ec7a70a217
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Major — Solr 8/9/10 Dual-Version Compatibility
|
|
6
|
+
|
|
7
|
+
This release adds support for Solr 9 and 10 alongside the existing Solr 8 support.
|
|
8
|
+
The internal API dispatch now detects the connected Solr major version and routes
|
|
9
|
+
requests to the correct endpoint, since several Solr APIs changed paths or response
|
|
10
|
+
envelopes between Solr 8 and Solr 9/10.
|
|
11
|
+
|
|
12
|
+
- **Feature: Solr 8/9/10 compatibility**
|
|
13
|
+
- Use V1 `CLUSTERSTATUS` action for collection info instead of V2 `api/collections/{name}`
|
|
14
|
+
(the V2 response envelope changed between Solr 8 and Solr 9/10 — the `cluster` wrapper
|
|
15
|
+
key is absent in Solr 10)
|
|
16
|
+
- Use V1 `LISTALIASES` action with `|| {}` guard for nil aliases on Solr 10
|
|
17
|
+
- Use V1 `admin/configs` endpoint for configset upload (path differs between Solr 8 and 9+)
|
|
18
|
+
- Add `solr9_or_later?` predicate to the connection for external version-gating
|
|
19
|
+
- **CI: Matrix testing** — run tests against Solr 8 & Solr 10 in parallel via GitHub Actions
|
|
20
|
+
- **Infrastructure** — Docker Compose healthchecks, env setup for Solr 10 test container, curl-based wait loops
|
|
21
|
+
|
|
22
|
+
### Chores
|
|
23
|
+
- Replace `wait-for-it` / raw curl loops with Docker Compose healthcheck + `docker compose wait`
|
|
24
|
+
- Add Solr 10 Docker image definition (`spec/data/solr10_docker/`)
|
|
25
|
+
- Update README
|
|
26
|
+
- Version bump to 1.0.0
|
|
27
|
+
|
|
28
|
+
## [0.6.0] - 2025-05-29
|
|
29
|
+
- Expose `username` and `password` getters on the connection
|
|
30
|
+
- Update README with "Common Usage" section
|
|
31
|
+
- Peg dependency versions in gemspec
|
|
32
|
+
- General gemfile/spec cleanup
|
|
33
|
+
|
|
34
|
+
## [0.5.0] - 2025-03-24
|
|
35
|
+
- Added some sugar for unifying aliases and collections
|
|
36
|
+
- Update tests
|
|
37
|
+
- Better errors
|
|
38
|
+
- Flesh out README.md to be a useful document
|
|
39
|
+
|
|
40
|
+
|
|
3
41
|
## [0.4.0] - 2023-12-07
|
|
4
42
|
- Fixed rules about what names are legal collections/configsets/aliases
|
|
5
43
|
- Update version and changelog
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2026, The Regents of the University of Michigan.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are
|
|
6
|
+
met:
|
|
7
|
+
|
|
8
|
+
* Redistributions of source code must retain the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
documentation and/or other materials provided with the distribution.
|
|
13
|
+
* Neither the name of the The University of Michigan nor the
|
|
14
|
+
names of its contributors may be used to endorse or promote products
|
|
15
|
+
derived from this software without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE REGENTS OF THE UNIVERSITY OF MICHIGAN AND
|
|
18
|
+
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
|
|
19
|
+
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OF THE
|
|
21
|
+
UNIVERSITY OF MICHIGAN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
22
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
23
|
+
TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
24
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
25
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
26
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
27
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# SolrCloud::Connection
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Common usage
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
```ruby
|
|
6
|
+
|
|
7
|
+
# Connect to the server, upload a config set, make a collection based on it,
|
|
8
|
+
# and make an alias pointing to that collection
|
|
9
|
+
|
|
10
|
+
server = SolrCloud::Connection.new(url: url, user: user, password: pass)
|
|
11
|
+
cfg = server.create_configset(name: "my_cfg", confdir: "/path/to/my/conf")
|
|
12
|
+
cars_v1 = server.create_collection(name: "cars_v1", configset: "my_cfg")
|
|
13
|
+
cars = cars_v1.alias_as("cars")
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic functionality / Roadmap
|
|
18
|
+
|
|
19
|
+
Do basic administrative tasks on a running Solr cloud instance
|
|
20
|
+
|
|
21
|
+
* [x] create (i.e., upload) a configSet when given a `conf` directory
|
|
22
|
+
* [x] list, create, and delete configsets, collections, and aliases
|
|
23
|
+
* [x] get basic version information for the running solr
|
|
24
|
+
* [x] check on the health of individual collections
|
|
25
|
+
* [x] treat an alias (mostly) as a collection
|
|
26
|
+
* [ ] automatically generate methods to talk to defined requestHandlers and updateHandlers
|
|
27
|
+
* [ ] provide a way to talk to the analyzer for testing of fieldTypes
|
|
28
|
+
* [ ] hook into the schema API
|
|
29
|
+
* [ ] allow it to work with cores, and not just solrcloud collections (which, you know bad naming then)
|
|
30
|
+
* [ ] figure out how to deal with managed resources
|
|
31
|
+
* [ ] get info from updateHandler metrics, esp. pending documents and cumulative errors
|
|
32
|
+
* [ ] hook into performance metrics for easy reporting and checks
|
|
33
|
+
* [ ] support more of the v2 API
|
|
12
34
|
|
|
13
35
|
In almost all cases, you can treat an alias to a collection like the underlying collection.
|
|
14
36
|
|
|
@@ -43,6 +65,8 @@ user = "solr"
|
|
|
43
65
|
password = "SolrRocks"
|
|
44
66
|
config_directory = "/path/to/myconfig/conf" # Directory 'conf' contains solrconfig.xml
|
|
45
67
|
|
|
68
|
+
require "solr_cloud/connection"
|
|
69
|
+
|
|
46
70
|
server = SolrCloud::Connection.new(url: url, user: user, password: pass)
|
|
47
71
|
#=> <SolrCloud::Connection http://localhost:9090>
|
|
48
72
|
|
|
@@ -66,19 +90,25 @@ server.configset_names #=> ["_default"]
|
|
|
66
90
|
|
|
67
91
|
# Create a new configset by taking a conf directory, zipping it up,
|
|
68
92
|
# and sending it to solr
|
|
69
|
-
cset = server.create_configset(name: "horseless", confdir: config_directory)
|
|
93
|
+
cset = server.create_configset(name: "horseless", confdir: config_directory)
|
|
94
|
+
#=> <SolrCloud::Configset 'horseless' at http://localhost:9090>
|
|
70
95
|
server.configset_names #=> ["_default", "horseless"]
|
|
71
96
|
|
|
72
97
|
# That's a dumb name for a config set. Delete it and try again.
|
|
73
98
|
cset.delete! #=> <SolrCloud::Connection http://localhost:9090>
|
|
74
|
-
cset = server.create_configset(name: "cars_cfg", confdir: config_directory)
|
|
75
|
-
|
|
99
|
+
cset = server.create_configset(name: "cars_cfg", confdir: config_directory)
|
|
100
|
+
#=> <SolrCloud::Configset 'cars_cfg' at http://localhost:9090>
|
|
101
|
+
server.configsets
|
|
102
|
+
#=> [<SolrCloud::Configset '_default' at http://localhost:9090>,
|
|
103
|
+
# <SolrCloud::Configset 'cars_cfg' at http://localhost:9090>]
|
|
76
104
|
|
|
77
105
|
# Can't be overwritten by accident
|
|
78
|
-
server.create_configset(name: "cars_cfg", confdir: config_directory)
|
|
106
|
+
server.create_configset(name: "cars_cfg", confdir: config_directory)
|
|
107
|
+
#=> raised #<SolrCloud::WontOverwriteError: Won't replace configset cars_cfg unless 'force: true' passed >
|
|
79
108
|
|
|
80
109
|
# But you can force it
|
|
81
|
-
server.create_configset(name: "cars_cfg", confdir: config_directory, force: true)
|
|
110
|
+
server.create_configset(name: "cars_cfg", confdir: config_directory, force: true)
|
|
111
|
+
#=> <SolrCloud::Configset 'cars_cfg' at http://localhost:9090>
|
|
82
112
|
|
|
83
113
|
cfg = server.get_configset("cars_cfg") #=> <SolrCloud::Configset 'cars_cfg' at http://localhost:9090>
|
|
84
114
|
cfg.in_use? #=> false
|
|
@@ -127,7 +157,8 @@ cars_v1.aliased? #=> true
|
|
|
127
157
|
|
|
128
158
|
cars_v1.has_alias?("cars") #=> true
|
|
129
159
|
cars_v1.alias_as("autos") #=> <SolrCloud::Alias 'autos' (alias of 'cars_v1')>
|
|
130
|
-
cars_v1.aliases
|
|
160
|
+
cars_v1.aliases
|
|
161
|
+
#=> [<SolrCloud::Alias 'cars' (alias of 'cars_v1')>, <SolrCloud::Alias 'autos' (alias of 'cars_v1')>]
|
|
131
162
|
|
|
132
163
|
cars_v1.get_alias("autos").delete! #=> <SolrCloud::Connection http://localhost:9090>
|
|
133
164
|
cars_v1.aliases #=> [<SolrCloud::Alias 'cars' (alias of 'cars_v1')>]
|
|
@@ -226,5 +257,5 @@ This repository is set up to run tests under docker.
|
|
|
226
257
|
|
|
227
258
|
## Contributing
|
|
228
259
|
|
|
229
|
-
Bugs, functionality suggestions, API suggestions, feature requests, etc. all
|
|
260
|
+
Bugs, functionality suggestions, API suggestions, feature requests, etc. all welcome
|
|
230
261
|
on GitHub at https://github.com/mlibrary/solr_cloud-connection.
|
data/Rakefile
CHANGED
data/compose.yml
CHANGED
|
@@ -11,21 +11,56 @@ services:
|
|
|
11
11
|
- .env
|
|
12
12
|
- env.test
|
|
13
13
|
command: "tail -f /dev/null"
|
|
14
|
+
depends_on:
|
|
15
|
+
solr:
|
|
16
|
+
condition: service_healthy
|
|
17
|
+
solr10:
|
|
18
|
+
condition: service_healthy
|
|
14
19
|
|
|
15
20
|
solr:
|
|
16
21
|
build: spec/data/solr_docker/.
|
|
17
22
|
ports:
|
|
18
|
-
- "
|
|
23
|
+
- "6781:8983"
|
|
19
24
|
environment:
|
|
20
25
|
- ZK_HOST=zoo:2181
|
|
21
26
|
depends_on:
|
|
22
27
|
- zoo
|
|
23
28
|
command: solr-foreground
|
|
29
|
+
healthcheck:
|
|
30
|
+
test: ["CMD-SHELL", "curl -sf -u solr:SolrRocks http://localhost:8983/solr/admin/info/system || exit 1"]
|
|
31
|
+
interval: 10s
|
|
32
|
+
timeout: 5s
|
|
33
|
+
retries: 12
|
|
34
|
+
start_period: 30s
|
|
24
35
|
|
|
25
36
|
zoo:
|
|
26
37
|
image: zookeeper
|
|
27
38
|
ports:
|
|
28
|
-
-
|
|
39
|
+
- 6782:2181
|
|
40
|
+
environment:
|
|
41
|
+
ZOO_MY_ID: 1
|
|
42
|
+
ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
|
|
43
|
+
|
|
44
|
+
solr10:
|
|
45
|
+
build: spec/data/solr10_docker/.
|
|
46
|
+
ports:
|
|
47
|
+
- "6783:8983"
|
|
48
|
+
environment:
|
|
49
|
+
- ZK_HOST=zoo10:2181
|
|
50
|
+
depends_on:
|
|
51
|
+
- zoo10
|
|
52
|
+
command: solr-foreground
|
|
53
|
+
healthcheck:
|
|
54
|
+
test: ["CMD-SHELL", "curl -sf -u solr:SolrRocks http://localhost:8983/solr/admin/info/system || exit 1"]
|
|
55
|
+
interval: 10s
|
|
56
|
+
timeout: 5s
|
|
57
|
+
retries: 12
|
|
58
|
+
start_period: 30s
|
|
59
|
+
|
|
60
|
+
zoo10:
|
|
61
|
+
image: zookeeper
|
|
62
|
+
ports:
|
|
63
|
+
- 6784:2181
|
|
29
64
|
environment:
|
|
30
65
|
ZOO_MY_ID: 1
|
|
31
66
|
ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
|
data/env.test.solr10
ADDED
data/lib/solr_cloud/alias.rb
CHANGED
|
@@ -41,13 +41,13 @@ module SolrCloud
|
|
|
41
41
|
# @return [Collection] the now-current collection
|
|
42
42
|
def switch_collection_to(coll)
|
|
43
43
|
collect_name = case coll
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
when String
|
|
45
|
+
coll
|
|
46
|
+
when Collection
|
|
47
|
+
coll.name
|
|
48
|
+
else
|
|
49
|
+
raise "Alias#switch_collection_to only takes a name(string) or a collection, not '#{coll}'"
|
|
50
|
+
end
|
|
51
51
|
raise NoSuchCollectionError unless connection.has_collection?(collect_name)
|
|
52
52
|
connection.create_alias(name: name, collection_name: collect_name, force: true)
|
|
53
53
|
end
|
|
@@ -55,10 +55,10 @@ module SolrCloud
|
|
|
55
55
|
|
|
56
56
|
def ==(other)
|
|
57
57
|
case other
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
when SolrCloud::Collection
|
|
59
|
+
collection.name == other.collection.name
|
|
60
|
+
else
|
|
61
|
+
false
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ module SolrCloud
|
|
|
68
68
|
def delete!
|
|
69
69
|
return connection unless exist?
|
|
70
70
|
raise CollectionAliasedError.new("Cannot delete collection #{name}; it has alias(s) #{alias_names.join(", ")}") if aliased?
|
|
71
|
-
connection.get("solr/admin/collections", {
|
|
71
|
+
connection.get("solr/admin/collections", {action: "DELETE", name: name})
|
|
72
72
|
connection
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -98,8 +98,15 @@ module SolrCloud
|
|
|
98
98
|
|
|
99
99
|
# Access to the root info from the api. Mostly for internal use, but not labeled as such
|
|
100
100
|
# 'cause users will almost certainly find a use for it.
|
|
101
|
+
#
|
|
102
|
+
# Uses the V1 +CLUSTERSTATUS+ action rather than the V2 +api/collections/{name}+
|
|
103
|
+
# endpoint because the V2 response envelope changed between Solr 8 and Solr 9/10
|
|
104
|
+
# (the +cluster+ wrapper key is absent in Solr 10). The V1 CLUSTERSTATUS response
|
|
105
|
+
# consistently returns +cluster.collections.{name}+ on all supported versions.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash] the collection status hash as returned by Solr
|
|
101
108
|
def info
|
|
102
|
-
connection.get("
|
|
109
|
+
connection.get("solr/admin/collections", action: "CLUSTERSTATUS", collection: name).body["cluster"]["collections"][name]
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
# Reported as healthy?
|
|
@@ -181,10 +188,10 @@ module SolrCloud
|
|
|
181
188
|
|
|
182
189
|
def add(docs)
|
|
183
190
|
docarray = if docs === Array
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
docs
|
|
192
|
+
else
|
|
193
|
+
[docs]
|
|
194
|
+
end
|
|
188
195
|
post("solr/#{name}/update/") do |r|
|
|
189
196
|
r.body = docarray
|
|
190
197
|
end
|
|
@@ -200,10 +207,10 @@ module SolrCloud
|
|
|
200
207
|
def inspect
|
|
201
208
|
anames = alias_names
|
|
202
209
|
astring = if anames.empty?
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
""
|
|
211
|
+
else
|
|
212
|
+
" (aliased by #{anames.map { |x| "'#{x}'" }.join(", ")})"
|
|
213
|
+
end
|
|
207
214
|
"<#{self.class} '#{name}'#{astring}>"
|
|
208
215
|
end
|
|
209
216
|
|
data/lib/solr_cloud/configset.rb
CHANGED
|
@@ -5,7 +5,6 @@ module SolrCloud
|
|
|
5
5
|
# methods having to do with aliases, to be included by the connection object.
|
|
6
6
|
# These are split out only to make it easier to deal with them.
|
|
7
7
|
module AliasAdmin
|
|
8
|
-
|
|
9
8
|
# A simple data-class to pair an alias with its collection
|
|
10
9
|
AliasCollectionPair = Struct.new(:alias, :collection)
|
|
11
10
|
|
|
@@ -24,7 +23,7 @@ module SolrCloud
|
|
|
24
23
|
end
|
|
25
24
|
raise NoSuchCollectionError.new("Can't find collection #{collection_name}") unless has_collection?(collection_name)
|
|
26
25
|
if has_alias?(name) && !force
|
|
27
|
-
raise WontOverwriteError.new("Alias '#{name}' already points to collection '#{
|
|
26
|
+
raise WontOverwriteError.new("Alias '#{name}' already points to collection '#{get_alias(name).collection.name}'; won't overwrite without force: true")
|
|
28
27
|
end
|
|
29
28
|
connection.get("solr/admin/collections", action: "CREATEALIAS", name: name, collections: collection_name)
|
|
30
29
|
get_alias(name)
|
|
@@ -75,13 +74,17 @@ module SolrCloud
|
|
|
75
74
|
end
|
|
76
75
|
end
|
|
77
76
|
|
|
78
|
-
# The "raw" alias map, which just maps alias names to collection names
|
|
79
|
-
#
|
|
77
|
+
# The "raw" alias map, which just maps alias names to collection names.
|
|
78
|
+
#
|
|
79
|
+
# The Solr +LISTALIASES+ action omits the +"aliases"+ key entirely (returning +null+
|
|
80
|
+
# or an absent key) when no aliases are defined. This is true on Solr 10 and can
|
|
81
|
+
# occur on earlier versions as well. The +|| {}+ guard ensures callers always receive
|
|
82
|
+
# a Hash rather than +nil+.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash<String, String>] map of alias name -> collection name, empty if none exist
|
|
80
85
|
def raw_alias_map
|
|
81
|
-
connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"]
|
|
86
|
+
connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"] || {}
|
|
82
87
|
end
|
|
83
88
|
end
|
|
84
89
|
end
|
|
85
90
|
end
|
|
86
|
-
|
|
87
|
-
|
|
@@ -20,17 +20,16 @@ module SolrCloud
|
|
|
20
20
|
# @raise [WontOverwriteError] if the collection already exists
|
|
21
21
|
# @return [Collection] the collection created
|
|
22
22
|
def create_collection(name:, configset:, shards: 1, replication_factor: 1)
|
|
23
|
-
|
|
24
23
|
unless legal_solr_name?(name)
|
|
25
24
|
raise IllegalNameError.new("'#{name}' is not a valid solr name. Use only ASCII letters/numbers, dash, and underscore")
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
configset_name = case configset
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
when Configset
|
|
29
|
+
configset.name
|
|
30
|
+
else
|
|
31
|
+
configset.to_s
|
|
32
|
+
end
|
|
34
33
|
raise WontOverwriteError.new("Collection #{name} already exists") if has_collection?(name)
|
|
35
34
|
raise NoSuchConfigSetError.new("Configset '#{configset_name}' doesn't exist") unless has_configset?(configset_name)
|
|
36
35
|
|
|
@@ -4,16 +4,30 @@ require "zip"
|
|
|
4
4
|
|
|
5
5
|
module SolrCloud
|
|
6
6
|
class Connection
|
|
7
|
-
#
|
|
7
|
+
# Methods having to do with configsets, to be included by the connection object.
|
|
8
8
|
# These are split out only to make it easier to deal with them.
|
|
9
9
|
module ConfigsetAdmin
|
|
10
|
-
|
|
11
10
|
# Given the path to a solr configuration "conf" directory (i.e., the one with
|
|
12
11
|
# solrconfig.xml in it), zip it up and send it to solr as a new configset.
|
|
12
|
+
#
|
|
13
|
+
# The upload API endpoint differs across major Solr versions:
|
|
14
|
+
#
|
|
15
|
+
# - *Solr 8*: uses the V2 API (+PUT /api/cluster/configs/<name>+), which was the
|
|
16
|
+
# canonical configset path in Solr 8.
|
|
17
|
+
# - *Solr 9+*: uses the V2 API (+PUT /api/configsets/<name>+).
|
|
18
|
+
# The V2 configset path moved from +/api/cluster/configs/<name>+ (Solr 8) to
|
|
19
|
+
# +/api/configsets/<name>+ (Solr 9+). The new V2 PUT defaults to overwriting an
|
|
20
|
+
# existing configset, which means in-use configsets can be replaced without
|
|
21
|
+
# deleting them first.
|
|
22
|
+
#
|
|
23
|
+
# Dispatches via {Connection#solr9_or_later?}; see {#upload_configset_v9} and
|
|
24
|
+
# {#upload_configset_v8} for the version-specific implementations.
|
|
25
|
+
#
|
|
13
26
|
# @param name [String] Name to give the new configset
|
|
14
27
|
# @param confdir [String, Pathname] Path to the solr configuration "conf" directory
|
|
15
28
|
# @param force [Boolean] Whether or not to overwrite an existing configset if there is one
|
|
16
|
-
# @raise [
|
|
29
|
+
# @raise [IllegalNameError] if +name+ is not a valid Solr identifier
|
|
30
|
+
# @raise [WontOverwriteError] if the configset already exists and +force+ is false
|
|
17
31
|
# @return [Configset] the configset created
|
|
18
32
|
def create_configset(name:, confdir:, force: false)
|
|
19
33
|
config_set_name = name
|
|
@@ -24,36 +38,58 @@ module SolrCloud
|
|
|
24
38
|
if has_configset?(config_set_name) && !force
|
|
25
39
|
raise WontOverwriteError.new("Won't replace configset #{config_set_name} unless 'force: true' passed ")
|
|
26
40
|
end
|
|
41
|
+
|
|
27
42
|
zfile = "#{Dir.tmpdir}/solr_add_configset_#{name}_#{Time.now.hash}.zip"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
ZipFileGenerator.new(confdir, zfile).write
|
|
44
|
+
|
|
45
|
+
if solr9_or_later?
|
|
46
|
+
upload_configset_v9(config_set_name, zfile)
|
|
47
|
+
else
|
|
48
|
+
upload_configset_v8(config_set_name, zfile)
|
|
32
49
|
end
|
|
33
|
-
|
|
34
|
-
FileUtils.rm(zfile, force: true)
|
|
50
|
+
|
|
35
51
|
get_configset(name)
|
|
52
|
+
ensure
|
|
53
|
+
FileUtils.rm(zfile, force: true) if zfile
|
|
36
54
|
end
|
|
37
55
|
|
|
38
|
-
# Get a list of the already-defined
|
|
39
|
-
# @return [Array<Configset>] possibly empty list of
|
|
56
|
+
# Get a list of the already-defined configsets.
|
|
57
|
+
# @return [Array<Configset>] possibly empty list of configsets
|
|
40
58
|
def configsets
|
|
41
59
|
configset_names.map { |cs| Configset.new(name: cs, connection: self) }
|
|
42
60
|
end
|
|
43
61
|
|
|
62
|
+
# Get the names of all defined configsets.
|
|
63
|
+
#
|
|
64
|
+
# The API endpoint differs across major Solr versions:
|
|
65
|
+
#
|
|
66
|
+
# - *Solr 8*: uses the V2 API (+GET /api/cluster/configs+).
|
|
67
|
+
# - *Solr 9+*: uses the V1 API (+GET /solr/admin/configs?action=LIST+).
|
|
68
|
+
# The V2 configset path changed between Solr 8 and Solr 9; V1 is used for
|
|
69
|
+
# consistency on Solr 9 and later.
|
|
70
|
+
#
|
|
44
71
|
# @return [Array<String>] the names of the config sets
|
|
45
72
|
def configset_names
|
|
46
|
-
|
|
73
|
+
if solr9_or_later?
|
|
74
|
+
connection.get("solr/admin/configs", action: "LIST").body["configSets"]
|
|
75
|
+
else
|
|
76
|
+
connection.get("api/cluster/configs").body["configSets"]
|
|
77
|
+
end
|
|
47
78
|
end
|
|
48
79
|
|
|
49
|
-
# Check to see if a configset is defined
|
|
50
|
-
# @param name [String] Name of the
|
|
80
|
+
# Check to see if a configset with the given name is defined.
|
|
81
|
+
# @param name [String] Name of the configset
|
|
51
82
|
# @return [Boolean] Whether a configset with that name exists
|
|
52
83
|
def has_configset?(name)
|
|
53
84
|
configset_names.include? name.to_s
|
|
54
85
|
end
|
|
55
86
|
|
|
56
|
-
# Get an existing configset
|
|
87
|
+
# Get an existing configset by name.
|
|
88
|
+
#
|
|
89
|
+
# @note Does not verify that the configset actually exists; use {#has_configset?}
|
|
90
|
+
# to check first if needed.
|
|
91
|
+
# @param name [String] Name of the configset
|
|
92
|
+
# @return [Configset]
|
|
57
93
|
def get_configset(name)
|
|
58
94
|
Configset.new(name: name, connection: self)
|
|
59
95
|
end
|
|
@@ -64,12 +100,25 @@ module SolrCloud
|
|
|
64
100
|
#
|
|
65
101
|
# In general, prefer using {Configset#delete!} instead of running everything
|
|
66
102
|
# through the connection object.
|
|
67
|
-
#
|
|
68
|
-
#
|
|
103
|
+
#
|
|
104
|
+
# The delete API endpoint differs across major Solr versions:
|
|
105
|
+
#
|
|
106
|
+
# - *Solr 8*: uses the V2 API (+DELETE /api/cluster/configs/<name>+).
|
|
107
|
+
# - *Solr 9+*: uses the V1 API (+GET /solr/admin/configs?action=DELETE+).
|
|
108
|
+
# The V2 configset path changed between Solr 8 and Solr 9; V1 is used for
|
|
109
|
+
# consistency on Solr 9 and later.
|
|
110
|
+
#
|
|
111
|
+
# @param name [String] The name of the configuration set
|
|
112
|
+
# @raise [ConfigSetInUseError] if the configset can't be deleted because it's in use
|
|
113
|
+
# by a live collection
|
|
69
114
|
# @return [Connection] self
|
|
70
115
|
def delete_configset(name)
|
|
71
|
-
if has_configset?
|
|
72
|
-
|
|
116
|
+
if has_configset?(name)
|
|
117
|
+
if solr9_or_later?
|
|
118
|
+
connection.get("solr/admin/configs", action: "DELETE", name: name)
|
|
119
|
+
else
|
|
120
|
+
connection.delete("api/cluster/configs/#{name}")
|
|
121
|
+
end
|
|
73
122
|
end
|
|
74
123
|
self
|
|
75
124
|
rescue Faraday::BadRequestError => e
|
|
@@ -126,6 +175,43 @@ module SolrCloud
|
|
|
126
175
|
zipfile.add(zipfile_path, disk_file_path)
|
|
127
176
|
end
|
|
128
177
|
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
# Upload a configset zip using the Solr 9+ V2 API.
|
|
182
|
+
#
|
|
183
|
+
# Sends a +PUT+ to +/api/configsets/<name>+ with the zip file as an
|
|
184
|
+
# octet-stream body. The V2 configset upload path moved from
|
|
185
|
+
# +/api/cluster/configs/<name>+ (Solr 8) to +/api/configsets/<name>+ (Solr 9+).
|
|
186
|
+
# The new V2 PUT defaults to overwriting any existing configset at that name,
|
|
187
|
+
# so in-use configsets can be replaced without deleting first.
|
|
188
|
+
#
|
|
189
|
+
# @param name [String] configset name
|
|
190
|
+
# @param zipfile_path [String] filesystem path to the zip archive to upload
|
|
191
|
+
# @return [void]
|
|
192
|
+
def upload_configset_v9(name, zipfile_path)
|
|
193
|
+
connection.put("api/configsets/#{name}") do |req|
|
|
194
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
195
|
+
req.body = File.binread(zipfile_path)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Upload a configset zip using the Solr 8 V2 API.
|
|
200
|
+
#
|
|
201
|
+
# Sends a +PUT+ to +/api/cluster/configs/<name>+ with the zip file as an
|
|
202
|
+
# octet-stream body. This was the canonical V2 configset upload path in Solr 8.
|
|
203
|
+
# The path changed to +/api/configsets/<name>+ in Solr 9; use {#upload_configset_v1}
|
|
204
|
+
# for Solr 9 and later.
|
|
205
|
+
#
|
|
206
|
+
# @param name [String] configset name
|
|
207
|
+
# @param zipfile_path [String] filesystem path to the zip archive to upload
|
|
208
|
+
# @return [void]
|
|
209
|
+
def upload_configset_v8(name, zipfile_path)
|
|
210
|
+
connection.put("api/cluster/configs/#{name}") do |req|
|
|
211
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
212
|
+
req.body = File.binread(zipfile_path)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
129
215
|
end
|
|
130
216
|
end
|
|
131
217
|
end
|
|
@@ -31,12 +31,20 @@ module SolrCloud
|
|
|
31
31
|
# @return [String] String representation of the URL to solr
|
|
32
32
|
attr_reader :url
|
|
33
33
|
|
|
34
|
+
# @return [String] solr user
|
|
35
|
+
attr_reader :user
|
|
36
|
+
alias_method :username, :user
|
|
37
|
+
|
|
38
|
+
# @return [String] Solr password
|
|
39
|
+
attr_reader :password
|
|
40
|
+
|
|
34
41
|
# @return [#info] The logger
|
|
35
42
|
attr_reader :logger
|
|
36
43
|
|
|
37
44
|
# @return [Faraday::Connection] the underlying Faraday connection
|
|
38
45
|
attr_reader :connection
|
|
39
46
|
|
|
47
|
+
|
|
40
48
|
# let the underlying connection handle HTTP verbs
|
|
41
49
|
|
|
42
50
|
# @!method get
|
|
@@ -72,12 +80,12 @@ module SolrCloud
|
|
|
72
80
|
@user = user
|
|
73
81
|
@password = password
|
|
74
82
|
@logger = case logger
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
when :off, :none
|
|
84
|
+
Logger.new(File::NULL, level: Logger::FATAL)
|
|
85
|
+
when nil
|
|
86
|
+
Logger.new($stderr, level: Logger::WARN)
|
|
87
|
+
else
|
|
88
|
+
logger
|
|
81
89
|
end
|
|
82
90
|
@connection = create_raw_connection(url: url, adapter: adapter, user: user, password: password, logger: @logger)
|
|
83
91
|
bail_if_incompatible!
|
|
@@ -112,10 +120,6 @@ module SolrCloud
|
|
|
112
120
|
end
|
|
113
121
|
end
|
|
114
122
|
|
|
115
|
-
# Allow accessing the raw_connection via "connection". Yes, connection.connection
|
|
116
|
-
# can be confusing, but it makes the *_admin stuff easier to read.
|
|
117
|
-
alias_method :connection, :connection
|
|
118
|
-
|
|
119
123
|
# Check to see if we can actually talk to the solr in question
|
|
120
124
|
# raise [UnsupportedSolr] if the solr version isn't at least 8
|
|
121
125
|
# raise [ConnectionFailed] if we can't connect for some reason
|
|
@@ -172,6 +176,17 @@ module SolrCloud
|
|
|
172
176
|
_version_part_int(2)
|
|
173
177
|
end
|
|
174
178
|
|
|
179
|
+
# Returns true if the connected Solr instance is version 9 or later (9, 10, 11, ...).
|
|
180
|
+
#
|
|
181
|
+
# Several Solr APIs changed paths between Solr 8 and Solr 9. This predicate is used
|
|
182
|
+
# internally to dispatch to the correct API endpoint when behavior differs across major
|
|
183
|
+
# versions. External callers may also use it to gate version-specific logic.
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] true if the server reports major version >= 9
|
|
186
|
+
def solr9_or_later?
|
|
187
|
+
major_version >= 9
|
|
188
|
+
end
|
|
189
|
+
|
|
175
190
|
# Check to see if the given string follows solr's rules for thing
|
|
176
191
|
# Solr only allows ASCII letters and numbers, underscore, and dash,
|
|
177
192
|
# and it can't start with an underscore.
|
|
@@ -192,6 +207,3 @@ module SolrCloud
|
|
|
192
207
|
end
|
|
193
208
|
end
|
|
194
209
|
end
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
data/readme.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "pathname"
|
|
3
4
|
$LOAD_PATH.unshift Pathname.new(__dir__) + "lib"
|
|
4
5
|
require_relative "lib/solr_cloud/connection"
|
|
@@ -7,13 +8,11 @@ url = "http://localhost:9090"
|
|
|
7
8
|
user = "solr"
|
|
8
9
|
pass = "SolrRocks"
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
server = SolrCloud::Connection.new(url: url, user: user, password: pass) #=>
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
server.
|
|
15
|
-
server.
|
|
16
|
-
server.configsets.reject {|c| c.name == "_default"}.each {|c| c.delete!}
|
|
13
|
+
server.aliases.each { |a| a.delete! }
|
|
14
|
+
server.collections.each { |c| c.delete! }
|
|
15
|
+
server.configsets.reject { |c| c.name == "_default" }.each { |c| c.delete! }
|
|
17
16
|
|
|
18
17
|
# or bring your own Faraday object
|
|
19
18
|
# server2 = SolrCloud::Connection.new_with_faraday(faraday_connection)
|
|
@@ -83,8 +82,6 @@ begin
|
|
|
83
82
|
rescue
|
|
84
83
|
end
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
85
|
##### Aliases
|
|
89
86
|
|
|
90
87
|
# We'll want to alias it so we can just use 'cars'
|
|
@@ -125,9 +122,6 @@ cars.switch_collection_to("cars_v1")
|
|
|
125
122
|
server.collection_names
|
|
126
123
|
|
|
127
124
|
# They even == to each other
|
|
128
|
-
cars
|
|
129
|
-
cars == cars_v1
|
|
130
|
-
cars == cars_v2
|
|
131
125
|
|
|
132
126
|
# But sometimes you want to differentiate them from each other
|
|
133
127
|
server.only_collection_names
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Solr 8 / 9 / 10 Compatibility Update Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Make `solr_cloud-connection` work correctly with Solr 8, 9, and 10 simultaneously. The approach:
|
|
6
|
+
branch on `major_version` where APIs differ, and use V1 (legacy) APIs where they work uniformly
|
|
7
|
+
across versions.
|
|
8
|
+
|
|
9
|
+
Version detection is already in place in `connection.rb` via `major_version`, `minor_version`,
|
|
10
|
+
and `patch_version` methods derived from `GET /solr/admin/info/system`.
|
|
11
|
+
|
|
12
|
+
Source examined: `main` branch and `fix/solr-10-api-compatibility` branch.
|
|
13
|
+
API docs source: Apache Solr Reference Guide 10.0 (live, fetched May 2026).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Root Cause
|
|
18
|
+
|
|
19
|
+
The main branch uses Solr 8-era V2 API paths for configset management
|
|
20
|
+
(`api/cluster/configs/*`). These paths changed in Solr 9/10 to `api/configsets/*`.
|
|
21
|
+
Additionally, the V1 `LISTALIASES` action returns `null` (not `{}`) in Solr 10 when
|
|
22
|
+
no aliases exist, causing a `NoMethodError` on `nil.keys`.
|
|
23
|
+
|
|
24
|
+
The `fix/solr-10-api-compatibility` branch already addresses this by switching configset
|
|
25
|
+
operations wholesale to V1 APIs and adding a `|| {}` nil guard, but it does so without
|
|
26
|
+
version branching. This plan implements proper version-based dispatch.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## API Compatibility Matrix
|
|
31
|
+
|
|
32
|
+
| Method / File | Solr 8 (main branch) | Solr 9/10 compat? | Notes |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `configset_admin#create_configset` | `PUT api/cluster/configs/{name}` (V2) | NO | V2 path changed |
|
|
35
|
+
| `configset_admin#configset_names` | `GET api/cluster/configs` (V2) | NO | V2 path changed |
|
|
36
|
+
| `configset_admin#delete_configset` | `DELETE api/cluster/configs/{name}` (V2) | NO | V2 path changed |
|
|
37
|
+
| `alias_admin#raw_alias_map` | `GET solr/admin/collections?action=LISTALIASES` | PARTIAL | returns nil when empty |
|
|
38
|
+
| `alias_admin#create_alias` | V1 CREATEALIAS | YES | V1 works in all versions |
|
|
39
|
+
| `alias#delete!` | V1 DELETEALIAS | YES | V1 works in all versions |
|
|
40
|
+
| `collection_admin#only_collections` | `GET api/collections` (V2) | YES | same response format |
|
|
41
|
+
| `collection_admin#create_collection` | V1 CREATE | YES | V1 works in all versions |
|
|
42
|
+
| `collection#delete!` | V1 DELETE | YES | V1 works in all versions |
|
|
43
|
+
| `collection#info` | `GET api/collections/{name}` (V2) | UNKNOWN | needs testing; see below |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Changes Required
|
|
48
|
+
|
|
49
|
+
### 1. `connection.rb` -- add version-helper predicate
|
|
50
|
+
|
|
51
|
+
Add a convenience predicate to avoid repeating `major_version >= 9` comparisons inline:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Returns true for Solr 9 and later (including 10, 11, ...)
|
|
55
|
+
def solr9_or_later?
|
|
56
|
+
major_version >= 9
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Place it alongside the existing `major_version` / `minor_version` / `patch_version` methods.
|
|
61
|
+
No changes to `bail_if_incompatible!`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### 2. `connection/configset_admin.rb` -- version-branch all three methods
|
|
66
|
+
|
|
67
|
+
The V2 configset paths changed between Solr 8 and Solr 9:
|
|
68
|
+
|
|
69
|
+
| Operation | Solr 8 V2 path | Solr 9/10 V2 path | V1 path (all versions) |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| Upload | `PUT api/cluster/configs/{name}` | `PUT api/configsets/{name}` | `POST solr/admin/configs?action=UPLOAD&name={name}` |
|
|
72
|
+
| List | `GET api/cluster/configs` -> `["configSets"]` | `GET api/configsets` -> `["configSets"]` | `GET solr/admin/configs?action=LIST` -> `["configSets"]` |
|
|
73
|
+
| Delete | `DELETE api/cluster/configs/{name}` | `DELETE api/configsets/{name}` | `GET solr/admin/configs?action=DELETE&name={name}` |
|
|
74
|
+
|
|
75
|
+
Strategy: use V1 for Solr 9+, keep existing V2 for Solr 8. V1 works in Solr 8 too, so
|
|
76
|
+
an alternative is to use V1 for all versions and avoid branching entirely -- but the task
|
|
77
|
+
requires explicit version dispatch.
|
|
78
|
+
|
|
79
|
+
#### `create_configset`
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
def create_configset(name:, confdir:, force: false)
|
|
83
|
+
raise WontOverwriteError.new("Configset '#{name}' already exists") if has_configset?(name) && !force
|
|
84
|
+
zipfile = zip_configset_dir(confdir)
|
|
85
|
+
if connection.solr9_or_later?
|
|
86
|
+
connection.post("solr/admin/configs?action=UPLOAD&name=#{name}") do |req|
|
|
87
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
88
|
+
req.body = File.binread(zipfile)
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
connection.put("api/cluster/configs/#{name}") do |req|
|
|
92
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
93
|
+
req.body = File.binread(zipfile)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
get_configset(name)
|
|
97
|
+
ensure
|
|
98
|
+
FileUtils.rm_f(zipfile) if zipfile
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Note: the `fix` branch uses `connection.post("solr/admin/configs", action: "UPLOAD",
|
|
103
|
+
name: name)` but that places action/name as query params on a POST. The Solr V1 API
|
|
104
|
+
docs specify these as query string parameters for the UPLOAD action, so either form works.
|
|
105
|
+
Verify with integration tests.
|
|
106
|
+
|
|
107
|
+
#### `configset_names`
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
def configset_names
|
|
111
|
+
if connection.solr9_or_later?
|
|
112
|
+
connection.get("solr/admin/configs", action: "LIST").body["configSets"]
|
|
113
|
+
else
|
|
114
|
+
connection.get("api/cluster/configs").body["configSets"]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### `delete_configset`
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
def delete_configset(name:)
|
|
123
|
+
return unless has_configset?(name)
|
|
124
|
+
if connection.solr9_or_later?
|
|
125
|
+
connection.get("solr/admin/configs", action: "DELETE", name: name)
|
|
126
|
+
else
|
|
127
|
+
connection.delete("api/cluster/configs/#{name}")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### 3. `connection/alias_admin.rb` -- nil guard (bug fix, not version-specific)
|
|
135
|
+
|
|
136
|
+
`raw_alias_map` crashes with `NoMethodError` when no aliases exist because the Solr
|
|
137
|
+
`LISTALIASES` response omits the `aliases` key entirely (returns `null` or simply absent).
|
|
138
|
+
This is confirmed on Solr 10 and the fix is straightforward:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
def raw_alias_map
|
|
142
|
+
connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"] || {}
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This fix is already present in `fix/solr-10-api-compatibility`. It is not version-specific --
|
|
147
|
+
apply it unconditionally, as it is defensive coding.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 4. `lib/solr_cloud/collection.rb` -- verify `info` endpoint (needs testing)
|
|
152
|
+
|
|
153
|
+
`Collection#info` calls:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
connection.get("api/collections/#{name}").body["cluster"]["collections"][name]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The response path `body["cluster"]["collections"][name]` matches the structure returned by
|
|
160
|
+
the V1 `CLUSTERSTATUS` API and the V2 `GET /api/cluster` endpoint. Whether
|
|
161
|
+
`GET /api/collections/{name}` returns this same nested structure in Solr 9/10 is not
|
|
162
|
+
confirmed from docs alone.
|
|
163
|
+
|
|
164
|
+
**Action required:**
|
|
165
|
+
- Run integration tests against Solr 9 and Solr 10 containers with the existing code
|
|
166
|
+
- If `api/collections/{name}` returns a different response structure in Solr 9/10,
|
|
167
|
+
add version branching:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
def info
|
|
171
|
+
if connection.solr9_or_later?
|
|
172
|
+
# Fallback to V1 CLUSTERSTATUS filtered to this collection
|
|
173
|
+
connection.get("solr/admin/collections",
|
|
174
|
+
action: "CLUSTERSTATUS",
|
|
175
|
+
collection: name
|
|
176
|
+
).body["cluster"]["collections"][name]
|
|
177
|
+
else
|
|
178
|
+
connection.get("api/collections/#{name}").body["cluster"]["collections"][name]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
V1 `CLUSTERSTATUS` with `collection` param is confirmed to work in both Solr 8 and 10
|
|
184
|
+
and returns the exact same `cluster.collections.{name}` structure.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 5. No changes needed
|
|
189
|
+
|
|
190
|
+
The following use V1 APIs or V2 APIs that are stable across Solr 8, 9, and 10:
|
|
191
|
+
|
|
192
|
+
- `alias_admin#create_alias` -- V1 `CREATEALIAS`
|
|
193
|
+
- `alias#delete!` -- V1 `DELETEALIAS`
|
|
194
|
+
- `collection_admin#create_collection` -- V1 `CREATE`
|
|
195
|
+
- `collection_admin#only_collections` -- `GET api/collections` response format confirmed
|
|
196
|
+
unchanged in Solr 10 (`{"collections": [...]}`)
|
|
197
|
+
- `collection#delete!` -- V1 `DELETE`
|
|
198
|
+
- `connection#system` / `version_string` / `major_version` etc. -- `/solr/admin/info/system`
|
|
199
|
+
unchanged
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Implementation Pattern
|
|
204
|
+
|
|
205
|
+
Use private dispatch methods to keep the public interface clean:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# In ConfigsetAdmin module:
|
|
209
|
+
|
|
210
|
+
def create_configset(name:, confdir:, force: false)
|
|
211
|
+
raise WontOverwriteError.new(...) if has_configset?(name) && !force
|
|
212
|
+
zipfile = zip_configset_dir(confdir)
|
|
213
|
+
solr9_or_later? ? upload_configset_v1(name, zipfile) : upload_configset_v8(name, zipfile)
|
|
214
|
+
get_configset(name)
|
|
215
|
+
ensure
|
|
216
|
+
FileUtils.rm_f(zipfile) if zipfile
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def upload_configset_v1(name, zipfile)
|
|
222
|
+
connection.post("solr/admin/configs?action=UPLOAD&name=#{name}") do |req|
|
|
223
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
224
|
+
req.body = File.binread(zipfile)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def upload_configset_v8(name, zipfile)
|
|
229
|
+
connection.put("api/cluster/configs/#{name}") do |req|
|
|
230
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
231
|
+
req.body = File.binread(zipfile)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Where the dispatch is simple (one line each branch), inline `if/else` is acceptable.
|
|
237
|
+
Prefer private methods when the per-version logic is more than 2 lines.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Test Infrastructure
|
|
242
|
+
|
|
243
|
+
The existing spec infrastructure uses Docker Compose in `spec/data/solr_docker/`.
|
|
244
|
+
|
|
245
|
+
Add a Solr 10 service (and optionally Solr 9) alongside the existing Solr 8 service:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
# In spec/data/solr_docker/docker-compose.yml (new service):
|
|
249
|
+
solr10:
|
|
250
|
+
image: solr:10-slim
|
|
251
|
+
ports:
|
|
252
|
+
- "18984:8983"
|
|
253
|
+
command: solr-demo
|
|
254
|
+
# or whatever start command the existing solr8 service uses
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Update the integration test runner (likely a Rake task or script) to run the full spec
|
|
258
|
+
suite twice: once against Solr 8 and once against Solr 10 (passing the base URL as an
|
|
259
|
+
environment variable).
|
|
260
|
+
|
|
261
|
+
If the test suite already accepts a `SOLR_URL` environment variable, this is straightforward.
|
|
262
|
+
If it hardcodes a port, parameterize it.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Implementation Order
|
|
267
|
+
|
|
268
|
+
1. Add `solr9_or_later?` to `connection.rb`
|
|
269
|
+
2. Apply `|| {}` nil guard to `alias_admin.rb#raw_alias_map` (already in fix branch --
|
|
270
|
+
cherry-pick or re-apply)
|
|
271
|
+
3. Version-branch `configset_admin.rb`: `configset_names`, `delete_configset`,
|
|
272
|
+
`create_configset` (in that order -- names and delete first so tests can verify the
|
|
273
|
+
cycle without broken upload)
|
|
274
|
+
4. Add Solr 10 container to Docker Compose test setup
|
|
275
|
+
5. Run integration tests against both Solr 8 and Solr 10
|
|
276
|
+
6. If `Collection#info` fails against Solr 10, add version-branch (step 4 above)
|
|
277
|
+
7. Update gem version (patch or minor, depending on whether the nil-guard fix is considered
|
|
278
|
+
a breaking change)
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Notes on the fix/solr-10-api-compatibility Branch
|
|
283
|
+
|
|
284
|
+
That branch's approach (switch all configset ops to V1 unconditionally) is functionally
|
|
285
|
+
correct and simpler. If backward compatibility with the V2 Solr 8 behavior is not a concern,
|
|
286
|
+
that approach is acceptable and lower-risk than version branching. The user has explicitly
|
|
287
|
+
requested version-based dispatch, which this plan implements.
|
|
288
|
+
|
|
289
|
+
If Solr 8 V2 API support is dropped in a future major version of the gem, the branches
|
|
290
|
+
can be collapsed to use V1 everywhere.
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solr_cloud-connection
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bill Dueber
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: faraday
|
|
@@ -53,76 +52,107 @@ dependencies:
|
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '2.0'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
55
|
+
name: rake
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
|
-
- - "
|
|
58
|
+
- - "~>"
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
60
|
+
version: '13.0'
|
|
62
61
|
type: :development
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
|
-
- - "
|
|
65
|
+
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
67
|
+
version: '13.0'
|
|
69
68
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
69
|
+
name: rspec
|
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
|
72
71
|
requirements:
|
|
73
|
-
- - "
|
|
72
|
+
- - "~>"
|
|
74
73
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
74
|
+
version: '3.0'
|
|
76
75
|
type: :development
|
|
77
76
|
prerelease: false
|
|
78
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
78
|
requirements:
|
|
80
|
-
- - "
|
|
79
|
+
- - "~>"
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
81
|
+
version: '3.0'
|
|
83
82
|
- !ruby/object:Gem::Dependency
|
|
84
83
|
name: standard
|
|
85
84
|
requirement: !ruby/object:Gem::Requirement
|
|
86
85
|
requirements:
|
|
87
86
|
- - ">="
|
|
88
87
|
- !ruby/object:Gem::Version
|
|
89
|
-
version:
|
|
88
|
+
version: 1.35.0
|
|
89
|
+
- - "~>"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '1.0'
|
|
90
92
|
type: :development
|
|
91
93
|
prerelease: false
|
|
92
94
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
95
|
requirements:
|
|
94
96
|
- - ">="
|
|
95
97
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
98
|
+
version: 1.35.0
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.0'
|
|
97
102
|
- !ruby/object:Gem::Dependency
|
|
98
103
|
name: simplecov
|
|
99
104
|
requirement: !ruby/object:Gem::Requirement
|
|
100
105
|
requirements:
|
|
101
106
|
- - ">="
|
|
102
107
|
- !ruby/object:Gem::Version
|
|
103
|
-
version:
|
|
108
|
+
version: 0.22.0
|
|
109
|
+
- - "~>"
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0.0'
|
|
104
112
|
type: :development
|
|
105
113
|
prerelease: false
|
|
106
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
115
|
requirements:
|
|
108
116
|
- - ">="
|
|
109
117
|
- !ruby/object:Gem::Version
|
|
110
|
-
version:
|
|
118
|
+
version: 0.22.0
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0.0'
|
|
111
122
|
- !ruby/object:Gem::Dependency
|
|
112
123
|
name: yard
|
|
113
124
|
requirement: !ruby/object:Gem::Requirement
|
|
114
125
|
requirements:
|
|
115
126
|
- - ">="
|
|
116
127
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
128
|
+
version: 0.9.0
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 0.9.0
|
|
118
132
|
type: :development
|
|
119
133
|
prerelease: false
|
|
120
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
135
|
requirements:
|
|
122
136
|
- - ">="
|
|
123
137
|
- !ruby/object:Gem::Version
|
|
124
|
-
version:
|
|
125
|
-
|
|
138
|
+
version: 0.9.0
|
|
139
|
+
- - "~>"
|
|
140
|
+
- !ruby/object:Gem::Version
|
|
141
|
+
version: 0.9.0
|
|
142
|
+
- !ruby/object:Gem::Dependency
|
|
143
|
+
name: dotenv
|
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - "~>"
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: '3.0'
|
|
149
|
+
type: :development
|
|
150
|
+
prerelease: false
|
|
151
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - "~>"
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: '3.0'
|
|
126
156
|
email:
|
|
127
157
|
- bill@dueber.com
|
|
128
158
|
executables: []
|
|
@@ -134,11 +164,13 @@ files:
|
|
|
134
164
|
- ".standard.yml"
|
|
135
165
|
- CHANGELOG.md
|
|
136
166
|
- Dockerfile
|
|
167
|
+
- LICENSE.md
|
|
137
168
|
- LICENSE.txt
|
|
138
169
|
- README.md
|
|
139
170
|
- Rakefile
|
|
140
171
|
- compose.yml
|
|
141
172
|
- env.test
|
|
173
|
+
- env.test.solr10
|
|
142
174
|
- lib/solr_cloud/alias.rb
|
|
143
175
|
- lib/solr_cloud/collection.rb
|
|
144
176
|
- lib/solr_cloud/configset.rb
|
|
@@ -149,13 +181,13 @@ files:
|
|
|
149
181
|
- lib/solr_cloud/connection/version.rb
|
|
150
182
|
- lib/solr_cloud/errors.rb
|
|
151
183
|
- readme.rb
|
|
184
|
+
- solr10_compat_update_plan.md
|
|
152
185
|
homepage: https://github.com/mlibrary/solr_cloud-connection
|
|
153
186
|
licenses: []
|
|
154
187
|
metadata:
|
|
155
188
|
homepage_uri: https://github.com/mlibrary/solr_cloud-connection
|
|
156
189
|
source_code_uri: https://github.com/mlibrary/solr_cloud-connection
|
|
157
190
|
changelog_uri: https://github.com/mlibrary/solr_cloud-connection/CHANGELOG.md
|
|
158
|
-
post_install_message:
|
|
159
191
|
rdoc_options: []
|
|
160
192
|
require_paths:
|
|
161
193
|
- lib
|
|
@@ -170,8 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
170
202
|
- !ruby/object:Gem::Version
|
|
171
203
|
version: '0'
|
|
172
204
|
requirements: []
|
|
173
|
-
rubygems_version: 3.
|
|
174
|
-
signing_key:
|
|
205
|
+
rubygems_version: 3.6.7
|
|
175
206
|
specification_version: 4
|
|
176
207
|
summary: Do basic administrative operations on a solr cloud instance and collections
|
|
177
208
|
within
|