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
298 SimpleFeatureSource fs = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmFeatureType);
299
300 Query q = new Query(fs.getName().toString());
301
302 tmFeatureType.getSettings().getHideAttributes().forEach(propertyNames::remove);
303 if (propertyNames.isEmpty()) {
304 logger.warn(
305 "No valid properties to index for feature type: {}, skipping index {}.",
306 tmFeatureType.getName(),
307 searchIndex.getName());
308 return searchIndexRepository.save(searchIndex
309 .setStatus(SearchIndex.Status.ERROR)
310 .setSummary(
311 summary.errorMessage("No valid properties to index. Check if any properties are hidden.")));
312 }
313 q.setPropertyNames(List.copyOf(propertyNames));
314 q.setStartIndex(0);
315
316
317 logger.trace("Indexing query: {}", q);
318 SimpleFeatureCollection simpleFeatureCollection = fs.getFeatures(q);
319 final int total = simpleFeatureCollection.size();
320 List<FeatureIndexingDocument> docsBatch = new ArrayList<>(solrBatchSize);
321
322
323 UpdateResponse updateResponse;
324 int indexCounter = 0;
325 int indexSkippedCounter = 0;
326 try (SimpleFeatureIterator iterator = simpleFeatureCollection.features()) {
327 searchIndex.setSourceCrs(GeoToolsHelper.crsToString(fs.getSchema().getCoordinateReferenceSystem()));
328 while (iterator.hasNext()) {
329 indexCounter++;
330 SimpleFeature feature = iterator.next();
331
332 FeatureIndexingDocument doc = new FeatureIndexingDocument(feature.getID(), searchIndex.getId());
333 List<String> searchValues = new ArrayList<>();
334 List<String> displayValues = new ArrayList<>();
335 propertyNames.forEach(propertyName -> {
336 Object value = feature.getAttribute(propertyName);
337 if (value != null) {
338 if (value instanceof Geometry
339 && propertyName.equals(tmFeatureType.getDefaultGeometryAttribute())) {
340
341
342 doc.setGeometry(GeometryProcessor.processGeometry(value, true, true, null));
343 } else {
344 if (searchFields.contains(propertyName)) {
345 searchValues.add(value.toString());
346 }
347 if (displayFields.contains(propertyName)) {
348 displayValues.add(value.toString());
349 }
350 }
351 }
352 });
353 if (searchValues.isEmpty() || displayValues.isEmpty()) {
354
355 logger.trace(
356 "No search or display values found for feature: {} in feature type: {}, skipped for indexing",
357 feature.getID(),
358 tmFeatureType.getName());
359 indexSkippedCounter++;
360 } else if (doc.getGeometry() == null || doc.getGeometry().matches("^[A-Z]+\\s*EMPTY\\s*$")) {
361 logger.trace(
362 "No or empty geometry found for feature: {} in feature type: {}, skipped for indexing",
363 feature.getID(),
364 tmFeatureType.getName());
365 indexSkippedCounter++;
366 } else {
367 doc.setSearchFields(searchValues.toArray(new String[0]));
368 doc.setDisplayFields(displayValues.toArray(new String[0]));
369 docsBatch.add(doc);
370 }
371
372 if (indexCounter % solrBatchSize == 0) {
373 updateResponse = solrClient.addBeans(docsBatch, solrQueryTimeout);
374 logger.info(
375 "Added {} documents of {} to index, result status: {}",
376 indexCounter - indexSkippedCounter,
377 total,
378 updateResponse.getStatus());
379 progressListener.accept(
380 taskProgressEvent.total(total).progress((indexCounter - indexSkippedCounter)));
381 docsBatch.clear();
382 }
383 }
384 } finally {
385 if (fs.getDataStore() != null) fs.getDataStore().dispose();
386 }
387
388 if (!docsBatch.isEmpty()) {
389 solrClient.addBeans(docsBatch, solrQueryTimeout);
390 logger.info("Added last {} documents of {} to index", docsBatch.size(), total);
391 progressListener.accept(taskProgressEvent
392 .progress((indexCounter - indexSkippedCounter))
393 .total(total));
394 }
395 final Instant finishedAt = Instant.now();
396 final OffsetDateTime finishedAtOffset =
397 finishedAt.atOffset(ZoneId.systemDefault().getRules().getOffset(finishedAt));
398 Duration processTime = Duration.between(startedAt, finishedAt).abs();
399 logger.info(
400 "Indexing finished for index: {}, feature type: {} at {} in {}",
401 searchIndex.getName(),
402 tmFeatureType.getName(),
403 finishedAtOffset,
404 processTime);
405 updateResponse = this.solrClient.commit();
406 logger.trace("Update response commit status: {}", updateResponse.getStatus());
407
408 if (indexSkippedCounter > 0) {
409 logger.warn(
410 "{} features were skipped because no search or display values were found.", indexSkippedCounter);
411 }
412
413 return searchIndexRepository.save(searchIndex
414 .setLastIndexed(finishedAtOffset)
415 .setStatus(SearchIndex.Status.INDEXED)
416 .setSummary(summary.total(total)
417 .skippedCounter(indexSkippedCounter)
418 .duration(BigDecimal.valueOf(processTime.getSeconds())
419 .add(BigDecimal.valueOf(processTime.getNano(), 9))
420 .doubleValue())
421 .errorMessage(null)));
422 }
423
424
425
426
427
428
429
430
431 public void clearIndexForLayer(@NotNull Long searchLayerId) throws IOException, SolrServerException {
432
433 QueryResponse response =
434 solrClient.query(new SolrQuery("exists(query(" + SEARCH_LAYER + ":" + searchLayerId + "))"));
435 if (response.getResults().getNumFound() > 0) {
436 logger.info("Clearing index for searchLayer {}", searchLayerId);
437 UpdateResponse updateResponse = solrClient.deleteByQuery(SEARCH_LAYER + ":" + searchLayerId);
438 logger.trace("Delete response status: {}", updateResponse.getStatus());
439 updateResponse = solrClient.commit();
440 logger.trace("Commit response status: {}", updateResponse.getStatus());
441 } else {
442 logger.info("No index to clear for layer {}", searchLayerId);
443 }
444 }
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459 public SearchResponse findInIndex(
460 @NotNull SearchIndex searchIndex,
461 String solrQuery,
462 String solrFilterQuery,
463 String solrPoint,
464 Double solrDistance,
465 int start,
466 int numResultsToReturn,
467 String applicationCrs)
468 throws IOException, SolrServerException, SolrException {
469
470 if (null == solrQuery || solrQuery.isBlank()) {
471 solrQuery = "*";
472 }
473
474 logger.info("Query index for '{}' in {} (id {})", solrQuery, searchIndex.getName(), searchIndex.getId());
475
476 MathTransform toApplication = null;
477 MathTransform toSource = null;
478
479 if (searchIndex.getSourceCrs() != null && applicationCrs != null) {
480 try {
481 toApplication = TransformationUtil.getTransformation(searchIndex.getSourceCrs(), applicationCrs);
482 toSource = TransformationUtil.getTransformation(applicationCrs, searchIndex.getSourceCrs());
483 } catch (FactoryException e) {
484 logger.error(
485 "Could not create transformations between search index CRS {} and application CRS {}",
486 searchIndex.getSourceCrs(),
487 applicationCrs,
488 e);
489 }
490 }
491
492 final MathTransform resultTransform = toApplication;
493
494
495
496
497
498 final SolrQuery query = new SolrQuery(INDEX_SEARCH_FIELD + ":" + solrQuery)
499 .setShowDebugInfo(logger.isDebugEnabled())
500 .setTimeAllowed(solrQueryTimeout)
501 .setIncludeScore(true)
502 .setFields(SEARCH_ID_FIELD, INDEX_DISPLAY_FIELD, INDEX_GEOM_FIELD)
503 .addFilterQuery(SEARCH_LAYER + ":" + searchIndex.getId())
504 .setSort("score", SolrQuery.ORDER.desc)
505 .addSort(SEARCH_ID_FIELD, SolrQuery.ORDER.asc)
506 .setRows(numResultsToReturn)
507 .setStart(start);
508
509 if (null != solrFilterQuery && !solrFilterQuery.isBlank()) {
510 query.addFilterQuery(solrFilterQuery);
511 }
512 if (null != solrPoint && null != solrDistance) {
513 if (null == solrFilterQuery
514 || !(solrFilterQuery.startsWith("{!geofilt") || solrFilterQuery.startsWith("{!bbox"))) {
515 query.addFilterQuery("{!geofilt sfield=" + INDEX_GEOM_FIELD + "}");
516 }
517
518 String transformedSolrPoint = solrPoint;
519
520 if (toSource != null) {
521 Geometry pointGeometry = GeometryProcessor.wktToGeometry("POINT (" + solrPoint + ")");
522 if (pointGeometry != null) {
523 Geometry transformedPoint = GeometryProcessor.transformGeometry(pointGeometry, toSource);
524 transformedSolrPoint =
525 transformedPoint.getCoordinate().x + " " + transformedPoint.getCoordinate().y;
526 }
527 }
528
529 query.add("pt", transformedSolrPoint);
530 query.add("d", solrDistance.toString());
531 }
532 query.set("q.op", "AND");
533 logger.debug("Solr query: {}", query);
534
535 final QueryResponse response = solrClient.query(query);
536 logger.trace("response: {}", response);
537
538 final SolrDocumentList solrDocumentList = response.getResults();
539 logger.debug("Found {} solr documents", solrDocumentList.getNumFound());
540 final SearchResponse searchResponse = new SearchResponse()
541 .total(solrDocumentList.getNumFound())
542 .start(response.getResults().getStart())
543 .maxScore(solrDocumentList.getMaxScore());
544 response.getResults().forEach(solrDocument -> {
545 List<String> displayValues = solrDocument.getFieldValues(INDEX_DISPLAY_FIELD).stream()
546 .map(Object::toString)
547 .toList();
548 Object geom = solrDocument.getFieldValue(INDEX_GEOM_FIELD);
549 if (geom != null) {
550 String geometry = geom.toString();
551
552 if (resultTransform != null) {
553 Geometry parsedGeometry = GeometryProcessor.wktToGeometry(geometry);
554 if (parsedGeometry != null) {
555 geometry = GeometryProcessor.geometryToWKT(
556 GeometryProcessor.transformGeometry(parsedGeometry, resultTransform));
557 }
558 }
559
560 searchResponse.addDocumentsItem(new SearchDocument()
561 .fid(solrDocument.getFieldValue(SEARCH_ID_FIELD).toString())
562 .geometry(geometry)
563 .displayValues(displayValues));
564 }
565 });
566
567 return searchResponse;
568 }
569
570
571
572
573
574
575 @Override
576 public void close() throws IOException {
577 if (null != this.solrClient) this.solrClient.close();
578 }
579
580 private boolean checkSchemaIfFieldExists(String fieldName) {
581 SchemaRequest.Field fieldCheck = new SchemaRequest.Field(fieldName);
582 try {
583 SchemaResponse.FieldResponse isField = fieldCheck.process(solrClient);
584 logger.debug("Field {} exists", isField.getField());
585 return true;
586 } catch (SolrServerException | RemoteSolrException e) {
587 logger.debug("Field {} does not exist or could not be retrieved. Assuming it does not exist.", fieldName);
588 } catch (IOException e) {
589 logger.error("Tried getting field: {}, but failed.", fieldName, e);
590 }
591 return false;
592 }
593
594
595
596
597
598
599 private void createSchemaFieldIfNotExists(String fieldName) throws SolrServerException, IOException {
600 if (!checkSchemaIfFieldExists(fieldName)) {
601 logger.info("Creating Solr field {}.", fieldName);
602 SchemaRequest.AddField schemaRequest = solrSearchFields.get(fieldName);
603 SolrResponse response = schemaRequest.process(solrClient);
604 logger.debug("Field type {} created", response);
605 solrClient.commit();
606 }
607 }
608
609
610 private void createSchemaIfNotExists() {
611 solrSearchFields.forEach((key, value) -> {
612 try {
613 if (key.equals(INDEX_GEOM_FIELD)) {
614 createGeometryFieldTypeIfNotExists();
615 }
616 createSchemaFieldIfNotExists(key);
617 } catch (SolrServerException | IOException e) {
618 logger.error(
619 "Error creating schema field: {} indexing may fail. Details: {}",
620 key,
621 e.getLocalizedMessage(),
622 e);
623 }
624 });
625 }
626
627 private void createGeometryFieldTypeIfNotExists() throws SolrServerException, IOException {
628 SchemaRequest.FieldType fieldTypeCheck = new SchemaRequest.FieldType(SOLR_SPATIAL_FIELDNAME);
629 try {
630 SchemaResponse.FieldTypeResponse isFieldType = fieldTypeCheck.process(solrClient);
631 logger.debug("Field type {} exists", isFieldType.getFieldType());
632 return;
633 } catch (SolrServerException | RemoteSolrException e) {
634 logger.debug(
635 "Field type {} does not exist or could not be retrieved. Assuming it does not exist.",
636 SOLR_SPATIAL_FIELDNAME);
637 } catch (IOException e) {
638 logger.error("Tried getting field type: {}, but failed.", SOLR_SPATIAL_FIELDNAME, e);
639 }
640
641 logger.info(
642 "Creating Solr field type for {} with validation rule {}",
643 SOLR_SPATIAL_FIELDNAME,
644 solrGeometryValidationRule);
645 FieldTypeDefinition spatialFieldTypeDef = new FieldTypeDefinition();
646 Map<String, Object> spatialFieldAttributes = new HashMap<>(Map.of(
647 "name", SOLR_SPATIAL_FIELDNAME,
648 "class", "solr.SpatialRecursivePrefixTreeFieldType",
649 "spatialContextFactory", "JTS",
650 "geo", false,
651 "distanceUnits", "kilometers",
652 "distCalculator", "cartesian",
653 "format", "WKT",
654 "autoIndex", true,
655 "distErrPct", "0.025",
656 "maxDistErr", "0.001"));
657 spatialFieldAttributes.putAll(Map.of(
658 "prefixTree",
659 "packedQuad",
660
661
662 "validationRule",
663 this.solrGeometryValidationRule,
664
665
666 "worldBounds",
667
668 "ENVELOPE(-20037508.34, 20037508.34, 20048966.1, -20048966.1)"
669
670
671 ));
672 spatialFieldTypeDef.setAttributes(spatialFieldAttributes);
673 SchemaRequest.AddFieldType spatialFieldType = new SchemaRequest.AddFieldType(spatialFieldTypeDef);
674 spatialFieldType.process(solrClient);
675 solrClient.commit();
676 }
677 }