Skip to content

Commit 213b754

Browse files
authored
Merge pull request #4692 from ntisseyre/inline_index_props
Inlining vertex properties into a CompositeIndex structure
2 parents 872a475 + 5aa68f5 commit 213b754

31 files changed

Lines changed: 1003 additions & 113 deletions

docs/changelog.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ For more information on features and bug fixes in 1.1.0, see the GitHub mileston
9898
* [JanusGraph zip](https://github.com/JanusGraph/janusgraph/releases/download/v1.1.0/janusgraph-1.1.0.zip)
9999
* [JanusGraph zip with embedded Cassandra and ElasticSearch](https://github.com/JanusGraph/janusgraph/releases/download/v1.1.0/janusgraph-full-1.1.0.zip)
100100

101+
##### Upgrade Instructions
102+
103+
##### Inlining vertex properties into a Composite Index
104+
105+
Inlining vertex properties into a Composite Index structure can offer significant performance and efficiency benefits.
106+
See [documentation](./schema/index-management/index-performance.md#inlining-vertex-properties-into-a-composite-index) on how to inline vertex properties into a composite index.
107+
108+
**Important Notes on Compatibility**
109+
110+
1. **Backward Incompatibility**
111+
Once a JanusGraph instance adopts this new schema feature, it cannot be rolled back to a prior version of JanusGraph.
112+
The changes in the schema structure are not compatible with earlier versions of the system.
113+
114+
2. **Migration Considerations**
115+
It is critical that users carefully plan their migration to this new version, as there is no automated or manual rollback process
116+
to revert to an older version of JanusGraph once this feature is used.
117+
101118
### Version 1.0.1 (Release Date: ???)
102119

103120
/// tab | Maven

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,44 @@ index with label restriction is defined as unique, the uniqueness
323323
constraint only applies to properties on vertices or edges for the
324324
specified label.
325325

326+
### Inlining vertex properties into a Composite Index
327+
328+
Inlining vertex properties into a Composite Index structure can offer significant performance and efficiency benefits.
329+
330+
1. **Performance Improvements**
331+
Faster Querying: Inlining vertex properties directly within the index allows the search engine to retrieve all relevant data from the index itself.
332+
This means, queries don’t need to make additional calls to data stores to fetch full vertex information, significantly reducing lookup time.
333+
334+
2. **Data Locality**
335+
In distributed storages, having inlined properties ensures that more complete data exists within individual partitions or shards.
336+
This reduces cross-node network calls and improves the overall query performance by ensuring data is more local to the request being processed.
337+
338+
3. **Cost of Indexing vs. Storage Trade-off**
339+
While inlining properties increases the size of the index (potentially leading to more extensive index storage requirements),
340+
it is often a worthwhile trade-off for performance, mainly when query speed is critical.
341+
This is a typical pattern in systems optimized for read-heavy workloads.
342+
343+
#### Usage
344+
In order to take advantage of the inlined properties feature, JanusGraph Transaction should be set to use `.propertyPrefetching(false)`
345+
346+
Example:
347+
348+
```groovy
349+
//Build index
350+
mgmt.buildIndex("composite", Vertex.class)
351+
.addKey(idKey)
352+
.addInlinePropertyKey(nameKey)
353+
.buildCompositeIndex()
354+
mgmt.commit()
355+
356+
//Query
357+
tx = graph.buildTransaction()
358+
.propertyPrefetching(false) //this is important
359+
.start()
360+
361+
tx.traversal().V().has("id", 100).next().value("name")
362+
```
363+
326364
### Composite versus Mixed Indexes
327365

328366
1. Use a composite index for exact match index retrievals. Composite

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

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.janusgraph.diskstorage.indexing.IndexInformation;
7171
import org.janusgraph.diskstorage.indexing.IndexProvider;
7272
import org.janusgraph.diskstorage.indexing.IndexTransaction;
73+
import org.janusgraph.diskstorage.keycolumnvalue.scan.ScanJobFuture;
7374
import org.janusgraph.diskstorage.log.kcvs.KCVSLog;
7475
import org.janusgraph.diskstorage.util.time.TimestampProvider;
7576
import org.janusgraph.example.GraphOfTheGodsFactory;
@@ -83,11 +84,14 @@
8384
import org.janusgraph.graphdb.internal.ElementCategory;
8485
import org.janusgraph.graphdb.internal.ElementLifeCycle;
8586
import org.janusgraph.graphdb.internal.Order;
87+
import org.janusgraph.graphdb.internal.RelationCategory;
8688
import org.janusgraph.graphdb.log.StandardTransactionLogProcessor;
8789
import org.janusgraph.graphdb.query.index.ApproximateIndexSelectionStrategy;
8890
import org.janusgraph.graphdb.query.index.BruteForceIndexSelectionStrategy;
8991
import org.janusgraph.graphdb.query.index.ThresholdBasedIndexSelectionStrategy;
9092
import org.janusgraph.graphdb.query.profile.QueryProfiler;
93+
import org.janusgraph.graphdb.query.vertex.BaseVertexCentricQuery;
94+
import org.janusgraph.graphdb.query.vertex.VertexCentricQueryBuilder;
9195
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphMixedIndexAggStep;
9296
import org.janusgraph.graphdb.tinkerpop.optimize.step.JanusGraphStep;
9397
import org.janusgraph.graphdb.tinkerpop.optimize.strategy.JanusGraphMixedIndexCountStrategy;
@@ -106,6 +110,8 @@
106110
import org.slf4j.Logger;
107111
import org.slf4j.LoggerFactory;
108112

113+
import java.lang.reflect.InvocationTargetException;
114+
import java.lang.reflect.Method;
109115
import java.time.Duration;
110116
import java.time.Instant;
111117
import java.time.temporal.ChronoUnit;
@@ -1445,6 +1451,199 @@ public void testCompositeVsMixedIndexing() {
14451451
assertTrue(tx.traversal().V().has("intId2", 234).hasNext());
14461452
}
14471453

1454+
@Test
1455+
public void testIndexInlineProperties() throws NoSuchMethodException {
1456+
1457+
clopen(option(FORCE_INDEX_USAGE), true);
1458+
1459+
final PropertyKey idKey = makeKey("id", Integer.class);
1460+
final PropertyKey nameKey = makeKey("name", String.class);
1461+
final PropertyKey cityKey = makeKey("city", String.class);
1462+
1463+
mgmt.buildIndex("composite", Vertex.class)
1464+
.addKey(idKey)
1465+
.addInlinePropertyKey(nameKey)
1466+
.buildCompositeIndex();
1467+
1468+
finishSchema();
1469+
1470+
String name = "Mizar";
1471+
String city = "Chicago";
1472+
tx.addVertex("id", 100, "name", name, "city", city);
1473+
tx.commit();
1474+
1475+
tx = graph.buildTransaction()
1476+
.propertyPrefetching(false) //this is important
1477+
.start();
1478+
1479+
Method m = VertexCentricQueryBuilder.class.getSuperclass().getDeclaredMethod("constructQuery", RelationCategory.class);
1480+
m.setAccessible(true);
1481+
1482+
CacheVertex v = (CacheVertex) (tx.traversal().V().has("id", 100).next());
1483+
1484+
verifyPropertyLoaded(v, "name", true, m);
1485+
verifyPropertyLoaded(v, "city", false, m);
1486+
1487+
assertEquals(name, v.value("name"));
1488+
assertEquals(city, v.value("city"));
1489+
}
1490+
1491+
@Test
1492+
public void testIndexInlinePropertiesReindex() throws NoSuchMethodException, InterruptedException {
1493+
clopen(option(FORCE_INDEX_USAGE), true);
1494+
1495+
PropertyKey idKey = makeKey("id", Integer.class);
1496+
PropertyKey nameKey = makeKey("name", String.class);
1497+
PropertyKey cityKey = makeKey("city", String.class);
1498+
1499+
mgmt.buildIndex("composite", Vertex.class)
1500+
.addKey(cityKey)
1501+
.buildCompositeIndex();
1502+
1503+
finishSchema();
1504+
1505+
String city = "Chicago";
1506+
for (int i = 0; i < 3; i++) {
1507+
tx.addVertex("id", i, "name", "name" + i, "city", city);
1508+
}
1509+
1510+
tx.commit();
1511+
1512+
tx = graph.buildTransaction()
1513+
.propertyPrefetching(false) //this is important
1514+
.start();
1515+
1516+
Method m = VertexCentricQueryBuilder.class.getSuperclass().getDeclaredMethod("constructQuery", RelationCategory.class);
1517+
m.setAccessible(true);
1518+
1519+
List<Vertex> vertices = tx.traversal().V().has("city", city).toList();
1520+
vertices.stream()
1521+
.map(v -> (CacheVertex) v)
1522+
.forEach(v -> verifyPropertyLoaded(v, "name", false, m));
1523+
1524+
tx.commit();
1525+
1526+
//Include inlined property
1527+
JanusGraphIndex index = mgmt.getGraphIndex("composite");
1528+
nameKey = mgmt.getPropertyKey("name");
1529+
mgmt.addInlinePropertyKey(index, nameKey);
1530+
finishSchema();
1531+
1532+
//Reindex
1533+
index = mgmt.getGraphIndex("composite");
1534+
ScanJobFuture scanJobFuture = mgmt.updateIndex(index, SchemaAction.REINDEX);
1535+
finishSchema();
1536+
1537+
while (!scanJobFuture.isDone()) {
1538+
Thread.sleep(1000);
1539+
}
1540+
1541+
//Try query now
1542+
tx = graph.buildTransaction()
1543+
.propertyPrefetching(false) //this is important
1544+
.start();
1545+
1546+
List<Vertex> vertices2 = tx.traversal().V().has("city", city).toList();
1547+
vertices2.stream()
1548+
.map(v -> (CacheVertex) v)
1549+
.forEach(v -> verifyPropertyLoaded(v, "name", true, m));
1550+
1551+
tx.commit();
1552+
}
1553+
1554+
@Test
1555+
public void testIndexInlinePropertiesUpdate() {
1556+
1557+
clopen(option(FORCE_INDEX_USAGE), true);
1558+
1559+
final PropertyKey idKey = makeKey("id", Integer.class);
1560+
final PropertyKey nameKey = makeKey("name", String.class);
1561+
final PropertyKey cityKey = makeKey("city", String.class);
1562+
1563+
mgmt.buildIndex("composite", Vertex.class)
1564+
.addKey(idKey)
1565+
.addInlinePropertyKey(nameKey)
1566+
.buildCompositeIndex();
1567+
1568+
finishSchema();
1569+
1570+
String name1 = "Mizar";
1571+
String name2 = "Alcor";
1572+
1573+
String city = "Chicago";
1574+
tx.addVertex("id", 100, "name", name1, "city", city);
1575+
tx.addVertex("id", 200, "name", name2, "city", city);
1576+
tx.commit();
1577+
1578+
tx = graph.buildTransaction()
1579+
.propertyPrefetching(false) //this is important
1580+
.start();
1581+
1582+
Vertex v = (tx.traversal().V().has("id", 100).next());
1583+
assertEquals(name1, v.value("name"));
1584+
1585+
//Update inlined property
1586+
v.property("name", "newName");
1587+
tx.commit();
1588+
1589+
tx = graph.buildTransaction()
1590+
.propertyPrefetching(false) //this is important
1591+
.start();
1592+
1593+
v = (tx.traversal().V().has("id", 100).next());
1594+
assertEquals("newName", v.value("name"));
1595+
}
1596+
1597+
@Test
1598+
public void testIndexInlinePropertiesLimit() throws NoSuchMethodException {
1599+
1600+
clopen(option(FORCE_INDEX_USAGE), true);
1601+
1602+
final PropertyKey nameKey = makeKey("name", String.class);
1603+
final PropertyKey cityKey = makeKey("city", String.class);
1604+
1605+
mgmt.buildIndex("composite", Vertex.class)
1606+
.addKey(cityKey)
1607+
.addInlinePropertyKey(nameKey)
1608+
.buildCompositeIndex();
1609+
1610+
finishSchema();
1611+
1612+
String city = "Chicago";
1613+
for (int i = 0; i < 10; i++) {
1614+
String name = "name_" + i;
1615+
tx.addVertex("name", name, "city", city);
1616+
}
1617+
tx.commit();
1618+
1619+
tx = graph.buildTransaction()
1620+
.propertyPrefetching(false) //this is important
1621+
.start();
1622+
1623+
Method m = VertexCentricQueryBuilder.class.getSuperclass().getDeclaredMethod("constructQuery", RelationCategory.class);
1624+
m.setAccessible(true);
1625+
1626+
List<Vertex> vertices = tx.traversal().V().has("city", city).limit(3).toList();
1627+
assertEquals(3, vertices.size());
1628+
vertices.stream().map(v -> (CacheVertex) v).forEach(v -> {
1629+
verifyPropertyLoaded(v, "name", true, m);
1630+
verifyPropertyLoaded(v, "city", false, m);
1631+
});
1632+
}
1633+
1634+
private void verifyPropertyLoaded(CacheVertex v, String propertyName, Boolean isPresent, Method m) {
1635+
VertexCentricQueryBuilder queryBuilder = v.query().direction(Direction.OUT);
1636+
//Verify the name property is already present in vertex cache
1637+
BaseVertexCentricQuery nameQuery = null;
1638+
try {
1639+
nameQuery = (BaseVertexCentricQuery) m.invoke(queryBuilder.keys(propertyName), RelationCategory.PROPERTY);
1640+
} catch (IllegalAccessException | InvocationTargetException e) {
1641+
throw new RuntimeException(e);
1642+
}
1643+
Boolean result = v.hasLoadedRelations(nameQuery.getSubQuery(0).getBackendQuery());
1644+
assertEquals(isPresent, result);
1645+
}
1646+
14481647
@Test
14491648
public void testCompositeAndMixedIndexing() {
14501649
final PropertyKey name = makeKey("name", String.class);

0 commit comments

Comments
 (0)