Java tutorial
package org.apache.solr.handler.component; /* * Licensed to OpenCommerceSearch under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. OpenCommerceSearch licenses this * file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexableField; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.beans.DocumentObjectBinder; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.params.*; import org.apache.solr.common.util.NamedList; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrCore; import org.apache.solr.request.LocalSolrQueryRequest; import org.apache.solr.response.ResultContext; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.search.DocIterator; import org.apache.solr.search.DocList; import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.util.RefCounted; import org.apache.solr.util.plugin.SolrCoreAware; import org.opencommercesearch.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; import java.util.Map.Entry; /** * Component that will read rules from the configured core and filter or modify search results accordingly. * <p/> * A simple example is a boosting rule, that provides parameters to move up/down products in the results. * @author Javier Mendez */ public class RuleManagerComponent extends SearchComponent implements SolrCoreAware { /** * Page size when looking for rules or facets. */ public static final int PAGE_SIZE = 40; /** * Ranking separator used to split a ranking rule into a boost rule and a customRankingRule */ public static final String RANKING_SEPARATOR = "|"; /** * Name of the parameter used when we split a ranking rule using the custom separator */ public static final String CUSTOM_RANKING_PARAM_NAME = "customRankingRule"; /** * Default token replacement for the $SEASON variable in rule category pages & ranking rules */ public static String DEFAULT_SEASON_MAPPER = "*"; /** * Logger instance */ private static Logger logger = LoggerFactory.getLogger(RuleManagerComponent.class); /** * The core container, used to get rules and facet cores. */ CoreContainer coreContainer = null; /** * Base name of the rules core */ String rulesCoreBaseName = "rule"; /** * Base name of the facets core */ String facetsCoreBaseName = "facets"; /** * Base name of the categories core. */ String categoriesCoreBaseName = "categories"; /** * The core to query for rule data. */ String rulesCoreName; /** * The core to query for facet fields data. */ String facetsCoreName; /** * List that contains a specific season value for each month used when replacing the $SEASON token in * ranking rules boost expressions */ String[] seasonMapper = new String[12]; /** * Binder to transform Lucene docs to objects. */ DocumentObjectBinder binder = new DocumentObjectBinder(); /** * Enumeration of valid page types understood by RuleManager * * <ul> * 'search' : all regular search pages * </ul> * <ul> * 'category' : all category based pages (including brand categories) * </ul> * <ul> * 'rule' : all rule based pages * <ul/> */ public enum PageType { search, category, rule } @Override public void init(NamedList args) { SolrParams initArgs = SolrParams.toSolrParams(args); //Load params if defined String rulesCoreBaseName = initArgs.get("rulesCore"); if (rulesCoreBaseName != null) { this.rulesCoreBaseName = rulesCoreBaseName; } String facetsCoreBaseName = initArgs.get("facetsCore"); if (facetsCoreBaseName != null) { this.facetsCoreBaseName = facetsCoreBaseName; } initSeasonMappings(initArgs.get("seasonMapping")); } /** * Initialize the season mappings configuration used during replacements of the * $SEASON token in rule base categories & ranking rule boost expressions. * * The season mapping string should have this format: * * expressionA:1,2,3,4,5,6;expressionB:7,8,9,10,11,12 * * where 1,2,3,4 are the months of the year (starting at 1=january) * * If there are missing months in the mapping, then they will be default to the following expression: * * * @param seasonMapping The season mapping parameter specified by the user in this component definition in solrconfig */ protected void initSeasonMappings(String seasonMapping) { for (int i = Calendar.JANUARY; i <= Calendar.DECEMBER; i++) { seasonMapper[i] = DEFAULT_SEASON_MAPPER; } if (seasonMapping != null) { logger.info("Initializing season mapping: [" + seasonMapping + "]"); String[] expressions = seasonMapping.split(";"); if (expressions != null) { for (String expression : expressions) { String[] entry = expression.split(":"); if (entry.length != 2) { logger.error("Invalid season mapping expression: [" + expression + "]. Correct format: [expression:1,2,3,4;] where 1,2,3,4 are the months of the year"); } else { String value = entry[0]; String[] months = entry[1].split(","); for (int i = 0; i < months.length; i++) { seasonMapper[Integer.parseInt(months[i]) - 1] = value; } } } } else { logger.error( "Please provide a valid season mapping configuration parameter. Valid format: [expressionA:1,2,3,4,5,6;expressionB:7,8,9,10,11,12]"); } } } /** * Whenever indexes change, update core references so the latest copy is used. */ public void inform(SolrCore core) { //Update core container with latest reference. coreContainer = core.getCoreDescriptor().getCoreContainer(); rulesCoreName = getCoreName(core, rulesCoreBaseName); facetsCoreName = getCoreName(core, facetsCoreBaseName); if (rulesCoreName == null || facetsCoreName == null) { logger.error("Running on core " + core.getName() + " that is not 'Preview' or 'Public'. Cannot associate appropiate Rules and Facets cores due that."); } else { logger.debug("Rules core name: " + rulesCoreName); logger.debug("Facets core name: " + facetsCoreName); } } @Override public String getDescription() { return "Rule Manager - adds additional params to a search request based on business rules"; } @Override public String getSource() { return "https://raw.github.com/rmerizalde/opencommercesearch/master/opencommercesearch-solr/src/main/java/org/apache/solr/handler/component/RulesManagerComponent.java"; } @Override public void prepare(ResponseBuilder rb) { SolrParams requestParams = rb.req.getParams(); Boolean rulesEnabled = requestParams.getBool(RuleManagerParams.RULE); if (rulesEnabled == null || !rulesEnabled) { logger.debug("Rule param is set to false. Nothing to do here."); return; } if (rulesCoreName == null || facetsCoreName == null) { logger.warn("There are initialization errors, bypassing this request."); return; } PageType pageType = getPageType(requestParams); if (pageType == null) { //Do nothing, this request is not supported by the RuleManager component logger.debug("No page type param was defined, bypassing this request."); return; } try { //Get matching rules from rulesCore Map<RuleType, List<Document>> rulesMap = searchRules(requestParams, pageType); //Now add params to the original query MergedSolrParams augmentedParams; //Check if there are any redirect rules if (rulesMap.containsKey(RuleType.redirectRule)) { //we need a bunch of the request param default values to avoid exceptions, but to //cut the rest of calculations and return only the redirect we are putting the q to be empty augmentedParams = new MergedSolrParams(requestParams); augmentedParams.set(CommonParams.Q, StringUtils.EMPTY); List<Document> redirects = rulesMap.get(RuleType.redirectRule); if (redirects != null && redirects.size() > 0) { rb.rsp.add("redirect_url", redirects.get(0).get(RuleConstants.FIELD_REDIRECT_URL)); } else { //Shouldn't happen logger.error("Found no redirect rules although there should be, bypassing this request"); return; } } else { augmentedParams = new MergedSolrParams(requestParams); //Set sorting options (re-arrange sort incoming fields) String sorts = requestParams.get(CommonParams.SORT); //Always push the products out of stock to the bottom, even when manual boosts have been selected augmentedParams.setSort(RuleConstants.FIELD_IS_TOOS, SolrQuery.ORDER.asc); //Now put any incoming sort options (if any) if (sorts != null) { String[] sortFields = sorts.split(","); Set<String> sortFieldSet = new HashSet<String>(sortFields.length); for (String sortField : sortFields) { String[] parts = StringUtils.split(sortField, ' '); String fieldName = parts[0]; String order = parts[1]; if (!("score".equals(fieldName) || sortFieldSet.contains(fieldName))) { augmentedParams.addSort(fieldName, SolrQuery.ORDER.valueOf(order)); sortFieldSet.add(fieldName); //Ensure there are no duplicates } } } //Initialize facet manager FacetHandler facetHandler = new FacetHandler(); for (Map.Entry<RuleType, List<Document>> rule : rulesMap.entrySet()) { RuleType type = rule.getKey(); if (type != null) { //If there are boost rules, these will change the sort parameters. type.setParams(this, augmentedParams, rule.getValue(), facetHandler); } } //Finally add the score and version fields to the sorting spec. These will be a tie breaker when other sort specs are added augmentedParams.addSort("score", SolrQuery.ORDER.desc); augmentedParams.addSort("_version_", SolrQuery.ORDER.desc); Map<String, NamedList> facets = facetHandler.getFacets(augmentedParams); setFilterQueries(facets, requestParams, augmentedParams); rb.rsp.add("rule_facets", facets.values()); } logger.debug("Augmented request: " + augmentedParams.toString()); String debug = requestParams.get(RuleManagerParams.DEBUG); if (debug != null && debug.equals("true")) { rb.rsp.add("rule_debug", getDebugInfo(rulesMap, augmentedParams)); } rb.rsp.add("category_prefix", augmentedParams.get("f.category." + FacetParams.FACET_PREFIX)); rb.req.setParams(augmentedParams); } catch (Throwable e) { e.printStackTrace(); logger.error("Failed to handle this request", e); } } @Override public void process(ResponseBuilder rb) throws IOException { //Nothing to do here } /** * Gets the corresponding core name based on the current core instance type (public or preview). * <p/> * For example, if performing a search to the public french product catalog, and the core baseName is <b>'rule'</b>, this method will return * <b>'rulePublic_fr'</b>. * @param core Current core serving a request. * @param baseName Base name of the core to build the name for. * @return Valid core name based on the current core instance type or null if the current core does not have a valid format (expecting '%baseName%instanceType' - example 'catalogPublic'). */ private String getCoreName(SolrCore core, String baseName) { return getCoreName(core, baseName, true); } /** * Gets the corresponding core name based on the current core instance type (public or preview). * <p/> * For example, if performing a search to the public french product catalog, and the core baseName is <b>'rule'</b>, this method will return * <b>'rulePublic_fr'</b>. * <p/> * You can specify whether or not the locale should be included. In the above example '_fr' could be removed if needed. * @param core Current core serving a request. * @param baseName Base name of the core to build the name for. * @param hasLocale Whether or not the given core has locale languages. If not, the language suffix won't be added to the obtained core name. * @return Valid core name based on the current core instance type or null if the current core does not have a valid format (expecting '%baseName%instanceType' - example 'catalogPublic'). */ private String getCoreName(SolrCore core, String baseName, boolean hasLocale) { String coreName = core.getName(); String lowerCaseCoreName = coreName.toLowerCase(); int suffixStart; if ((suffixStart = lowerCaseCoreName.indexOf("public")) < 0 && (suffixStart = lowerCaseCoreName.indexOf("preview")) < 0) { return null; } if (hasLocale) { return baseName + coreName.substring(suffixStart); } else { return baseName + coreName.substring(suffixStart, coreName.length() - 3); //Remove locale, i.e. '_fr', or '_en' } } /** * Get a search handler from the configured core. This method assumes the search handler is looked for is '/select'. * @param core Core to get the search handler from. * @return The search handler at the '/select' path. * @throws IOException If the provided core is null or the search handler does not exist. */ public SearchHandler getSearchHandler(SolrCore core) throws IOException { if (core == null) { throw new IOException( "Cannot process any requests because a required core was not found. Check that you created a core called " + rulesCoreName + ", and " + facetsCoreName + "."); } SearchHandler searchHandler = (SearchHandler) core.getRequestHandler("/select"); if (searchHandler == null) { throw new IOException( "Cannot process any requests because the core search handler was not found. Check that " + core.getName() + " has a valid '/select' request handler configured."); } return searchHandler; } /** * Get the type of page performing this request (search, category, rule page). * @param requestParams Current request params. * @return The page type based on the provided request params, or null if none was specified. */ private static PageType getPageType(SolrParams requestParams) { String pageTypeParam = requestParams.get(RuleManagerParams.PAGE_TYPE); return pageTypeParam == null ? null : PageType.valueOf(pageTypeParam); } /** * Search for matching rules. Any found rules are added to a map to process them later. * @param requestParams Incoming search params. * @param pageType Current page type. * @return Map of rules where the key is the rule type. * @throws IOException If there are issues getting the rules core. */ private Map<RuleType, List<Document>> searchRules(SolrParams requestParams, PageType pageType) throws IOException { Map<RuleType, List<Document>> rulesMap = new HashMap<RuleType, List<Document>>(); //Prepare query to rules index String qParam = requestParams.get(CommonParams.Q); SolrQuery ruleParams = getRulesQuery(requestParams, pageType); if (ruleParams == null) { //Do nothing, this request is not supported by the RuleManager component return rulesMap; } SolrCore rulesCore = coreContainer.getCore(rulesCoreName); SearchHandler searchHandler = getSearchHandler(rulesCore); RefCounted<SolrIndexSearcher> rulesSearcher = rulesCore.getSearcher(); int ruleCounter = 0, matchedRules = 0; try { do { ruleParams.set(CommonParams.START, ruleCounter); logger.debug("Searching rules core - rules processed so far " + ruleCounter); SolrQueryResponse response = new SolrQueryResponse(); rulesCore.execute(searchHandler, new LocalSolrQueryRequest(rulesCore, ruleParams), response); ResultContext result = (ResultContext) response.getValues().get("response"); if (result != null) { DocList rules = result.docs; DocIterator ruleIterator = rules.iterator(); matchedRules = rules.matches(); //Process rules while (ruleIterator.hasNext()) { ruleCounter++; Document ruleDoc = rulesSearcher.get().doc(ruleIterator.nextDoc()); String ruleId = ruleDoc.getField("id").stringValue(); String excludeRuleParam = requestParams.get("excludeRules"); if (excludeRuleParam != null) { List<String> excludeRules = Arrays.asList(excludeRuleParam.split(",")); if (excludeRules.contains(ruleId)) { continue; } } IndexableField experimentalField = ruleDoc.getField(RuleConstants.FIELD_EXPERIMENTAL); if (experimentalField != null) { Boolean experimental = (BooleanUtils.toBooleanObject(experimentalField.stringValue())); String includeRuleParam = requestParams.get("includeRules"); if (experimental && includeRuleParam != null) { List<String> includeRules = Arrays.asList(includeRuleParam.split(",")); if (!includeRules.contains(ruleId)) { continue; } } } if (PageType.search == pageType && !filterExactMatch(qParam, ruleDoc)) { // Skip this rule as an exact match was required, but got only a partial match. continue; } RuleType ruleType = RuleType.valueOf(ruleDoc.get(RuleConstants.FIELD_RULE_TYPE)); addRuleToMap(rulesMap, ruleType, ruleDoc); } } else { logger.error("An error occurred when searching for matching rules. Processed rules so far: " + ruleCounter, response.getException()); break; } } while (ruleCounter < matchedRules); logger.debug("Rules found: " + matchedRules); } finally { rulesSearcher.decref(); } return rulesMap; } /** * Checks if the given rule was configured as an exact match and the query 'q' matches the query in the rule. * <p/> * This method is used to filter out rules when exact matches are required. Rules query field is tokenized, so it * will always matches even when the query is not exactly the same. * <p/> * We want to support either partial or exact query matches. So if the rule query field value is between brackets, it * tells the rule manager that an exact match is desired. If the rule query has brackets and is not an exact match, this * method will return false. * @param q Query to check for * @param rule Rule whose query field should match the given query * @return true if the given query ('q') matches the rule query field and an exact was required, true otherwise. */ private boolean filterExactMatch(String q, Document rule) { String targetQuery = rule.get(RuleConstants.FIELD_QUERY); if (isExactMatch(targetQuery)) { targetQuery = removeBrackets(targetQuery).toLowerCase(); return targetQuery.equals(q.toLowerCase()); } return true; } /** * Return true if the given query is an exact match, owtherwise false. The exact match syntax is to put the query * in the rule between brackets. For example ("the bike"). */ private boolean isExactMatch(String query) { return query != null && query.startsWith("[") && query.endsWith("]"); } /** * Just a helper method to strip off the characters */ private String removeBrackets(String query) { return query.substring(1, query.length() - 1); } /** * Prepares a query to the rules index based on the type of request received and the provided params. * <p/> * This query is intended to match any rules that apply for the current request. * @param requestParams Parameters on the current request being processed (i.e. search, category browsing, rule based pages, etc). * @param pageType Type of page to which this request belongs to. * @return Query ready to be sent to the rules core, to fetch all rules that apply for the given request. * @throws IOException If there are missing required values. */ protected SolrQuery getRulesQuery(SolrParams requestParams, PageType pageType) throws IOException { String catalogId = requestParams.get(RuleManagerParams.CATALOG_ID); if (catalogId == null) { logger.debug("No catalog ID provided, bypassing this request."); return null; } SolrQuery ruleParams = new SolrQuery("*:*"); //Common params ruleParams.set(CommonParams.ROWS, PAGE_SIZE); ruleParams.set(CommonParams.FL, RuleConstants.FIELD_ID, RuleConstants.FIELD_BOOST_FUNCTION, RuleConstants.FIELD_FACET_FIELD, RuleConstants.FIELD_COMBINE_MODE, RuleConstants.FIELD_QUERY, RuleConstants.FIELD_CATEGORY); //Sorting options ruleParams.addSort(RuleConstants.FIELD_SORT_PRIORITY, SolrQuery.ORDER.asc); ruleParams.addSort(RuleConstants.FIELD_SCORE, SolrQuery.ORDER.asc); ruleParams.addSort(RuleConstants.FIELD_ID, SolrQuery.ORDER.asc); //Filter queries StringBuilder reusableStringBuilder = new StringBuilder(); ruleParams.addFilterQuery( getTargetFilter(reusableStringBuilder, pageType, requestParams.get(CommonParams.Q))); ruleParams.addFilterQuery( getCategoryFilter(reusableStringBuilder, requestParams.get(RuleManagerParams.CATEGORY_FILTER))); ruleParams.addFilterQuery( getSiteFilter(reusableStringBuilder, requestParams.getParams(RuleManagerParams.SITE_IDS))); ruleParams.addFilterQuery( getBrandFilter(reusableStringBuilder, requestParams.get(RuleManagerParams.BRAND_ID))); ruleParams.addFilterQuery(getSubTargetFilter(reusableStringBuilder, isOutletRequest(requestParams.getParams(CommonParams.FQ)))); StringBuilder catalogFilter = reuseStringBuilder(reusableStringBuilder); catalogFilter.append(RuleConstants.FIELD_CATALOG_ID).append(":").append(RuleConstants.WILDCARD) .append(" OR ").append(RuleConstants.FIELD_CATALOG_ID).append(":").append(catalogId); ruleParams.addFilterQuery(catalogFilter.toString()); if (requestParams.get("redirects") != null) { if (!requestParams.getBool("redirects")) { ruleParams.addFilterQuery("-ruleType:redirectRule"); } } if (!BooleanUtils.toBoolean(requestParams.getBool("facet"))) { ruleParams.addFilterQuery("-ruleType:facetRule"); } //Notice how the current datetime (NOW wildcard on Solr) is rounded to days (NOW/DAY). This allows filter caches //to be reused and hopefully improve performance. If you don't round to day, NOW is very precise (up to milliseconds); so every query //would need a new entry on the filter cache... //Also, notice that NOW/DAY is midnight from last night, and NOW/DAY+1DAY is midnight today. //The below query is intended to match rules with null start or end dates, or start and end dates in the proper range. StringBuilder dateFilter = reuseStringBuilder(reusableStringBuilder); dateFilter.append("-(((").append(RuleConstants.FIELD_START_DATE).append(":[* TO *]) AND -(") .append(RuleConstants.FIELD_START_DATE).append(":[* TO NOW/DAY+1DAY])) OR (") .append(RuleConstants.FIELD_END_DATE).append(":[* TO *] AND -").append(RuleConstants.FIELD_END_DATE) .append(":[NOW/DAY+1DAY TO *]))"); ruleParams.addFilterQuery(dateFilter.toString()); return ruleParams; } /** * Gets the target filter * @param reusableStringBuilder String builder to put data into * @param pageType The current page type * @param q the query param from original request * @return Target filter for rules */ private String getTargetFilter(StringBuilder reusableStringBuilder, PageType pageType, String q) throws IOException { StringBuilder targetFilter = reuseStringBuilder(reusableStringBuilder); if (pageType == PageType.search) { if (StringUtils.isEmpty(q)) { throw new IOException("Cannot process search request because the 'q' param is empty."); } targetFilter.append("(target:allpages OR target:searchpages) AND ("); if (!(q.equals("*") || q.equals("*:*"))) { targetFilter.append("(").append(q).append(")^2 OR "); } targetFilter.append("query:__all__)"); } else { targetFilter.append("target:allpages OR target:categorypages"); } return targetFilter.toString(); } /** * Gets the category filter * @param reusableStringBuilder String builder to put data into * @param categoryToken Category search tokens to filter out rules * @return Category filter for rules */ private String getCategoryFilter(StringBuilder reusableStringBuilder, String categoryToken) { StringBuilder categoryFilter = reuseStringBuilder(reusableStringBuilder); categoryFilter.append(RuleConstants.FIELD_CATEGORY).append(":").append(RuleConstants.WILDCARD); if (StringUtils.isNotBlank(categoryToken)) { categoryFilter.append(" OR ").append(RuleConstants.FIELD_CATEGORY).append(":").append(categoryToken); } return categoryFilter.toString(); } /** * Gets the site filter * @param reusableStringBuilder String builder to put data into * @param sites Array of site codes to look for * @return The site filter */ private String getSiteFilter(StringBuilder reusableStringBuilder, String[] sites) { StringBuilder siteFilter = reuseStringBuilder(reusableStringBuilder); siteFilter.append(RuleConstants.FIELD_SITE_ID).append(":").append(RuleConstants.WILDCARD); if (sites != null) { for (String site : sites) { siteFilter.append(" OR ").append(RuleConstants.FIELD_SITE_ID).append(":").append(site); } } return siteFilter.toString(); } /** * Gets the brand filter * @param reusableStringBuilder String builder to put data into * @param brandId Brand ID used to filter by if any * @return The brand filter */ private String getBrandFilter(StringBuilder reusableStringBuilder, String brandId) { StringBuilder brandFilter = reuseStringBuilder(reusableStringBuilder); brandFilter.append(RuleConstants.FIELD_BRAND_ID).append(":").append(RuleConstants.WILDCARD); if (StringUtils.isNotBlank(brandId)) { brandFilter.append(" OR ").append(RuleConstants.FIELD_BRAND_ID).append(":").append(brandId); } return brandFilter.toString(); } /** * Gets the subTarget filter * @param reusableStringBuilder String builder to put data into * @param isOutletPage whether or not the current page is outlet * @return The subTarget filter */ private String getSubTargetFilter(StringBuilder reusableStringBuilder, boolean isOutletPage) { StringBuilder subTargetFilter = reuseStringBuilder(reusableStringBuilder); subTargetFilter.append(RuleConstants.FIELD_SUB_TARGET).append(":").append(RuleConstants.WILDCARD); subTargetFilter.append(" OR ").append(RuleConstants.FIELD_SUB_TARGET).append(":"); if (isOutletPage) { subTargetFilter.append(RuleConstants.SUB_TARGET_OUTLET); } else { subTargetFilter.append(RuleConstants.SUB_TARGET_RETAIL); } return subTargetFilter.toString(); } /** * Resets a string builder so it can be reused. * @param stringBuilder String builder to reuse. * @return new empty string builder based on the one provided. */ private StringBuilder reuseStringBuilder(StringBuilder stringBuilder) { stringBuilder.reverse().setLength(0); return stringBuilder; } /** * Tells whether or not the current search should include outlet results or not. * <p/> * This is done by inspecting the filter queries set for the current search request. * @param filterQueries Array of filter queries to inspect. * @return True if the current search will include outlet results, false otherwise. */ private boolean isOutletRequest(String[] filterQueries) { if (filterQueries != null) { for (String filterQuery : filterQueries) { if (filterQuery.startsWith(SearchConstants.FIELD_IS_CLOSEOUT + ":true")) { return true; } } } return false; } /** * Get core for facet fields. * @return SolrCore for facet fields, or null if not found. */ SolrCore getFacetsCore() { return coreContainer.getCore(facetsCoreName); } /** * Helper method that updates a given rule map. * @param rulesMap Rule map to update. * @param ruleType The type of rule being added. * @param ruleDoc Rule doc to insert into the map. */ private void addRuleToMap(Map<RuleType, List<Document>> rulesMap, RuleType ruleType, Document ruleDoc) { List<Document> ruleList = rulesMap.get(ruleType); if (ruleList == null) { ruleList = new LinkedList<Document>(); rulesMap.put(ruleType, ruleList); } ruleList.add(ruleDoc); } /** * Set filter queries params, used for faceting and filtering of results. * @param requestParams Original request params * @param ruleParams The new query parameters added by the rules component. */ private void setFilterQueries(Map<String, NamedList> facets, SolrParams requestParams, MergedSolrParams ruleParams) { String isRulePage = requestParams.get(RuleManagerParams.RULE_PAGE); String catalogId = requestParams.get(RuleManagerParams.CATALOG_ID); ruleParams.setFacetPrefix(RuleConstants.FIELD_CATEGORY, "1." + catalogId + "."); ruleParams.addFilterQuery(RuleConstants.FIELD_CATEGORY + ":0." + catalogId); String categoryFilter = requestParams.get(RuleManagerParams.CATEGORY_FILTER); //if we don't have a category filter, or we are in a rule page, skip adding the facet prefix //to the category facet. This is cause we don't index the rule path in the category facet if (StringUtils.isNotBlank(categoryFilter) && !BooleanUtils.toBoolean(isRulePage)) { int index = categoryFilter.indexOf(SearchConstants.CATEGORY_SEPARATOR); if (index != -1) { int level = Integer.parseInt(categoryFilter.substring(0, index)); categoryFilter = ++level + FilterQuery.unescapeQueryChars(categoryFilter.substring(index)) + "."; ruleParams.setFacetPrefix(RuleConstants.FIELD_CATEGORY, categoryFilter); } } if (BooleanUtils.toBoolean(isRulePage)) { //if we are in a rule page, we need to replace the FQ from the original request for possible //appearances of the $YEAR & $SEASON variables, to set the corresponding value Calendar calendar = Calendar.getInstance(); ruleParams.replaceVariable(CommonParams.FQ, "$YEAR", Integer.toString(calendar.get(Calendar.YEAR))); ruleParams.replaceVariable(CommonParams.FQ, "$SEASON", seasonMapper[calendar.get(Calendar.MONTH)]); } String[] filterQueries = requestParams.getParams("rule.fq"); if (filterQueries == null) { return; } Map<String, Set<String>> multiExpressionFilters = new HashMap<String, Set<String>>(); for (String filterQuery : filterQueries) { String[] parts = StringUtils.split(filterQuery, ":", 2); if (parts.length != 2) { logger.error("Invalid filter query: " + filterQuery); continue; } else { String fieldName = parts[0]; String fieldExpression = parts[1]; if (fieldName.equals("category")) { int index = fieldExpression.indexOf(SearchConstants.CATEGORY_SEPARATOR); if (index != -1) { int level = Integer.parseInt(fieldExpression.substring(0, index)); fieldExpression = ++level + FilterQuery.unescapeQueryChars(fieldExpression.substring(index)) + "."; ruleParams.setFacetPrefix("category", fieldExpression); } } NamedList facetItem = facets.get(fieldName); if (facetItem != null) { String multiSelect = (String) facetItem.get("isMultiSelect"); if (facetItem != null && StringUtils.isNotBlank(multiSelect)) { if (FacetHandler.getBooleanFromField(multiSelect)) { Set<String> expressions = multiExpressionFilters.get(fieldName); if (expressions == null) { expressions = new HashSet<String>(); multiExpressionFilters.put(fieldName, expressions); } expressions.add(fieldExpression); continue; } } } } ruleParams.addFilterQuery(filterQuery); } StringBuilder b = new StringBuilder(); for (Entry<String, Set<String>> entry : multiExpressionFilters.entrySet()) { String operator = " OR "; String fieldName = entry.getKey(); b.append("{!tag=").append(fieldName).append("}"); for (String expression : entry.getValue()) { b.append(fieldName).append(FilterQuery.SEPARATOR).append(expression).append(operator); } b.setLength(b.length() - operator.length()); ruleParams.addFilterQuery(b.toString()); b.setLength(0); } } /** * Creates a named list with all rules that are being applied to the current request. * <p/> * The final named list can be added to the search response, for debugging purposes. * @param rulesMap Map of rules that were found by the manager component. * @param ruleParams List of params that will be added to the original query due found rules. * @return named list can be added to the search response, for debugging purposes. */ private Map<String, Object> getDebugInfo(Map<RuleType, List<Document>> rulesMap, SolrParams ruleParams) { Map<String, Object> debugInfo = Maps.newHashMapWithExpectedSize(2); //We are transforming the response cause we only need the rule type and the rule id in the final response //If we get this via solrj, then the response was actually returning the entire rule document and the //full package + enum name to the RuleType enumerator. Map<String, List<Map<String, String>>> transformedRulesMap = Maps .newHashMapWithExpectedSize(rulesMap.size()); for (Entry<RuleType, List<Document>> entry : rulesMap.entrySet()) { transformedRulesMap.put(entry.getKey().name(), Lists.transform(entry.getValue(), docToMapWithId)); } debugInfo.put("rules", transformedRulesMap); debugInfo.put("ruleParams", Joiner.on(",").withKeyValueSeparator("=").join(ruleParams.toNamedList())); return debugInfo; } Function<Document, Map<String, String>> docToMapWithId = new Function<Document, Map<String, String>>() { @Override public Map<String, String> apply(Document indexableFields) { Map<String, String> map = Maps.newHashMapWithExpectedSize(1); map.put("id", indexableFields.get("id")); return map; } }; /** * Enumeration of valid rule types understood by RuleManager */ public enum RuleType { facetRule() { void setParams(RuleManagerComponent component, MergedSolrParams ruleParams, List<Document> rules, FacetHandler facetHandler) throws IOException { for (Document rule : rules) { if (RuleConstants.COMBINE_MODE_REPLACE.equals(rule.get(RuleConstants.FIELD_COMBINE_MODE))) { facetHandler.clear(); } String[] facetField = rule.getValues(RuleConstants.FIELD_FACET_FIELD); if (facetField != null) { String facetsQueryString = getQueryString(rule); if (StringUtils.isBlank(facetsQueryString)) { continue; } facetHandler.addFacet(facetField); searchFacets(component, facetsQueryString, facetHandler); } } facetHandler.setParams(ruleParams); } /** * Search for matching facets. * @param component The rules component. * @param facetsQueryString Facets query string to search for. * @param facetHandler Facet handler where found facets will be stored. * @throws IOException If can't lookup for facet fields. */ private void searchFacets(RuleManagerComponent component, String facetsQueryString, FacetHandler facetHandler) throws IOException { SolrCore facetsCore = component.getFacetsCore(); SearchHandler searchHandler = component.getSearchHandler(facetsCore); RefCounted<SolrIndexSearcher> facetsSearcher = facetsCore.getSearcher(); try { //Prepare query to rules index SolrQuery params = new SolrQuery(facetsQueryString); params.set(CommonParams.ROWS, PAGE_SIZE); int facetCounter = 0, matchedFacets = 0; do { params.set(CommonParams.START, facetCounter); logger.debug("Searching facets core - facets processed so far " + facetCounter); SolrQueryResponse response = new SolrQueryResponse(); facetsCore.execute(searchHandler, new LocalSolrQueryRequest(facetsCore, params), response); ResultContext result = (ResultContext) response.getValues().get("response"); if (result != null) { DocList facets = result.docs; DocIterator facetsIterator = facets.iterator(); matchedFacets = facets.matches(); while (facetsIterator.hasNext()) { facetCounter++; int facetId = facetsIterator.nextDoc(); facetHandler.addFacet(facetsSearcher.get().doc(facetId)); } } else { logger.error("An error occurred when searching for facets. Processed facets so far: " + facetCounter, response.getException()); break; } } while (facetCounter < matchedFacets); logger.debug("Matched facets: " + matchedFacets); } finally { facetsSearcher.decref(); } } /** * Gets a valid facets query out of a given rule facet ids. * @param rule Rule to get the ids from. * @return A query string composed of the given rule facet ids. Null if there were no facet IDs. */ private String getQueryString(Document rule) { String[] facetIds = rule.getValues(RuleConstants.FIELD_FACET_ID); if (facetIds.length > 0) { StringBuilder facetsQueryString = new StringBuilder(); for (String facetId : facetIds) { facetsQueryString.append(RuleConstants.FIELD_ID).append(":").append(facetId).append(" OR "); } facetsQueryString.setLength(facetsQueryString.length() - 4); return facetsQueryString.toString(); } else { return null; } } }, boostRule() { void setParams(RuleManagerComponent component, MergedSolrParams query, List<Document> rules, FacetHandler facetHandler) { String[] sortFields = query.getParams(CommonParams.SORT); if (sortFields != null && sortFields.length > 1) { //User has selected a sorting option, ignore manual boosts return; } if (rules.size() >= 1) { for (Document rule : rules) { if (filterRule(query, rule)) { continue; } String[] products = rule.getValues(RuleConstants.FIELD_BOOSTED_PRODUCTS); if (products != null && products.length > 0) { StringBuilder b = new StringBuilder("fixedBoost(productId,"); for (String product : products) { b.append("'").append(product).append("',"); } b.setLength(b.length() - 1); b.append(")"); query.addSort(b.toString(), SolrQuery.ORDER.asc); break; } } //TODO handle multiple boost rules } } }, blockRule() { void setParams(RuleManagerComponent component, MergedSolrParams query, List<Document> rules, FacetHandler facetHandler) { for (Document rule : rules) { String[] products = rule.getValues(RuleConstants.FIELD_BLOCKED_PRODUCTS); if (products != null) { for (String product : products) { query.addFilterQuery("-productId:" + product); } } } } }, redirectRule() { @Override void setParams(RuleManagerComponent component, MergedSolrParams query, List<Document> rules, FacetHandler facetHandler) { //for redirect rules we don't need a enum entry to add parameters to the query //but to avoid an exception while on: RuleType.valueOf(entry.getKey()); we are adding //this empty entry. The redirect itself will be handled by the rule manager } }, rankingRule() { @Override void setParams(RuleManagerComponent component, MergedSolrParams query, List<Document> rules, FacetHandler facetHandler) { for (Document rule : rules) { if (filterRule(query, rule)) { continue; } String boostFunction = rule.get(RuleConstants.FIELD_BOOST_FUNCTION); if (boostFunction != null) { //If the $YEAR or $SEASON variables appear in the boost function, replace them for the corresponding value Calendar calendar = Calendar.getInstance(); boostFunction = StringUtils.replace(boostFunction, "$YEAR", Integer.toString(calendar.get(Calendar.YEAR))); boostFunction = StringUtils.replace(boostFunction, "$SEASON", component.seasonMapper[calendar.get(Calendar.MONTH)]); if (StringUtils.contains(boostFunction, RANKING_SEPARATOR)) { // If the ranking rule has our custom ranking separator, split the rule by that // separator. // Set the first part of the rule as a regular boost param in the solr query // and set the second part using the CUSTOM_BOOST_PARAM_NAME // This is useful to provide a solr expression to a custom component so that you can take advantage of // the a/b test framework to test many variations of the same expression String[] boostRules = StringUtils.split(boostFunction, RANKING_SEPARATOR); if (boostRules.length == 2) { query.add(RuleConstants.FIELD_BOOST, boostRules[0]); query.add(CUSTOM_RANKING_PARAM_NAME, boostRules[1]); } else { logger.error("Incorrect use of the '" + RANKING_SEPARATOR + "' operator in the following ranking rule:" + boostFunction); } } else { query.add(RuleConstants.FIELD_BOOST, boostFunction); } } } } }; /** * Filter a rule if we are processing a rule category page and the rule * field: 'category' doesn't contain the rule category path * @param query The solr params * @param doc The rule to process */ boolean filterRule(MergedSolrParams query, Document doc) { String isRulePage = query.get(RuleManagerParams.RULE_PAGE); String categoryFilter = query.get(RuleManagerParams.CATEGORY_FILTER); if (StringUtils.isNotBlank(categoryFilter) && BooleanUtils.toBoolean(isRulePage)) { String[] categories = doc.getValues(RuleConstants.FIELD_CATEGORY); for (String category : categories) { //the categoryFilter comes already escaped, so we need to escape the category before comparing //otherwise all of them will be filtered if (ClientUtils.escapeQueryChars(category).equals(categoryFilter)) { return false; } } return true; } return false; } abstract void setParams(RuleManagerComponent component, MergedSolrParams query, List<Document> rules, FacetHandler facetHandler) throws IOException; } }