1
2
3
4
5
6 package org.tailormap.api.solr;
7
8 import static org.tailormap.api.scheduling.IndexTask.INDEX_KEY;
9
10 import jakarta.validation.constraints.NotNull;
11 import jakarta.validation.constraints.Positive;
12 import java.io.IOException;
13 import java.lang.invoke.MethodHandles;
14 import java.math.BigDecimal;
15 import java.time.Duration;
16 import java.time.Instant;
17 import java.time.OffsetDateTime;
18 import java.time.ZoneId;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.UUID;
26 import java.util.function.Consumer;
27 import org.apache.solr.client.solrj.RemoteSolrException;
28 import org.apache.solr.client.solrj.SolrClient;
29 import org.apache.solr.client.solrj.SolrResponse;
30 import org.apache.solr.client.solrj.SolrServerException;
31 import org.apache.solr.client.solrj.request.SolrQuery;
32 import org.apache.solr.client.solrj.request.schema.FieldTypeDefinition;
33 import org.apache.solr.client.solrj.request.schema.SchemaRequest;
34 import org.apache.solr.client.solrj.response.QueryResponse;
35 import org.apache.solr.client.solrj.response.UpdateResponse;
36 import org.apache.solr.client.solrj.response.schema.SchemaResponse;
37 import org.apache.solr.common.SolrDocumentList;
38 import org.apache.solr.common.SolrException;
39 import org.geotools.api.data.Query;
40 import org.geotools.api.data.SimpleFeatureSource;
41 import org.geotools.api.feature.simple.SimpleFeature;
42 import org.geotools.api.referencing.FactoryException;
43 import org.geotools.api.referencing.operation.MathTransform;
44 import org.geotools.data.simple.SimpleFeatureCollection;
45 import org.geotools.data.simple.SimpleFeatureIterator;
46 import org.jspecify.annotations.NonNull;
47 import org.jspecify.annotations.Nullable;
48 import org.locationtech.jts.geom.Geometry;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.tailormap.api.admin.model.SearchIndexSummary;
52 import org.tailormap.api.admin.model.TaskProgressEvent;
53 import org.tailormap.api.geotools.TransformationUtil;
54 import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
55 import org.tailormap.api.geotools.processing.GeometryProcessor;
56 import org.tailormap.api.persistence.SearchIndex;
57 import org.tailormap.api.persistence.TMFeatureType;
58 import org.tailormap.api.persistence.helper.GeoToolsHelper;
59 import org.tailormap.api.repository.SearchIndexRepository;
60 import org.tailormap.api.scheduling.TaskType;
61 import org.tailormap.api.util.Constants;
62 import org.tailormap.api.viewer.model.SearchDocument;
63 import org.tailormap.api.viewer.model.SearchResponse;
64
65
66
67
68
69
70 public class SolrHelper implements AutoCloseable, Constants {
71 private static final Logger logger =
72 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
73
74
75 private static final String SOLR_SPATIAL_FIELDNAME = "tm_geometry_rpt";
76
77 private final SolrClient solrClient;
78
79
80 private final Map<String, SchemaRequest.AddField> solrSearchFields = Map.of(
81 SEARCH_LAYER,
82 new SchemaRequest.AddField(Map.of(
83 "name", SEARCH_LAYER,
84 "type", "string",
85 "indexed", true,
86 "stored", true,
87 "multiValued", false,
88 "required", true,
89 "uninvertible", false)),
90 INDEX_GEOM_FIELD,
91 new SchemaRequest.AddField(
92 Map.of("name", INDEX_GEOM_FIELD, "type", SOLR_SPATIAL_FIELDNAME, "stored", true)),
93 INDEX_SEARCH_FIELD,
94 new SchemaRequest.AddField(Map.of(
95 "name", INDEX_SEARCH_FIELD,
96 "type", "text_general",
97 "indexed", true,
98 "stored", true,
99 "multiValued", true,
100 "required", true,
101 "uninvertible", false)),
102 INDEX_DISPLAY_FIELD,
103 new SchemaRequest.AddField(Map.of(
104 "name", INDEX_DISPLAY_FIELD,
105 "type", "text_general",
106 "indexed", false,
107 "stored", true,
108 "multiValued", true,
109 "required", true,
110 "uninvertible", false)));
111
112 private int solrQueryTimeout = 7000;
113 private int solrBatchSize = 1000;
114 private String solrGeometryValidationRule = "repairBuffer0";
115
116
117
118
119
120
121 public SolrHelper(@NotNull SolrClient solrClient) {
122 this.solrClient = solrClient;
123 }
124
125
126
127
128
129
130 public SolrHelper withQueryTimeout(
131 @Positive(message = "Must use a positive integer for query timeout") int solrQueryTimeout) {
132 this.solrQueryTimeout = solrQueryTimeout * 1000;
133 return this;
134 }
135
136
137
138
139
140
141 public SolrHelper withBatchSize(@Positive(message = "Must use a positive integer for batching") int solrBatchSize) {
142 this.solrBatchSize = solrBatchSize;
143 return this;
144 }
145
146
147
148
149
150
151
152
153 public SolrHelper withGeometryValidationRule(@NonNull String solrGeometryValidationRule) {
154 if (List.of("error", "none", "repairBuffer0", "repairConvexHull").contains(solrGeometryValidationRule)) {
155 logger.trace("Setting geometry validation rule for Solr geometry field to {}", solrGeometryValidationRule);
156 this.solrGeometryValidationRule = solrGeometryValidationRule;
157 }
158 return this;
159 }
160
161
162
163
164
165
166
167
168
169
170
171
172 @SuppressWarnings("FromTemporalAccessor")
173 public SearchIndex addFeatureTypeIndex(
174 @NotNull SearchIndex searchIndex,
175 @NotNull TMFeatureType tmFeatureType,
176 @NotNull FeatureSourceFactoryHelper featureSourceFactoryHelper,
177 @NotNull SearchIndexRepository searchIndexRepository)
178 throws IOException, SolrServerException {
179
180 Consumer<TaskProgressEvent> progressListener = (event) -> logger.debug("Progress event: {}", event);
181
182 return this.addFeatureTypeIndex(
183 searchIndex, tmFeatureType, featureSourceFactoryHelper, searchIndexRepository, progressListener, null);
184 }
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200 @SuppressWarnings("FromTemporalAccessor")
201 public SearchIndex addFeatureTypeIndex(
202 @NotNull SearchIndex searchIndex,
203 @NotNull TMFeatureType tmFeatureType,
204 @NotNull FeatureSourceFactoryHelper featureSourceFactoryHelper,
205 @NotNull SearchIndexRepository searchIndexRepository,
206 @NotNull Consumer<TaskProgressEvent> progressListener,
207 @Nullable UUID taskUuid)
208 throws IOException, SolrServerException {
209
210 createSchemaIfNotExists();
211
212 final Instant startedAt = Instant.now();
213 final OffsetDateTime startedAtOffset =
214 startedAt.atOffset(ZoneId.systemDefault().getRules().getOffset(startedAt));
215
216 if (null == taskUuid && null != searchIndex.getSchedule()) {
217
218
219
220 taskUuid = searchIndex.getSchedule().getUuid();
221 }
222
223 SearchIndexSummary summary =
224 new SearchIndexSummary().startedAt(startedAtOffset).total(0).duration(0.0);
225
226 if (null == searchIndex.getSearchFieldsUsed()) {
227 logger.warn(
228 "No search fields configured for search index: {}, skipping index {}.",
229 tmFeatureType.getName(),
230 searchIndex.getName());
231 return searchIndexRepository.save(searchIndex
232 .setStatus(SearchIndex.Status.ERROR)
233 .setSummary(summary.errorMessage("No search fields configured")));
234 }
235
236 TaskProgressEvent taskProgressEvent = new TaskProgressEvent()
237 .type(TaskType.INDEX.getValue())
238 .uuid(taskUuid)
239 .startedAt(startedAtOffset)
240 .progress(0)
241 .taskData(Map.of(INDEX_KEY, searchIndex.getId()));
242 progressListener.accept(taskProgressEvent);
243
244
245 List<String> searchFields = searchIndex.getSearchFieldsUsed().stream()
246 .filter(s -> !tmFeatureType.getSettings().getHideAttributes().contains(s))
247 .toList();
248 List<String> displayFields = searchIndex.getSearchDisplayFieldsUsed().stream()
249 .filter(s -> !tmFeatureType.getSettings().getHideAttributes().contains(s))
250 .toList();
251
252 if (searchFields.isEmpty()) {
253 logger.warn(
254 "No valid search fields configured for feature type: {}, skipping index {}.",
255 tmFeatureType.getName(),
256 searchIndex.getName());
257 return searchIndexRepository.save(searchIndex
258 .setStatus(SearchIndex.Status.ERROR)
259 .setSummary(summary.errorMessage("No search fields configured")));
260 }
261
262
263 Set<String> propertyNames = new HashSet<>();
264
265
266 if (null == tmFeatureType.getPrimaryKeyAttribute()) {
267 logger.error(
268 "No primary key attribute configured for feature type: {}, skipping index {}.",
269 tmFeatureType.getName(),
270 searchIndex.getName());
271 return searchIndexRepository.save(searchIndex
272 .setStatus(SearchIndex.Status.ERROR)
273 .setSummary(summary.errorMessage("No primary key attribute configured")));
274 }
275 propertyNames.add(tmFeatureType.getPrimaryKeyAttribute());
276 if (null == tmFeatureType.getDefaultGeometryAttribute()) {
277 logger.error(
278 "No default geometry attribute configured for feature type: {}, skipping index {}.",
279 tmFeatureType.getName(),
280 searchIndex.getName());
281 return searchIndexRepository.save(searchIndex
282 .setStatus(SearchIndex.Status.ERROR)
283 .setSummary(summary.errorMessage("No default geometry attribute configured")));
284 }
285 propertyNames.add(tmFeatureType.getDefaultGeometryAttribute());
286 propertyNames.addAll(searchFields);
287
288 if (!displayFields.isEmpty()) {
289 propertyNames.addAll(displayFields);
290 }
291
292 clearIndexForLayer(searchIndex.getId());
293
294 logger.info("Indexing started for index: {}, feature type: {}", searchIndex.getName(), tmFeatureType.getName());
295 searchIndex = searchIndexRepository.save(searchIndex.setStatus(SearchIndex.Status.INDEXING));
296
297 final int total;
298 List<FeatureIndexingDocument> docsBatch = new ArrayList<>(solrBatchSize);
299 UpdateResponse updateResponse;
300 int indexCounter = 0;
301 int indexSkippedCounter = 0;
302
303
304 SimpleFeatureSource fs = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmFeatureType);
305
306 try {
307 Query q = new Query(fs.getName().toString());
308
309 tmFeatureType.getSettings().getHideAttributes().forEach(propertyNames::remove);
310 if (propertyNames.isEmpty()) {
311 logger.warn(
312 "No valid properties to index for feature type: {}, skipping index {}.",
313 tmFeatureType.getName(),
314 searchIndex.getName());
315 return searchIndexRepository.save(searchIndex
316 .setStatus(SearchIndex.Status.ERROR)
317 .setSummary(summary.errorMessage(
318 "No valid properties to index. Check if any properties are hidden.")));
319 }
320 q.setPropertyNames(List.copyOf(propertyNames));
321 q.setStartIndex(0);
322
323
324 logger.trace("Indexing query: {}", q);
325 SimpleFeatureCollection simpleFeatureCollection = fs.getFeatures(q);
326 total = simpleFeatureCollection.size();
327
328
329 try (SimpleFeatureIterator iterator = simpleFeatureCollection.features()) {
330 searchIndex.setSourceCrs(
331 GeoToolsHelper.crsToString(fs.getSchema().getCoordinateReferenceSystem()));
332 while (iterator.hasNext()) {
333 indexCounter++;
334 SimpleFeature feature = iterator.next();
335
336 FeatureIndexingDocument doc = new FeatureIndexingDocument(feature.getID(), searchIndex.getId());
337 List<String> searchValues = new ArrayList<>();
338 List<String> displayValues = new ArrayList<>();
339 propertyNames.forEach(propertyName -> {
340 Object value = feature.getAttribute(propertyName);
341 if (value != null) {
342 if (value instanceof Geometry
343 && propertyName.equals(tmFeatureType.getDefaultGeometryAttribute())) {
344
345
346 doc.setGeometry(GeometryProcessor.processGeometry(value, true, true, null));
347 } else {
348 if (searchFields.contains(propertyName)) {
349 searchValues.add(value.toString());
350 }
351 if (displayFields.contains(propertyName)) {
352 displayValues.add(value.toString());
353 }
354 }
355 }
356 });
357 if (searchValues.isEmpty() || displayValues.isEmpty()) {
358
359 logger.trace(
360 "No search or display values found for feature: {} in feature type: {}, skipped for indexing",
361 feature.getID(),
362 tmFeatureType.getName());
363 indexSkippedCounter++;
364 } else if (doc.getGeometry() == null || doc.getGeometry().matches("^[A-Z]+\\s*EMPTY\\s*$")) {
365 logger.trace(
366 "No or empty geometry found for feature: {} in feature type: {}, skipped for indexing",
367 feature.getID(),
368 tmFeatureType.getName());
369 indexSkippedCounter++;
370 } else {
371 doc.setSearchFields(searchValues.toArray(new String[0]));
372 doc.setDisplayFields(displayValues.toArray(new String[0]));
373 docsBatch.add(doc);
374 }
375
376 if (indexCounter % solrBatchSize == 0) {
377 updateResponse = solrClient.addBeans(docsBatch, solrQueryTimeout);
378 logger.info(
379 "Added {} documents of {} to index, result status: {}",
380 indexCounter - indexSkippedCounter,
381 total,
382 updateResponse.getStatus());
383 progressListener.accept(
384 taskProgressEvent.total(total).progress((indexCounter - indexSkippedCounter)));
385 docsBatch.clear();
386 }
387 }
388 }
389 } finally {
390 if (fs.getDataStore() != null) fs.getDataStore().dispose();
391 }
392
393 if (!docsBatch.isEmpty()) {
394 solrClient.addBeans(docsBatch, solrQueryTimeout);
395 logger.info("Added last {} documents of {} to index", docsBatch.size(), total);
396 progressListener.accept(taskProgressEvent
397 .progress((indexCounter - indexSkippedCounter))
398 .total(total));
399 }
400 final Instant finishedAt = Instant.now();
401 final OffsetDateTime finishedAtOffset =
402 finishedAt.atOffset(ZoneId.systemDefault().getRules().getOffset(finishedAt));
403 Duration processTime = Duration.between(startedAt, finishedAt).abs();
404 logger.info(
405 "Indexing finished for index: {}, feature type: {} at {} in {}",
406 searchIndex.getName(),
407 tmFeatureType.getName(),
408 finishedAtOffset,
409 processTime);
410 updateResponse = this.solrClient.commit();
411 logger.trace("Update response commit status: {}", updateResponse.getStatus());
412
413 if (indexSkippedCounter > 0) {
414 logger.warn(
415 "{} features were skipped because no search or display values were found.", indexSkippedCounter);
416 }
417
418 return searchIndexRepository.save(searchIndex
419 .setLastIndexed(finishedAtOffset)
420 .setStatus(SearchIndex.Status.INDEXED)
421 .setSummary(summary.total(total)
422 .skippedCounter(indexSkippedCounter)
423 .duration(BigDecimal.valueOf(processTime.getSeconds())
424 .add(BigDecimal.valueOf(processTime.getNano(), 9))
425 .doubleValue())
426 .errorMessage(null)));
427 }
428
429
430
431
432
433
434
435
436 public void clearIndexForLayer(@NotNull Long searchLayerId) throws IOException, SolrServerException {
437
438 QueryResponse response =
439 solrClient.query(new SolrQuery("exists(query(" + SEARCH_LAYER + ":" + searchLayerId + "))"));
440 if (response.getResults().getNumFound() > 0) {
441 logger.info("Clearing index for searchLayer {}", searchLayerId);
442 UpdateResponse updateResponse = solrClient.deleteByQuery(SEARCH_LAYER + ":" + searchLayerId);
443 logger.trace("Delete response status: {}", updateResponse.getStatus());
444 updateResponse = solrClient.commit();
445 logger.trace("Commit response status: {}", updateResponse.getStatus());
446 } else {
447 logger.info("No index to clear for layer {}", searchLayerId);
448 }
449 }
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464 public SearchResponse findInIndex(
465 @NotNull SearchIndex searchIndex,
466 String solrQuery,
467 String solrFilterQuery,
468 String solrPoint,
469 Double solrDistance,
470 int start,
471 int numResultsToReturn,
472 String applicationCrs)
473 throws IOException, SolrServerException, SolrException {
474
475 if (null == solrQuery || solrQuery.isBlank()) {
476 solrQuery = "*";
477 }
478
479 logger.info("Query index for '{}' in {} (id {})", solrQuery, searchIndex.getName(), searchIndex.getId());
480
481 MathTransform toApplication = null;
482 MathTransform toSource = null;
483
484 if (searchIndex.getSourceCrs() != null && applicationCrs != null) {
485 try {
486 toApplication = TransformationUtil.getTransformation(searchIndex.getSourceCrs(), applicationCrs);
487 toSource = TransformationUtil.getTransformation(applicationCrs, searchIndex.getSourceCrs());
488 } catch (FactoryException e) {
489 logger.error(
490 "Could not create transformations between search index CRS {} and application CRS {}",
491 searchIndex.getSourceCrs(),
492 applicationCrs,
493 e);
494 }
495 }
496
497 final MathTransform resultTransform = toApplication;
498
499
500
501
502
503 final SolrQuery query = new SolrQuery(INDEX_SEARCH_FIELD + ":" + solrQuery)
504 .setShowDebugInfo(logger.isDebugEnabled())
505 .setTimeAllowed(solrQueryTimeout)
506 .setIncludeScore(true)
507 .setFields(SEARCH_ID_FIELD, INDEX_DISPLAY_FIELD, INDEX_GEOM_FIELD)
508 .addFilterQuery(SEARCH_LAYER + ":" + searchIndex.getId())
509 .setSort("score", SolrQuery.ORDER.desc)
510 .addSort(SEARCH_ID_FIELD, SolrQuery.ORDER.asc)
511 .setRows(numResultsToReturn)
512 .setStart(start);
513
514 if (null != solrFilterQuery && !solrFilterQuery.isBlank()) {
515 query.addFilterQuery(solrFilterQuery);
516 }
517 if (null != solrPoint && null != solrDistance) {
518 if (null == solrFilterQuery
519 || !(solrFilterQuery.startsWith("{!geofilt") || solrFilterQuery.startsWith("{!bbox"))) {
520 query.addFilterQuery("{!geofilt sfield=" + INDEX_GEOM_FIELD + "}");
521 }
522
523 String transformedSolrPoint = solrPoint;
524
525 if (toSource != null) {
526 Geometry pointGeometry = GeometryProcessor.wktToGeometry("POINT (" + solrPoint + ")");
527 if (pointGeometry != null) {
528 Geometry transformedPoint = GeometryProcessor.transformGeometry(pointGeometry, toSource);
529 transformedSolrPoint =
530 transformedPoint.getCoordinate().x + " " + transformedPoint.getCoordinate().y;
531 }
532 }
533
534 query.add("pt", transformedSolrPoint);
535 query.add("d", solrDistance.toString());
536 }
537 query.set("q.op", "AND");
538 logger.debug("Solr query: {}", query);
539
540 final QueryResponse response = solrClient.query(query);
541 logger.trace("response: {}", response);
542
543 final SolrDocumentList solrDocumentList = response.getResults();
544 logger.debug("Found {} solr documents", solrDocumentList.getNumFound());
545 final SearchResponse searchResponse = new SearchResponse()
546 .total(solrDocumentList.getNumFound())
547 .start(response.getResults().getStart())
548 .maxScore(solrDocumentList.getMaxScore());
549 response.getResults().forEach(solrDocument -> {
550 List<String> displayValues = solrDocument.getFieldValues(INDEX_DISPLAY_FIELD).stream()
551 .map(Object::toString)
552 .toList();
553 Object geom = solrDocument.getFieldValue(INDEX_GEOM_FIELD);
554 if (geom != null) {
555 String geometry = geom.toString();
556
557 if (resultTransform != null) {
558 Geometry parsedGeometry = GeometryProcessor.wktToGeometry(geometry);
559 if (parsedGeometry != null) {
560 geometry = GeometryProcessor.geometryToWKT(
561 GeometryProcessor.transformGeometry(parsedGeometry, resultTransform));
562 }
563 }
564
565 searchResponse.addDocumentsItem(new SearchDocument()
566 .fid(solrDocument.getFieldValue(SEARCH_ID_FIELD).toString())
567 .geometry(geometry)
568 .displayValues(displayValues));
569 }
570 });
571
572 return searchResponse;
573 }
574
575
576
577
578
579
580 @Override
581 public void close() throws IOException {
582 if (null != this.solrClient) this.solrClient.close();
583 }
584
585 private boolean checkSchemaIfFieldExists(String fieldName) {
586 SchemaRequest.Field fieldCheck = new SchemaRequest.Field(fieldName);
587 try {
588 SchemaResponse.FieldResponse isField = fieldCheck.process(solrClient);
589 logger.debug("Field {} exists", isField.getField());
590 return true;
591 } catch (SolrServerException | RemoteSolrException e) {
592 logger.debug("Field {} does not exist or could not be retrieved. Assuming it does not exist.", fieldName);
593 } catch (IOException e) {
594 logger.error("Tried getting field: {}, but failed.", fieldName, e);
595 }
596 return false;
597 }
598
599
600
601
602
603
604 private void createSchemaFieldIfNotExists(String fieldName) throws SolrServerException, IOException {
605 if (!checkSchemaIfFieldExists(fieldName)) {
606 logger.info("Creating Solr field {}.", fieldName);
607 SchemaRequest.AddField schemaRequest = solrSearchFields.get(fieldName);
608 SolrResponse response = schemaRequest.process(solrClient);
609 logger.debug("Field type {} created", response);
610 solrClient.commit();
611 }
612 }
613
614
615 private void createSchemaIfNotExists() {
616 solrSearchFields.forEach((key, value) -> {
617 try {
618 if (key.equals(INDEX_GEOM_FIELD)) {
619 createGeometryFieldTypeIfNotExists();
620 }
621 createSchemaFieldIfNotExists(key);
622 } catch (SolrServerException | IOException e) {
623 logger.error(
624 "Error creating schema field: {} indexing may fail. Details: {}",
625 key,
626 e.getLocalizedMessage(),
627 e);
628 }
629 });
630 }
631
632 private void createGeometryFieldTypeIfNotExists() throws SolrServerException, IOException {
633 SchemaRequest.FieldType fieldTypeCheck = new SchemaRequest.FieldType(SOLR_SPATIAL_FIELDNAME);
634 try {
635 SchemaResponse.FieldTypeResponse isFieldType = fieldTypeCheck.process(solrClient);
636 logger.debug("Field type {} exists", isFieldType.getFieldType());
637 return;
638 } catch (SolrServerException | RemoteSolrException e) {
639 logger.debug(
640 "Field type {} does not exist or could not be retrieved. Assuming it does not exist.",
641 SOLR_SPATIAL_FIELDNAME);
642 } catch (IOException e) {
643 logger.error("Tried getting field type: {}, but failed.", SOLR_SPATIAL_FIELDNAME, e);
644 }
645
646 logger.info(
647 "Creating Solr field type for {} with validation rule {}",
648 SOLR_SPATIAL_FIELDNAME,
649 solrGeometryValidationRule);
650 FieldTypeDefinition spatialFieldTypeDef = new FieldTypeDefinition();
651 Map<String, Object> spatialFieldAttributes = new HashMap<>(Map.of(
652 "name", SOLR_SPATIAL_FIELDNAME,
653 "class", "solr.SpatialRecursivePrefixTreeFieldType",
654 "spatialContextFactory", "JTS",
655 "geo", false,
656 "distanceUnits", "kilometers",
657 "distCalculator", "cartesian",
658 "format", "WKT",
659 "autoIndex", true,
660 "distErrPct", "0.025",
661 "maxDistErr", "0.001"));
662 spatialFieldAttributes.putAll(Map.of(
663 "prefixTree",
664 "packedQuad",
665
666
667 "validationRule",
668 this.solrGeometryValidationRule,
669
670
671 "worldBounds",
672
673 "ENVELOPE(-20037508.34, 20037508.34, 20048966.1, -20048966.1)"
674
675
676 ));
677 spatialFieldTypeDef.setAttributes(spatialFieldAttributes);
678 SchemaRequest.AddFieldType spatialFieldType = new SchemaRequest.AddFieldType(spatialFieldTypeDef);
679 spatialFieldType.process(solrClient);
680 solrClient.commit();
681 }
682 }