Skip to content

Commit 2fa5bca

Browse files
pm-oscporunov
authored andcommitted
#4860 allow ordering list properties via mixed index backend
Signed-off-by: pm-osc <[email protected]>
1 parent 92a1b98 commit 2fa5bca

File tree

13 files changed

+265
-12
lines changed

13 files changed

+265
-12
lines changed

docs/schema/index-management/index-performance.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,17 @@ When using `order().by()` it is important to note that:
285285
results. All results will be retrieved and then sorted in-memory.
286286
For large result sets, this can be very expensive.
287287

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

296+
- Only Elasticsearch and Solr mixed index backends support ordering
297+
property keys with **LIST** cardinality natively and efficiently.
298+
295299
### Label Constraint
296300

297301
In many cases it is desirable to only index vertices or edges with a

janusgraph-backend-testutils/src/main/java/org/janusgraph/graphdb/JanusGraphIndexTest.java

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
import static org.janusgraph.testutil.JanusGraphAssert.assertNoBackendHit;
155155
import static org.janusgraph.testutil.JanusGraphAssert.assertNotEmpty;
156156
import static org.janusgraph.testutil.JanusGraphAssert.assertTraversal;
157+
import static org.janusgraph.testutil.JanusGraphAssert.assertTraversalAndIndexUsage;
157158
import static org.junit.jupiter.api.Assertions.assertEquals;
158159
import static org.junit.jupiter.api.Assertions.assertFalse;
159160
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -186,16 +187,18 @@ public abstract class JanusGraphIndexTest extends JanusGraphBaseTest {
186187
public final boolean supportsGeoPoint;
187188
public final boolean supportsNumeric;
188189
public final boolean supportsText;
190+
public final boolean supportsOrderingListProperty;
189191

190192
public IndexFeatures indexFeatures;
191193

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

195-
protected JanusGraphIndexTest(boolean supportsGeoPoint, boolean supportsNumeric, boolean supportsText) {
197+
protected JanusGraphIndexTest(boolean supportsGeoPoint, boolean supportsNumeric, boolean supportsText, boolean supportsOrderingListProperty) {
196198
this.supportsGeoPoint = supportsGeoPoint;
197199
this.supportsNumeric = supportsNumeric;
198200
this.supportsText = supportsText;
201+
this.supportsOrderingListProperty = supportsOrderingListProperty;
199202
}
200203

201204
protected String[] getIndexBackends() {
@@ -4754,4 +4757,189 @@ public void testGetIndexInfo() throws DecoderException {
47544757
assertEquals(1, indexInfo.getCompositeIndexType().getInlineFieldKeys().length);
47554758
assertEquals("id", indexInfo.getCompositeIndexType().getInlineFieldKeys()[0]);
47564759
}
4760+
4761+
@Test
4762+
public void testOrderingListProperty() {
4763+
PropertyKey name1 = mgmt.makePropertyKey("name1").dataType(String.class).cardinality(Cardinality.SINGLE)
4764+
.make();
4765+
PropertyKey name2 = mgmt.makePropertyKey("name2").dataType(String.class).cardinality(Cardinality.LIST)
4766+
.make();
4767+
PropertyKey gender = mgmt.makePropertyKey("gender").dataType(String.class).cardinality(Cardinality.SINGLE).make();
4768+
PropertyKey age1 = mgmt.makePropertyKey("age1").dataType(Double.class).cardinality(Cardinality.SINGLE).make();
4769+
PropertyKey age2 = mgmt.makePropertyKey("age2").dataType(Double.class).cardinality(Cardinality.LIST).make();
4770+
PropertyKey birth1 = mgmt.makePropertyKey("birth1").dataType(Date.class).cardinality(Cardinality.SINGLE).make();
4771+
PropertyKey birth2 = mgmt.makePropertyKey("birth2").dataType(Date.class).cardinality(Cardinality.LIST).make();
4772+
4773+
mgmt.buildIndex("listPropertyOrdering", Vertex.class)
4774+
.addKey(name1, Mapping.STRING.asParameter())
4775+
.addKey(name2, Mapping.STRING.asParameter())
4776+
.addKey(gender, Mapping.STRING.asParameter())
4777+
.addKey(age1)
4778+
.addKey(age2)
4779+
.addKey(birth1)
4780+
.addKey(birth2)
4781+
.buildMixedIndex(INDEX);
4782+
finishSchema();
4783+
4784+
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");
4785+
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");
4786+
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");
4787+
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");
4788+
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");
4789+
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");
4790+
tx.commit();
4791+
4792+
clopen(option(FORCE_INDEX_USAGE), false);
4793+
4794+
final GraphTraversalSource g = tx.traversal();
4795+
org.apache.tinkerpop.gremlin.process.traversal.Order ORDER_DESC = org.apache.tinkerpop.gremlin.process.traversal.Order.desc;
4796+
4797+
// ordering without using index on SINGLE cardinality property
4798+
Supplier<GraphTraversal<?, Vertex>> tFullscanSingle = () -> g.V().order().by("name1");
4799+
assertTraversalAndIndexUsage(
4800+
Arrays.asList(
4801+
"query=[]",
4802+
"_fullscan=true"
4803+
),
4804+
tFullscanSingle, v1, v2, v3, v4, v5, v6
4805+
);
4806+
4807+
// ordering without using index on LIST cardinality property with multiple values
4808+
// throws IllegalStateException with message "Multiple properties exist for the provided key, use Vertex.properties(name2)"
4809+
Exception exception = assertThrows(IllegalStateException.class, () -> {
4810+
g.V().order().by("name2").toList();
4811+
});
4812+
assertTrue(exception.getMessage().contains("Multiple properties exist for the provided key, use Vertex.properties(name2)"));
4813+
4814+
///////////////////////////////////////////////////
4815+
// ordering SINGLE cardinality properties
4816+
// ordering SINGLE cardinality String property
4817+
Supplier<GraphTraversal<?, Vertex>> tSingleString = () -> g.V().has("name1").order().by("name1", ORDER_DESC);
4818+
assertTraversalAndIndexUsage(
4819+
Arrays.asList(
4820+
"_condition=(name1 <> null)",
4821+
"_query=[(name1 <> null)][DESC(name1)]:listPropertyOrdering"
4822+
),
4823+
tSingleString, v6, v5, v4, v3, v2, v1
4824+
);
4825+
4826+
// ordering SINGLE cardinality Double property
4827+
Supplier<GraphTraversal<?, Vertex>> tSingleDouble = () -> g.V().has("age1").order().by("age1", ORDER_DESC);
4828+
assertTraversalAndIndexUsage(
4829+
Arrays.asList(
4830+
"_condition=(age1 <> null)",
4831+
"_query=[(age1 <> null)][DESC(age1)]:listPropertyOrdering"
4832+
),
4833+
tSingleDouble, v6, v5, v4, v3, v2, v1
4834+
);
4835+
4836+
// ordering SINGLE cardinality Date property
4837+
Supplier<GraphTraversal<?, Vertex>> tSingleDate = () -> g.V().has("birth1").order().by("birth1", ORDER_DESC);
4838+
assertTraversalAndIndexUsage(
4839+
Arrays.asList(
4840+
"_condition=(birth1 <> null)",
4841+
"_query=[(birth1 <> null)][DESC(birth1)]:listPropertyOrdering"
4842+
),
4843+
tSingleDate, v6, v5, v4, v3, v2, v1
4844+
);
4845+
4846+
/////////////////////////////////////////////////
4847+
// ordering SINGLE cardinality properties with filtering
4848+
// ordering SINGLE cardinality String property
4849+
Supplier<GraphTraversal<?, Vertex>> tSingleStringFilter = () -> g.V().has("gender", "female").has("name1").order().by("name1", ORDER_DESC);
4850+
assertTraversalAndIndexUsage(
4851+
Arrays.asList(
4852+
"_condition=(gender = female AND name1 <> null)",
4853+
"_query=[(gender = female AND name1 <> null)][DESC(name1)]:listPropertyOrdering"
4854+
),
4855+
tSingleStringFilter, v6, v4, v2
4856+
);
4857+
4858+
// ordering SINGLE cardinality Double property
4859+
Supplier<GraphTraversal<?, Vertex>> tSingleDoubleFilter = () -> g.V().has("gender", "female").has("age1").order().by("age1", ORDER_DESC);
4860+
assertTraversalAndIndexUsage(
4861+
Arrays.asList(
4862+
"_condition=(gender = female AND age1 <> null)",
4863+
"_query=[(gender = female AND age1 <> null)][DESC(age1)]:listPropertyOrdering"
4864+
),
4865+
tSingleDoubleFilter, v6, v4, v2
4866+
);
4867+
4868+
// ordering SINGLE cardinality Date property
4869+
Supplier<GraphTraversal<?, Vertex>> tSingleDateFilter = () -> g.V().has("gender", "female").has("birth1").order().by("birth1", ORDER_DESC);
4870+
assertTraversalAndIndexUsage(
4871+
Arrays.asList(
4872+
"_condition=(gender = female AND birth1 <> null)",
4873+
"_query=[(gender = female AND birth1 <> null)][DESC(birth1)]:listPropertyOrdering"
4874+
),
4875+
tSingleDateFilter, v6, v4, v2
4876+
);
4877+
4878+
// certain mixed index backend specific part since Lucene does not support ordering for list properties
4879+
if (supportsOrderingListProperty) {
4880+
///////////////////////////////////////////////////
4881+
// ordering LIST cardinality properties
4882+
// ordering LIST cardinality String property
4883+
Supplier<GraphTraversal<?, Vertex>> tListString = () -> g.V().has("name2").order().by("name2", ORDER_DESC);
4884+
assertTraversalAndIndexUsage(
4885+
Arrays.asList(
4886+
"_condition=(name2 <> null)",
4887+
"_query=[(name2 <> null)][DESC(name2)]:listPropertyOrdering"
4888+
),
4889+
tListString, v3, v4, v6, v5, v2, v1
4890+
);
4891+
4892+
// ordering LIST cardinality Double property
4893+
Supplier<GraphTraversal<?, Vertex>> tListDouble = () -> g.V().has("age2").order().by("age2", ORDER_DESC);
4894+
assertTraversalAndIndexUsage(
4895+
Arrays.asList(
4896+
"_condition=(age2 <> null)",
4897+
"_query=[(age2 <> null)][DESC(age2)]:listPropertyOrdering"
4898+
),
4899+
tListDouble, v6, v5, v4, v3, v2, v1
4900+
);
4901+
4902+
// ordering LIST cardinality Date property
4903+
Supplier<GraphTraversal<?, Vertex>> tListDate = () -> g.V().has("birth2").order().by("birth2", ORDER_DESC);
4904+
assertTraversalAndIndexUsage(
4905+
Arrays.asList(
4906+
"_condition=(birth2 <> null)",
4907+
"_query=[(birth2 <> null)][DESC(birth2)]:listPropertyOrdering"
4908+
),
4909+
tListDate, v6, v5, v4, v3, v2, v1
4910+
);
4911+
4912+
/////////////////////////////////////////////////
4913+
// ordering LIST cardinality properties with filtering
4914+
// ordering LIST cardinality String property
4915+
Supplier<GraphTraversal<?, Vertex>> tListStringFilter = () -> g.V().has("gender", "female").has("name2").order().by("name2", ORDER_DESC);
4916+
assertTraversalAndIndexUsage(
4917+
Arrays.asList(
4918+
"_condition=(gender = female AND name2 <> null)",
4919+
"_query=[(gender = female AND name2 <> null)][DESC(name2)]:listPropertyOrdering"
4920+
),
4921+
tListStringFilter, v4, v6, v2
4922+
);
4923+
4924+
// ordering LIST cardinality Double property
4925+
Supplier<GraphTraversal<?, Vertex>> tListDoubleFilter = () -> g.V().has("gender", "female").has("age2").order().by("age2", ORDER_DESC);
4926+
assertTraversalAndIndexUsage(
4927+
Arrays.asList(
4928+
"_condition=(gender = female AND age2 <> null)",
4929+
"_query=[(gender = female AND age2 <> null)][DESC(age2)]:listPropertyOrdering"
4930+
),
4931+
tListDoubleFilter, v6, v4, v2
4932+
);
4933+
4934+
// ordering LIST cardinality Date property
4935+
Supplier<GraphTraversal<?, Vertex>> tListDateFilter = () -> g.V().has("gender", "female").has("birth2").order().by("birth2", ORDER_DESC);
4936+
assertTraversalAndIndexUsage(
4937+
Arrays.asList(
4938+
"_condition=(gender = female AND birth2 <> null)",
4939+
"_query=[(gender = female AND birth2 <> null)][DESC(birth2)]:listPropertyOrdering"
4940+
),
4941+
tListDateFilter, v6, v4, v2
4942+
);
4943+
}
4944+
}
47574945
}

janusgraph-backend-testutils/src/main/java/org/janusgraph/testutil/JanusGraphAssert.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.List;
3838
import java.util.Map;
3939
import java.util.Set;
40+
import java.util.function.Supplier;
4041
import java.util.stream.IntStream;
4142
import java.util.stream.Stream;
4243

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

83+
public static <E extends Element> void assertTraversalAndIndexUsage(List<String> containsText,
84+
Supplier<GraphTraversal<?, E>> traversal,
85+
E... expectedElements) {
86+
87+
// assert expected results
88+
assertTraversal(traversal.get(), expectedElements);
89+
90+
// assert index usage
91+
String profileStr = traversal.get().profile().toList().toString();
92+
93+
// let's drop the postfix coming from dynamic field definition so comparison will not fail for Solr
94+
// all dynamic field definitions are in janusgraph-solr/src/test/resources/solr/core-template/schema.xml
95+
// multi-valued date and date postfix
96+
profileStr = profileStr.replace("_dts", "").replace("_dt", "");
97+
// multi-valued string and string postfix
98+
profileStr = profileStr.replace("_ss", "").replace("_s", "");
99+
// multi-valued double and double postfix
100+
profileStr = profileStr.replace("_ds", "").replace("_d", "");
101+
102+
for (String text : containsText) {
103+
assertTrue(profileStr.contains(text));
104+
}
105+
}
106+
82107
public static<E extends Element> void assertTraversal(GraphTraversal<?, E> req, E... expectedElements) {
83108
for (final E expectedElement : expectedElements) {
84109
assertEquals(expectedElement, req.next());

janusgraph-core/src/main/java/org/janusgraph/diskstorage/indexing/IndexFeatures.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ public class IndexFeatures {
3939
private final boolean supportsGeoContains;
4040
private final boolean supportsGeoExists;
4141
private final boolean supportsNotQueryNormalForm;
42+
private final boolean supportsOrderingListProperty;
4243
private final Set<Cardinality> supportedCardinalities;
4344

4445
public IndexFeatures(boolean supportsDocumentTTL, Mapping defaultMap, Set<Mapping> supportedMap,
4546
String wildcardField, Set<Cardinality> supportedCardinalities, boolean supportsNanoseconds,
4647
boolean supportCustomAnalyzer, boolean supportsGeoContains, boolean supportGeoExists,
47-
boolean supportsNotQueryNormalForm) {
48+
boolean supportsNotQueryNormalForm, boolean supportsOrderingListProperty) {
4849

4950
Preconditions.checkArgument(defaultMap!=null && defaultMap!=Mapping.DEFAULT);
5051
Preconditions.checkArgument(supportedMap!=null && !supportedMap.isEmpty()
@@ -59,6 +60,7 @@ public IndexFeatures(boolean supportsDocumentTTL, Mapping defaultMap, Set<Mappin
5960
this.supportsGeoContains = supportsGeoContains;
6061
this.supportsGeoExists = supportGeoExists;
6162
this.supportsNotQueryNormalForm = supportsNotQueryNormalForm;
63+
this.supportsOrderingListProperty = supportsOrderingListProperty;
6264
}
6365

6466
public boolean supportsDocumentTTL() {
@@ -101,6 +103,10 @@ public boolean supportNotQueryNormalForm() {
101103
return supportsNotQueryNormalForm;
102104
}
103105

106+
public boolean supportsOrderingListProperty() {
107+
return supportsOrderingListProperty;
108+
}
109+
104110
public static class Builder {
105111

106112
private boolean supportsDocumentTTL = false;
@@ -113,6 +119,7 @@ public static class Builder {
113119
private boolean supportsGeoContains;
114120
private boolean supportsGeoExists;
115121
private boolean supportNotQueryNormalForm;
122+
private boolean supportsOrderingListProperty;
116123

117124
public Builder supportsDocumentTTL() {
118125
supportsDocumentTTL=true;
@@ -164,10 +171,15 @@ public Builder supportNotQueryNormalForm() {
164171
return this;
165172
}
166173

174+
public Builder supportsOrderingListProperty() {
175+
this.supportsOrderingListProperty = true;
176+
return this;
177+
}
178+
167179
public IndexFeatures build() {
168180
return new IndexFeatures(supportsDocumentTTL, defaultStringMapping, Collections.unmodifiableSet(new HashSet<>(supportedMappings)),
169181
wildcardField, Collections.unmodifiableSet(new HashSet<>(supportedCardinalities)), supportsNanoseconds, supportsCustomAnalyzer,
170-
supportsGeoContains, supportsGeoExists, supportNotQueryNormalForm);
182+
supportsGeoContains, supportsGeoExists, supportNotQueryNormalForm, supportsOrderingListProperty);
171183
}
172184
}
173185
}

janusgraph-core/src/main/java/org/janusgraph/graphdb/database/IndexSerializer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ public boolean supportsExistsQuery(final MixedIndexType index, final ParameterIn
181181
return true;
182182
}
183183

184+
public boolean allMixedIndexBackendSupportsOrderingListProperty() {
185+
if (mixedIndexes.isEmpty()) {
186+
return false;
187+
}
188+
for (Map.Entry<String, ? extends IndexInformation> entry : mixedIndexes.entrySet()) {
189+
// if any of the mixed index backends does not support ordering list property, let's return false
190+
if (!entry.getValue().getFeatures().supportsOrderingListProperty()) {
191+
return false;
192+
}
193+
}
194+
195+
return true;
196+
}
197+
184198
public IndexFeatures features(final MixedIndexType index) {
185199
return getMixedIndex(index).getFeatures();
186200
}

janusgraph-core/src/main/java/org/janusgraph/graphdb/query/graph/GraphCentricQueryBuilder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
package org.janusgraph.graphdb.query.graph;
1616

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

0 commit comments

Comments
 (0)