Skip to content
Merged
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
6 changes: 5 additions & 1 deletion docs/schema/index-management/index-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,17 @@ When using `order().by()` it is important to note that:
results. All results will be retrieved and then sorted in-memory.
For large result sets, this can be very expensive.

- Mixed indexes support ordering natively and efficiently. However,
- Mixed indexes support ordering property keys with **SINGLE** cardinality
natively and efficiently. However,
the property key used in the order().by() method must have been
previously added to the mixed indexed for native result ordering
support. This is important in cases where the the order().by() key
is different from the query keys. If the property key is not part of
the index, then sorting requires loading all results into memory.

- Only Elasticsearch and Solr mixed index backends support ordering
property keys with **LIST** cardinality natively and efficiently.

### Label Constraint

In many cases it is desirable to only index vertices or edges with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
import static org.janusgraph.testutil.JanusGraphAssert.assertNoBackendHit;
import static org.janusgraph.testutil.JanusGraphAssert.assertNotEmpty;
import static org.janusgraph.testutil.JanusGraphAssert.assertTraversal;
import static org.janusgraph.testutil.JanusGraphAssert.assertTraversalAndIndexUsage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -186,16 +187,18 @@ public abstract class JanusGraphIndexTest extends JanusGraphBaseTest {
public final boolean supportsGeoPoint;
public final boolean supportsNumeric;
public final boolean supportsText;
public final boolean supportsOrderingListProperty;

public IndexFeatures indexFeatures;

private static final Logger log =
LoggerFactory.getLogger(JanusGraphIndexTest.class);

protected JanusGraphIndexTest(boolean supportsGeoPoint, boolean supportsNumeric, boolean supportsText) {
protected JanusGraphIndexTest(boolean supportsGeoPoint, boolean supportsNumeric, boolean supportsText, boolean supportsOrderingListProperty) {
this.supportsGeoPoint = supportsGeoPoint;
this.supportsNumeric = supportsNumeric;
this.supportsText = supportsText;
this.supportsOrderingListProperty = supportsOrderingListProperty;
}

protected String[] getIndexBackends() {
Expand Down Expand Up @@ -4754,4 +4757,189 @@ public void testGetIndexInfo() throws DecoderException {
assertEquals(1, indexInfo.getCompositeIndexType().getInlineFieldKeys().length);
assertEquals("id", indexInfo.getCompositeIndexType().getInlineFieldKeys()[0]);
}

@Test
public void testOrderingListProperty() {
PropertyKey name1 = mgmt.makePropertyKey("name1").dataType(String.class).cardinality(Cardinality.SINGLE)
.make();
PropertyKey name2 = mgmt.makePropertyKey("name2").dataType(String.class).cardinality(Cardinality.LIST)
.make();
PropertyKey gender = mgmt.makePropertyKey("gender").dataType(String.class).cardinality(Cardinality.SINGLE).make();
PropertyKey age1 = mgmt.makePropertyKey("age1").dataType(Double.class).cardinality(Cardinality.SINGLE).make();
PropertyKey age2 = mgmt.makePropertyKey("age2").dataType(Double.class).cardinality(Cardinality.LIST).make();
PropertyKey birth1 = mgmt.makePropertyKey("birth1").dataType(Date.class).cardinality(Cardinality.SINGLE).make();
PropertyKey birth2 = mgmt.makePropertyKey("birth2").dataType(Date.class).cardinality(Cardinality.LIST).make();

mgmt.buildIndex("listPropertyOrdering", Vertex.class)
.addKey(name1, Mapping.STRING.asParameter())
.addKey(name2, Mapping.STRING.asParameter())
.addKey(gender, Mapping.STRING.asParameter())
.addKey(age1)
.addKey(age2)
.addKey(birth1)
.addKey(birth2)
.buildMixedIndex(INDEX);
finishSchema();

Vertex v1 = tx.addVertex("gender", "male", "name1", "value1", "name2", "value1", "age1", 1, "age2", 1, "birth1", "2010-01-01T00:00:00", "birth2", "2010-01-01T00:00:00");
Vertex v2 = tx.addVertex("gender", "female", "name1", "value2", "name2", "value2", "age1", 2, "age2", 2, "birth1", "2011-01-01T00:00:00", "birth2", "2011-01-01T00:00:00");
Vertex v3 = tx.addVertex("gender", "male", "name1", "value3", "name2", "value3", "name2", "value8", "age1", 3, "age2", 3, "birth1", "2012-01-01T00:00:00", "birth2", "2012-01-01T00:00:00");
Vertex v4 = tx.addVertex("gender", "female", "name1", "value4", "name2", "value4", "name2", "value7", "age1", 4, "age2", 4, "birth1", "2013-01-01T00:00:00", "birth2", "2013-01-01T00:00:00");
Vertex v5 = tx.addVertex("gender", "male", "name1", "value5", "name2", "value5", "age1", 5, "age2", 5, "birth1", "2014-01-01T00:00:00", "birth2", "2014-01-01T00:00:00");
Vertex v6 = tx.addVertex("gender", "female", "name1", "value6", "name2", "value6", "age1", 6, "age2", 6, "birth1", "2015-01-01T00:00:00", "birth2", "2015-01-01T00:00:00");
tx.commit();

clopen(option(FORCE_INDEX_USAGE), false);

final GraphTraversalSource g = tx.traversal();
org.apache.tinkerpop.gremlin.process.traversal.Order ORDER_DESC = org.apache.tinkerpop.gremlin.process.traversal.Order.desc;

// ordering without using index on SINGLE cardinality property
Supplier<GraphTraversal<?, Vertex>> tFullscanSingle = () -> g.V().order().by("name1");
assertTraversalAndIndexUsage(
Arrays.asList(
"query=[]",
"_fullscan=true"
),
tFullscanSingle, v1, v2, v3, v4, v5, v6
);

// ordering without using index on LIST cardinality property with multiple values
// throws IllegalStateException with message "Multiple properties exist for the provided key, use Vertex.properties(name2)"
Exception exception = assertThrows(IllegalStateException.class, () -> {
g.V().order().by("name2").toList();
});
assertTrue(exception.getMessage().contains("Multiple properties exist for the provided key, use Vertex.properties(name2)"));

///////////////////////////////////////////////////
// ordering SINGLE cardinality properties
// ordering SINGLE cardinality String property
Supplier<GraphTraversal<?, Vertex>> tSingleString = () -> g.V().has("name1").order().by("name1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(name1 <> null)",
"_query=[(name1 <> null)][DESC(name1)]:listPropertyOrdering"
),
tSingleString, v6, v5, v4, v3, v2, v1
);

// ordering SINGLE cardinality Double property
Supplier<GraphTraversal<?, Vertex>> tSingleDouble = () -> g.V().has("age1").order().by("age1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(age1 <> null)",
"_query=[(age1 <> null)][DESC(age1)]:listPropertyOrdering"
),
tSingleDouble, v6, v5, v4, v3, v2, v1
);

// ordering SINGLE cardinality Date property
Supplier<GraphTraversal<?, Vertex>> tSingleDate = () -> g.V().has("birth1").order().by("birth1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(birth1 <> null)",
"_query=[(birth1 <> null)][DESC(birth1)]:listPropertyOrdering"
),
tSingleDate, v6, v5, v4, v3, v2, v1
);

/////////////////////////////////////////////////
// ordering SINGLE cardinality properties with filtering
// ordering SINGLE cardinality String property
Supplier<GraphTraversal<?, Vertex>> tSingleStringFilter = () -> g.V().has("gender", "female").has("name1").order().by("name1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND name1 <> null)",
"_query=[(gender = female AND name1 <> null)][DESC(name1)]:listPropertyOrdering"
),
tSingleStringFilter, v6, v4, v2
);

// ordering SINGLE cardinality Double property
Supplier<GraphTraversal<?, Vertex>> tSingleDoubleFilter = () -> g.V().has("gender", "female").has("age1").order().by("age1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND age1 <> null)",
"_query=[(gender = female AND age1 <> null)][DESC(age1)]:listPropertyOrdering"
),
tSingleDoubleFilter, v6, v4, v2
);

// ordering SINGLE cardinality Date property
Supplier<GraphTraversal<?, Vertex>> tSingleDateFilter = () -> g.V().has("gender", "female").has("birth1").order().by("birth1", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND birth1 <> null)",
"_query=[(gender = female AND birth1 <> null)][DESC(birth1)]:listPropertyOrdering"
),
tSingleDateFilter, v6, v4, v2
);

// certain mixed index backend specific part since Lucene does not support ordering for list properties
if (supportsOrderingListProperty) {
///////////////////////////////////////////////////
// ordering LIST cardinality properties
// ordering LIST cardinality String property
Supplier<GraphTraversal<?, Vertex>> tListString = () -> g.V().has("name2").order().by("name2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(name2 <> null)",
"_query=[(name2 <> null)][DESC(name2)]:listPropertyOrdering"
),
tListString, v3, v4, v6, v5, v2, v1
);

// ordering LIST cardinality Double property
Supplier<GraphTraversal<?, Vertex>> tListDouble = () -> g.V().has("age2").order().by("age2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(age2 <> null)",
"_query=[(age2 <> null)][DESC(age2)]:listPropertyOrdering"
),
tListDouble, v6, v5, v4, v3, v2, v1
);

// ordering LIST cardinality Date property
Supplier<GraphTraversal<?, Vertex>> tListDate = () -> g.V().has("birth2").order().by("birth2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(birth2 <> null)",
"_query=[(birth2 <> null)][DESC(birth2)]:listPropertyOrdering"
),
tListDate, v6, v5, v4, v3, v2, v1
);

/////////////////////////////////////////////////
// ordering LIST cardinality properties with filtering
// ordering LIST cardinality String property
Supplier<GraphTraversal<?, Vertex>> tListStringFilter = () -> g.V().has("gender", "female").has("name2").order().by("name2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND name2 <> null)",
"_query=[(gender = female AND name2 <> null)][DESC(name2)]:listPropertyOrdering"
),
tListStringFilter, v4, v6, v2
);

// ordering LIST cardinality Double property
Supplier<GraphTraversal<?, Vertex>> tListDoubleFilter = () -> g.V().has("gender", "female").has("age2").order().by("age2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND age2 <> null)",
"_query=[(gender = female AND age2 <> null)][DESC(age2)]:listPropertyOrdering"
),
tListDoubleFilter, v6, v4, v2
);

// ordering LIST cardinality Date property
Supplier<GraphTraversal<?, Vertex>> tListDateFilter = () -> g.V().has("gender", "female").has("birth2").order().by("birth2", ORDER_DESC);
assertTraversalAndIndexUsage(
Arrays.asList(
"_condition=(gender = female AND birth2 <> null)",
"_query=[(gender = female AND birth2 <> null)][DESC(birth2)]:listPropertyOrdering"
),
tListDateFilter, v6, v4, v2
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand Down Expand Up @@ -79,6 +80,30 @@ public static<V extends Element> void assertNotEmpty(Object object) {
assertFalse(isEmpty(object));
}

public static <E extends Element> void assertTraversalAndIndexUsage(List<String> containsText,
Supplier<GraphTraversal<?, E>> traversal,
E... expectedElements) {

// assert expected results
assertTraversal(traversal.get(), expectedElements);

// assert index usage
String profileStr = traversal.get().profile().toList().toString();

// let's drop the postfix coming from dynamic field definition so comparison will not fail for Solr
// all dynamic field definitions are in janusgraph-solr/src/test/resources/solr/core-template/schema.xml
// multi-valued date and date postfix
profileStr = profileStr.replace("_dts", "").replace("_dt", "");
// multi-valued string and string postfix
profileStr = profileStr.replace("_ss", "").replace("_s", "");
// multi-valued double and double postfix
profileStr = profileStr.replace("_ds", "").replace("_d", "");

for (String text : containsText) {
assertTrue(profileStr.contains(text));
}
}

public static<E extends Element> void assertTraversal(GraphTraversal<?, E> req, E... expectedElements) {
for (final E expectedElement : expectedElements) {
assertEquals(expectedElement, req.next());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ public class IndexFeatures {
private final boolean supportsGeoContains;
private final boolean supportsGeoExists;
private final boolean supportsNotQueryNormalForm;
private final boolean supportsOrderingListProperty;
private final Set<Cardinality> supportedCardinalities;

public IndexFeatures(boolean supportsDocumentTTL, Mapping defaultMap, Set<Mapping> supportedMap,
String wildcardField, Set<Cardinality> supportedCardinalities, boolean supportsNanoseconds,
boolean supportCustomAnalyzer, boolean supportsGeoContains, boolean supportGeoExists,
boolean supportsNotQueryNormalForm) {
boolean supportsNotQueryNormalForm, boolean supportsOrderingListProperty) {

Preconditions.checkArgument(defaultMap!=null && defaultMap!=Mapping.DEFAULT);
Preconditions.checkArgument(supportedMap!=null && !supportedMap.isEmpty()
Expand All @@ -59,6 +60,7 @@ public IndexFeatures(boolean supportsDocumentTTL, Mapping defaultMap, Set<Mappin
this.supportsGeoContains = supportsGeoContains;
this.supportsGeoExists = supportGeoExists;
this.supportsNotQueryNormalForm = supportsNotQueryNormalForm;
this.supportsOrderingListProperty = supportsOrderingListProperty;
}

public boolean supportsDocumentTTL() {
Expand Down Expand Up @@ -101,6 +103,10 @@ public boolean supportNotQueryNormalForm() {
return supportsNotQueryNormalForm;
}

public boolean supportsOrderingListProperty() {
return supportsOrderingListProperty;
}

public static class Builder {

private boolean supportsDocumentTTL = false;
Expand All @@ -113,6 +119,7 @@ public static class Builder {
private boolean supportsGeoContains;
private boolean supportsGeoExists;
private boolean supportNotQueryNormalForm;
private boolean supportsOrderingListProperty;

public Builder supportsDocumentTTL() {
supportsDocumentTTL=true;
Expand Down Expand Up @@ -164,10 +171,15 @@ public Builder supportNotQueryNormalForm() {
return this;
}

public Builder supportsOrderingListProperty() {
this.supportsOrderingListProperty = true;
return this;
}

public IndexFeatures build() {
return new IndexFeatures(supportsDocumentTTL, defaultStringMapping, Collections.unmodifiableSet(new HashSet<>(supportedMappings)),
wildcardField, Collections.unmodifiableSet(new HashSet<>(supportedCardinalities)), supportsNanoseconds, supportsCustomAnalyzer,
supportsGeoContains, supportsGeoExists, supportNotQueryNormalForm);
supportsGeoContains, supportsGeoExists, supportNotQueryNormalForm, supportsOrderingListProperty);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ public boolean supportsExistsQuery(final MixedIndexType index, final ParameterIn
return true;
}

public boolean allMixedIndexBackendSupportsOrderingListProperty() {
if (mixedIndexes.isEmpty()) {
return false;
}
for (Map.Entry<String, ? extends IndexInformation> entry : mixedIndexes.entrySet()) {
// if any of the mixed index backends does not support ordering list property, let's return false
if (!entry.getValue().getFeatures().supportsOrderingListProperty()) {
return false;
}
}

return true;
}

public IndexFeatures features(final MixedIndexType index) {
return getMixedIndex(index).getFeatures();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
package org.janusgraph.graphdb.query.graph;

import com.google.common.base.Preconditions;
import org.janusgraph.core.Cardinality;
//import org.janusgraph.core.Cardinality;
import org.janusgraph.core.JanusGraphEdge;
import org.janusgraph.core.JanusGraphElement;
import org.janusgraph.core.JanusGraphQuery;
Expand Down Expand Up @@ -201,8 +201,8 @@ public GraphCentricQueryBuilder orderBy(String keyName, org.apache.tinkerpop.gr
Preconditions.checkArgument(key!=null && order!=null,"Need to specify and key and an order");
Preconditions.checkArgument(Comparable.class.isAssignableFrom(key.dataType()),
"Can only order on keys with comparable data type. [%s] has datatype [%s]", key.name(), key.dataType());
Preconditions.checkArgument(key.cardinality()== Cardinality.SINGLE,
"Ordering is undefined on multi-valued key [%s]", key.name());
//Preconditions.checkArgument(key.cardinality()== Cardinality.SINGLE,
// "Ordering is undefined on multi-valued key [%s]", key.name());
Preconditions.checkArgument(!orders.containsKey(key), "orders [%s] already contains key [%s]", orders, key);
orders.add(key, Order.convert(order));
return this;
Expand Down
Loading
Loading