// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.query.profile; import com.yahoo.component.provider.Freezable; import com.yahoo.search.query.profile.types.QueryProfileType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This class represent a set of query profiles virtually - rather * than storing and instantiating each profile this structure represents explicitly only * the values set in the various virtual profiles. The set of virtual profiles are defined by a set of * dimensions. Values may be set for any point in this multi-dimensional space, and may also be set for * any regular hyper-region by setting values for any point in certain of these dimensions. * The set of virtual profiles defined by this consists of all the combinations of dimension points for * which one or more values is set in this, as well as any possible less specified regions. *

* A set of virtual profiles are always owned by a single profile, which is also their parent * in the inheritance hierarchy. * * @author bratseth */ public class QueryProfileVariants implements Freezable, Cloneable { private boolean frozen = false; /** Properties indexed by name, to support fast lookup of single values */ private Map fieldValuesByName = new HashMap<>(); /** The inherited profiles for various dimensions settings - a set of fieldvalues of List<QueryProfile> */ private FieldValues inheritedProfiles = new FieldValues(); /** * Field and inherited profiles sorted by specificity used for all-value visiting. * This is the same as how the source data looks (apart from the sorting). */ private List variants = new ArrayList<>(); /** * The names of the dimensions (which are possible properties in the context given on lookup) of this. * Order matters - more specific values to the left in this list are more significant than more specific values * to the right */ private final List dimensions; /** The query profile this variants of */ private final QueryProfile owner; /** * Creates a set of virtual query profiles which may return varying values over the set of dimensions given. * Each dimension is a name for which a key-value may be supplied in the context properties * on lookup time to influence the value returned. */ public QueryProfileVariants(String[] dimensions, QueryProfile owner) { this(List.of(dimensions), owner); } /** * Creates a set of virtual query profiles which may return varying values over the set of dimensions given. * Each dimension is a name for which a key-value may be supplied in the context properties * on lookup time to influence the value returned. * * @param dimensions the dimension names this may vary over. The list gets owned by this, so it must not be further * modified from outside). This will not modify the list. */ public QueryProfileVariants(List dimensions, QueryProfile owner) { // Note: This is not made unmodifiable (here or in freeze) because we depend on map identity comparisons of this // list (in dimensionBinding) for performance reasons. this.dimensions = dimensions; this.owner = owner; } /** Irreversibly prevents any further modifications to this */ public void freeze() { if (frozen) return; for (FieldValues fieldValues : fieldValuesByName.values()) fieldValues.freeze(); fieldValuesByName = Map.copyOf(fieldValuesByName); inheritedProfiles.freeze(); Collections.sort(variants); for (QueryProfileVariant variant : variants) variant.freeze(); variants = List.copyOf(variants); frozen=true; } @Override public boolean isFrozen() { return frozen; } /** Visits the most specific match to the dimension binding of each variable (or the one named by the visitor) */ void accept(boolean allowContent, QueryProfileType type, QueryProfileVisitor visitor, DimensionBinding dimensionBinding) { String contentName = null; if (allowContent) contentName = visitor.getLocalKey(); if (contentName != null) { if (type != null) contentName = type.unalias(contentName); acceptSingleValue(contentName, allowContent, visitor, dimensionBinding); // Special cased for performance } else { acceptAllValues(allowContent, visitor, type, dimensionBinding); } } void acceptSingleValue(String name, boolean allowContent, QueryProfileVisitor visitor, DimensionBinding dimensionBinding) { FieldValues fieldValues = fieldValuesByName.get(name); if (fieldValues == null || ! allowContent) fieldValues = new FieldValues(); fieldValues.sort(); inheritedProfiles.sort(); int inheritedIndex = 0; int fieldIndex = 0; // Go through both the fields and the inherited profiles at the same time and try the single must specific pick // from either of the lists at each step while(fieldIndex < fieldValues.size() || inheritedIndex < inheritedProfiles.size()) { // Get the next most specific from field and inherited FieldValue fieldValue = fieldValues.getIfExists(fieldIndex); FieldValue inheritedProfileValue = inheritedProfiles.getIfExists(inheritedIndex); // Try the most specific first, then the other if (inheritedProfileValue == null || (fieldValue != null && fieldValue.compareTo(inheritedProfileValue) <= 0)) { // Field is most specific, or both are equally specific if (fieldValue.matches(dimensionBinding.getValues())) { visitor.acceptValue(name, fieldValue.getValue(), dimensionBinding, owner, fieldValue.getDimensionValues()); } if (visitor.isDone()) return; fieldIndex++; } else { // Inherited is most specific at this point if (inheritedProfileValue.matches(dimensionBinding.getValues())) { @SuppressWarnings("unchecked") List inheritedProfileList = (List)inheritedProfileValue.getValue(); for (QueryProfile inheritedProfile : inheritedProfileList) { if (visitor.visitInherited()) { inheritedProfile.accept(allowContent, visitor, dimensionBinding.createFor(inheritedProfile.getDimensions()), owner); } if (visitor.isDone()) return; } } inheritedIndex++; } if (visitor.isDone()) return; } } void acceptAllValues(boolean allowContent, QueryProfileVisitor visitor, QueryProfileType type, DimensionBinding dimensionBinding) { if ( ! frozen) Collections.sort(variants); for (QueryProfileVariant variant : variants) { if (variant.matches(dimensionBinding.getValues())) variant.accept(allowContent, type, visitor, dimensionBinding); if (visitor.isDone()) return; } } /** * Returns the most specific matching value of a name for a given set of canonical dimension values. * * @param name the name to return the best matching value of * @param dimensionBinding the dimension bindings to use in this */ public Object get(String name, QueryProfileType type, boolean allowQueryProfileResult, DimensionBinding dimensionBinding) { SingleValueQueryProfileVisitor visitor = new SingleValueQueryProfileVisitor(Collections.singletonList(name),allowQueryProfileResult); visitor.enter(""); accept(true, type, visitor, dimensionBinding); visitor.leave(""); return visitor.getResult(); } /** Inherits a particular profile in a variant of this */ public void inherit(QueryProfile profile, DimensionValues dimensionValues) { ensureNotFrozen(); // Update variant getVariant(dimensionValues,true).inherit(profile); // Update per-variable optimized structure @SuppressWarnings("unchecked") List inheritedAtDimensionValues = (List)inheritedProfiles.getExact(dimensionValues); if (inheritedAtDimensionValues == null) { inheritedAtDimensionValues = new ArrayList<>(); inheritedProfiles.put(dimensionValues, inheritedAtDimensionValues); } inheritedAtDimensionValues.add(profile); } /** * Sets a value to this * * @param fieldName the name of the field to set. This cannot be a compound (dotted) name * @param dimensionValues the dimension values for which this value applies * @param value the value to set */ public void set(String fieldName, DimensionValues dimensionValues, Object value) { ensureNotFrozen(); // Update variant Object combinedValue = getVariant(dimensionValues, true).set(fieldName, value); // Update per-variable optimized structure FieldValues fieldValues = fieldValuesByName.get(fieldName); if (fieldValues == null) { fieldValues = new FieldValues(); fieldValuesByName.put(fieldName, fieldValues); } if (combinedValue != null) fieldValues.put(dimensionValues, combinedValue); } /** * Makes a value unoverridable in a given context. */ public void setOverridable(String fieldName, boolean overridable, DimensionValues dimensionValues) { getVariant(dimensionValues, true).setOverridable(fieldName, overridable); } public Boolean isOverridable(String fieldName, DimensionValues dimensionValues) { QueryProfileVariant variant = getVariant(dimensionValues, false); if (variant == null) return null; return variant.isOverridable(fieldName); } /** * Returns the dimensions over which the virtual profiles in this may return different values. * Each dimension is a name for which a key-value may be supplied in the context properties * on lookup time to influence the value returned. * The dimensions may not be modified - the returned list is always read only. */ // Note: A performance optimization in DimensionBinding depends on the identity of the list returned from this public List getDimensions() { return dimensions; } /** Returns the map of field values of this indexed by field name. */ public Map getFieldValues() { return fieldValuesByName; } /** Returns the profiles inherited from various variants of this */ public FieldValues getInherited() { return inheritedProfiles; } /** * Returns all the variants of this, sorted by specificity. This is content as declared. * The returned list is always unmodifiable. */ public List getVariants() { if (frozen) return variants; // Already unmodifiable return Collections.unmodifiableList(variants); } @Override public QueryProfileVariants clone() { try { if (frozen) return this; QueryProfileVariants clone = (QueryProfileVariants)super.clone(); clone.inheritedProfiles = inheritedProfiles.clone(); clone.variants = new ArrayList<>(); for (QueryProfileVariant variant : variants) clone.variants.add(variant.clone()); clone.fieldValuesByName = new HashMap<>(); for (Map.Entry entry : fieldValuesByName.entrySet()) clone.fieldValuesByName.put(entry.getKey(), entry.getValue().clone(entry.getKey(), clone.variants)); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } /** Throws an IllegalStateException if this is frozen */ protected void ensureNotFrozen() { if (frozen) throw new IllegalStateException(this + " is frozen and cannot be modified"); } /** * Returns the query profile variant having exactly the given dimensions, and creates it if create is set and * it is missing * * @param dimensionValues the dimension values * @param create whether or not to create the variant if missing * @return the profile variant, or null if not found and create is false */ public QueryProfileVariant getVariant(DimensionValues dimensionValues, boolean create) { for (QueryProfileVariant profileVariant : variants) if (profileVariant.getDimensionValues().equals(dimensionValues)) return profileVariant; // Not found if ( ! create) return null; QueryProfileVariant variant = new QueryProfileVariant(dimensionValues, owner); variants.add(variant); return variant; } @Override public String toString() { return "variants of " + owner; } public static class FieldValues implements Freezable, Cloneable { private List resolutionList = null; private boolean frozen = false; @Override public void freeze() { if (frozen) return; sort(); if (resolutionList != null) resolutionList = List.copyOf(resolutionList); frozen = true; } @Override public boolean isFrozen() { return frozen; } public void put(DimensionValues dimensionValues, Object value) { ensureNotFrozen(); if (resolutionList == null) resolutionList = new ArrayList<>(); FieldValue fieldValue = getExactFieldValue(dimensionValues); if (fieldValue != null) // Replace fieldValue.setValue(value); else resolutionList.add(new FieldValue(dimensionValues, value)); } /** Returns the value having exactly the given dimensions, or null if none */ public Object getExact(DimensionValues dimensionValues) { FieldValue value = getExactFieldValue(dimensionValues); if (value == null) return null; return value.getValue(); } /** Returns the field value having exactly the given dimensions, or null if none */ private FieldValue getExactFieldValue(DimensionValues dimensionValues) { for (FieldValue fieldValue : asList()) if (fieldValue.getDimensionValues().equals(dimensionValues)) return fieldValue; return null; } /** Returns the field values (values for various dimensions) for this field as a read-only list (never null) */ public List asList() { if (resolutionList == null) return Collections.emptyList(); return resolutionList; } public FieldValue getIfExists(int index) { if (index >= size()) return null; return resolutionList.get(index); } public void sort() { if (frozen) return ; // sorted already if (resolutionList != null) Collections.sort(resolutionList); } /** Same as asList().size() */ public int size() { if (resolutionList == null) return 0; return resolutionList.size(); } /** Throws an IllegalStateException if this is frozen */ protected void ensureNotFrozen() { if (frozen) throw new IllegalStateException(this + " is frozen and cannot be modified"); } /** Clone by filling in values from the given variants */ public FieldValues clone(String fieldName,List clonedVariants) { try { if (frozen) return this; FieldValues clone = (FieldValues)super.clone(); if (resolutionList != null) { clone.resolutionList = new ArrayList<>(resolutionList.size()); for (FieldValue value : resolutionList) clone.resolutionList.add(value.clone(fieldName, clonedVariants)); } return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } @Override public FieldValues clone() { try { if (frozen) return this; FieldValues clone = (FieldValues)super.clone(); if (resolutionList != null) { clone.resolutionList = new ArrayList<>(resolutionList.size()); for (FieldValue value : resolutionList) clone.resolutionList.add(value.clone()); } return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } } public static class FieldValue implements Comparable, Cloneable { private final DimensionValues dimensionValues; private Object value; public FieldValue(DimensionValues dimensionValues, Object value) { this.dimensionValues = dimensionValues; this.value = value; } /** * Returns the dimension values for which this value should be used. * The dimension array is always of the exact size of the dimensions specified by the owning QueryProfileVariants, * and the values appear in the order defined. "Wildcard" dimensions are represented by a null. */ public DimensionValues getDimensionValues() { return dimensionValues; } /** Returns the value to use for this set of dimension values */ public Object getValue() { return value; } /** Sets the value to use for this set of dimension values */ public void setValue(Object value) { this.value = value; } public boolean matches(DimensionValues givenDimensionValues) { return dimensionValues.matches(givenDimensionValues); } /** * Implements the sort order of this which is based on specificity * where dimensions to the left are more significant. *

* Note: This ordering is not consistent with equals - it returns 0 when the same dimensions * are set, regardless of what they are set to. */ @Override public int compareTo(FieldValue other) { return this.dimensionValues.compareTo(other.dimensionValues); } /** Clone by filling in the value from the given variants */ public FieldValue clone(String fieldName, List clonedVariants) { try { FieldValue clone = (FieldValue)super.clone(); if (this.value instanceof QueryProfile) clone.value = lookupInVariants(fieldName, dimensionValues, clonedVariants); // Otherwise the value is immutable, so keep it as-is return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } @Override public FieldValue clone() { try { FieldValue clone = (FieldValue)super.clone(); clone.value = QueryProfile.cloneIfNecessary(this.value); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } private Object lookupInVariants(String fieldName, DimensionValues dimensionValues, List variants) { for (QueryProfileVariant variant : variants) { if ( ! variant.getDimensionValues().equals(dimensionValues)) continue; return variant.values().get(fieldName); } return null; } @Override public String toString() { return "field value " + value + " for " + dimensionValues; } } }