/* * Copyright (2008-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.Serializable; import java.lang.ref.Reference; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import no.sesat.commons.ioc.BaseContext; import no.sesat.commons.ioc.ContextWrapper; import no.sesat.commons.ref.ReferenceMap; import no.sesat.search.mode.config.SolrCommandConfig; import no.sesat.search.result.BasicResultItem; import no.sesat.search.result.BasicResultList; import no.sesat.search.result.FacetedSearchResult; import no.sesat.search.result.FacetedSearchResultImpl; import no.sesat.search.result.ResultItem; import no.sesat.search.result.ResultList; import no.sesat.search.site.Site; import no.sesat.search.site.config.SiteClassLoaderFactory; import no.sesat.search.site.config.SiteConfiguration; import no.sesat.search.site.config.Spi; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; /** Searching against a Solr index using the Solrj client. * see http://wiki.apache.org/solr/Solrj * * The query syntax could be improved * see http://lucene.apache.org/java/docs/queryparsersyntax.html * * @version $Id$ */ public class SolrSearchCommand extends AbstractSearchCommand{ // Constants ----------------------------------------------------- private static final Logger LOG = Logger.getLogger(SolrSearchCommand.class); // Attributes ---------------------------------------------------- private SolrServer server; private final FacetToolkit facetToolkit; // Static -------------------------------------------------------- private static final ReferenceMap<String,SolrServer> SERVERS = new ReferenceMap<String,SolrServer>( ReferenceMap.Type.SOFT, new ConcurrentHashMap<String, Reference<SolrServer>>()); // Constructors -------------------------------------------------- public SolrSearchCommand(final Context cxt) { super(cxt); try { final String serverUrlKey = ((SolrCommandConfig)cxt.getSearchConfiguration()).getServerUrl(); final SiteConfiguration siteConf = cxt.getDataModel().getSite().getSiteConfiguration(); final String serverUrl = siteConf.getProperty(serverUrlKey); server = SERVERS.get(serverUrl); if(null == server){ server = new CommonsHttpSolrServer(serverUrl); SERVERS.put(serverUrl, server); } } catch (MalformedURLException ex) { LOG.error(ex.getMessage(), ex); } facetToolkit = createFacetToolkit(); } // Public -------------------------------------------------------- @Override public ResultList<ResultItem> execute() { final ResultList<ResultItem> searchResult = null != facetToolkit ? new FacetedSearchResultImpl<ResultItem>() : new BasicResultList<ResultItem>(); try { // set up query final SolrQuery query = new SolrQuery() .setQuery(getTransformedQuery()) .setStart(getOffset()) .setRows(getSearchConfiguration().getResultsToReturn()); modifyQuery(query); DUMP.info(query.toString()); // query final QueryResponse response = server.query(query); final SolrDocumentList docs = response.getResults(); searchResult.setHitCount((int)docs.getNumFound()); // iterate through docs for(SolrDocument doc : docs){ searchResult.addResult(createItem(doc)); } collectFacets(response, searchResult); } catch (SolrServerException ex) { LOG.error(ex.getMessage(), ex); } return searchResult; } @Override public SolrCommandConfig getSearchConfiguration() { return (SolrCommandConfig)super.getSearchConfiguration(); } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- /** Override this to set additional parameters in the SolrQuery. * Crucial for any override to call super.modifyQuery(query) **/ protected void modifyQuery(final SolrQuery query){ // @XXX does this ruin solr caching query.set("uniqueId", context.getDataModel().getParameters().getUniqueId()); // add any filtering query if(0 < getSearchConfiguration().getFilteringQuery().length()){ query.setFilterQueries(getSearchConfiguration().getFilteringQuery()); } // also add any filter if(0 < getFilter().length()){ query.addFilterQuery(getFilter()); } // custom query type if(null != getSearchConfiguration().getQueryType() && 0 < getSearchConfiguration().getQueryType().length()){ query.setQueryType(getSearchConfiguration().getQueryType()); } // The request handler may be configured in the index which fields to return in the results if(0 < getSearchConfiguration().getResultFieldMap().size()){ query.setFields(getSearchConfiguration().getResultFieldMap().keySet().toArray(new String[]{})); } createFacets(query); // when the root logger is set to DEBUG do not limit connection times if(Logger.getRootLogger().getLevel().isGreaterOrEqual(Level.INFO)){ query.setTimeAllowed(getSearchConfiguration().getTimeout()); } // sorting if(isUserSortable()){ final String sort = getUserSortBy(); if(null != sort){ final String[] sortSplit = sort.split(" "); query.addSortField(sortSplit[0], SolrQuery.ORDER.valueOf(sortSplit[1])); } } final Map<String,String> sortMap = getSearchConfiguration().getSortMap(); for(Map.Entry<String,String> entry : sortMap.entrySet()){ final SolrQuery.ORDER order = SolrQuery.ORDER.valueOf(entry.getValue()); query.addSortField(entry.getKey(), order); } } protected FacetToolkit createFacetToolkit(){ FacetToolkit toolkit = null; final String toolkitName = getSearchConfiguration().getFacetToolkit(); if(null != toolkitName && 0 < toolkitName.length()){ toolkit = FacetToolkitFactory.getInstance(context, toolkitName); } return toolkit; } protected final void createFacets(final SolrQuery query){ if(null != facetToolkit){ facetToolkit.createFacets(context, query); } } protected final void collectFacets(final QueryResponse response, final ResultList<ResultItem> searchResult){ if(null != facetToolkit && searchResult instanceof FacetedSearchResult){ facetToolkit.collectFacets(context, response, (FacetedSearchResult<? extends ResultItem>)searchResult); } } protected BasicResultItem createItem(final SolrDocument doc) { Map<String,String> fieldNames; if(0 < getSearchConfiguration().getResultFieldMap().size()){ fieldNames = getSearchConfiguration().getResultFieldMap(); }else{ // The request handler must be configured in the index as to which fields to return in the results fieldNames = new HashMap<String,String>(); for(String fieldName : doc.getFieldNames()){ fieldNames.put(fieldName, fieldName); } } BasicResultItem item = new BasicResultItem(); for (final Map.Entry<String,String> entry : fieldNames.entrySet()){ final Object value = doc.getFieldValue(entry.getKey()); if(value instanceof String){ item = item.addField(entry.getValue(), (String)doc.getFieldValue(entry.getKey())); }else if(value instanceof Serializable){ item = item.addObjectField(entry.getValue(), (Serializable)doc.getFieldValue(entry.getKey())); }else if(null == value) { LOG.debug("Unable to add to ResultItem, field " + entry.getKey() + " does not exist"); }else{ LOG.warn("Unable to add to ResultItem this non Serializable object: " + value); } } return item; } @Override protected Collection<String> getReservedWords() { final Collection<String> words = new ArrayList<String>(super.getReservedWords()); // ampersand is treated as parameter separator just like in the restful URLs words.add("&"); return words; } // Private ------------------------------------------------------- // Inner classes ------------------------------------------------- /** * Provider to add facets from request to SolrQuery. */ public interface FacetToolkit{ void createFacets(SearchCommand.Context context, SolrQuery query); void collectFacets( SearchCommand.Context context, QueryResponse response, FacetedSearchResult<? extends ResultItem> searchResult); } protected static final class FacetToolkitFactory { // Constructors -------------------------------------------------- /** Not possible to create a new instance of FacetToolkitFactory */ private FacetToolkitFactory() { } // Public -------------------------------------------------------- /** Factory call to instiantate a FacetToolkit. * * @param context context providing Resource * @param name the name of the class implementing FacetToolkit * @return */ public static FacetToolkit getInstance( final Context context, final String name){ try{ final Site site = context.getDataModel().getSite().getSite(); final SiteClassLoaderFactory.Context ctlContext = ContextWrapper.wrap( SiteClassLoaderFactory.Context.class, new BaseContext() { public Spi getSpi() { return Spi.SEARCH_COMMAND_CONTROL; } public Site getSite(){ return site; } }, context ); final ClassLoader ctlLoader = SiteClassLoaderFactory.instanceOf(ctlContext).getClassLoader(); @SuppressWarnings("unchecked") final Class<? extends FacetToolkit> cls = (Class<? extends FacetToolkit>)ctlLoader.loadClass(name); return cls.newInstance(); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException(ex); } catch (InstantiationException ex) { throw new IllegalArgumentException(ex); } catch (IllegalAccessException ex) { throw new IllegalArgumentException(ex); } } } }