package gov.nysenate.openleg.service.calendar.search; import com.google.common.collect.Range; 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.calendar.search.ElasticCalendarSearchDao; import gov.nysenate.openleg.model.calendar.Calendar; import gov.nysenate.openleg.model.calendar.CalendarId; import gov.nysenate.openleg.model.search.*; import gov.nysenate.openleg.service.base.search.ElasticSearchServiceUtils; import gov.nysenate.openleg.service.calendar.data.CalendarDataService; import gov.nysenate.openleg.service.calendar.event.BulkCalendarUpdateEvent; import gov.nysenate.openleg.service.calendar.event.CalendarUpdateEvent; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.index.query.*; 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.util.Collection; import java.util.Optional; import java.util.regex.Matcher; @Service public class ElasticCalendarSearchService implements CalendarSearchService { private static final Logger logger = LoggerFactory.getLogger(ElasticCalendarSearchService.class); @Autowired private ElasticCalendarSearchDao calendarSearchDao; @Autowired private CalendarDataService calendarDataService; @Autowired private Environment env; @Autowired private EventBus eventBus; @PostConstruct private void init() { eventBus.register(this); } /** {@inheritDoc} */ @Override public SearchResults<CalendarId> searchForCalendars(String query, String sort, LimitOffset limitOffset) throws SearchException { return searchCalendars(QueryBuilders.queryStringQuery(smartSearch(query)), null, sort, limitOffset); } /** {@inheritDoc} */ @Override public SearchResults<CalendarId> searchForCalendarsByYear(Integer year, String query, String sort, LimitOffset limitOffset) throws SearchException { return searchCalendars(getCalendarYearQuery(year, smartSearch(query)), null, sort, limitOffset); } /** {@inheritDoc} */ @Subscribe @Override public synchronized void handleCalendarUpdateEvent(CalendarUpdateEvent calendarUpdateEvent) { updateIndex(calendarUpdateEvent.getCalendar()); } /** {@inheritDoc} */ @Subscribe @Override public void handleBulkCalendarUpdateEvent(BulkCalendarUpdateEvent bulkCalendarUpdateEvent) { updateIndex(bulkCalendarUpdateEvent.getCalendars()); } /** {@inheritDoc} */ @Override public void updateIndex(Calendar content) { if (env.isElasticIndexing()) { logger.info("Indexing calendar {} into elastic search", content.getId()); calendarSearchDao.updateCalendarIndex(content); } } /** {@inheritDoc} */ @Override public void updateIndex(Collection<Calendar> content) { if (env.isElasticIndexing()) { logger.info("Indexing {} calendars into elastic search", content.size()); calendarSearchDao.updateCalendarIndexBulk(content); } } /** {@inheritDoc} */ @Override public void clearIndex() { calendarSearchDao.purgeIndices(); calendarSearchDao.createIndices(); } /** {@inheritDoc} */ @Override public void rebuildIndex() { clearIndex(); Optional<Range<Integer>> calendarYearRange = calendarDataService.getCalendarYearRange(); if (calendarYearRange.isPresent()) { for (int year = calendarYearRange.get().lowerEndpoint(); year <= calendarYearRange.get().upperEndpoint(); year++) { updateIndex(calendarDataService.getCalendars(year, SortOrder.NONE, LimitOffset.ALL)); } } } /** {@inheritDoc} */ @Subscribe @Override public void handleRebuildEvent(RebuildIndexEvent event) { if (event.affects(SearchIndex.CALENDAR)) { rebuildIndex(); } } /** {@inheritDoc} */ @Override @Subscribe public void handleClearEvent(ClearIndexEvent event) { if (event.affects(SearchIndex.CALENDAR)) { clearIndex(); } } /** --- Helper Methods --- */ /** * Returns a query that can be used for all calendar types that matches calendars for the given year * in addition to the criteria specified by the given query string * * @param year * @param query * @return */ private QueryBuilder getCalendarYearQuery(Integer year, String query) { return QueryBuilders.boolQuery() .must(QueryBuilders.queryStringQuery(query)) .filter(QueryBuilders.termQuery("year", year)); } /** * Performs a search on the calendar index using the search dao, handling any exceptions that may arise * * @param query * @param postFilter * @param sort * @param limitOffset * @return * @throws SearchException */ private SearchResults<CalendarId> searchCalendars(QueryBuilder query, QueryBuilder postFilter, String sort, LimitOffset limitOffset) throws SearchException { if (limitOffset == null) { limitOffset = LimitOffset.ALL; } try { return calendarSearchDao.searchCalendars(query, postFilter, ElasticSearchServiceUtils.extractSortBuilders(sort), limitOffset); } catch (SearchParseException ex) { throw new SearchException("There was a problem parsing the supplied query string.", ex); } catch (ElasticsearchException ex) { throw new UnexpectedSearchException(ex); } } private String smartSearch(String query) { if (query != null && !query.contains(":")) { Matcher matcher = CalendarId.calendarIdPattern.matcher(query.replace("\\s+", "")); if (matcher.matches()) { query = String.format("year:%s AND calendarNumber:%s", matcher.group(1), matcher.group(2)); } } return query; } }