package com.tyndalehouse.step.core.service.impl.suggestion; import com.tyndalehouse.step.core.data.common.TermsAndMaxCount; import com.tyndalehouse.step.core.exceptions.StepInternalException; import com.tyndalehouse.step.core.models.BookName; import com.tyndalehouse.step.core.models.SearchToken; import com.tyndalehouse.step.core.models.SingleSuggestionsSummary; import com.tyndalehouse.step.core.models.SuggestionsSummary; import com.tyndalehouse.step.core.models.search.PopularSuggestion; import com.tyndalehouse.step.core.service.SingleTypeSuggestionService; import com.tyndalehouse.step.core.service.SuggestionService; import com.tyndalehouse.step.core.service.helpers.SuggestionContext; import org.apache.lucene.search.TopFieldCollector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Suggestion service, helping the auto suggestion search dropdown. * * @author chrisburrell */ public class SuggestionServiceImpl implements SuggestionService { private static final Logger LOGGER = LoggerFactory.getLogger(SuggestionServiceImpl.class); //show the total number of ungrouped results at any one time. private static final int MAX_RESULTS = 3; //determines how many values are shown on expanding line 'see 7 more, e.g. abc def' private static final int PREVIEW_GROUP = 2; private final Map<String, SingleTypeSuggestionService> queryProviders = new LinkedHashMap<String, SingleTypeSuggestionService>(); private final Map<String, String[]> dependencies = new HashMap<String, String[]>(8); private final Map<String, Integer> extraSlots = new HashMap<String, Integer>(4); @Inject public SuggestionServiceImpl(final HebrewAncientMeaningServiceImpl hebrewAncientMeaningService, final GreekAncientMeaningServiceImpl greekAncientMeaningService, final HebrewAncientLanguageServiceImpl hebrewAncientLanguageService, final GreekAncientLanguageServiceImpl greekAncientLanguageService, final MeaningSuggestionServiceImpl meaningSuggestionService, final SubjectSuggestionServiceImpl subjectSuggestionService, final ReferenceSuggestionServiceImpl referenceSuggestionService, final TextSuggestionServiceImpl textSuggestionService ) { queryProviders.put(SearchToken.REFERENCE, referenceSuggestionService); queryProviders.put(SearchToken.GREEK_MEANINGS, greekAncientMeaningService); queryProviders.put(SearchToken.HEBREW_MEANINGS, hebrewAncientMeaningService); queryProviders.put(SearchToken.GREEK, greekAncientLanguageService); queryProviders.put(SearchToken.HEBREW, hebrewAncientLanguageService); queryProviders.put(SearchToken.MEANINGS, meaningSuggestionService); queryProviders.put(SearchToken.SUBJECT_SEARCH, subjectSuggestionService); queryProviders.put(SearchToken.TEXT_SEARCH, textSuggestionService); //the following lines mean we won't pull extra words for all data sources. //e.g. if we have 2 greek meanings, we will only pull 1 one more hebrew meaning //this is not a full map, as processing is dependent on the order set out above dependencies.put(SearchToken.HEBREW_MEANINGS, new String[]{SearchToken.GREEK_MEANINGS}); dependencies.put(SearchToken.GREEK, new String[]{SearchToken.GREEK_MEANINGS, SearchToken.HEBREW_MEANINGS}); dependencies.put(SearchToken.HEBREW, new String[]{SearchToken.GREEK, SearchToken.GREEK_MEANINGS, SearchToken.HEBREW_MEANINGS}); //spare capcacity, will fudge the group total. -1 means we will attempt to retrieve 1 less than we could //+1 means we will attempt to retrieve 1 more than we should. //for GREEK and HEBREW, we will attempt to retrieve 2+2, rather than 3 and 0 extraSlots.put(SearchToken.GREEK_MEANINGS, -1); extraSlots.put(SearchToken.HEBREW_MEANINGS, 1); // for GREEK and Hebrew, we can attempt to retrieve one more, but these won't show if the slots have been taken above extraSlots.put(SearchToken.GREEK, 1); extraSlots.put(SearchToken.HEBREW, 1); } @SuppressWarnings("unchecked") @Override public SuggestionsSummary getTopSuggestions(final SuggestionContext context) { final SuggestionsSummary summary = new SuggestionsSummary(); final Map<String, SingleSuggestionsSummary> results = new LinkedHashMap<String, SingleSuggestionsSummary>(); //go through each search type for (Map.Entry<String, SingleTypeSuggestionService> query : queryProviders.entrySet()) { final SingleTypeSuggestionService searchService = query.getValue(); //run exact query against index final int groupTotal = this.getGroupTotal(query.getKey(), results); final int totalGroupLeftToRetrieve = MAX_RESULTS - groupTotal + PREVIEW_GROUP; Object[] docs = totalGroupLeftToRetrieve > 0 ? searchService.getExactTerms(context, totalGroupLeftToRetrieve, true) : null; int docLength = docs != null ? docs.length : 0; //how many do we need to collect int leftToCollect = docLength < totalGroupLeftToRetrieve ? totalGroupLeftToRetrieve - docLength : 0; //create collector to collect some more results, if required, but also the total hit count Object o = searchService.getNewCollector(leftToCollect, true); final Object[] extraDocs = searchService.collectNonExactMatches(o, context, docs, leftToCollect); final List<? extends PopularSuggestion> suggestions = searchService.convertToSuggestions(docs, extraDocs); final SingleSuggestionsSummary singleTypeSummary = new SingleSuggestionsSummary(); setSuggestionsAndExamples(singleTypeSummary, suggestions, groupTotal); fillInTotalHits(o, extraDocs.length, singleTypeSummary); singleTypeSummary.setSearchType(query.getKey()); results.put(query.getKey(), singleTypeSummary); } //return results summary.setSuggestionsSummaries(new ArrayList<SingleSuggestionsSummary>(results.values())); return summary; } /** * Total number of results retrieved so far in a particular grouping of providers * * @param searchType the current type of search * @param resultsSoFar the results retrieved so far * @return the total number of elements retrieved */ private int getGroupTotal(final String searchType, Map<String, SingleSuggestionsSummary> resultsSoFar) { final String[] dependents = this.dependencies.get(searchType); if (dependents == null) { //no dependencies return 0 - getSpareSlotCapacity(searchType); } int totalDocsRetrieved = 0; for (String d : dependents) { final SingleSuggestionsSummary singleSuggestionsSummary = resultsSoFar.get(d); if (singleSuggestionsSummary == null) { LOGGER.warn("Dependencies setup is incorrect"); continue; } final int totalMinusGroupExamples = singleSuggestionsSummary.getPopularSuggestions().size(); totalDocsRetrieved += (totalMinusGroupExamples > 0 ? totalMinusGroupExamples : 0); } return totalDocsRetrieved - getSpareSlotCapacity(searchType); } private int getSpareSlotCapacity(final String searchType) { final Integer spareCapacity = extraSlots.get(searchType); return spareCapacity == null ? 0 : spareCapacity; } private void fillInTotalHits(final Object collector, int alreadyCollected, final SingleSuggestionsSummary singleTypeSummary) { int numExamples = singleTypeSummary.getExtraExamples() != null ? singleTypeSummary.getExtraExamples().size() : 0; if (collector instanceof TopFieldCollector) { singleTypeSummary.setMoreResults(((TopFieldCollector) collector).getTotalHits() - alreadyCollected + numExamples); } else if (collector instanceof TermsAndMaxCount) { singleTypeSummary.setMoreResults(((TermsAndMaxCount) collector).getTotalCount() - alreadyCollected + numExamples); } else { throw new StepInternalException("Unsupported collector"); } } @SuppressWarnings("unchecked") @Override public SuggestionsSummary getFirstNSuggestions(SuggestionContext context) { final String searchType = context.getSearchType(); final SingleTypeSuggestionService searchService = queryProviders.get(searchType); final Object[] docs = searchService.getExactTerms(context, MAX_RESULTS, false); //create collector to collect some more results, if required, but also the total hit count final Object collector = searchService.getNewCollector(MAX_RESULTS_NON_GROUPED - docs.length, false); final Object[] extraDocs = searchService.collectNonExactMatches(collector, context, docs, MAX_RESULTS_NON_GROUPED); final List<? extends PopularSuggestion> suggestions = searchService.convertToSuggestions(docs, extraDocs); final SuggestionsSummary summary = new SuggestionsSummary(); final List<SingleSuggestionsSummary> results = new ArrayList<SingleSuggestionsSummary>(); summary.setSuggestionsSummaries(results); final SingleSuggestionsSummary singleTypeSummary = new SingleSuggestionsSummary(); fillInTotalHits(collector, extraDocs.length, singleTypeSummary); singleTypeSummary.setPopularSuggestions(suggestions); singleTypeSummary.setSearchType(searchType); results.add(singleTypeSummary); //return results return summary; } private void setSuggestionsAndExamples(final SingleSuggestionsSummary singleTypeSummary, final List<? extends PopularSuggestion> suggestions, final int groupTotal) { //total number of suggestions to keep as suggestions final int keep = MAX_RESULTS - groupTotal; //set popular suggestions List<PopularSuggestion> keepSuggestions = new ArrayList<PopularSuggestion>(3); int ii; final boolean isReferenceSuggestion = suggestions.size() > 0 && suggestions.get(0) instanceof BookName; for (ii = 0; (ii < keep || isReferenceSuggestion) && ii < suggestions.size(); ii++) { keepSuggestions.add(suggestions.get(ii)); } singleTypeSummary.setPopularSuggestions(keepSuggestions); //set example suggestions List<PopularSuggestion> examples = new ArrayList<PopularSuggestion>(2); for (; ii < suggestions.size(); ii++) { examples.add(suggestions.get(ii)); } singleTypeSummary.setExtraExamples(examples); } }