// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.pagetemplates; import com.yahoo.component.annotation.Inject; import com.yahoo.component.chain.dependencies.Provides; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.processing.IllegalInputException; 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.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.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Map; /** * 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 queried (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 = CompoundName.from("page.PageTemplateList"); /** The name of the query property containing a list of candidate pages to consider */ public static final CompoundName pageIdListName = CompoundName.from("page.idList"); /** The name of the query property containing the page id to use */ public static final CompoundName pageIdName = CompoundName.from("page.id"); /** The name of the query property containing the resolver id to use */ public static final CompoundName pageResolverName = CompoundName.from("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 IllegalInputException("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()); } } }