/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.index.service.impl.index; import static java.lang.String.format; import static org.opencastproject.util.data.functions.Misc.chuck; import org.opencastproject.index.IndexProducer; import org.opencastproject.index.service.exception.IndexServiceException; import org.opencastproject.index.service.impl.index.event.Event; import org.opencastproject.index.service.impl.index.event.EventIndexUtils; import org.opencastproject.index.service.impl.index.event.EventQueryBuilder; import org.opencastproject.index.service.impl.index.event.EventSearchQuery; import org.opencastproject.index.service.impl.index.group.Group; import org.opencastproject.index.service.impl.index.group.GroupIndexUtils; import org.opencastproject.index.service.impl.index.group.GroupQueryBuilder; import org.opencastproject.index.service.impl.index.group.GroupSearchQuery; import org.opencastproject.index.service.impl.index.series.Series; import org.opencastproject.index.service.impl.index.series.SeriesIndexUtils; import org.opencastproject.index.service.impl.index.series.SeriesQueryBuilder; import org.opencastproject.index.service.impl.index.series.SeriesSearchQuery; import org.opencastproject.index.service.impl.index.theme.Theme; import org.opencastproject.index.service.impl.index.theme.ThemeIndexUtils; import org.opencastproject.index.service.impl.index.theme.ThemeQueryBuilder; import org.opencastproject.index.service.impl.index.theme.ThemeSearchQuery; import org.opencastproject.matterhorn.search.SearchIndexException; import org.opencastproject.matterhorn.search.SearchMetadata; import org.opencastproject.matterhorn.search.SearchQuery; import org.opencastproject.matterhorn.search.SearchResult; import org.opencastproject.matterhorn.search.SearchResultItem; import org.opencastproject.matterhorn.search.impl.AbstractElasticsearchIndex; import org.opencastproject.matterhorn.search.impl.ElasticsearchDocument; import org.opencastproject.matterhorn.search.impl.SearchMetadataCollection; import org.opencastproject.matterhorn.search.impl.SearchMetadataImpl; import org.opencastproject.matterhorn.search.impl.SearchResultImpl; import org.opencastproject.matterhorn.search.impl.SearchResultItemImpl; import org.opencastproject.message.broker.api.BaseMessage; import org.opencastproject.message.broker.api.MessageReceiver; import org.opencastproject.message.broker.api.MessageSender; import org.opencastproject.message.broker.api.index.IndexRecreateObject; import org.opencastproject.security.api.User; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.data.Option; import com.entwinemedia.fn.Fn; import org.elasticsearch.action.delete.DeleteRequestBuilder; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHitField; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket; import org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; public abstract class AbstractSearchIndex extends AbstractElasticsearchIndex { private static final Logger logger = LoggerFactory.getLogger(AbstractSearchIndex.class); /** The message sender */ private MessageSender messageSender; /** The message receiver */ private MessageReceiver messageReceiver; /** An Executor to get messages */ private ExecutorService executor = Executors.newSingleThreadExecutor(); @Override public abstract String getIndexName(); /** OSGi DI. */ public void setMessageSender(MessageSender messageSender) { this.messageSender = messageSender; } /** OSGi DI. */ public void setMessageReceiver(MessageReceiver messageReceiver) { this.messageReceiver = messageReceiver; } /** * Recreate the index from all of the services that provide data. * * @throws InterruptedException * Thrown if the process is interupted. * @throws CancellationException * Thrown if listeing to messages has been canceled. * @throws ExecutionException * Thrown if there is a problem executing the process. * @throws IOException * Thrown if the index cannot be cleared. * @throws IndexServiceException * Thrown if there was a problem adding some of the data back into the index. */ public synchronized void recreateIndex() throws InterruptedException, CancellationException, ExecutionException, IOException, IndexServiceException { // Clear index first clear(); recreateService(IndexRecreateObject.Service.Groups); recreateService(IndexRecreateObject.Service.Acl); recreateService(IndexRecreateObject.Service.Themes); recreateService(IndexRecreateObject.Service.Series); recreateService(IndexRecreateObject.Service.Scheduler); recreateService(IndexRecreateObject.Service.Workflow); recreateService(IndexRecreateObject.Service.AssetManager); recreateService(IndexRecreateObject.Service.Comments); } /** * Ask for data to be rebuilt from a service. * * @param service * The {@link IndexRecreateObject.Service} representing the service to start re-sending the data from. * @throws IndexServiceException * Thrown if there is a problem re-sending the data from the service. * @throws InterruptedException * Thrown if the process of re-sending the data is interupted. * @throws CancellationException * Thrown if listening to messages has been canceled. * @throws ExecutionException * Thrown if the process of re-sending the data has an error. */ private void recreateService(IndexRecreateObject.Service service) throws IndexServiceException, InterruptedException, CancellationException, ExecutionException { logger.info("Starting to recreate index for service '{}'", service); messageSender.sendObjectMessage(IndexProducer.RECEIVER_QUEUE + "." + service, MessageSender.DestinationType.Queue, IndexRecreateObject.start(getIndexName(), service)); boolean done = false; // TODO Add a timeout for services that are not going to respond. while (!done) { FutureTask<Serializable> future = messageReceiver.receiveSerializable(IndexProducer.RESPONSE_QUEUE, MessageSender.DestinationType.Queue); executor.execute(future); BaseMessage message = (BaseMessage) future.get(); if (message.getObject() instanceof IndexRecreateObject) { IndexRecreateObject indexRecreateObject = (IndexRecreateObject) message.getObject(); switch (indexRecreateObject.getStatus()) { case Update: logger.info("Updating service: '{}' with {}/{} finished, {}% complete.", new Object[] { indexRecreateObject.getService(), indexRecreateObject.getCurrent(), indexRecreateObject.getTotal(), (int) (indexRecreateObject.getCurrent() * 100 / indexRecreateObject.getTotal()) }); if (indexRecreateObject.getCurrent() == indexRecreateObject.getTotal()) { logger.info("Waiting for service '{}' indexing to complete", indexRecreateObject.getService()); } break; case End: done = true; logger.info("Finished re-creating data for service '{}'", indexRecreateObject.getService()); break; case Error: logger.error("Error updating service '{}' with {}/{} finished.", new Object[] { indexRecreateObject.getService(), indexRecreateObject.getCurrent(), indexRecreateObject.getTotal() }); throw new IndexServiceException( format("Error updating service '%s' with %s/%s finished.", indexRecreateObject.getService(), indexRecreateObject.getCurrent(), indexRecreateObject.getTotal())); default: logger.error("Unable to handle the status '{}' for service '{}'", indexRecreateObject.getStatus(), indexRecreateObject.getService()); throw new IllegalArgumentException(format("Unable to handle the status '%s' for service '%s'", indexRecreateObject.getStatus(), indexRecreateObject.getService())); } } } } /** * Adds the recording event to the search index or updates it accordingly if it is there. * * @param event * the recording event * @throws SearchIndexException * if the event cannot be added or updated */ public void addOrUpdate(Event event) throws SearchIndexException { logger.debug("Adding resource {} to search index", event); // if (!preparedIndices.contains(resource.getURI().getSite().getIdentifier())) { // try { // createIndex(resource.getURI().getSite()); // } catch (IOException e) { // throw new SearchIndexException(e); // } // } // Add the resource to the index SearchMetadataCollection inputDocument = EventIndexUtils.toSearchMetadata(event); List<SearchMetadata<?>> resourceMetadata = inputDocument.getMetadata(); ElasticsearchDocument doc = new ElasticsearchDocument(inputDocument.getIdentifier(), inputDocument.getDocumentType(), resourceMetadata); try { update(doc); } catch (Throwable t) { throw new SearchIndexException("Cannot write resource " + event + " to index", t); } } /** * Adds or updates the group in the search index. * * @param group * The group to add * @throws SearchIndexException * Thrown if unable to add or update the group. */ public void addOrUpdate(Group group) throws SearchIndexException { logger.debug("Adding resource {} to search index", group); // if (!preparedIndices.contains(resource.getURI().getSite().getIdentifier())) { // try { // createIndex(resource.getURI().getSite()); // } catch (IOException e) { // throw new SearchIndexException(e); // } // } // Add the resource to the index SearchMetadataCollection inputDocument = GroupIndexUtils.toSearchMetadata(group); List<SearchMetadata<?>> resourceMetadata = inputDocument.getMetadata(); ElasticsearchDocument doc = new ElasticsearchDocument(inputDocument.getIdentifier(), inputDocument.getDocumentType(), resourceMetadata); try { update(doc); } catch (Throwable t) { throw new SearchIndexException("Cannot write resource " + group + " to index", t); } } /** * Add or update a series in the search index. * * @param series * @throws SearchIndexException */ public void addOrUpdate(Series series) throws SearchIndexException { logger.debug("Adding resource {} to search index", series); // if (!preparedIndices.contains(resource.getURI().getSite().getIdentifier())) { // try { // createIndex(resource.getURI().getSite()); // } catch (IOException e) { // throw new SearchIndexException(e); // } // } // Add the resource to the index SearchMetadataCollection inputDocument = SeriesIndexUtils.toSearchMetadata(series); List<SearchMetadata<?>> resourceMetadata = inputDocument.getMetadata(); ElasticsearchDocument doc = new ElasticsearchDocument(inputDocument.getIdentifier(), inputDocument.getDocumentType(), resourceMetadata); try { update(doc); } catch (Throwable t) { throw new SearchIndexException("Cannot write resource " + series + " to index", t); } } /** * Adds or updates the theme in the search index. * * @param theme * The theme to add * @throws SearchIndexException * Thrown if unable to add or update the theme. */ public void addOrUpdate(Theme theme) throws SearchIndexException { logger.debug("Adding resource {} to search index", theme); // if (!preparedIndices.contains(resource.getURI().getSite().getIdentifier())) { // try { // createIndex(resource.getURI().getSite()); // } catch (IOException e) { // throw new SearchIndexException(e); // } // } // Add the resource to the index SearchMetadataCollection inputDocument = ThemeIndexUtils.toSearchMetadata(theme); List<SearchMetadata<?>> resourceMetadata = inputDocument.getMetadata(); ElasticsearchDocument doc = new ElasticsearchDocument(inputDocument.getIdentifier(), inputDocument.getDocumentType(), resourceMetadata); try { update(doc); } catch (Throwable t) { throw new SearchIndexException("Cannot write resource " + theme + " to index", t); } } @Override public boolean delete(String documentType, String uid) throws SearchIndexException { logger.debug("Removing element with id '{}' from searching index '{}'", uid, getIndexName()); DeleteRequestBuilder deleteRequest = getSearchClient().prepareDelete(getIndexName(), documentType, uid); deleteRequest.setRefresh(true); DeleteResponse delete = deleteRequest.execute().actionGet(); if (!delete.isFound()) { logger.trace("Document {} to delete was not found on index '{}'", uid, getIndexName()); return false; } return true; } /** * @param event * The event to check if it * @return If an event has a record of being a schedule, workflow or archive event. */ protected boolean toDelete(Event event) { boolean hasScheduling = event.getSchedulingStatus() != null; boolean hasWorkflow = event.getWorkflowId() != null; boolean hasArchive = event.getArchiveVersion() != null; return !hasScheduling && !hasWorkflow && !hasArchive; } /** * Delete an event from the asset manager. * * @param organization * The organization the event is a part of. * @param user * The user that is requesting to delete the event. * @param uid * The identifier of the event. * @throws SearchIndexException * Thrown if there is an issue with deleting the event. * @throws NotFoundException * Thrown if the event cannot be found. */ public void deleteAssets(String organization, User user, String uid) throws SearchIndexException, NotFoundException { Event event = EventIndexUtils.getEvent(uid, organization, user, this); if (event == null) throw new NotFoundException("No event with id " + uid + " found."); event.setArchiveVersion(null); if (toDelete(event)) { delete(Event.DOCUMENT_TYPE, uid.concat(organization)); } else { addOrUpdate(event); } } /** * Delete an event from the scheduling service * * @param organization * The organization the event is a part of. * @param user * The user that is requesting to delete the event. * @param uid * The identifier of the event. * @throws SearchIndexException * Thrown if there is an issue with deleting the event. * @throws NotFoundException * Thrown if the event cannot be found. */ public void deleteScheduling(String organization, User user, String uid) throws SearchIndexException, NotFoundException { Event event = EventIndexUtils.getEvent(uid, organization, user, this); if (event == null) throw new NotFoundException("No event with id " + uid + " found."); event.setOptedOut(null); event.setBlacklisted(null); event.setReviewDate(null); event.setReviewStatus(null); event.setSchedulingStatus(null); event.setRecordingStatus(null); if (toDelete(event)) { delete(Event.DOCUMENT_TYPE, uid.concat(organization)); } else { addOrUpdate(event); } } /** * Delete an event from the workflow service * * @param organization * The organization the event is a part of. * @param user * The user that is requesting to delete the event. * @param uid * The identifier of the event. * @throws SearchIndexException * Thrown if there is an issue with deleting the event. * @throws NotFoundException * Thrown if the event cannot be found. */ public void deleteWorkflow(String organization, User user, String uid) throws SearchIndexException, NotFoundException { Event event = EventIndexUtils.getEvent(uid, organization, user, this); if (event == null) throw new NotFoundException("No event with id " + uid + " found."); event.setWorkflowId(null); event.setWorkflowDefinitionId(null); event.setWorkflowState(null); event.setWorkflowScheduledDate(null); if (toDelete(event)) { delete(Event.DOCUMENT_TYPE, uid.concat(organization)); } else { addOrUpdate(event); } } /** * @param query * The query to use to retrieve the events that match the query * @return {@link SearchResult} collection of {@link Event} from a query. * @throws SearchIndexException * Thrown if there is an error getting the results. */ public SearchResult<Event> getByQuery(EventSearchQuery query) throws SearchIndexException { logger.debug("Searching index using event query '{}'", query); // Create the request builder SearchRequestBuilder requestBuilder = getSearchRequestBuilder(query, new EventQueryBuilder(query)); try { return executeQuery(query, requestBuilder, new Fn<SearchMetadataCollection, Event>() { @Override public Event apply(SearchMetadataCollection metadata) { try { return EventIndexUtils.toRecordingEvent(metadata); } catch (IOException e) { return chuck(e); } } }); } catch (Throwable t) { throw new SearchIndexException("Error querying event index", t); } } /** * @param query * The query to use to retrieve the groups that match the query * @return {@link SearchResult} collection of {@link Group} from a query. * @throws SearchIndexException * Thrown if there is an error getting the results. */ public SearchResult<Group> getByQuery(GroupSearchQuery query) throws SearchIndexException { logger.debug("Searching index using group query '{}'", query); // Create the request builder SearchRequestBuilder requestBuilder = getSearchRequestBuilder(query, new GroupQueryBuilder(query)); try { return executeQuery(query, requestBuilder, new Fn<SearchMetadataCollection, Group>() { @Override public Group apply(SearchMetadataCollection metadata) { try { return GroupIndexUtils.toGroup(metadata); } catch (IOException e) { return chuck(e); } } }); } catch (Throwable t) { throw new SearchIndexException("Error querying series index", t); } } /** * @param query * The query to use to retrieve the series that match the query * @return {@link SearchResult} collection of {@link Series} from a query. * @throws SearchIndexException * Thrown if there is an error getting the results. */ public SearchResult<Series> getByQuery(SeriesSearchQuery query) throws SearchIndexException { logger.debug("Searching index using series query '{}'", query); // Create the request builder SearchRequestBuilder requestBuilder = getSearchRequestBuilder(query, new SeriesQueryBuilder(query)); try { return executeQuery(query, requestBuilder, new Fn<SearchMetadataCollection, Series>() { @Override public Series apply(SearchMetadataCollection metadata) { try { return SeriesIndexUtils.toSeries(metadata); } catch (IOException e) { return chuck(e); } } }); } catch (Throwable t) { throw new SearchIndexException("Error querying series index", t); } } /** * @param query * The query to use to retrieve the themes that match the query * @return {@link SearchResult} collection of {@link Theme} from a query. * @throws SearchIndexException * Thrown if there is an error getting the results. */ public SearchResult<Theme> getByQuery(ThemeSearchQuery query) throws SearchIndexException { logger.debug("Searching index using theme query '{}'", query); // Create the request builder SearchRequestBuilder requestBuilder = getSearchRequestBuilder(query, new ThemeQueryBuilder(query)); try { return executeQuery(query, requestBuilder, new Fn<SearchMetadataCollection, Theme>() { @Override public Theme apply(SearchMetadataCollection metadata) { try { return ThemeIndexUtils.toTheme(metadata); } catch (IOException e) { return chuck(e); } } }); } catch (Throwable t) { throw new SearchIndexException("Error querying theme index", t); } } /** * Returns all the known terms for a field (aka facets). * * @param field * the field name * @param types * an optional array of document types, if none is set all types are searched * @return the list of terms */ public List<String> getTermsForField(String field, Option<String[]> types) { final String facetName = "terms"; TermsBuilder aggBuilder = AggregationBuilders.terms(facetName).field(field); SearchRequestBuilder search = getSearchClient().prepareSearch(getIndexName()).addAggregation(aggBuilder); if (types.isSome()) search = search.setTypes(types.get()); SearchResponse response = search.execute().actionGet(); List<String> terms = new ArrayList<>(); Terms aggs = response.getAggregations().get(facetName); for (Bucket bucket : aggs.getBuckets()) { terms.add(bucket.getKey()); } return terms; } /** * Execute a query on the index. * * @param query * The query to use to find the results * @param requestBuilder * The builder to use to create the query. * @param toSearchResult * The function to convert the results to a {@link SearchResult} * @return A {@link SearchResult} containing the relevant objects. * @throws SearchIndexException */ protected <T> SearchResult<T> executeQuery(SearchQuery query, SearchRequestBuilder requestBuilder, Fn<SearchMetadataCollection, T> toSearchResult) throws SearchIndexException { // Execute the query and try to get hold of a query response SearchResponse response = null; try { response = getSearchClient().search(requestBuilder.request()).actionGet(); } catch (Throwable t) { throw new SearchIndexException(t); } // Create and configure the query result long hits = response.getHits().getTotalHits(); long size = response.getHits().getHits().length; SearchResultImpl<T> result = new SearchResultImpl<>(query, hits, size); result.setSearchTime(response.getTookInMillis()); // Walk through response and create new items with title, creator, etc: for (SearchHit doc : response.getHits()) { // Wrap the search resulting metadata SearchMetadataCollection metadata = new SearchMetadataCollection(doc.getType()); metadata.setIdentifier(doc.getId()); for (SearchHitField field : doc.getFields().values()) { String name = field.getName(); SearchMetadata<Object> m = new SearchMetadataImpl<>(name); // TODO: Add values with more care (localized, correct type etc.) // Add the field values if (field.getValues().size() > 1) { for (Object v : field.getValues()) { m.addValue(v); } } else { m.addValue(field.getValue()); } // Add the metadata metadata.add(m); } // Get the score for this item float score = doc.getScore(); // Have the serializer in charge create a type-specific search result // item try { T document = toSearchResult.apply(metadata); SearchResultItem<T> item = new SearchResultItemImpl<>(score, document); result.addResultItem(item); } catch (Throwable t) { logger.warn("Error during search result serialization: '{}'. Skipping this search result.", t.getMessage()); size--; continue; } } // Set the number of resulting documents result.setDocumentCount(size); return result; } }