package gov.nysenate.openleg.service.transcript.search; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import gov.nysenate.openleg.config.Environment; import gov.nysenate.openleg.dao.base.LimitOffset; import gov.nysenate.openleg.dao.base.SearchIndex; import gov.nysenate.openleg.dao.base.SortOrder; import gov.nysenate.openleg.dao.transcript.search.ElasticTranscriptSearchDao; import gov.nysenate.openleg.model.search.*; import gov.nysenate.openleg.model.transcript.Transcript; import gov.nysenate.openleg.model.transcript.TranscriptId; import gov.nysenate.openleg.service.base.search.ElasticSearchServiceUtils; import gov.nysenate.openleg.service.base.search.IndexedSearchService; import gov.nysenate.openleg.service.transcript.data.TranscriptDataService; import gov.nysenate.openleg.service.transcript.event.BulkTranscriptUpdateEvent; import gov.nysenate.openleg.service.transcript.event.TranscriptUpdateEvent; import org.apache.lucene.queryparser.xml.builders.RangeFilterBuilder; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.search.SearchParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Service public class ElasticTranscriptSearchService implements TranscriptSearchService, IndexedSearchService<Transcript> { private static final Logger logger = LoggerFactory.getLogger(ElasticTranscriptSearchService.class); @Autowired protected Environment env; @Autowired protected EventBus eventBus; @Autowired protected ElasticTranscriptSearchDao transcriptSearchDao; @Autowired protected TranscriptDataService transcriptDataService; @PostConstruct protected void init() { eventBus.register(this); } /** {@inheritDoc} */ @Override public SearchResults<TranscriptId> searchTranscripts(String sort, LimitOffset limOff) throws SearchException { return search(QueryBuilders.matchAllQuery(), null, sort, limOff); } /** {@inheritDoc} */ @Override public SearchResults<TranscriptId> searchTranscripts(int year, String sort, LimitOffset limOff) throws SearchException { RangeQueryBuilder rangeFilter = new RangeQueryBuilder("dateTime") .from(LocalDate.of(year, 1, 1)) .to(LocalDate.of(year, 12, 31)); return search( QueryBuilders.boolQuery() .must(QueryBuilders.matchAllQuery()) .filter(rangeFilter), null, sort, limOff); } /** {@inheritDoc} */ @Override public SearchResults<TranscriptId> searchTranscripts(String query, String sort, LimitOffset limOff) throws SearchException { return search(QueryBuilders.queryStringQuery(query), null, sort, limOff); } /** {@inheritDoc} */ @Override public SearchResults<TranscriptId> searchTranscripts(String query, int year, String sort, LimitOffset limOff) throws SearchException { RangeQueryBuilder rangeFilter = new RangeQueryBuilder("dateTime") .from(LocalDate.of(year, 1, 1)) .to(LocalDate.of(year, 12, 31)); return search( QueryBuilders.boolQuery() .must(QueryBuilders.queryStringQuery(query)) .filter(rangeFilter), null, sort, limOff); } private SearchResults<TranscriptId> search(QueryBuilder query, QueryBuilder postFilter, String sort, LimitOffset limOff) throws SearchException { if (limOff == null) limOff = LimitOffset.TEN; try { return transcriptSearchDao.searchTranscripts(query, postFilter, ElasticSearchServiceUtils.extractSortBuilders(sort), limOff); } catch (SearchParseException ex) { throw new SearchException("Invalid query string", ex); } catch (ElasticsearchException ex) { throw new UnexpectedSearchException(ex); } } /** {@inheritDoc} */ @Override @Subscribe public void handleTranscriptUpdate(TranscriptUpdateEvent transcriptUpdateEvent) { if (transcriptUpdateEvent.getTranscript() != null) { updateIndex(transcriptUpdateEvent.getTranscript()); } } /** {@inheritDoc} */ @Override @Subscribe public void handleBulkTranscriptUpdate(BulkTranscriptUpdateEvent bulkTranscriptUpdateEvent) { if (bulkTranscriptUpdateEvent.getTranscripts() != null) { updateIndex(bulkTranscriptUpdateEvent.getTranscripts()); } } /** {@inheritDoc} */ @Override public void updateIndex(Transcript transcript) { if (env.isElasticIndexing() && transcript != null) { logger.info("Indexing transcript {} into elastic search.", transcript.getDateTime().toString()); transcriptSearchDao.updateTranscriptIndex(transcript); } } /** {@inheritDoc} */ @Override public void updateIndex(Collection<Transcript> transcripts) { if (env.isElasticIndexing() && !transcripts.isEmpty()) { List<Transcript> indexableTranscripts = transcripts.stream().filter(t -> t != null).collect(Collectors.toList()); logger.info("Indexing {} valid transcripts into elastic search.", indexableTranscripts.size()); transcriptSearchDao.updateTranscriptIndex(indexableTranscripts); } } /** {@inheritDoc} */ @Override public void clearIndex() { transcriptSearchDao.purgeIndices(); transcriptSearchDao.createIndices(); } /** {@inheritDoc} */ @Override public void rebuildIndex() { clearIndex(); for (int year = 1993; year <= LocalDate.now().getYear(); year++) { LimitOffset limOff = LimitOffset.TWENTY_FIVE; List<TranscriptId> transcriptIds = transcriptDataService.getTranscriptIds(SortOrder.DESC, limOff); while (!transcriptIds.isEmpty()) { logger.info("Indexing {} transcripts starting from {}", transcriptIds.size(), year); List<Transcript> transcripts = transcriptIds.stream().map(transcriptDataService::getTranscript).collect(Collectors.toList()); updateIndex(transcripts); limOff = limOff.next(); transcriptIds = transcriptDataService.getTranscriptIds(SortOrder.DESC, limOff); } } } /** {@inheritDoc} */ @Override public void handleRebuildEvent(RebuildIndexEvent event) { if (event.affects(SearchIndex.TRANSCRIPT)) { logger.info("Handling transcript re-index event."); try { rebuildIndex(); } catch (Exception ex) { logger.error("Unexpected exception during handling of transcript index rebuild event.", ex); } } } /** {@inheritDoc} */ @Override @Subscribe public void handleClearEvent(ClearIndexEvent event) { if (event.affects(SearchIndex.TRANSCRIPT)) { clearIndex(); } } }