// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.pagetemplates; import com.google.inject.Inject; import com.yahoo.component.ComponentId; import com.yahoo.component.chain.dependencies.Provides; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.search.Query; import com.yahoo.search.Result; import com.yahoo.search.Searcher; import com.yahoo.search.intent.model.IntentModel; import com.yahoo.search.pagetemplates.config.PageTemplateConfigurer; import com.yahoo.search.pagetemplates.engine.Organizer; import com.yahoo.search.pagetemplates.engine.Resolution; import com.yahoo.search.pagetemplates.engine.Resolver; import com.yahoo.search.pagetemplates.engine.resolvers.DeterministicResolver; import com.yahoo.search.pagetemplates.engine.resolvers.RandomResolver; import com.yahoo.search.pagetemplates.engine.resolvers.ResolverRegistry; import com.yahoo.search.pagetemplates.model.Choice; import com.yahoo.search.pagetemplates.model.PageElement; import com.yahoo.search.pagetemplates.model.Source; import com.yahoo.processing.request.CompoundName; import com.yahoo.search.result.ErrorMessage; import com.yahoo.search.searchchain.Execution; import java.util.*; /** * Enables page optimization templates. * This searcher should be placed before federation points in the search chain. *

* Input query properties: *

* * Output query properties: * * *

* The set of page templates chosen for the query specifies a list of sources to be queries (the page template sources). * In addition, the query may contain *

* This searcher combines these sources into a single set in query.model by the following rules: * * * @author bratseth */ @Provides("PageTemplates") public class PageTemplateSearcher extends Searcher { /** The name of the query property containing the resolved candidate page template list */ public static final CompoundName pagePageTemplateListName=new CompoundName("page.PageTemplateList"); /** The name of the query property containing a list of candidate pages to consider */ public static final CompoundName pageIdListName=new CompoundName("page.idList"); /** The name of the query property containing the page id to use */ public static final CompoundName pageIdName=new CompoundName("page.id"); /** The name of the query property containing the resolver id to use */ public static final CompoundName pageResolverName=new CompoundName("page.resolver"); private final ResolverRegistry resolverRegistry; private final Organizer organizer = new Organizer(); private final PageTemplateRegistry templateRegistry; /** Creates this from a configuration. This will be called by the container. */ @Inject public PageTemplateSearcher(PageTemplatesConfig pageTemplatesConfig, ComponentRegistry resolverRegistry) { this(PageTemplateConfigurer.toRegistry(pageTemplatesConfig), resolverRegistry.allComponents()); } /** * Creates this from an existing page template registry, using only built-in resolvers * * @param templateRegistry the page template registry. This will be frozen by this call. * @param resolvers the resolvers to use, in addition to the default resolvers */ public PageTemplateSearcher(PageTemplateRegistry templateRegistry, Resolver... resolvers) { this(templateRegistry, Arrays.asList(resolvers)); } private PageTemplateSearcher(PageTemplateRegistry templateRegistry, List resolvers) { this.templateRegistry = templateRegistry; templateRegistry.freeze(); this.resolverRegistry = new ResolverRegistry(resolvers); } @Override public Result search(Query query, Execution execution) { // Pre execution: Choose template and sources List pages=selectPageTemplates(query); if (pages.isEmpty()) return execution.search(query); // Bypass if no page template chosen addSources(pages,query); // Set the page template list for inspection by other searchers query.properties().set(pagePageTemplateListName, pages); // Execute Result result=execution.search(query); // Post execution: Resolve choices and organize the result as dictated by the resolved template Choice pageTemplateChoice=Choice.createSingletons(pages); Resolution resolution=selectResolver(query).resolve(pageTemplateChoice,query,result); organizer.organize(pageTemplateChoice,resolution,result); return result; } /** * Returns the list of page templates specified in the query, or the default if none, or the * empty list if no default, never null. */ private List selectPageTemplates(Query query) { // Determine the list of page template ids @SuppressWarnings("unchecked") List pageIds = (List) query.properties().get(pageIdListName); if (pageIds==null) { String pageIdString=query.properties().getString(pageIdName,"").trim(); if (pageIdString.length()>0) pageIds=Arrays.asList(pageIdString.split(" ")); } // If none set, just return the default or null if none if (pageIds==null) { PageElement defaultPage=templateRegistry.getComponent("default"); return (defaultPage==null ? Collections.emptyList() : Collections.singletonList(defaultPage)); } // Resolve the id list to page templates List pages=new ArrayList<>(pageIds.size()); for (String pageId : pageIds) { PageTemplate page=templateRegistry.getComponent(pageId); if (page==null) query.errors().add(ErrorMessage.createInvalidQueryParameter("Could not resolve requested page template '" + pageId + "'")); else pages.add(page); } return pages; } private Resolver selectResolver(Query query) { String resolverId=query.properties().getString(pageResolverName); if (resolverId==null) return resolverRegistry.defaultResolver(); Resolver resolver=resolverRegistry.getComponent(resolverId); if (resolver==null) throw new IllegalArgumentException("No page template resolver '" + resolverId + "'"); return resolver; } /** Sets query.getModel().getSources() to the right value and add source parameters specified in templates */ private void addSources(List pages,Query query) { // Determine all wanted sources Set pageSources=new HashSet<>(); for (PageElement page : pages) pageSources.addAll(((PageTemplate)page).getSources()); addErrorIfSameSourceMultipleTimes(pages,pageSources,query); if (query.getModel().getSources().size() > 0) { // Add properties if the source list is set explicitly, but do not modify otherwise addParametersForIncludedSources(pageSources,query); return; } if (pageSources.contains(Source.any)) { IntentModel intentModel=IntentModel.getFrom(query); if (intentModel!=null) { query.getModel().getSources().addAll(intentModel.getSourceNames()); addPageTemplateSources(pageSources,query); } // otherwise leave empty to search all } else { // Let the page templates decide addPageTemplateSources(pageSources,query); } } private void addPageTemplateSources(Set pageSources,Query query) { for (Source pageSource : pageSources) { if (pageSource==Source.any) continue; query.getModel().getSources().add(pageSource.getName()); addParameters(pageSource,query); } } private void addParametersForIncludedSources(Set sources,Query query) { for (Source source : sources) { if (source.parameters().size()>0 && query.getModel().getSources().contains(source.getName())) addParameters(source,query); } } /** Adds parameters specified in the source to the correct namespace in the query */ private void addParameters(Source source,Query query) { for (Map.Entry parameter : source.parameters().entrySet()) query.properties().set("source." + source.getName() + "." + parameter.getKey(),parameter.getValue()); } /** * Currently executing multiple queries to the same source with different parameter sets, * is not supported. (Same parameter sets in multiple templates is supported, * and will be just one entry in this set). */ private void addErrorIfSameSourceMultipleTimes(List pages,Set sources,Query query) { Set sourceNames=new HashSet<>(); for (Source source : sources) { if (sourceNames.contains(source.getName())) query.errors().add(ErrorMessage.createInvalidQueryParameter( "Querying the same source multiple times with different parameter sets as part of one query " + "is not supported. " + pages + " requests this for source '" + source + "'")); sourceNames.add(source.getName()); } } }