fix(server): handle match() in no index case (#3039)

This commit is contained in:
legendpei 2026-06-04 17:50:07 +08:00 committed by GitHub
parent d6e6216222
commit 7f0a44adc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 534 additions and 0 deletions

View File

@ -42,6 +42,7 @@ import org.apache.hugegraph.backend.query.Query;
import org.apache.hugegraph.exception.NotFoundException;
import org.apache.hugegraph.exception.NotSupportException;
import org.apache.hugegraph.iterator.FilterIterator;
import org.apache.hugegraph.schema.IndexLabel;
import org.apache.hugegraph.schema.PropertyKey;
import org.apache.hugegraph.schema.SchemaLabel;
import org.apache.hugegraph.structure.HugeElement;
@ -68,6 +69,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinGlobalStep;
@ -172,6 +174,27 @@ public final class TraversalUtil {
Step<?, ?> nextStep = step.getNextStep();
if (step instanceof HasStep) {
HasContainerHolder holder = (HasContainerHolder) step;
/*
* Range/neq predicates before match() may trigger a no-index
* query after MatchStep reorders filters. Keep known-indexed
* boolean predicates pushed down, and leave the rest for
* TinkerPop to evaluate.
*/
if (followedByMatchStep(step) &&
hasUnusableMatchPredicate(newStep, holder)) {
List<HasContainer> extracted =
extractUsableHasContainers(newStep, holder);
for (HasContainer has : extracted) {
holder.removeHasContainer(has);
}
if (holder.getHasContainers().isEmpty()) {
TraversalHelper.copyLabels(step, step.getPreviousStep(),
false);
traversal.removeStep(step);
}
step = nextStep;
continue;
}
if (extractHasContainers(newStep, holder)) {
TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
traversal.removeStep(step);
@ -181,6 +204,203 @@ public final class TraversalUtil {
}
}
private static boolean followedByMatchStep(Step<?, ?> step) {
Step<?, ?> next = step.getNextStep();
while (next instanceof HasStep ||
next instanceof NoOpBarrierStep ||
next instanceof IdentityStep) {
next = next.getNextStep();
}
return next instanceof MatchStep;
}
private static boolean hasUnusableMatchPredicate(HugeGraphStep<?, ?> step,
HasContainerHolder holder) {
HugeGraph graph = tryGetGraph(step);
for (HasContainer has : holder.getHasContainers()) {
if (!hasMatchIndexSensitivePredicate(has)) {
continue;
}
if (graph == null || !hasUsableMatchIndex(graph, step, has)) {
return true;
}
}
return false;
}
private static List<HasContainer> extractUsableHasContainers(
HugeGraphStep<?, ?> step, HasContainerHolder holder) {
List<HasContainer> extracted = new ArrayList<>();
HugeGraph graph = tryGetGraph(step);
for (HasContainer has : holder.getHasContainers()) {
if (hasMatchIndexSensitivePredicate(has) &&
(graph == null || !hasUsableMatchIndex(graph, step, has))) {
continue;
}
if (!canExtractHasContainer(graph, has)) {
continue;
}
if (!GraphStep.processHasContainerIds(step, has)) {
step.addHasContainer(has);
}
extracted.add(has);
}
return extracted;
}
private static boolean hasMatchIndexSensitivePredicate(HasContainer has) {
if (hasNullPredicate(has)) {
return true;
}
List<P<Object>> predicates = new ArrayList<>();
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
for (P<Object> pred : predicates) {
BiPredicate<?, ?> bp = pred.getBiPredicate();
if (bp == Compare.neq ||
bp == Compare.gt || bp == Compare.gte ||
bp == Compare.lt || bp == Compare.lte) {
return true;
}
}
return false;
}
private static boolean hasUsableMatchIndex(HugeGraph graph,
HugeGraphStep<?, ?> step,
HasContainer has) {
if (isSysProp(has.getKey())) {
return false;
}
PropertyKey pkey;
try {
pkey = graph.propertyKey(has.getKey());
} catch (NotFoundException e) {
return false;
}
if (!hasOnlyUsableNeqPredicates(pkey, has)) {
return false;
}
if (!canExtractHasContainer(graph, has)) {
return false;
}
Collection<? extends SchemaLabel> schemaLabels = step.returnsVertex() ?
graph.vertexLabels() :
graph.edgeLabels();
boolean seen = false;
for (SchemaLabel schemaLabel : schemaLabels) {
if (!schemaLabel.properties().contains(pkey.id())) {
continue;
}
seen = true;
if (pkey.dataType() == DataType.BOOLEAN &&
!hasBooleanIndex(graph, schemaLabel, pkey)) {
return false;
}
if (pkey.dataType().isNumber() &&
(!hasOnlyRangePredicates(has) ||
!hasRangeIndex(graph, schemaLabel, pkey))) {
return false;
}
if (pkey.dataType() != DataType.BOOLEAN &&
!pkey.dataType().isNumber()) {
return false;
}
}
return seen;
}
private static boolean hasOnlyUsableNeqPredicates(PropertyKey pkey,
HasContainer has) {
if (hasNullPredicate(has)) {
return false;
}
List<P<Object>> predicates = new ArrayList<>();
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
for (P<Object> pred : predicates) {
if (pred.getBiPredicate() != Compare.neq) {
continue;
}
if (pkey.dataType() == DataType.BOOLEAN &&
pred.getValue() instanceof Boolean) {
continue;
}
return false;
}
return true;
}
private static boolean hasNullPredicate(HasContainer has) {
List<P<Object>> predicates = new ArrayList<>();
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
for (P<Object> pred : predicates) {
if (pred.getValue() == null) {
return true;
}
}
return false;
}
private static boolean hasBooleanIndex(HugeGraph graph,
SchemaLabel schemaLabel,
PropertyKey pkey) {
for (Id id : schemaLabel.indexLabels()) {
IndexLabel indexLabel = indexLabelOrNull(graph, id);
if (indexLabel == null ||
!matchSingleFieldIndex(indexLabel, pkey)) {
continue;
}
if (indexLabel.indexType().isSecondary()) {
return true;
}
}
return false;
}
private static boolean hasRangeIndex(HugeGraph graph,
SchemaLabel schemaLabel,
PropertyKey pkey) {
for (Id id : schemaLabel.indexLabels()) {
IndexLabel indexLabel = indexLabelOrNull(graph, id);
if (indexLabel == null ||
!matchSingleFieldIndex(indexLabel, pkey)) {
continue;
}
if (indexLabel.indexType().isRange()) {
return true;
}
}
return false;
}
static IndexLabel indexLabelOrNull(HugeGraph graph, Id id) {
try {
return graph.indexLabel(id);
} catch (IllegalArgumentException e) {
return null;
}
}
private static boolean matchSingleFieldIndex(IndexLabel indexLabel,
PropertyKey pkey) {
return indexLabel.indexFields().size() == 1 &&
indexLabel.indexField().equals(pkey.id());
}
private static boolean hasOnlyRangePredicates(HasContainer has) {
List<P<Object>> predicates = new ArrayList<>();
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
for (P<Object> pred : predicates) {
BiPredicate<?, ?> bp = pred.getBiPredicate();
if (bp != Compare.gt && bp != Compare.gte &&
bp != Compare.lt && bp != Compare.lte) {
return false;
}
}
return true;
}
public static void extractHasContainer(HugeVertexStep<?> newStep,
Traversal.Admin<?, ?> traversal) {
Step<?, ?> step = newStep;
@ -248,6 +468,9 @@ public final class TraversalUtil {
} catch (NotFoundException e) {
return false;
}
if (hasNullPredicate(has)) {
return false;
}
if (!pkey.dataType().isText()) {
return true;
}
@ -775,6 +998,10 @@ public final class TraversalUtil {
List<P<Object>> leafPredicates = new ArrayList<>();
collectPredicates(leafPredicates, ImmutableList.of(predicate));
for (P<Object> pred : leafPredicates) {
if (pred.getBiPredicate() == Compare.neq &&
pred.getValue() == null) {
continue;
}
Object value = validPropertyValue(pred.getValue(), pkey);
pred.setValue(value);
}

View File

@ -19,8 +19,15 @@ package org.apache.hugegraph.core;
import org.apache.hugegraph.schema.SchemaManager;
import org.apache.hugegraph.testutil.Assert;
import org.apache.hugegraph.traversal.optimize.HugeGraphStep;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.junit.Test;
@ -48,6 +55,57 @@ public class CountStrategyCoreTest extends BaseCoreTest {
commitTx();
}
private void initMatchNoIndexSchema() {
SchemaManager schema = graph().schema();
schema.propertyKey("vp2").asBoolean().create();
schema.propertyKey("vp3").asLong().create();
schema.propertyKey("vp4").asText().create();
schema.propertyKey("ep2").asBoolean().create();
schema.vertexLabel("vl1").properties("vp2", "vp4")
.nullableKeys("vp2", "vp4").create();
schema.vertexLabel("vl0").properties("vp3")
.nullableKeys("vp3").create();
schema.edgeLabel("el1").link("vl1", "vl0")
.properties("ep2").nullableKeys("ep2").create();
}
private void initMatchNoIndexGraph() {
Vertex v1 = graph().addVertex(T.label, "vl1", "vp2", true,
"vp4", "foo");
Vertex v2 = graph().addVertex(T.label, "vl1", "vp2", false,
"vp4", "J2O");
Vertex v3 = graph().addVertex(T.label, "vl0",
"vp3", 4592737712018141719L);
Vertex v4 = graph().addVertex(T.label, "vl0",
"vp3", 4592737712018141717L);
v1.addEdge("el1", v3, "ep2", true);
v2.addEdge("el1", v4, "ep2", false);
commitTx();
}
private static HugeGraphStep<?, ?> applyAndGetGraphStep(
GraphTraversal<?, ?> traversal) {
traversal.asAdmin().applyStrategies();
return (HugeGraphStep<?, ?>) traversal.asAdmin().getStartStep();
}
private static boolean hasRemainingHasStep(GraphTraversal<?, ?> traversal,
String key) {
for (Step<?, ?> step : traversal.asAdmin().getSteps()) {
if (!(step instanceof HasStep)) {
continue;
}
HasContainerHolder holder = (HasContainerHolder) step;
for (HasContainer has : holder.getHasContainers()) {
if (key.equals(has.getKey())) {
return true;
}
}
}
return false;
}
private void initTextRangeSchema(boolean withEdge) {
SchemaManager schema = graph().schema();
schema.propertyKey("vp4").asText().create();
@ -227,4 +285,229 @@ public class CountStrategyCoreTest extends BaseCoreTest {
Assert.assertEquals(direct, viaMatch);
}
@Test
public void testMatchWithNoIndexConditionMatchesDirectTraversal() {
this.initMatchNoIndexSchema();
this.initMatchNoIndexGraph();
long direct = graph().traversal().V()
.has("vp4", P.neq("J2O"))
.has("vl1", "vp2", P.gte(false))
.has("vp2")
.has("vl0", "vp3", P.gt(4592737712018141718L))
.out("el1")
.count().next();
long viaMatch = graph().traversal().V()
.has("vp4", P.neq("J2O"))
.has("vl1", "vp2", P.gte(false))
.match(__.<Vertex>as("start0")
.has("vp2")
.has("vl0", "vp3",
P.gt(4592737712018141718L))
.repeat(__.out("el1"))
.times(1)
.as("m0"))
.<Vertex>select("m0").count().next();
Assert.assertEquals(0L, direct);
Assert.assertEquals(direct, viaMatch);
}
@Test
public void testMatchWithIndexedRangeConditionStillExtractsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl1ByVp2").onV("vl1")
.by("vp2").secondary().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp2", P.lt(true))
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(1, graphStep.getHasContainers().size());
Assert.assertEquals("vp2", graphStep.getHasContainers().get(0).getKey());
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithNoIndexConditionKeepsExtractingNextHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl1ByVp2").onV("vl1")
.by("vp2").secondary().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp4", P.neq("J2O"))
.has("vp2", true)
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(1, graphStep.getHasContainers().size());
Assert.assertEquals("vp2", graphStep.getHasContainers().get(0).getKey());
Assert.assertTrue(hasRemainingHasStep(traversal, "vp4"));
Assert.assertFalse(hasRemainingHasStep(traversal, "vp2"));
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithBooleanExistsConditionKeepsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl1ByVp2").onV("vl1")
.by("vp2").secondary().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp2", P.neq(null))
.match(__.<Vertex>as("s")
.has("vp2", true)
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(0, graphStep.getHasContainers().size());
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithIdentityKeepsNoIndexConditionLocal() {
this.initMatchNoIndexSchema();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp4", P.neq("J2O"))
.identity()
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(0, graphStep.getHasContainers().size());
Assert.assertTrue(hasRemainingHasStep(traversal, "vp4"));
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithIndexedEdgeRangeConditionStillExtractsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("el1ByEp2").onE("el1")
.by("ep2").secondary().create();
this.initMatchNoIndexGraph();
GraphTraversal<Edge, Long> traversal = graph().traversal().E()
.has("ep2", P.lt(true))
.match(__.<Edge>as("s")
.has("ep2")
.as("m"))
.<Edge>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(1, graphStep.getHasContainers().size());
Assert.assertEquals("ep2", graphStep.getHasContainers().get(0).getKey());
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithIndexedNumericRangeConditionStillExtractsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl0ByVp3").onV("vl0")
.by("vp3").range().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp3",
P.gt(4592737712018141718L))
.match(__.<Vertex>as("s")
.has("vp3")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(1, graphStep.getHasContainers().size());
Assert.assertEquals("vp3", graphStep.getHasContainers().get(0).getKey());
Assert.assertFalse(hasRemainingHasStep(traversal, "vp3"));
Assert.assertEquals(1L, traversal.next());
}
@Test
public void testMatchWithIndexedNumericNeqConditionKeepsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl0ByVp3").onV("vl0")
.by("vp3").range().create();
graph().schema().indexLabel("vl1ByVp2").onV("vl1")
.by("vp2").secondary().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp3",
P.neq(4592737712018141719L))
.has("vp2", true)
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(1, graphStep.getHasContainers().size());
Assert.assertEquals("vp2", graphStep.getHasContainers().get(0).getKey());
Assert.assertTrue(hasRemainingHasStep(traversal, "vp3"));
Assert.assertEquals(0L, traversal.next());
}
@Test
public void testMatchWithSystemRangeConditionMatchesDirectTraversal() {
this.initMatchNoIndexSchema();
this.initMatchNoIndexGraph();
long direct = graph().traversal().V()
.hasLabel("vl1")
.has("vp2")
.count().next();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.hasLabel(P.neq("vl0"))
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertTrue(hasRemainingHasStep(traversal, T.label.getAccessor()));
Assert.assertEquals(direct, traversal.next().longValue());
}
@Test
public void testMatchWithUniqueBooleanRangeConditionKeepsHas() {
this.initMatchNoIndexSchema();
graph().schema().indexLabel("vl1ByUniqueVp2").onV("vl1")
.by("vp2").unique().create();
this.initMatchNoIndexGraph();
GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
.has("vp2", P.lt(true))
.match(__.<Vertex>as("s")
.has("vp2")
.as("m"))
.<Vertex>select("m")
.count();
HugeGraphStep<?, ?> graphStep = applyAndGetGraphStep(traversal);
Assert.assertEquals(0, graphStep.getHasContainers().size());
Assert.assertTrue(hasRemainingHasStep(traversal, "vp2"));
Assert.assertEquals(1L, traversal.next());
}
}

View File

@ -60,6 +60,16 @@ public class TraversalUtilOptimizeTest {
graph, new HasContainer("missing", P.eq("marko"))));
}
@Test
public void testIndexLabelOrNullWithMissingIndexLabel() {
HugeGraph graph = Mockito.mock(HugeGraph.class);
Id id = IdGenerator.of(1L);
Mockito.when(graph.indexLabel(id))
.thenThrow(new IllegalArgumentException("missing"));
Assert.assertNull(TraversalUtil.indexLabelOrNull(graph, id));
}
@Test
public void testCanExtractHasContainerWithNonTextProperty() {
HugeGraph graph = Mockito.mock(HugeGraph.class);
@ -116,6 +126,20 @@ public class TraversalUtilOptimizeTest {
Assert.assertTrue(hasStepExists(traversal));
}
@Test
public void testExtractHasContainerKeepsMatchRangeWithoutGraph() {
Traversal.Admin<?, ?> traversal = __.V()
.has("age", P.gt(18))
.match(__.as("v").identity().as("m"))
.asAdmin();
HugeGraphStep<?, ?> newStep = replaceGraphStep(traversal);
TraversalUtil.extractHasContainer(newStep, traversal);
Assert.assertTrue(newStep.getHasContainers().isEmpty());
Assert.assertTrue(hasStepExists(traversal));
}
@Test
public void testExtractHasContainerKeepsTextBetweenGraphHasStep() {
HugeGraph graph = Mockito.mock(HugeGraph.class);