/* Copyright (2007-2012) Schibsted ASA * This file is part of Possom. * * Possom is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Possom is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Possom. If not, see <http://www.gnu.org/licenses/>. */ package no.sesat.search.mode.command; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import no.sesat.search.datamodel.generic.StringDataObject; import no.sesat.search.mode.config.NewsEspCommandConfig; import no.sesat.search.result.BasicResultList; import no.sesat.search.result.FastSearchResult; import no.sesat.search.result.ResultItem; import no.sesat.search.result.ResultList; import org.apache.log4j.Logger; import com.fastsearch.esp.search.query.BaseParameter; import com.fastsearch.esp.search.query.IQuery; import com.fastsearch.esp.search.result.EmptyValueException; import com.fastsearch.esp.search.result.IDocumentSummary; import com.fastsearch.esp.search.result.IDocumentSummaryField; import com.fastsearch.esp.search.result.IQueryResult; import com.fastsearch.esp.search.result.IllegalType; /** * Navigatable ESP search command for news. * @todo documentation what additional functionality actually amounts to it benefitting news verticals. * * * @version $Id$ */ public class NewsEspSearchCommand extends NavigatableESPFastCommand { private static final String PARAM_NEXT_OFFSET = "nextOffset"; private static final Logger LOG = Logger.getLogger(NewsEspSearchCommand.class); private static final String FAST_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public NewsEspSearchCommand(final Context cxt) { super(cxt); } /** Add the next offset field. * @param nextOffset * @param searchResult */ protected static void addNextOffsetField( final int nextOffset, final ResultList<ResultItem> searchResult) { searchResult.addField(NewsEspSearchCommand.PARAM_NEXT_OFFSET, Integer.toString(nextOffset)); } @Override protected void modifyQuery(final IQuery query) { super.modifyQuery(query); final NewsEspCommandConfig config = getSearchConfiguration(); // @TODO: There are some mixing of sort field and sort direction that should have been cleaned up // Because of a bug in FAST ESP5 related to collapsing and sorting, we must use sort direction, // and not the + field name syntax final StringDataObject sort = datamodel.getParameters().getValue(config.getUserSortParameter()); String sortType; if (sort != null) { sortType = sort.getString(); } else { sortType = config.getDefaultSort(); } if (sortType.equals("relevance")) { if (getQuery().getTermCount() == 1 && !"".equals(config.getRelevanceSingleTermSortField())) { query.setParameter(BaseParameter.SORT_BY, config.getRelevanceSingleTermSortField()); } else if (getQuery().getTermCount() > 1 && !"".equals(config.getRelevanceMultipleTermSortField())) { query.setParameter(BaseParameter.SORT_BY, config.getRelevanceMultipleTermSortField()); } else { query.setParameter(BaseParameter.SORT_BY, config.getRelevanceSortField()); } query.setParameter(BaseParameter.SORT_DIRECTION, "descending"); } else { query.setParameter(BaseParameter.SORT_BY, config.getSortField()); query.setParameter(BaseParameter.SORT_DIRECTION, sortType); } query.setParameter(BaseParameter.HITS, Math.max(config.getCollapsingMaxFetch(), config.getResultsToReturn())); if (config.getMaxAge() != null) { final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); final int age = config.getMaxAgeAmount(); switch (config.getAgeSymbol()) { case 'w': cal.add(Calendar.WEEK_OF_YEAR, -age); break; case 'M': cal.add(Calendar.MONTH, -age); break; case 'd': cal.add(Calendar.DATE, -age); break; case 'h': cal.add(Calendar.HOUR, -age); break; case 'm': cal.add(Calendar.MINUTE, -age); break; default: throw new IllegalArgumentException("Unknown age symbol " + config.getAgeSymbol()); } final String latestDate = new SimpleDateFormat(FAST_DATE_FORMAT).format(cal.getTime()); final StringBuilder q = new StringBuilder(); if (query.getQueryString().length() > 0) { q.append("and(").append(query.getQueryString()).append(","); } q.append("filter(").append(config.getAgeField()).append(":range(").append(latestDate).append(",max))"); if (query.getQueryString().length() > 0) { q.append(")"); } query.setQueryString(q.toString()); } } @Override protected FastSearchResult<ResultItem> createSearchResult(final IQueryResult result) throws IOException { FastSearchResult<ResultItem> fastResult = null; final NewsEspCommandConfig config = getSearchConfiguration(); if(config.isCollapsingEnabled()){ try { fastResult = createCollapsedResults(config, getOffset(), result); } catch (final IllegalType e) { LOG.error("Could not convert result", e); } catch (final EmptyValueException e) { LOG.error("Could not convert result", e); } }else{ fastResult = super.createSearchResult(result); } return fastResult; } @Override protected synchronized String getQueryRepresentation() { String result = super.getQueryRepresentation(); final NewsEspCommandConfig config = getSearchConfiguration(); String medium = (String) datamodel.getJunkYard().getValue(config.getMediumParameter()); if (medium == null || medium.length() == 0) { medium = config.getDefaultMedium(); } final int qLength = result.length(); if(!NewsEspCommandConfig.ALL_MEDIUMS.equals(medium)) { if (0 < qLength) { result = "and(" + result + ',' + config.getMediumPrefix() + ":\"" + medium + "\")"; }else if(null != getQuery().getQueryString() && "*".equals(getQuery().getQueryString().trim()) ){ result = config.getMediumPrefix() + ":\"" + medium + "\""; } } if(result.length() == qLength){ LOG.debug("Did not add medium on rootclause: medium=" + medium + ", queryLength=" + qLength); }else{ LOG.debug("Added medium"); } return result; } @Override public NewsEspCommandConfig getSearchConfiguration() { return (NewsEspCommandConfig) super.getSearchConfiguration(); } /** Create the collapsed result list. * @param config * @param offset * @param result * @return * @throws com.fastsearch.esp.search.result.IllegalType * * @throws com.fastsearch.esp.search.result.EmptyValueException * */ protected FastSearchResult<ResultItem> createCollapsedResults( final NewsEspCommandConfig config, final int offset, final IQueryResult result) throws IllegalType, EmptyValueException { final FastSearchResult<ResultItem> searchResult = new FastSearchResult<ResultItem>(); final Map<String, ResultList<ResultItem>> collapseMap = new HashMap<String, ResultList<ResultItem>>(); searchResult.setHitCount(result.getDocCount()); int collectedHits = 0; int analyzedHits = 0; final int firstHit = offset; for (int i = firstHit; i < result.getDocCount() && analyzedHits < config.getCollapsingMaxFetch(); i++) { try { final IDocumentSummary document = result.getDocument(i + 1); final String collapseId = document.getSummaryField("collapseid").getStringValue(); ResultList<ResultItem> parentResult = collapseMap.get(collapseId); if (parentResult == null) { if (collapseMap.size() < config.getResultsToReturn()) { parentResult = addResult(config, searchResult, document); parentResult.setHitCount(1); collapseMap.put(collapseId, parentResult); collectedHits++; } } else { if(config.isExpansionEnabled()){ addResult(config, parentResult, document); } parentResult.setHitCount(parentResult.getHitCount() + 1); collectedHits++; } analyzedHits++; } catch (final NullPointerException e) { // The doc count is not 100% accurate. LOG.debug("Error finding document ", e); break; } } if (offset + collectedHits < result.getDocCount()) { addNextOffsetField(offset + collectedHits, searchResult); } return searchResult; } /** Add a result (document) as a new child result list to an existing result list. * @param config * @param searchResult * @param document * @return */ protected static ResultList<ResultItem> addResult( final NewsEspCommandConfig config, final ResultList<ResultItem> searchResult, final IDocumentSummary document) { ResultList<ResultItem> newResult = new BasicResultList<ResultItem>(); for (final Map.Entry<String, String> entry : config.getResultFieldMap().entrySet()) { final IDocumentSummaryField summary = document.getSummaryField(entry.getKey()); if (summary != null && !summary.isEmpty()) { newResult = newResult.addField(entry.getValue(), summary.getStringValue().trim()); } } searchResult.addResult(newResult); return newResult; } }