Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ public interface IndexConstants {
String ENTRY_COUNT_PROPERTY_NAME = "entryCount";

String KEY_COUNT_PROPERTY_NAME = "keyCount";

/**
* Multiplier (Double) applied to the estimated entry count when computing this
* index's query cost. Optional; defaults to 1.0 (no change to the estimate).
* Same property name as oak-search's FulltextIndexConstants#COST_PER_ENTRY,
* used for the same purpose on property/node type indexes.
*/
String COST_PER_ENTRY = "costPerEntry";

/**
* Fixed cost (Double) added once per query plan that uses this index. Optional;
* defaults to the index's built-in overhead. Same property name as
* oak-search's FulltextIndexConstants#COST_PER_EXECUTION.
*/
String COST_PER_EXECUTION = "costPerExecution";

/**
* The regular expression pattern of the values to be indexes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,24 @@ public static String getAsyncLaneName(NodeState idxState, String indexPath, Prop
return null;
}

/**
* Reads an optional double-valued property from an index definition node,
* falling back to a default when the property is absent. Mirrors the
* {@code getOptionalValue} helper oak-search's {@code IndexDefinition} uses
* for the same purpose ({@code costPerEntry}, {@code costPerExecution}, etc.)
* so index types outside oak-search (which cannot depend on it) can read
* the same kind of tunable index-definition properties.
*
* @param defn the index definition node
* @param propertyName the property name
* @param defaultValue the value to return if the property is not set
* @return the property's double value, or {@code defaultValue}
*/
public static double getOptionalValue(NodeState defn, String propertyName, double defaultValue) {
PropertyState ps = defn.getProperty(propertyName);
return ps != null ? ps.getValue(Type.DOUBLE) : defaultValue;
}

/**
* Retrieves the calling class and method from the call stack; this is determined by unwinding
* the stack until it finds a combination of full qualified classname + method (separated by ".") which
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
Expand Down Expand Up @@ -75,6 +77,24 @@ public class PropertyIndexLookup {
*/
static final int MAX_COST = 100;

/**
* Feature toggle name for the configurable costPerEntry/costPerExecution
* cost formula (OAK-12348).
*/
public static final String FT_OAK_12348 = "FT_OAK-12348";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this toggle usable? Where is it wired into the whiteboard mechanism?

In my opinion this kind of wiring is missing:

public static final String FT_IGNORE_LIMIT_IN_INDEX_SELECTION = "FT_OAK-12057";

Feature ignoreLimitInIndexSelection = newFeature(QueryEngineSettings.FT_IGNORE_LIMIT_IN_INDEX_SELECTION, whiteboard);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the whiteboard registration was missed.


/**
* When {@code true} (the default), {@link #getCost} reads {@code costPerEntry}/
* {@code costPerExecution} from the index definition ({@link #getCostConfigurable}).
* When {@code false}, {@link #getCost} uses the original hardcoded formula
* ({@link #getCostLegacy}) unconditionally, ignoring those properties even if
* set. Enabled by default: the new formula reproduces the legacy one exactly
* whenever {@code costPerEntry}/{@code costPerExecution} are absent, so this is
* a behavior-preserving default for anyone not using the new properties -- the
* toggle exists as an escape hatch, not as an opt-in gate.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to AI: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java:137 has an early return, that get's around this PR. However, in my opinion it should not cause any issues, as it only happens for cost == 2.0 which is rare, and anyways a very fast case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But there seem to be things no longer correct in PropertyIndex.java like getMinimumCost()

Probably worth checking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch :)

public static final AtomicBoolean FT_OAK_12348_ENABLE = new AtomicBoolean(true);
Comment thread
bhabegger marked this conversation as resolved.

private final NodeState root;

private final MountInfoProvider mountInfoProvider;
Expand Down Expand Up @@ -135,7 +155,22 @@ Set<IndexStoreStrategy> getStrategies(NodeState definition) {
definition, INDEX_CONTENT_NODE_NAME);
}

/**
* Dispatches to {@link #getCostConfigurable} or {@link #getCostLegacy}
* depending on {@link #FT_OAK_12348_ENABLE}.
*/
public double getCost(Filter filter, String propertyName, PropertyValue value) {
return FT_OAK_12348_ENABLE.get()
? getCostConfigurable(filter, propertyName, value)
: getCostLegacy(filter, propertyName, value);
}

/**
* Original cost formula: {@code COST_OVERHEAD + entryCount}. Ignores
* {@code costPerEntry}/{@code costPerExecution} even if set on the index
* definition.
*/
public double getCostLegacy(Filter filter, String propertyName, PropertyValue value) {
NodeState indexMeta = getIndexNode(root, propertyName, filter);
if (indexMeta == null) {
return Double.POSITIVE_INFINITY;
Expand All @@ -149,6 +184,31 @@ public double getCost(Filter filter, String propertyName, PropertyValue value) {
return cost;
}

/**
* {@code cost = costPerExecution + costPerEntry * entryCount}, both
* optionally configured on the index definition (OAK-12348). Defaults
* ({@code costPerEntry=1.0}, {@code costPerExecution=COST_OVERHEAD})
* reproduce {@link #getCostLegacy} exactly.
*/
public double getCostConfigurable(Filter filter, String propertyName, PropertyValue value) {
NodeState indexMeta = getIndexNode(root, propertyName, filter);
if (indexMeta == null) {
return Double.POSITIVE_INFINITY;
}
Set<IndexStoreStrategy> strategies = getStrategies(indexMeta);
if (strategies.isEmpty()) {
return MAX_COST;
}
ValuePattern pattern = new ValuePattern(indexMeta);
double entryCount = 0;
for (IndexStoreStrategy s : strategies) {
entryCount += s.count(filter, root, indexMeta, encode(value, pattern), MAX_COST);
}
double costPerEntry = IndexUtils.getOptionalValue(indexMeta, IndexConstants.COST_PER_ENTRY, 1.0);
double costPerExecution = IndexUtils.getOptionalValue(indexMeta, IndexConstants.COST_PER_EXECUTION, COST_OVERHEAD);
return costPerExecution + costPerEntry * entryCount;
}

/**
* Get the node with the index definition for the given property, if there
* is an applicable index with data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ public class PropertyIndexPlan {

private boolean matchesNodeTypes;

private final double cost;
/**
* The number of matching entries for the best-matching property found by
* the constructor's search loop, or {@code Double.POSITIVE_INFINITY} if
* none matched. {@link #getCostLegacy} and {@link #getCostConfigurable}
* both derive the final cost from this count.
*/
private final double bestCount;

private final Set<String> values;

Expand Down Expand Up @@ -113,7 +119,7 @@ public class PropertyIndexPlan {

ValuePattern valuePattern = new ValuePattern(definition);

double bestCost = Double.POSITIVE_INFINITY;
double bestCount = Double.POSITIVE_INFINITY;
Set<String> bestValues = emptySet();
int bestDepth = 1;

Expand Down Expand Up @@ -168,22 +174,22 @@ public class PropertyIndexPlan {
}
}
values = PropertyIndexUtil.encode(values);
double cost = strategies.isEmpty() ? MAX_COST : 0;
double count = strategies.isEmpty() ? MAX_COST : 0;
for (IndexStoreStrategy strategy : strategies) {
cost += strategy.count(filter, root, definition,
count += strategy.count(filter, root, definition,
values, MAX_COST);
}
if (unique && cost <= 1) {
if (unique && count <= 1) {
// for unique index, for the normal case
// (that is, for a regular lookup)
// no further reads are needed
cost = 0;
count = 0;
}
if (cost < bestCost) {
if (count < bestCount) {
bestDepth = depth;
bestValues = values;
bestCost = cost;
if (bestCost == 0) {
bestCount = count;
if (bestCount == 0) {
// shortcut: not possible to top this
break;
}
Expand All @@ -194,15 +200,45 @@ public class PropertyIndexPlan {

this.depth = bestDepth;
this.values = bestValues;
this.cost = COST_OVERHEAD + bestCost;
this.bestCount = bestCount;
}

String getName() {
return name;
}

/**
* Dispatches to {@link #getCostConfigurable} or {@link #getCostLegacy}
* depending on {@link PropertyIndexLookup#FT_OAK_12348_ENABLE}, evaluated
* once per plan (a new plan is built whenever the filter changes, so a
* toggle flip is picked up on the next query, not on this cached plan).
*/
double getCost() {
return cost;
return PropertyIndexLookup.FT_OAK_12348_ENABLE.get() ? getCostConfigurable() : getCostLegacy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Computed each time we call getCost() (called multiple times in createPlan), but probably works best with toggle this way.

}

/**
* Original cost formula: {@code COST_OVERHEAD + bestCount}. Ignores
* {@code costPerEntry}/{@code costPerExecution} even if set on the index
* definition.
*/
double getCostLegacy() {
return bestCount == Double.POSITIVE_INFINITY ? Double.POSITIVE_INFINITY : COST_OVERHEAD + bestCount;
}

/**
* {@code cost = costPerExecution + costPerEntry * bestCount}, both
* optionally configured on the index definition (OAK-12348). Defaults
* ({@code costPerEntry=1.0}, {@code costPerExecution=COST_OVERHEAD})
* reproduce {@link #getCostLegacy} exactly.
*/
double getCostConfigurable() {
if (bestCount == Double.POSITIVE_INFINITY) {
return Double.POSITIVE_INFINITY;
}
double costPerEntry = IndexUtils.getOptionalValue(definition, IndexConstants.COST_PER_ENTRY, 1.0);
double costPerExecution = IndexUtils.getOptionalValue(definition, IndexConstants.COST_PER_EXECUTION, COST_OVERHEAD);
return costPerExecution + costPerEntry * bestCount;
}

Cursor execute() {
Expand Down Expand Up @@ -260,7 +296,7 @@ public String toString() {
}
}
buffer.append("\n");
buffer.append(" estimatedCost: ").append(cost).append("\n");
buffer.append(" estimatedCost: ").append(getCost()).append("\n");
return buffer.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.jackrabbit.oak.plugins.index.nodetype;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -91,6 +92,57 @@ public void nodeType() throws Exception {
checkCursor(index.query(filter, rootState), "/folder-1", "/folder-2", "/file-1");
}

/**
* NodeTypeIndex has no cost logic of its own: its cost is the sum of two
* {@code PropertyIndexLookup.getCost()} calls (jcr:primaryType, jcr:mixinTypes)
* against the single "nodetype" property index definition. Setting
* costPerEntry/costPerExecution on that definition must change the total,
* with no code changes needed in the nodetype package itself.
*/
@Test
public void nodeTypeCostOverride() throws Exception {
NodeBuilder root = store.getRoot().builder();

// remove "rep:security" as it interferes with tests
root.getChildNode("rep:security").remove();

NodeBuilder nodetypeIndex = root.getChildNode("oak:index").getChildNode("nodetype");
// set "entryCount", so the node type index counts the nodes
// and the approximation is not used
nodetypeIndex.setProperty("entryCount", -1);
nodetypeIndex.setProperty(org.apache.jackrabbit.oak.plugins.index.IndexConstants.COST_PER_ENTRY, 3.0);
nodetypeIndex.setProperty(org.apache.jackrabbit.oak.plugins.index.IndexConstants.COST_PER_EXECUTION, 10.0);

addFolder(root, "folder-1");
addFolder(root, "folder-2");
addFile(root, "file-1");

store.merge(root, new EditorHook(new IndexUpdateProvider(
new PropertyIndexEditorProvider())), CommitInfo.EMPTY);

NodeState rootState = store.getRoot();
NodeTypeIndex index = new NodeTypeIndex(
Mounts.defaultMountInfoProvider());
FilterImpl filter;

// NodeTypeIndex has no toggle of its own -- it inherits whichever formula
// PropertyIndexLookup.getCost() is currently dispatching to, which is
// FT_OAK_12348_ENABLE, on by default -- no opt-in needed here.
assertTrue("toggle must be on by default",
org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexLookup.FT_OAK_12348_ENABLE.get());

// default (see nodeType() above) is 2*COST_OVERHEAD(2) + entrySum;
// with the override it is 2*costPerExecution + costPerEntry*entrySum
filter = createFilter(rootState, JcrConstants.NT_FOLDER);
assertEquals(2 * 10.0 + 3.0 * 2, index.getCost(filter, rootState), 0.0);

filter = createFilter(rootState, JcrConstants.NT_FILE);
assertEquals(2 * 10.0 + 3.0 * 1, index.getCost(filter, rootState), 0.0);

filter = createFilter(rootState, JcrConstants.NT_HIERARCHYNODE);
assertEquals(2 * 10.0 + 3.0 * 3, index.getCost(filter, rootState), 0.0);
}

private static FilterImpl createFilter(NodeState root, String nodeTypeName) {
NodeTypeInfoProvider nodeTypes = new NodeStateNodeTypeInfoProvider(root);
NodeTypeInfo type = nodeTypes.getNodeTypeInfo(nodeTypeName);
Expand Down
Loading
Loading