Skip to content

Commit

Permalink
WIP [HTM-1076][HTM-1077] TMFeatureType event handler to propagate cha…
Browse files Browse the repository at this point in the history
…nges to solr indexes
  • Loading branch information
mprins committed Aug 29, 2024
1 parent b9a8c65 commit 0c16cad
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 19 deletions.
20 changes: 5 additions & 15 deletions src/main/java/org/tailormap/api/controller/SearchController.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,14 @@ public class SearchController {

private final SearchIndexRepository searchIndexRepository;

@Value("${tailormap-api.solr-url}")
private String solrUrl;

@Value("${tailormap-api.solr-core-name:tailormap}")
private String solrCoreName;

@Value("${tailormap-api.pageSize:100}")
private int numResultsToReturn;

public SearchController(SearchIndexRepository searchIndexRepository) {
private final SolrService solrService;

public SearchController(SearchIndexRepository searchIndexRepository, SolrService solrService) {
this.searchIndexRepository = searchIndexRepository;
this.solrService = solrService;
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -92,7 +89,7 @@ public ResponseEntity<Serializable> search(
"Layer '%s' does not have a search index"
.formatted(appTreeLayerNode.getLayerName())));

try (SolrClient solrClient = getSolrClient();
try (SolrClient solrClient = solrService.getSolrClientForSearching();
SolrHelper solrHelper = new SolrHelper(solrClient)) {
final SearchResponse searchResponse =
solrHelper.findInIndex(searchIndex, solrQuery, start, numResultsToReturn);
Expand All @@ -109,11 +106,4 @@ public ResponseEntity<Serializable> search(
HttpStatus.BAD_REQUEST, "Error while searching with given query", e);
}
}

private SolrClient getSolrClient() {
return new Http2SolrClient.Builder(solrUrl + solrCoreName)
.withConnectionTimeout(10, TimeUnit.SECONDS)
.withFollowRedirects(true)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class SolrAdminController {

private final FeatureTypeRepository featureTypeRepository;
private final SearchIndexRepository searchIndexRepository;
private final SolrService solrService;

public SolrAdminController(
FeatureSourceFactoryHelper featureSourceFactoryHelper,
Expand All @@ -65,6 +66,7 @@ public SolrAdminController(
this.featureSourceFactoryHelper = featureSourceFactoryHelper;
this.featureTypeRepository = featureTypeRepository;
this.searchIndexRepository = searchIndexRepository;
this.solrService = solrService;
}

/**
Expand All @@ -91,7 +93,7 @@ public SolrAdminController(
path = "${tailormap-api.admin.base-path}/index/ping",
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> pingSolr() {
try (SolrClient solrClient = getSolrClient()) {
try (SolrClient solrClient = solrService.getSolrClientForIndexing()) {
final SolrPingResponse ping = solrClient.ping();
logger.info("Solr ping status {}", ping.getResponse().get("status"));
return ResponseEntity.ok(
Expand Down Expand Up @@ -177,7 +179,7 @@ public ResponseEntity<?> index(@PathVariable Long searchIndexId) {
boolean createNewIndex =
(null == searchIndex.getLastIndexed()
|| searchIndex.getStatus() == SearchIndex.Status.INITIAL);
try (SolrClient solrClient = getSolrClient();
try (SolrClient solrClient = solrService.getSolrClientForIndexing();
SolrHelper solrHelper = new SolrHelper(solrClient)) {
solrHelper.addFeatureTypeIndex(searchIndex, indexingFT, featureSourceFactoryHelper);
searchIndexRepository.save(searchIndex);
Expand Down Expand Up @@ -226,7 +228,7 @@ public ResponseEntity<?> index(@PathVariable Long searchIndexId) {
produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
public ResponseEntity<?> clearIndex(@PathVariable Long searchIndexId) {
try (SolrClient solrClient = getSolrClient();
try (SolrClient solrClient = solrService.getSolrClientForIndexing();
SolrHelper solrHelper = new SolrHelper(solrClient)) {
solrHelper.clearIndexForLayer(searchIndexId);
// do not delete the SearchIndex metadata object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,17 @@ public interface GeoServiceRepository extends JpaRepository<GeoService, String>
@PreAuthorize("permitAll()")
@Query("from GeoService s where id not in :ids")
List<GeoService> getAllExcludingIds(@Param("ids") List<String> ids);

/**
* Find all geo-services that have a layer that is linked to a specific (Solr) index.
*
* @param indexId The index id to search for
*/
@NonNull
@PreAuthorize("permitAll()")
@Query(
value =
"select * from geo_service gs, lateral jsonb_path_query(gs.settings, ('$.layerSettings.**{1}.searchIndex.searchIndexId ? (@ == '||:indexId||')')::jsonpath)",
nativeQuery = true)
List<GeoService> findByIndexId(@Param("indexId") @NonNull Long indexId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright (C) 2024 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.repository.events;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import nl.b3p.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
import nl.b3p.tailormap.api.persistence.SearchIndex;
import nl.b3p.tailormap.api.persistence.TMFeatureType;
import nl.b3p.tailormap.api.repository.GeoServiceRepository;
import nl.b3p.tailormap.api.repository.SearchIndexRepository;
import nl.b3p.tailormap.api.solr.SolrHelper;
import nl.b3p.tailormap.api.solr.SolrService;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.rest.core.annotation.HandleAfterDelete;
import org.springframework.data.rest.core.annotation.HandleBeforeSave;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;

/** Event handler for Solr indexes when a {@code TMFeatureType} is updated or deleted. */
@RepositoryEventHandler
public class SolrTMFeatureTypeEventHandler {

private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private final SearchIndexRepository searchIndexRepository;
private final SolrService solrService;
private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
private final GeoServiceRepository geoServiceRepository;

public SolrTMFeatureTypeEventHandler(
SearchIndexRepository searchIndexRepository,
SolrService solrService,
FeatureSourceFactoryHelper featureSourceFactoryHelper,
GeoServiceRepository geoServiceRepository) {
this.searchIndexRepository = searchIndexRepository;
this.solrService = solrService;
this.featureSourceFactoryHelper = featureSourceFactoryHelper;
this.geoServiceRepository = geoServiceRepository;
}

/**
* Handle the update of a TMFeatureType.
*
* @param tmFeatureType the TMFeatureType to handle
*/
@HandleBeforeSave
public void handleTMFeatureTypeUpdate(TMFeatureType tmFeatureType) {
logger.debug("Handling TMFeatureType save event for: {}", tmFeatureType);
// determine if it is a new FT or an update
if (null == tmFeatureType.getId()) {
// do nothing as there is no index defined for a new feature type
logger.debug("New TMFeatureType: {}", tmFeatureType);
} else {
logger.debug("Updated TMFeatureType: {}", tmFeatureType);
searchIndexRepository.findByFeatureTypeId(tmFeatureType.getId()).stream()
.findAny()
.ifPresent(
searchIndex -> {
logger.debug(
"Updating search index {} for feature type: {}",
searchIndex.getName(),
searchIndex);

try (SolrHelper solrHelper =
new SolrHelper(solrService.getSolrClientForIndexing())) {
solrHelper.addFeatureTypeIndex(
searchIndex, tmFeatureType, featureSourceFactoryHelper);
} catch (UnsupportedOperationException
| IOException
| SolrServerException
| SolrException e) {
logger.error("Error re-indexing", e);
searchIndex.setStatus(SearchIndex.Status.ERROR);
searchIndexRepository.save(searchIndex);
}
});
}
}

/**
* Handle the deletion of a TMFeatureType.
*
* @param tmFeatureType the TMFeatureType to handle
*/
@HandleAfterDelete
public void handleTMFeatureTypeDeleteForSolr(TMFeatureType tmFeatureType) {
logger.debug("Handling TMFeatureType delete event for: {}", tmFeatureType);
searchIndexRepository.findByFeatureTypeId(tmFeatureType.getId()).stream()
.findAny()
.ifPresent(
searchIndex -> {
logger.info(
"Deleting search index {} for feature type: {}",
searchIndex.getName(),
searchIndex);

try (SolrHelper solrHelper = new SolrHelper(solrService.getSolrClientForIndexing())) {
solrHelper.clearIndexForLayer(searchIndex.getId());
searchIndexRepository.delete(searchIndex);
// find layers that use this index in the layer settings and clear them
geoServiceRepository
.findByIndexId(searchIndex.getId())
.forEach(
geoService ->
geoService
.getLayers()
.forEach(
layer -> {
logger.debug(
"Checking layer {} for search index {}",
layer.getName(),
searchIndex.getName());
geoService
.findSearchIndexForLayer(layer, searchIndexRepository)
.ifPresent(
searchIndex1 -> {
if (searchIndex1
.getId()
.equals(searchIndex.getId())) {
logger.debug(
"Clearing search index for layer {}",
layer.getName());
geoService
.getLayerSettings(layer.getName())
.setSearchIndex(null);
geoServiceRepository.save(geoService);
}
});
}));
} catch (UnsupportedOperationException
| IOException
| SolrServerException
| SolrException e) {
logger.error("Error deleting index for {}", searchIndex, e);
}
});
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/tailormap/api/solr/SolrHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public class SolrHelper implements AutoCloseable, Constants {
/**
* Constructor
*
* @param solrClient the Solr client, this will be closed when this class is closed
* @param solrClient the Solr client, this client will be closed when this class is closed.
*/
public SolrHelper(@NotNull SolrClient solrClient) {
this.solrClient = solrClient;
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/org/tailormap/api/solr/SolrService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.solr;

import java.util.concurrent.TimeUnit;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.ConcurrentUpdateHttp2SolrClient;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class SolrService {
@Value("${tailormap-api.solr-url}")
private String solrUrl;

@Value("${tailormap-api.solr-core-name:tailormap}")
private String solrCoreName;

/**
* Get a concurrent update Solr client for bulk operations.
*
* @return the Solr client
*/
public SolrClient getSolrClientForIndexing() {
return new ConcurrentUpdateHttp2SolrClient.Builder(
solrUrl + solrCoreName,
new Http2SolrClient.Builder()
.withFollowRedirects(true)
.withConnectionTimeout(10000, TimeUnit.MILLISECONDS)
.withRequestTimeout(60000, TimeUnit.MILLISECONDS)
.build())
.withQueueSize(SolrHelper.SOLR_BATCH_SIZE * 2)
.withThreadCount(10)
.build();
}

/**
* Get a Solr client for searching.
*
* @return the Solr client
*/
public SolrClient getSolrClientForSearching() {
return new Http2SolrClient.Builder(solrUrl + solrCoreName)
.withConnectionTimeout(10, TimeUnit.SECONDS)
.withFollowRedirects(true)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2024 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.repository;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import nl.b3p.tailormap.api.annotation.PostgresIntegrationTest;
import nl.b3p.tailormap.api.persistence.GeoService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

@PostgresIntegrationTest
public class GeoServiceRepositoryIntegrationTest {

@Autowired private GeoServiceRepository geoServiceRepository;

@Test
void it_should_find_service_using_findByIndexId_with_valid_ID() {
final GeoService geoService = geoServiceRepository.findByIndexId(2L).get(0);
assertNotNull(geoService);
assertEquals("snapshot-geoserver", geoService.getId());
}

@Test
void it_should_not_find_services_findByIndexId_with_invalid_ID() {
final List<GeoService> geoServices = geoServiceRepository.findByIndexId(-2L);
assertTrue(geoServices.isEmpty());
}
}

0 comments on commit 0c16cad

Please sign in to comment.